Skip to content

Commit e4261e2

Browse files
committed
Refactor keyboard input modules
1 parent 11e2055 commit e4261e2

6 files changed

Lines changed: 1120 additions & 1091 deletions

File tree

gambaterm/keyboard_input.py

Lines changed: 9 additions & 297 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
11
from __future__ import annotations
22

3-
from enum import Flag, IntEnum
43
import sys
54
import 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
97
from 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

149
from .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
)
2215
from .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

3025
def 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-
19467
def make_get_input(
19568
console: Console,
19669
get_pressed: Callable[[], set[Keys]],
@@ -227,7 +100,7 @@ def console_input_from_keyboard_protocol_context(
227100
def 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
423135
def 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

Comments
 (0)