1515import json
1616import os
1717import platform
18- import selectors
1918import signal
2019import sys
2120from codecs import getincrementaldecoder
2827from ..driver import Driver
2928from ..geometry import Size
3029from ._byte_stream import ByteStream
30+ from ._input_reader import InputReader
3131
3232WINDOWS = platform .system () == "Windows"
3333
3434
35+ class _ExitInput (Exception ):
36+ """Internal exception to force exit of input loop."""
37+
38+
3539class WebDriver (Driver ):
3640 """A headless driver that may be run remotely."""
3741
@@ -41,10 +45,10 @@ def __init__(
4145 super ().__init__ (app , debug = debug , size = size )
4246 self .stdout = sys .__stdout__
4347 self .fileno = sys .__stdout__ .fileno ()
44- self .in_fileno = sys .__stdin__ .fileno ()
4548 self ._write = partial (os .write , self .fileno )
4649 self .exit_event = Event ()
4750 self ._key_thread : Thread = Thread (target = self .run_input_thread )
51+ self ._input_reader = InputReader ()
4852
4953 def write (self , data : str ) -> None :
5054 """Write data to the output device.
@@ -56,6 +60,15 @@ def write(self, data: str) -> None:
5660 data_bytes = data .encode ("utf-8" )
5761 self ._write (b"D%s%s" % (len (data_bytes ).to_bytes (4 , "big" ), data_bytes ))
5862
63+ def write_meta (self , data : dict [str , object ]) -> None :
64+ """Write meta to the controlling process (i.e. textual-web)
65+
66+ Args:
67+ data: Meta dict.
68+ """
69+ meta_bytes = json .dumps (data ).encode ("utf-8" , errors = "ignore" )
70+ self ._write (b"M%s%s" % (len (meta_bytes ).to_bytes (4 , "big" ), meta_bytes ))
71+
5972 def flush (self ) -> None :
6073 pass
6174
@@ -128,68 +141,61 @@ def disable_input(self) -> None:
128141 def stop_application_mode (self ) -> None :
129142 """Stop application mode, restore state."""
130143 self .exit_event .set ()
131- self ._key_thread .join ()
144+ self ._input_reader .close ()
145+ self .write_meta ({"type" : "exit" })
132146
133147 def run_input_thread (self ) -> None :
134148 """Wait for input and dispatch events."""
135- selector = selectors .DefaultSelector ()
136- fileno = self .in_fileno
137- selector .register (fileno , selectors .EVENT_READ )
138-
139- def more_data () -> bool :
140- """Check if there is more data to parse."""
141- for key , events in selector .select (0.01 ):
142- if events :
143- return True
144- return False
145-
146- parser = XTermParser (more_data , debug = self ._debug )
147- feed = parser .feed
148-
149+ input_reader = self ._input_reader
150+ parser = XTermParser (input_reader .more_data , debug = self ._debug )
149151 utf8_decoder = getincrementaldecoder ("utf-8" )().decode
150152 decode = utf8_decoder
151- read = os .read
152- EVENT_READ = selectors .EVENT_READ
153-
154153 # The server sends us a stream of bytes, which contains the equivalent of stdin, plus
155154 # in band data packets.
156155 byte_stream = ByteStream ()
157156 try :
158- while not self .exit_event .is_set ():
159- selector_events = selector .select (0.1 )
160- for _selector_key , mask in selector_events :
161- if mask | EVENT_READ :
162- data = read (fileno , 1024 ) # raw data
163-
164- for packet_type , payload in byte_stream .feed (data ):
165- if packet_type == "D" :
166- # Treat as stdin
167- for event in feed (decode (payload )):
168- self .process_event (event )
169- else :
170- # Process meta information separately
171- self ._on_meta (packet_type , payload )
172- except Exception as error :
173- log (error )
157+ for data in input_reader :
158+ for packet_type , payload in byte_stream .feed (data ):
159+ if packet_type == "D" :
160+ # Treat as stdin
161+ for event in parser .feed (decode (payload )):
162+ self .process_event (event )
163+ else :
164+ # Process meta information separately
165+ self ._on_meta (packet_type , payload )
166+ except _ExitInput :
167+ pass
168+ except Exception :
169+ from traceback import format_exc
170+
171+ log (format_exc ())
174172 finally :
175- selector .close ()
173+ input_reader .close ()
176174
177175 def _on_meta (self , packet_type : str , payload : bytes ) -> None :
176+ """Private method to dispatch meta.
177+
178+ Args:
179+ packet_type: Packet type (currently always "M")
180+ payload: Meta payload (JSON encoded as bytes).
181+ """
178182 payload_map = json .loads (payload )
179183 _type = payload_map .get ("type" )
180184 if isinstance (payload_map , dict ):
181185 self .on_meta (_type , payload_map )
182186
183187 def on_meta (self , packet_type : str , payload : dict ) -> None :
188+ """Process meta information.
189+
190+ Args:
191+ packet_type: The type of the packet.
192+ payload: meta dict.
193+ """
184194 if packet_type == "resize" :
185195 self ._size = (payload ["width" ], payload ["height" ])
186196 size = Size (* self ._size )
187- event = events .Resize (size , size )
188- asyncio .run_coroutine_threadsafe (
189- self ._app ._post_message (event ),
190- loop = self ._loop ,
191- )
197+ self ._app .post_message (events .Resize (size , size ))
192198 elif packet_type == "quit" :
193- asyncio . run_coroutine_threadsafe (
194- self . _app . _post_message ( messages . ExitApp ()), loop = self . _loop
195- )
199+ self . _app . post_message ( messages . ExitApp ())
200+ elif packet_type == "exit" :
201+ raise _ExitInput ( )
0 commit comments