Files
2026-07-13 13:39:38 +08:00

280 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import sys
from collections.abc import Callable
from typing import ClassVar
__all__ = ["readchar", "readkey", "key"]
class _BaseKey:
# Common control characters
LF: ClassVar[str] = "\x0a"
CR: ClassVar[str] = "\x0d"
SPACE: ClassVar[str] = "\x20"
ESC: ClassVar[str] = "\x1b"
TAB: ClassVar[str] = "\x09"
# CTRL keys mapping from letter to control code
CTRL_A: ClassVar[str] = "\x01"
CTRL_B: ClassVar[str] = "\x02"
CTRL_C: ClassVar[str] = "\x03"
CTRL_D: ClassVar[str] = "\x04"
CTRL_E: ClassVar[str] = "\x05"
CTRL_F: ClassVar[str] = "\x06"
CTRL_G: ClassVar[str] = "\x07"
CTRL_H: ClassVar[str] = "\x08"
CTRL_I: ClassVar[str] = TAB # Alias for TAB
CTRL_J: ClassVar[str] = LF # Alias for LF
CTRL_K: ClassVar[str] = "\x0b"
CTRL_L: ClassVar[str] = "\x0c"
CTRL_M: ClassVar[str] = CR # Alias for CR
CTRL_N: ClassVar[str] = "\x0e"
CTRL_O: ClassVar[str] = "\x0f"
CTRL_P: ClassVar[str] = "\x10"
CTRL_Q: ClassVar[str] = "\x11"
CTRL_R: ClassVar[str] = "\x12"
CTRL_S: ClassVar[str] = "\x13"
CTRL_T: ClassVar[str] = "\x14"
CTRL_U: ClassVar[str] = "\x15"
CTRL_V: ClassVar[str] = "\x16"
CTRL_W: ClassVar[str] = "\x17"
CTRL_X: ClassVar[str] = "\x18"
CTRL_Y: ClassVar[str] = "\x19"
CTRL_Z: ClassVar[str] = "\x1a"
class _PosixKey(_BaseKey):
"""Namespace of key codes specific to POSIX platforms (Linux, macOS, BSD).
These values mirror those defined in the upstream ``_posix_key.py``
module. All attributes from :class:`_BaseKey` are inherited.
"""
# Common additional control character
BACKSPACE: ClassVar[str] = "\x7f"
# Cursor movement (escape sequences)
UP: ClassVar[str] = "\x1b\x5b\x41"
DOWN: ClassVar[str] = "\x1b\x5b\x42"
LEFT: ClassVar[str] = "\x1b\x5b\x44"
RIGHT: ClassVar[str] = "\x1b\x5b\x43"
# Navigation keys
INSERT: ClassVar[str] = "\x1b\x5b\x32\x7e"
SUPR: ClassVar[str] = "\x1b\x5b\x33\x7e"
HOME: ClassVar[str] = "\x1b\x5b\x48"
END: ClassVar[str] = "\x1b\x5b\x46"
PAGE_UP: ClassVar[str] = "\x1b\x5b\x35\x7e"
PAGE_DOWN: ClassVar[str] = "\x1b\x5b\x36\x7e"
# Function keys
F1: ClassVar[str] = "\x1b\x4f\x50"
F2: ClassVar[str] = "\x1b\x4f\x51"
F3: ClassVar[str] = "\x1b\x4f\x52"
F4: ClassVar[str] = "\x1b\x4f\x53"
F5: ClassVar[str] = "\x1b\x5b\x31\x35\x7e"
F6: ClassVar[str] = "\x1b\x5b\x31\x37\x7e"
F7: ClassVar[str] = "\x1b\x5b\x31\x38\x7e"
F8: ClassVar[str] = "\x1b\x5b\x31\x39\x7e"
F9: ClassVar[str] = "\x1b\x5b\x32\x30\x7e"
F10: ClassVar[str] = "\x1b\x5b\x32\x31\x7e"
F11: ClassVar[str] = "\x1b\x5b\x32\x33\x7e"
F12: ClassVar[str] = "\x1b\x5b\x32\x34\x7e"
# Shift/other combinations
SHIFT_TAB: ClassVar[str] = "\x1b\x5b\x5a"
CTRL_ALT_SUPR: ClassVar[str] = "\x1b\x5b\x33\x5e"
# ALT combinations
ALT_A: ClassVar[str] = "\x1b\x61"
# CTRL+ALT combinations
CTRL_ALT_A: ClassVar[str] = "\x1b\x01"
# Aliases to improve readability
ENTER: ClassVar[str] = _BaseKey.LF
DELETE: ClassVar[str] = SUPR
class _WinKey(_BaseKey):
"""Namespace of key codes specific to Windows platforms.
These values mirror those defined in the upstream ``_win_key.py``
module. All attributes from :class:`_BaseKey` are inherited.
"""
# Additional common control character on Windows
BACKSPACE: ClassVar[str] = "\x08"
# Cursor movement (twobyte scan codes)
UP: ClassVar[str] = "\x00\x48"
DOWN: ClassVar[str] = "\x00\x50"
LEFT: ClassVar[str] = "\x00\x4b"
RIGHT: ClassVar[str] = "\x00\x4d"
# Navigation keys
INSERT: ClassVar[str] = "\x00\x52"
SUPR: ClassVar[str] = "\x00\x53"
HOME: ClassVar[str] = "\x00\x47"
END: ClassVar[str] = "\x00\x4f"
PAGE_UP: ClassVar[str] = "\x00\x49"
PAGE_DOWN: ClassVar[str] = "\x00\x51"
# Function keys
F1: ClassVar[str] = "\x00\x3b"
F2: ClassVar[str] = "\x00\x3c"
F3: ClassVar[str] = "\x00\x3d"
F4: ClassVar[str] = "\x00\x3e"
F5: ClassVar[str] = "\x00\x3f"
F6: ClassVar[str] = "\x00\x40"
F7: ClassVar[str] = "\x00\x41"
F8: ClassVar[str] = "\x00\x42"
F9: ClassVar[str] = "\x00\x43"
F10: ClassVar[str] = "\x00\x44"
# F11 and F12 values are taken from FreePascal documentation
F11: ClassVar[str] = "\x00\x85"
F12: ClassVar[str] = "\x00\x86"
# Other special sequences
ESC_2: ClassVar[str] = "\x00\x01"
ENTER_2: ClassVar[str] = "\x00\x1c"
# Aliases to improve readability
ENTER: ClassVar[str] = _BaseKey.CR
DELETE: ClassVar[str] = SUPR
# Use the CTRL+C definition from the base key set. The choice of
# base key here mirrors the upstream behaviour: on both Windows and
# POSIX, pressing CTRL+C should raise KeyboardInterrupt instead of
# being returned by readkey().
INTERRUPT_KEYS: list[str] = [_BaseKey.CTRL_C]
def _posix_readchar() -> str:
"""Read a single character from standard input on POSIX systems.
This function blocks until a character is available. It uses
``termios`` to disable canonical input processing and echo so
characters are returned immediately and without being echoed to
the terminal. The implementation closely follows the upstream
``_posix_read.readchar`` function.
"""
import termios
import tty
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
term = termios.tcgetattr(fd)
try:
term[3] &= ~(termios.ICANON | termios.ECHO)
term[3] |= termios.ISIG
termios.tcsetattr(fd, termios.TCSAFLUSH, term)
ch = sys.stdin.read(1)
finally:
try:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
except Exception:
try:
tty.setcbreak(fd)
cur = termios.tcgetattr(fd)
cur[3] |= termios.ICANON | termios.ECHO | termios.ISIG
termios.tcsetattr(fd, termios.TCSADRAIN, cur)
except Exception:
pass
return ch
def _posix_readkey() -> str:
"""Read the next keypress on POSIX systems.
If a multibyte escape sequence is encountered (for example, an arrow
key or function key), the entire sequence is read and returned.
``KeyboardInterrupt`` is raised when a key listed in
:data:`config.INTERRUPT_KEYS` is pressed.
"""
c1 = _posix_readchar()
if c1 in INTERRUPT_KEYS:
raise KeyboardInterrupt
# Not an escape sequence; return immediately
if c1 != "\x1b":
return c1
# Escape sequence read second byte
c2 = _posix_readchar()
if c2 not in "\x4f\x5b":
return c1 + c2
# Third byte distinguishes between simple arrows and multibyte
c3 = _posix_readchar()
if c3 not in "\x31\x32\x33\x35\x36":
return c1 + c2 + c3
# Fourth byte for multibyte function/navigation keys
c4 = _posix_readchar()
if c4 not in "\x30\x31\x33\x34\x35\x37\x38\x39":
return c1 + c2 + c3 + c4
# Fifth byte for the remainder of the sequence
c5 = _posix_readchar()
return c1 + c2 + c3 + c4 + c5
def _win_readchar() -> str:
"""Read a single UTF16 code unit from standard input on Windows systems.
This function blocks until a character is available. It wraps
``msvcrt.getwch()`` from the standard library, which returns a
single wide character (as a Python string). The implementation is
equivalent to the upstream ``_win_read.readchar`` function.
"""
import msvcrt
return msvcrt.getwch() # type: ignore
def _win_readkey() -> str:
"""Read the next keypress on Windows systems.
This function interprets Windows scan codes and surrogate pairs to
return a key sequence that is compatible with the constants defined
in :class:`_WinKey`. ``KeyboardInterrupt`` is raised when a key
listed in :data:`config.INTERRUPT_KEYS` is pressed.
"""
ch = _win_readchar()
if ch in INTERRUPT_KEYS:
raise KeyboardInterrupt
if ch in "\x00\xe0":
ch = "\x00" + _win_readchar()
# Handle UTF16 surrogate pairs (high surrogate from \uD800 to \uDFFF)
# See https://docs.python.org/3/c-api/unicode.html#c.Py_UNICODE_IS_SURROGATE
if "\ud800" <= ch <= "\udfff":
ch += _win_readchar()
# Combine surrogate pair into a single UTF16 character
ch = ch.encode("utf-16", errors="surrogatepass").decode("utf-16")
return ch
key: type[_PosixKey | _WinKey]
readchar: Callable[[], str]
readkey: Callable[[], str]
if sys.platform.startswith(("linux", "darwin", "freebsd", "openbsd")):
key = _PosixKey
readchar = _posix_readchar
readkey = _posix_readkey
elif sys.platform in ("win32", "cygwin"):
key = _WinKey
readchar = _win_readchar
readkey = _win_readkey
else:
raise NotImplementedError(f"The platform {sys.platform} is not supported yet")