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 (two‑byte 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 multi‑byte 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 multi‑byte c3 = _posix_readchar() if c3 not in "\x31\x32\x33\x35\x36": return c1 + c2 + c3 # Fourth byte for multi‑byte 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 UTF‑16 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 UTF‑16 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 UTF‑16 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")