11from __future__ import annotations
22
3- from enum import Flag , IntEnum
43import sys
54import time
6- import logging
7- from contextlib import contextmanager , closing
8- from typing import Callable , Iterator , NamedTuple
5+ from contextlib import contextmanager
6+ from typing import Callable , Iterator
97from prompt_toolkit .application import create_app_session , AppSession
10- from prompt_toolkit .input .vt100_parser import Vt100Parser
11- from prompt_toolkit .keys import Keys as PromptToolkitKeys
12- from prompt_toolkit .key_binding import KeyPress
138
149from .console import Console , InputGetter
15- from .keyboard_protocol import (
16- CSI_TO_FUNCTIONAL_KEY ,
17- FUNCTIONAL_KEYS_TO_PT_KEYS ,
10+ from .keys import (
1811 FunctionalKeys ,
1912 LatinKeys ,
2013 Keys ,
2114)
2215from .ansi_escape_code import (
23- CSI ,
2416 detect_keyboard_protocol_support_parser ,
25- parse_ansi_escape_code ,
2617 run_parser_in_app_session ,
2718)
2819
20+ from .pynput_keyboard_input import pynput_key_pressed_context
21+ from .x11_keyboard_input import x11_key_pressed_context
22+ from .keyboard_protocol_input import keyboard_protocol_key_pressed_context
23+
2924
3025def get_input_mapping (console : Console ) -> dict [Keys , Console .Input ]:
3126 return {
@@ -69,128 +64,6 @@ def get_event_mapping(console: Console) -> dict[Keys, Console.Event]:
6964 }
7065
7166
72- @contextmanager
73- def xlib_key_pressed_context (
74- display : str | None = None ,
75- ) -> Iterator [Callable [[], set [Keys ]]]:
76- from Xlib .ext import xinput
77- from Xlib .display import Display
78-
79- with closing (Display (display )) as xdisplay :
80- extension_info = xdisplay .query_extension ("XInputExtension" )
81- xinput_major = extension_info is not None and extension_info .major_opcode
82- # Set of currently pressed keys and focused flag
83- pressed : set [Keys ] = set ()
84- focused = True
85- # Save current focus, as it is likely to be the terminal window
86- term_window = xdisplay .get_input_focus ().focus
87- term_window .xinput_select_events (
88- [(xinput .AllDevices , xinput .FocusInMask | xinput .FocusOutMask )]
89- )
90- # It is possible the select events directly on the terminal window, but for some
91- # reasons, the events won't be propagated for some terminals like kitty.
92- # Instead, we select the events on the root windows and then perform some
93- # filtering.
94- xdisplay .screen ().root .xinput_select_events (
95- [(xinput .AllDevices , xinput .KeyPressMask | xinput .KeyReleaseMask )]
96- )
97-
98- def get_pressed () -> set [Keys ]:
99- nonlocal focused
100- # Loop over pending events
101- while xdisplay .pending_events ():
102- event = xdisplay .next_event ()
103- # Unexpected events
104- if not hasattr (event , "extension" ):
105- continue
106- if event .extension != xinput_major :
107- continue
108- # Focus has been lost
109- if event .evtype == xinput .FocusOut :
110- focused = False
111- pressed .clear ()
112- continue
113- # Focus has been retrieved
114- if event .evtype == xinput .FocusIn :
115- focused = True
116- continue
117- # The window is currently not focused
118- if not focused :
119- continue
120- # Extract key press/release information
121- keycode = event .data .detail
122- mods = event .data .mods .effective_mods
123- group = event .data .groups .effective_group * 2
124- keysym = xdisplay .keycode_to_keysym (keycode , group )
125- modkeysym = xdisplay .keycode_to_keysym (keycode , mods )
126- keystr = xdisplay .lookup_string (keysym )
127- repeat = event .data .flags & 0x10000
128- is_key_pressed = event .evtype == xinput .KeyPress and not repeat
129- is_key_released = event .evtype == xinput .KeyRelease
130-
131- # Convert into
132- key : Keys | None = LatinKeys .from_latin (keysym )
133- if key is None :
134- key = FunctionalKeys .from_xlib_keysym (keysym )
135- if key is None :
136- continue
137-
138- # Prepare info string
139- info_string = f"keycode={ keycode } , keysym={ keysym } , modkeysym={ modkeysym } , keystr={ keystr } , key={ key } "
140-
141- # Update the `pressed` set accordingly
142- if is_key_pressed :
143- pressed .add (key )
144- logging .info ("Key pressed: " + info_string )
145- if is_key_released :
146- pressed .discard (key )
147- logging .info ("Key released: " + info_string )
148-
149- # Return the currently pressed keys
150- return pressed
151-
152- try :
153- yield get_pressed
154- finally :
155- pressed .clear ()
156-
157-
158- @contextmanager
159- def pynput_key_pressed_context () -> Iterator [Callable [[], set [Keys ]]]:
160- import pynput .keyboard # type: ignore
161-
162- def on_press (key : pynput .keyboard .Key | pynput .keyboard .KeyCode | None ) -> None :
163- value : Keys | None
164- if isinstance (key , pynput .keyboard .KeyCode ):
165- value = None if key .char is None else LatinKeys .from_latin (key .char )
166- elif isinstance (key , pynput .keyboard .Key ):
167- value = FunctionalKeys .from_pynput_key (key )
168- else :
169- return
170- if value is not None :
171- pressed .add (value )
172-
173- def on_release (key : pynput .keyboard .Key | pynput .keyboard .KeyCode | None ) -> None :
174- value : Keys | None
175- if isinstance (key , pynput .keyboard .KeyCode ):
176- value = None if key .char is None else LatinKeys .from_latin (key .char )
177- elif isinstance (key , pynput .keyboard .Key ):
178- value = FunctionalKeys .from_pynput_key (key )
179- else :
180- return
181- if value is not None :
182- pressed .discard (value )
183-
184- pressed : set [Keys ] = set ()
185- listener = pynput .keyboard .Listener (on_press = on_press , on_release = on_release )
186- try :
187- listener .start ()
188- yield lambda : pressed
189- finally :
190- pressed .clear ()
191- listener .stop ()
192-
193-
19467def make_get_input (
19568 console : Console ,
19669 get_pressed : Callable [[], set [Keys ]],
@@ -227,7 +100,7 @@ def console_input_from_keyboard_protocol_context(
227100def console_input_from_x11_keyboard_context (
228101 console : Console , display : str | None = None
229102) -> Iterator [InputGetter ]:
230- with xlib_key_pressed_context (display ) as get_pressed :
103+ with x11_key_pressed_context (display ) as get_pressed :
231104 yield make_get_input (console , get_pressed )
232105
233106
@@ -258,167 +131,6 @@ def console_input_from_keyboard_context(
258131 yield get_input
259132
260133
261- class EventType (IntEnum ):
262- PRESSED = 1
263- REPEAT = 2
264- RELEASED = 3
265-
266-
267- class Modifiers (Flag ):
268- shift = 0b1
269- alt = 0b10
270- ctrl = 0b100
271- super = 0b1000
272- hyper = 0b10000
273- meta = 0b100000
274- caps_lock = 0b1000000
275- num_lock = 0b10000000
276-
277-
278- ASCII_SYMBOL_TO_NAME = {
279- "@" : "At" ,
280- "\\ " : "Backslash" ,
281- "]" : "SquareClose" ,
282- "^" : "ControlCircumflex" ,
283- "_" : "Underscore" ,
284- }
285-
286-
287- class KeyboardProtocolEvent (NamedTuple ):
288- code : str
289- char : int
290- modifiers : Modifiers
291- event_type : EventType
292- raw_data : str
293-
294- def to_key (self ) -> FunctionalKeys | LatinKeys | None :
295- if self .code == "u" :
296- latin_key = LatinKeys .from_latin (self .char )
297- if latin_key is not None :
298- return latin_key
299- return CSI_TO_FUNCTIONAL_KEY .get ((self .code , self .char ))
300-
301- def to_prompt_toolkit_key (self ) -> PromptToolkitKeys | str :
302- ctrl = "Control" if Modifiers .ctrl in self .modifiers else ""
303- shift = "Shift" if Modifiers .shift in self .modifiers else ""
304- # ASCII keys
305- if self .code == "u" and 0 <= self .char < 256 :
306- ascii = chr (self .char )
307- upper = ascii .upper ()
308- name = f"{ ctrl } { shift } { ASCII_SYMBOL_TO_NAME .get (ascii , upper )} "
309- return getattr (PromptToolkitKeys , name , upper if shift else ascii )
310- # Functional keys
311- maybe_key = CSI_TO_FUNCTIONAL_KEY .get ((self .code , self .char ))
312- if maybe_key is None :
313- return PromptToolkitKeys .Ignore
314- prompt_toolkit_key = FUNCTIONAL_KEYS_TO_PT_KEYS .get (maybe_key )
315- if prompt_toolkit_key is None :
316- return PromptToolkitKeys .Ignore
317- name = f"{ ctrl } { shift } { prompt_toolkit_key .name } "
318- return getattr (PromptToolkitKeys , name , PromptToolkitKeys .Ignore )
319-
320- def to_prompt_toolkit_key_press (self ) -> KeyPress | None :
321- if self .event_type == self .event_type .RELEASED :
322- return KeyPress (PromptToolkitKeys .Ignore , self .raw_data )
323- return KeyPress (self .to_prompt_toolkit_key (), self .raw_data )
324-
325-
326- class KeyboardProtocolParser (Vt100Parser ):
327- def __init__ (self , vt100_input : Vt100Parser ) -> None :
328- super ().__init__ (vt100_input .feed_key_callback )
329- self .pressed : set [Keys ] = set ()
330- self .ansi_escape_code_parser = parse_ansi_escape_code ()
331- assert next (self .ansi_escape_code_parser ) is None
332-
333- def get_pressed (self ) -> set [Keys ]:
334- return self .pressed
335-
336- def _handle_event (self , event : KeyboardProtocolEvent ) -> None :
337- key = event .to_key ()
338- if key is None :
339- return
340- if event .event_type == event .event_type .PRESSED :
341- self .pressed .add (key )
342- elif event .event_type == event .event_type .RELEASED :
343- self .pressed .discard (key )
344-
345- def feed (self , data : str ) -> None :
346- data_out : list [str ] = []
347- for char in data :
348- item = self .ansi_escape_code_parser .send (char )
349- if item is None :
350- continue
351- if isinstance (item , str ):
352- data_out .append (item )
353- continue
354- if not isinstance (item , CSI ):
355- data_out .append (item .raw ())
356- continue
357- event = self ._process_csi (item )
358- if event is None :
359- data_out .append (item .raw ())
360- continue
361- self ._handle_event (event )
362- key_press = event .to_prompt_toolkit_key_press ()
363- if key_press is None :
364- continue
365- self .feed_key_callback (key_press )
366- super ().feed ("" .join (data_out ))
367-
368- def _process_csi (self , csi : CSI ) -> KeyboardProtocolEvent | None :
369- if csi .code not in "ABCDEFHPQSu~" :
370- return None
371- if ";" not in csi .payload :
372- raw_char = csi .payload
373- raw_modifier = "1"
374- raw_event = "1"
375- else :
376- raw_char , subpayload = csi .payload .split (";" , maxsplit = 1 )
377- if ":" not in subpayload :
378- raw_modifier = subpayload
379- raw_event = "1"
380- else :
381- raw_modifier , raw_event = subpayload .split (":" , maxsplit = 1 )
382- try :
383- char = int (raw_char )
384- except ValueError :
385- return None
386- try :
387- modifier = Modifiers (int (raw_modifier ) - 1 )
388- except ValueError :
389- return None
390- try :
391- event = EventType (int (raw_event ))
392- except ValueError :
393- return None
394- return KeyboardProtocolEvent (csi .code , char , modifier , event , csi .raw ())
395-
396-
397- @contextmanager
398- def keyboard_protocol_key_pressed_context (
399- app_session : AppSession ,
400- ) -> Iterator [Callable [[], set [Keys ]]]:
401- app_session .output .write_raw ("\033 [>11u" )
402- app_session .output .flush ()
403- if sys .platform == "win32" :
404- from prompt_toolkit .input .win32 import Win32Input
405-
406- assert isinstance (app_session .input , Win32Input )
407- parser = KeyboardProtocolParser (app_session .input ._vt100_parser )
408- app_session .input ._vt100_parser = parser
409- else :
410- from prompt_toolkit .input .vt100 import Vt100Input
411-
412- assert isinstance (app_session .input , Vt100Input )
413- parser = KeyboardProtocolParser (app_session .input .vt100_parser )
414- app_session .input .vt100_parser = parser
415- try :
416- yield parser .get_pressed
417- finally :
418- app_session .output .write_raw ("\033 [<u" )
419- app_session .output .flush ()
420-
421-
422134@contextmanager
423135def key_pressed_context (
424136 app_session : AppSession ,
@@ -430,7 +142,7 @@ def key_pressed_context(
430142 with keyboard_protocol_key_pressed_context (app_session ) as get_pressed :
431143 yield get_pressed
432144 elif sys .platform == "linux" :
433- with xlib_key_pressed_context (display ) as get_pressed :
145+ with x11_key_pressed_context (display ) as get_pressed :
434146 yield get_pressed
435147 else :
436148 with pynput_key_pressed_context () as get_pressed :
0 commit comments