home · contact · privacy
Refactor client, add potential map widget.
[plomrogue2-experiments] / client.py
1 #!/usr/bin/env python3
2
3 import urwid
4 import plom_socket_io
5 import socket
6 import threading
7
8
9 class UrwidSetup():
10
11     def __init__(self, socket):
12         """Build client urwid interface around socket communication.
13
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:
18
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 the urwid thread), and 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 much easier to
31         just tell the urwid code where it finds a full new server message to
32         handle.)
33         """
34         self.socket = socket
35         self.main_loop = urwid.MainLoop(self.setup_widgets())
36         self.server_output = ['']
37         input_handler = getattr(self.InputHandler(self.reply_widget,
38                                                   self.map_widget,
39                                                   self.server_output),
40                                 'handle_input')
41         self.urwid_pipe_write_fd = self.main_loop.watch_pipe(input_handler)
42         self.recv_loop_thread = threading.Thread(target=self.recv_loop)
43
44     def setup_widgets(self):
45         """Return container widget with all widgets we want on our screen.
46
47         Sets up an urwid.Pile inside a returned urwid.Filler; top to bottom:
48         - an EditToSocket widget, prefixing self.socket input with 'SEND: '
49         - self.reply_widget, a urwid.Text widget printing self.socket replies
50         - a 50-col wide urwid.Padding container for self.map_widget, which is
51           to print clipped map representations
52         """
53         edit_widget = self.EditToSocket(self.socket, 'SEND: ')
54         self.reply_widget = urwid.Text('')
55         self.map_widget = urwid.Text('', wrap='clip')
56         map_box = urwid.Padding(self.map_widget, width=50)
57         widget_pile = urwid.Pile([edit_widget, self.reply_widget, map_box])
58         return urwid.Filler(widget_pile)
59
60     class EditToSocket(urwid.Edit):
61         """Extends urwid.Edit with socket to send input on 'enter' to."""
62
63         def __init__(self, socket, *args, **kwargs):
64             super().__init__(*args, **kwargs)
65             self.socket = socket
66
67         def keypress(self, size, key):
68             """Extend super(): on Enter, send .edit_text, and empty it."""
69             if key != 'enter':
70                 return super().keypress(size, key)
71             plom_socket_io.send(self.socket, self.edit_text)
72             self.edit_text = ''
73
74     class InputHandler:
75         """Delivers data from other thread to widget via message_container.
76
77         The class only exists to provide handle_input as a bound method, with
78         widget and message_container pre-set, as (bound) handle_input is used
79         as a callback in urwid's watch_pipe – which merely provides its
80         callback target with one parameter for a pipe to read data from an
81         urwid-external thread.
82         """
83
84         def __init__(self, widget1, widget2, message_container):
85             self.widget1 = widget1
86             self.widget2 = widget2
87             self.message_container = message_container
88
89         def handle_input(self, trigger):
90             """On input from other thread, either quit or write to widget text.
91
92             Serves as a receiver to urwid's watch_pipe mechanism, with trigger
93             the data that a pipe defined by watch_pipe delivers. To avoid
94             buffering trouble, we don't care for that data beyond the fact that
95             its receival triggers this function: The sender is to write the
96             data it wants to deliver into the container referenced by
97             self.message_container, and just pipe the trigger to inform us
98             about this.
99
100             If the message delivered is 'BYE', quits Urbit.
101             """
102             if self.message_container[0] == 'BYE':
103                 raise urwid.ExitMainLoop()
104                 return
105             self.widget1.set_text('SERVER: ' + self.message_container[0])
106             self.widget2.set_text('loremipsumdolorsitamet '
107                                   'loremipsumdolorsitamet'
108                                   'loremipsumdolorsitamet '
109                                   'loremipsumdolorsitamet\n'
110                                   'loremipsumdolorsitamet '
111                                   'loremipsumdolorsitamet')
112
113     def recv_loop(self):
114         """Loop to receive messages from socket and deliver them to urwid.
115
116         Writes finished messages from the socket to self.server_output[0],
117         then sends a single b' ' through self.urwid_pipe_write_fd to trigger
118         the urwid code to read from it.
119         """
120         import os
121         for msg in plom_socket_io.recv(self.socket):
122             self.server_output[0] = msg
123             os.write(self.urwid_pipe_write_fd, b' ')
124
125     def run(self):
126         """Run in parallel main and recv_loop thread."""
127         self.recv_loop_thread.start()
128         self.main_loop.run()
129         self.recv_loop_thread.join()
130
131
132 s = socket.create_connection(('127.0.0.1', 5000))
133 u = UrwidSetup(s)
134 u.run()
135 s.close()