项目文件夹

文件
2026-07-13 13:39:38 +08:00

280 行
8.7 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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")