pytermgui.input
File providing the getch() function to easily read character inputs.
Credits:
- Original getch implementation: Danny Yoo (https://code.activestate.com/recipes/134892)
- Modern additions & idea: kcsaff (https://github.com/kcsaff/getkey)
View Source
""" File providing the getch() function to easily read character inputs. Credits: - Original getch implementation: Danny Yoo (https://code.activestate.com/recipes/134892) - Modern additions & idea: kcsaff (https://github.com/kcsaff/getkey) """ # pylint doesn't see the C source # pylint: disable=c-extension-no-member, no-name-in-module from __future__ import annotations import os import sys from typing import ( IO, Any, Union, AnyStr, Optional, KeysView, Generator, ItemsView, ValuesView, ) from select import select from codecs import getincrementaldecoder def _is_ready(file: IO[AnyStr]) -> bool: """Determine if IO object is reading to read""" result = select([file], [], [], 0.0) return len(result[0]) > 0 class _GetchUnix: """Getch implementation for UNIX systems""" def __init__(self) -> None: """Initialize object""" if sys.stdin.encoding is not None: self.decode = getincrementaldecoder(sys.stdin.encoding)().decode else: self.decode = lambda item: item def _read(self, num: int) -> str: """Read `num` characters from sys.stdin""" buff = "" while len(buff) < num: char = os.read(sys.stdin.fileno(), 1) try: buff += self.decode(char) except UnicodeDecodeError: buff += str(char) return buff def get_chars(self) -> Generator[str, None, None]: """Yield characters while there are some available""" descriptor = sys.stdin.fileno() old_settings = termios.tcgetattr(descriptor) tty.setcbreak(descriptor) try: yield self._read(1) while _is_ready(sys.stdin): yield self._read(1) finally: # reset terminal state, set echo on termios.tcsetattr(descriptor, termios.TCSADRAIN, old_settings) def __call__(self) -> str: """Return all characters that can be read""" buff = "".join(self.get_chars()) return buff class _GetchWindows: """Getch implementation for Windows""" @staticmethod def _ensure_str(string: AnyStr) -> str: """Ensure return value is always a `str` and not `bytes`""" if isinstance(string, bytes): return string.decode("utf-8") return string def get_chars(self) -> str: """Read `num` characters from sys.stdin""" char = msvcrt.getch() if char == b"\xe0": char = "\x1b" buff = self._ensure_str(char) while msvcrt.kbhit(): char = msvcrt.getch() buff += self._ensure_str(char) return buff def __call__(self) -> str: """Return all characters that can be read""" buff = self.get_chars() return buff class _Keys: """Class for easy access to key-codes The keys for CTRL_{ascii_letter}-s can be generated with the following code: ```python3 for i, letter in enumerate(ascii_lowercase): key = f"CTRL_{letter.upper()}" code = chr(i+1).encode('unicode_escape').decode('utf-8') print(key, code) ``` """ def __init__(self, platform_keys: dict[str, str], platform: str) -> None: """Set up key values""" self._keys = { "SPACE": " ", "ESC": "\x1b", # The ALT character in key combinations is the same as ESC "ALT": "\x1b", "TAB": "\t", "ENTER": "\n", "RETURN": "\n", "CTRL_A": "\x01", "CTRL_B": "\x02", "CTRL_C": "\x03", "CTRL_D": "\x04", "CTRL_E": "\x05", "CTRL_F": "\x06", "CTRL_G": "\x07", "CTRL_H": "\x08", "CTRL_I": "\t", "CTRL_J": "\n", "CTRL_K": "\x0b", "CTRL_L": "\x0c", "CTRL_M": "\r", "CTRL_N": "\x0e", "CTRL_O": "\x0f", "CTRL_P": "\x10", "CTRL_Q": "\x11", "CTRL_R": "\x12", "CTRL_S": "\x13", "CTRL_T": "\x14", "CTRL_U": "\x15", "CTRL_V": "\x16", "CTRL_W": "\x17", "CTRL_X": "\x18", "CTRL_Y": "\x19", "CTRL_Z": "\x1a", } self.platform = platform if platform_keys is not None: for key, code in platform_keys.items(): if key == "name": self.name = code continue self._keys[key] = code def __getattr__(self, attr: str) -> str: """Get attr from self._keys""" if attr == "ANY_KEY": return attr return self._keys[attr] def get_name(self, key: str, default: Optional[str] = None) -> Optional[str]: """Get canonical name of a key code""" for name, value in self._keys.items(): if key == value: return name return default def values(self) -> ValuesView[str]: """Return values() of self._keys""" return self._keys.values() def keys(self) -> KeysView[str]: """Return keys() of self._keys""" return self._keys.keys() def items(self) -> ItemsView[str, str]: """Return items() of self._keys""" return self._keys.items() _getch: Union[_GetchWindows, _GetchUnix] keys: _Keys """Instance storing platform specific key codes""" try: import msvcrt # TODO: Add shift+arrow keys _platform_keys = { "ESC": "\x1b", "LEFT": "\x1bK", "RIGHT": "\x1bM", "UP": "\x1bH", "DOWN": "\x1bP", "ENTER": "\r", "RETURN": "\r", "BACKSPACE": "\x08", } _getch = _GetchWindows() keys = _Keys(_platform_keys, "nt") except ImportError as import_error: if not os.name == "posix": raise NotImplementedError( f"Platform {os.name} is not supported." ) from import_error import termios import tty _platform_keys = { "name": "posix", "UP": "\x1b[A", "DOWN": "\x1b[B", "RIGHT": "\x1b[C", "LEFT": "\x1b[D", "SHIFT_UP": "\x1b[1;2A", "SHIFT_DOWN": "\x1b[1;2B", "SHIFT_RIGHT": "\x1b[1;2C", "SHIFT_LEFT": "\x1b[1;2D", "BACKSPACE": "\x7f", "INSERT": "\x1b[2~", "DELETE": "\x1b[3~", "BACKTAB": "\x1b[Z", } _getch = _GetchUnix() keys = _Keys(_platform_keys, "posix") def getch(printable: bool = False, interrupts: bool = True) -> Any: """Wrapper to call the platform-appropriate character getter""" try: key = _getch() except KeyboardInterrupt as error: if interrupts: raise KeyboardInterrupt("Unhandled interrupt") from error return chr(3) if printable: key = key.encode("unicode_escape").decode("utf-8") return key
Instance storing platform specific key codes
View Source
def getch(printable: bool = False, interrupts: bool = True) -> Any: """Wrapper to call the platform-appropriate character getter""" try: key = _getch() except KeyboardInterrupt as error: if interrupts: raise KeyboardInterrupt("Unhandled interrupt") from error return chr(3) if printable: key = key.encode("unicode_escape").decode("utf-8") return key
Wrapper to call the platform-appropriate character getter