home · contact · privacy
92d555e359cd757a985748b2eba72120eee1a045
[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 RecvThread(threading.Thread):
10     """Background thread that delivers messages from the socket to urwid.
11
12     The message transfer to urwid is a bit weird. The urwid developers warn
13     against sharing urwid resources among threads, and recommend using urwid's
14     watch_pipe mechanism: using a pipe from non-urwid threads into a single
15     urwid thread. We could pipe the recv output directly, but then we get
16     complicated buffering situations here as well as in the urwid code that
17     receives the pipe content. It's much easier to update a third resource
18     (server_output, which references an object that's also known to the urwid
19     code) to contain the new message, and then just use the urwid pipe
20     (urwid_pipe_write_fd) to trigger the urwid code to pull the message in from
21     that third resource. We send a single b' ' through the pipe to trigger it.
22     """
23
24     def __init__(self, socket, urwid_pipe_write_fd, server_output):
25         super().__init__()
26         self.socket = socket
27         self.urwid_pipe = urwid_pipe_write_fd
28         self.server_output = server_output
29
30     def run(self):
31         """On message receive, write to self.server_output, ping urwid pipe."""
32         import os
33         for msg in plom_socket_io.recv(self.socket):
34             self.server_output[0] = msg
35             os.write(self.urwid_pipe, b' ')
36
37
38 class InputHandler:
39     """Helps delivering data from other thread to widget via message_container.
40     
41     The whole class only exists to provide handle_input as a bound method, with
42     widget and message_container pre-set, as (bound) handle_input is used as a
43     callback in urwid's watch_pipe – which merely provides its callback target
44     with one parameter for a pipe to read data from an urwid-external thread.
45     """
46
47     def __init__(self, widget, message_container):
48         self.widget = widget
49         self.message_container = message_container
50
51     def handle_input(self, trigger):
52         """On input from other thread, either quit, or write to widget text.
53
54         Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
55         data that a pipe defined by watch_pipe delivers. To avoid buffering
56         trouble, we don't care for that data beyond the fact that its receival
57         triggers this function: The sender is to write the data it wants to
58         deliver into the container referenced by self.message_container, and
59         just pipe the trigger to inform us about this.
60
61         If the message delivered is 'BYE', quits Urbit.
62         """
63         if self.message_container[0] == 'BYE':
64             raise urwid.ExitMainLoop()
65             return
66         self.widget.set_text('REPLY: ' + self.message_container[0])
67
68
69 class SocketInputWidget(urwid.Filler):
70
71     def __init__(self, socket, *args, **kwargs):
72         super().__init__(*args, **kwargs)
73         self.socket = socket
74
75     def keypress(self, size, key):
76         """Act like super(), except on Enter: send .edit_text, and empty it."""
77         if key != 'enter':
78             return super().keypress(size, key)
79         plom_socket_io.send(self.socket, edit.edit_text)
80         edit.edit_text = ''
81
82
83 s = socket.create_connection(('127.0.0.1', 5000))
84
85 edit = urwid.Edit('SEND: ')
86 txt = urwid.Text('')
87 pile = urwid.Pile([edit, txt])
88 fill = SocketInputWidget(s, pile)
89 loop = urwid.MainLoop(fill)
90
91 server_output = ['']
92 write_fd = loop.watch_pipe(getattr(InputHandler(txt, server_output),
93                                    'handle_input'))
94 thread = RecvThread(s, write_fd, server_output)
95 thread.start()
96
97 loop.run()
98
99 thread.join()
100 s.close()