home · contact · privacy
3e50b2ec43dfd7ef6427ca864e05b174fface59d
[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 def recv_loop(socket, urwid_pipe_write_fd, server_output):
10     """Loop to receive messages from socket and deliver them 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 socket.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     import os
24     for msg in plom_socket_io.recv(socket):
25         server_output[0] = msg
26         os.write(urwid_pipe_write_fd, b' ')
27
28
29 class InputHandler:
30     """Helps delivering data from other thread to widget via message_container.
31
32     The whole class only exists to provide handle_input as a bound method, with
33     widget and message_container pre-set, as (bound) handle_input is used as a
34     callback in urwid's watch_pipe – which merely provides its callback target
35     with one parameter for a pipe to read data from an urwid-external thread.
36     """
37
38     def __init__(self, widget, message_container):
39         self.widget = widget
40         self.message_container = message_container
41
42     def handle_input(self, trigger):
43         """On input from other thread, either quit, or write to widget text.
44
45         Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
46         data that a pipe defined by watch_pipe delivers. To avoid buffering
47         trouble, we don't care for that data beyond the fact that its receival
48         triggers this function: The sender is to write the data it wants to
49         deliver into the container referenced by self.message_container, and
50         just pipe the trigger to inform us about this.
51
52         If the message delivered is 'BYE', quits Urbit.
53         """
54         if self.message_container[0] == 'BYE':
55             raise urwid.ExitMainLoop()
56             return
57         self.widget.set_text('SERVER: ' + self.message_container[0])
58
59
60 class SocketInputWidget(urwid.Filler):
61
62     def __init__(self, socket, *args, **kwargs):
63         super().__init__(*args, **kwargs)
64         self.socket = socket
65
66     def keypress(self, size, key):
67         """Act like super(), except on Enter: send .edit_text, and empty it."""
68         if key != 'enter':
69             return super().keypress(size, key)
70         plom_socket_io.send(self.socket, edit.edit_text)
71         edit.edit_text = ''
72
73
74 s = socket.create_connection(('127.0.0.1', 5000))
75
76 edit = urwid.Edit('SEND: ')
77 txt = urwid.Text('')
78 pile = urwid.Pile([edit, txt])
79 fill = SocketInputWidget(s, pile)
80 loop = urwid.MainLoop(fill)
81
82 server_output = ['']
83 write_fd = loop.watch_pipe(getattr(InputHandler(txt, server_output),
84                                    'handle_input'))
85 thread = threading.Thread(target=recv_loop,
86                           kwargs={'socket': s, 'server_output': server_output,
87                                   'urwid_pipe_write_fd': write_fd})
88 thread.start()
89
90 loop.run()
91
92 thread.join()
93 s.close()