11 def __init__(self, socket):
12 """Build client urwid interface around socket communication.
14 Sets up all widgets for writing to the socket and representing data
15 from it. Sending via a self.EditToSocket widget is straightforward;
16 polling the socket for input from the server in parallel to the urwid
17 main loop not so much:
19 The urwid developers warn against sharing urwid resources among
20 threads, so having a socket polling thread for writing to an urwid
21 widget while other widgets are handled in other threads would be
22 dangerous. Urwid developers recommend using urwid's watch_pipe
23 mechanism instead: using a pipe from non-urwid threads into a single
24 urwid thread. We use self.recv_loop_thread to poll the socket, therein
25 write socket.recv output to an object that is then linked to by
26 self.server_output (which is known to the urwid thread), then use the
27 pipe to urwid to trigger it pulling new data from self.server_output to
28 handle via self.InputHandler. (We *could* pipe socket.recv output
29 directly, but then we get complicated buffering situations here as well
30 as in the urwid code that receives the pipe output. It's easier to just
31 tell the urwid code where it finds full new server messages to handle.)
34 self.main_loop = urwid.MainLoop(self.setup_widgets())
35 self.server_output = []
36 input_handler = getattr(self.InputHandler(self.reply_widget,
40 self.urwid_pipe_write_fd = self.main_loop.watch_pipe(input_handler)
41 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
43 def setup_widgets(self):
44 """Return container widget with all widgets we want on our screen.
46 Sets up an urwid.Pile inside a returned urwid.Filler; top to bottom:
47 - an EditToSocketWidget, prefixing self.socket input with 'SEND: '
48 - self.reply_widget, a urwid.Text widget printing self.socket replies
49 - a 50-col wide urwid.Padding container for self.map_widget, which is
50 to print clipped map representations
52 edit_widget = self.EditToSocketWidget(self.socket, 'SEND: ')
53 self.reply_widget = urwid.Text('')
54 self.map_widget = self.MapWidget('', wrap='clip')
55 map_box = urwid.Padding(self.map_widget, width=50)
56 widget_pile = urwid.Pile([edit_widget, self.reply_widget, map_box])
57 return urwid.Filler(widget_pile, valign='top')
59 class EditToSocketWidget(urwid.Edit):
60 """Extends urwid.Edit with socket to send input on 'enter' to."""
62 def __init__(self, socket, *args, **kwargs):
63 super().__init__(*args, **kwargs)
66 def keypress(self, size, key):
67 """Extend super(): on Enter, send .edit_text, and empty it."""
69 return super().keypress(size, key)
70 plom_socket_io.send(self.socket, self.edit_text)
73 class MapWidget(urwid.Text):
74 """Stores/updates/draws game map."""
75 terrain_map = ' ' * 25
79 """Draw map view from .terrain_map, .position."""
81 for c in self.terrain_map:
83 pos_i = self.position[0] * (5 + 1) + self.position[1]
84 whole_map[pos_i] = '@'
85 self.set_text(''.join(whole_map))
87 def update_terrain(self, terrain_map):
88 """Update self.terrain_map."""
89 self.terrain_map = terrain_map
92 def update_position_y(self, position_y_string):
93 """Update self.position[0]."""
94 self.position[0] = int(position_y_string)
97 def update_position_x(self, position_x_string):
98 """Update self.position[1]."""
99 self.position[1] = int(position_x_string)
103 """Delivers data from other thread to widget via message_container.
105 The class only exists to provide handle_input as a bound method, with
106 widget and message_container pre-set, as (bound) handle_input is used
107 as a callback in urwid's watch_pipe – which merely provides its
108 callback target with one parameter for a pipe to read data from an
109 urwid-external thread.
112 def __init__(self, widget1, widget2, message_container):
113 self.widget1 = widget1
114 self.widget2 = widget2
115 self.message_container = message_container
117 def handle_input(self, trigger):
118 """On input from other thread, either quit or write to widget text.
120 Serves as a receiver to urwid's watch_pipe mechanism, with trigger
121 the data that a pipe defined by watch_pipe delivers. To avoid
122 buffering trouble, we don't care for that data beyond the fact that
123 its receival triggers this function: The sender is to write the
124 data it wants to deliver into the container referenced by
125 self.message_container, and just pipe the trigger to inform us
128 If the message delivered is 'BYE', quits Urbit.
130 msg = self.message_container[0]
132 raise urwid.ExitMainLoop()
134 if len(msg) > 8 and msg[:8] == 'TERRAIN ':
135 self.widget2.update_terrain(msg[8:])
136 elif len(msg) > 11 and msg[:11] == 'POSITION_Y ':
137 self.widget2.update_position_y(msg[11:])
138 elif len(msg) > 11 and msg[:11] == 'POSITION_X ':
139 self.widget2.update_position_x(msg[11:])
141 self.widget1.set_text('SERVER: ' + msg)
142 del self.message_container[0]
145 """Loop to receive messages from socket and deliver them to urwid.
147 Waits for self.server_output to become empty (this signals that the
148 input handler is finished / ready to receive new input), then writes
149 finished message from socket to self.server_output, then sends a single
150 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
153 for msg in plom_socket_io.recv(self.socket):
154 while len(self.server_output) > 0:
156 self.server_output += [msg]
157 os.write(self.urwid_pipe_write_fd, b' ')
160 """Run in parallel main and recv_loop thread."""
161 self.recv_loop_thread.start()
163 self.recv_loop_thread.join()
166 s = socket.create_connection(('127.0.0.1', 5000))