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 contextmanager
118from pathlib import Path
12- from typing import TYPE_CHECKING , Any , Callable , Coroutine , Iterator
9+ from typing import TYPE_CHECKING , Any , Callable , Coroutine , ContextManager
1310from concurrent .futures import ThreadPoolExecutor
1411
1512if TYPE_CHECKING :
2017from .colors import ColorMode
2118from .file_input import console_input_from_file_context
2219from .main import add_base_arguments , add_optional_arguments , AppConfig
23- from .console import Console , InputGetter , GameboyColor
24- from .keyboard_input import MESSAGE_SUGGESTING_KITTY_SUPPORT
25- from .telnet_input import TelnetInputState , read_telnet_input
20+ from .console import Console , GameboyColor
21+ from .input_getter import BaseInputGetter
22+ from .keyboard_input import (
23+ MESSAGE_SUGGESTING_KITTY_SUPPORT ,
24+ console_input_from_keyboard_protocol_context ,
25+ is_kitty_keyboard_protocol_supported ,
26+ )
2627from .remote_terminal import RemoteTerminal
2728from .telnet_app_session import (
2829 set_tcp_nodelay ,
2930 telnet_to_terminal ,
3031)
3132
3233
33- @contextmanager
34- def no_input_context (console : Console ) -> Iterator [InputGetter ]:
35- """Provide a no-op input getter that returns no button presses."""
36- yield lambda : set ()
37-
38-
39- @contextmanager
40- def telnet_input_context (
41- console : Console , state : TelnetInputState
42- ) -> Iterator [InputGetter ]:
43- """Provide input from telnet keyboard state."""
44-
45- def get_input () -> set [Console .Input ]:
46- for event in state .pop_events ():
47- console .handle_event (event )
48- return state .get_input ()
49-
50- yield get_input
51-
52-
5334def _save_dir_name (username : str | None ) -> str :
5435 """Hash the username into a safe directory name.
5536
@@ -62,90 +43,60 @@ def _save_dir_name(username: str | None) -> str:
6243
6344
6445def thread_target (
65- term : RemoteTerminal ,
46+ terminal : RemoteTerminal ,
6647 console_callback : Callable [[], Console ],
6748 app_config : AppConfig ,
6849 color_mode : ColorMode ,
69- input_state : TelnetInputState | None = None ,
50+ username : str | None ,
7051) -> int :
7152 """Run the emulator in a thread with the given RemoteTerminal."""
7253 console : Console = console_callback ()
7354
55+ console_input_context : ContextManager [BaseInputGetter ]
7456 if app_config .input_file is not None :
7557 console_input_context = console_input_from_file_context (
76- console , app_config .input_file , app_config .skip_inputs
58+ console , terminal , app_config .input_file , app_config .skip_inputs
59+ )
60+ elif is_kitty_keyboard_protocol_supported (terminal , timeout = 3 ):
61+ console_input_context = console_input_from_keyboard_protocol_context (
62+ console ,
63+ terminal ,
7764 )
78- elif input_state is not None :
79- console_input_context = telnet_input_context (console , input_state )
8065 else :
81- console_input_context = no_input_context (console )
82-
83- with console_input_context as get_console_input :
84- try :
85- term .stream .write (term .enter_fullscreen + term .clear + term .hide_cursor )
86- term .stream .flush ()
66+ message = MESSAGE_SUGGESTING_KITTY_SUPPORT
67+ terminal .stream .write (message )
68+ terminal .stream .flush ()
69+ print (f"< User `{ username } ` did not support keyboard protocol" )
70+ return 1
8771
72+ try :
73+ terminal .stream .write (
74+ terminal .enter_fullscreen + terminal .clear + terminal .hide_cursor
75+ )
76+ terminal .stream .flush ()
77+ with console_input_context as get_console_input :
8878 run (
8979 console ,
90- get_input = get_console_input ,
91- term = term ,
80+ input_getter = get_console_input ,
81+ term = terminal ,
9282 frame_advance = app_config .frame_advance ,
9383 color_mode = color_mode ,
9484 break_after = app_config .break_after ,
95- speed_factor = app_config .speed_factor ,
85+ speed = app_config .speed ,
9686 )
97- except (KeyboardInterrupt , OSError ):
98- return 0
99- else :
100- return 0
101- finally :
102- time .sleep (0.1 )
103- term .stream .write (term .clear + term .exit_fullscreen + term .normal_cursor )
104- try :
105- term .stream .flush ()
106- except BrokenPipeError :
107- pass
108-
109-
110- _KITTY_RESPONSE_RE = re .compile (rb"\x1b\[\?([0-9]*)u" )
111-
112-
113- async def _detect_kitty_keyboard (
114- reader : TelnetReader , writer : TelnetWriter , timeout : float = 3.0
115- ) -> bool :
116- """Check if the telnet client supports the kitty keyboard protocol.
117-
118- Must be called before the input reading task starts.
119-
120- :param reader: telnetlib3 reader
121- :param writer: telnetlib3 writer
122- :param timeout: seconds to wait for response
123- :returns: ``True`` if the terminal responds to the kitty keyboard query
124- """
125- writer .write (b"\x1b [?u" )
126- await writer .drain ()
127-
128- buf = b""
129- loop = asyncio .get_event_loop ()
130- deadline = loop .time () + timeout
131- while True :
132- remaining = deadline - loop .time ()
133- if remaining <= 0 :
134- break
87+ except (KeyboardInterrupt , OSError ):
88+ return 0
89+ else :
90+ return 0
91+ finally :
92+ time .sleep (0.1 )
93+ terminal .stream .write (
94+ terminal .clear + terminal .exit_fullscreen + terminal .normal_cursor
95+ )
13596 try :
136- chunk = await asyncio .wait_for (
137- reader .read (256 ),
138- timeout = remaining ,
139- )
140- if not chunk :
141- break
142- buf += chunk if isinstance (chunk , bytes ) else chunk .encode ("latin-1" )
143- if _KITTY_RESPONSE_RE .search (buf ):
144- return True
145- except asyncio .TimeoutError :
146- break
147-
148- return False
97+ terminal .stream .flush ()
98+ except BrokenPipeError :
99+ pass
149100
150101
151102ShellCallback = Callable [["TelnetReader" , "TelnetWriter" ], Coroutine [Any , Any , None ]]
@@ -298,25 +249,6 @@ async def _telnet_shell(
298249 # Set TCP_NODELAY to disable Nagle's algorithm for paced output
299250 set_tcp_nodelay (writer )
300251
301- # Require kitty keyboard protocol (must be checked before input task starts)
302- if getattr (app_config , "input_file" , None ) is None :
303- if not await _detect_kitty_keyboard (reader , writer ):
304- print (
305- f"< Telnet client { peer_host } does not support "
306- f"kitty keyboard protocol"
307- )
308- msg = MESSAGE_SUGGESTING_KITTY_SUPPORT .replace ("\n " , "\r \n " )
309- writer .write (msg .encode ("utf-8" ))
310- await writer .drain ()
311- return 1
312-
313- # Create input pipe for Ctrl+C/D forwarding
314- input_read_fd , input_write_fd = os .pipe ()
315-
316- state = TelnetInputState ()
317- input_task = asyncio .create_task (
318- read_telnet_input (reader , writer , state , input_write_fd )
319- )
320252 stats_task = asyncio .create_task (
321253 _log_connection_stats (writer , peer_host , peer_port )
322254 )
@@ -346,33 +278,20 @@ async def _telnet_shell(
346278 config = AppConfig (** vars (namespace ))
347279
348280 def target (term : RemoteTerminal ) -> int :
349- return thread_target (term , console_callback , config , color_mode , state )
281+ return thread_target (term , console_callback , config , color_mode , username )
350282
351283 return await telnet_to_terminal (
284+ reader ,
352285 writer ,
353286 executor ,
354287 target ,
355- input_read_fd ,
356288 )
357289 finally :
358- input_task .cancel ()
359290 stats_task .cancel ()
360- try :
361- await input_task
362- except asyncio .CancelledError :
363- pass
364291 try :
365292 await stats_task
366293 except asyncio .CancelledError :
367294 pass
368- try :
369- os .close (input_write_fd )
370- except OSError :
371- pass
372- try :
373- os .close (input_read_fd )
374- except OSError :
375- pass
376295
377296
378297async def run_server (
0 commit comments