11from __future__ import annotations
22
3- import os
4- import re
53import time
64import hashlib
75import asyncio
86import argparse
97import traceback
10- from contextlib import AbstractContextManager as ContextManager , nullcontext
8+ from contextlib import AbstractContextManager as ContextManager
119from pathlib import Path
1210from typing import TYPE_CHECKING , Any , Callable , Coroutine
1311from concurrent .futures import ThreadPoolExecutor
2422from .main import add_base_arguments , add_optional_arguments , AppConfig
2523from .console import Console , GameboyColor
2624from .input_getter import BaseInputGetter
27- from .keyboard_input import MESSAGE_SUGGESTING_KITTY_SUPPORT
28- from .telnet_input import TelnetInputState , read_telnet_input
25+ from .keyboard_input import (
26+ MESSAGE_SUGGESTING_KITTY_SUPPORT ,
27+ console_input_from_keyboard_protocol_context ,
28+ is_kitty_keyboard_protocol_supported ,
29+ )
2930from .remote_terminal import RemoteTerminal
3031from .telnet_app_session import (
3132 set_tcp_nodelay ,
3233 telnet_to_terminal ,
3334)
3435
3536
36- class TelnetInputGetter (BaseInputGetter ):
37- """Input getter that reads from telnet input state."""
38-
39- def __init__ (
40- self , console : Console , terminal : RemoteTerminal , state : TelnetInputState
41- ) -> None :
42- super ().__init__ (console , terminal )
43- self ._state = state
44-
45- def get_pressed (self ) -> set [Console .Input ]:
46- for event in self ._state .pop_events ():
47- self ._console .handle_event (event )
48- return self ._state .get_input ()
49-
50- def pop_keystrokes (self ) -> list [Keystroke ]:
51- return []
52-
53-
5437class NoInputGetter (BaseInputGetter ):
5538 """Input getter that returns no button presses."""
5639
@@ -77,7 +60,7 @@ def thread_target(
7760 console_callback : Callable [[], Console ],
7861 app_config : AppConfig ,
7962 color_mode : ColorMode ,
80- input_state : TelnetInputState | None = None ,
63+ username : str | None ,
8164) -> int :
8265 """Run the emulator in a thread with the given RemoteTerminal."""
8366 console : Console = console_callback ()
@@ -88,18 +71,22 @@ def thread_target(
8871 ] = console_input_from_file_context (
8972 console , term , app_config .input_file , app_config .skip_inputs
9073 )
91- elif input_state is not None :
92- console_input_context = nullcontext (
93- TelnetInputGetter (console , term , input_state )
74+ elif is_kitty_keyboard_protocol_supported (term , timeout = 3 ):
75+ console_input_context = console_input_from_keyboard_protocol_context (
76+ console ,
77+ term ,
9478 )
9579 else :
96- console_input_context = nullcontext (NoInputGetter (console , term ))
97-
98- with console_input_context as get_console_input :
99- try :
100- term .stream .write (term .enter_fullscreen + term .clear + term .hide_cursor )
101- term .stream .flush ()
80+ message = MESSAGE_SUGGESTING_KITTY_SUPPORT
81+ term .stream .write (message )
82+ term .stream .flush ()
83+ print (f"< User `{ username } ` did not support keyboard protocol" )
84+ return 1
10285
86+ try :
87+ term .stream .write (term .enter_fullscreen + term .clear + term .hide_cursor )
88+ term .stream .flush ()
89+ with console_input_context as get_console_input :
10390 run (
10491 console ,
10592 input_getter = get_console_input ,
@@ -109,58 +96,17 @@ def thread_target(
10996 break_after = app_config .break_after ,
11097 speed_factor = app_config .speed_factor ,
11198 )
112- except (KeyboardInterrupt , OSError ):
113- return 0
114- else :
115- return 0
116- finally :
117- time .sleep (0.1 )
118- term .stream .write (term .clear + term .exit_fullscreen + term .normal_cursor )
119- try :
120- term .stream .flush ()
121- except BrokenPipeError :
122- pass
123-
124-
125- _KITTY_RESPONSE_RE = re .compile (rb"\x1b\[\?([0-9]*)u" )
126-
127-
128- async def _detect_kitty_keyboard (
129- reader : TelnetReader , writer : TelnetWriter , timeout : float = 3.0
130- ) -> bool :
131- """Check if the telnet client supports the kitty keyboard protocol.
132-
133- Must be called before the input reading task starts.
134-
135- :param reader: telnetlib3 reader
136- :param writer: telnetlib3 writer
137- :param timeout: seconds to wait for response
138- :returns: ``True`` if the terminal responds to the kitty keyboard query
139- """
140- writer .write (b"\x1b [?u" )
141- await writer .drain ()
142-
143- buf = b""
144- loop = asyncio .get_event_loop ()
145- deadline = loop .time () + timeout
146- while True :
147- remaining = deadline - loop .time ()
148- if remaining <= 0 :
149- break
99+ except (KeyboardInterrupt , OSError ):
100+ return 0
101+ else :
102+ return 0
103+ finally :
104+ time .sleep (0.1 )
105+ term .stream .write (term .clear + term .exit_fullscreen + term .normal_cursor )
150106 try :
151- chunk = await asyncio .wait_for (
152- reader .read (256 ),
153- timeout = remaining ,
154- )
155- if not chunk :
156- break
157- buf += chunk if isinstance (chunk , bytes ) else chunk .encode ("latin-1" )
158- if _KITTY_RESPONSE_RE .search (buf ):
159- return True
160- except asyncio .TimeoutError :
161- break
162-
163- return False
107+ term .stream .flush ()
108+ except BrokenPipeError :
109+ pass
164110
165111
166112ShellCallback = Callable [["TelnetReader" , "TelnetWriter" ], Coroutine [Any , Any , None ]]
@@ -313,25 +259,6 @@ async def _telnet_shell(
313259 # Set TCP_NODELAY to disable Nagle's algorithm for paced output
314260 set_tcp_nodelay (writer )
315261
316- # Require kitty keyboard protocol (must be checked before input task starts)
317- if getattr (app_config , "input_file" , None ) is None :
318- if not await _detect_kitty_keyboard (reader , writer ):
319- print (
320- f"< Telnet client { peer_host } does not support "
321- f"kitty keyboard protocol"
322- )
323- msg = MESSAGE_SUGGESTING_KITTY_SUPPORT .replace ("\n " , "\r \n " )
324- writer .write (msg .encode ("utf-8" ))
325- await writer .drain ()
326- return 1
327-
328- # Create input pipe for Ctrl+C/D forwarding
329- input_read_fd , input_write_fd = os .pipe ()
330-
331- state = TelnetInputState ()
332- input_task = asyncio .create_task (
333- read_telnet_input (reader , writer , state , input_write_fd )
334- )
335262 stats_task = asyncio .create_task (
336263 _log_connection_stats (writer , peer_host , peer_port )
337264 )
@@ -361,33 +288,20 @@ async def _telnet_shell(
361288 config = AppConfig (** vars (namespace ))
362289
363290 def target (term : RemoteTerminal ) -> int :
364- return thread_target (term , console_callback , config , color_mode , state )
291+ return thread_target (term , console_callback , config , color_mode , username )
365292
366293 return await telnet_to_terminal (
294+ reader ,
367295 writer ,
368296 executor ,
369297 target ,
370- input_read_fd ,
371298 )
372299 finally :
373- input_task .cancel ()
374300 stats_task .cancel ()
375- try :
376- await input_task
377- except asyncio .CancelledError :
378- pass
379301 try :
380302 await stats_task
381303 except asyncio .CancelledError :
382304 pass
383- try :
384- os .close (input_write_fd )
385- except OSError :
386- pass
387- try :
388- os .close (input_read_fd )
389- except OSError :
390- pass
391305
392306
393307async def run_server (
0 commit comments