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)
1""" 2File providing the getch() function to easily read character inputs. 3 4Credits: 5- Original getch implementation: Danny Yoo (https://code.activestate.com/recipes/134892) 6- Modern additions & idea: kcsaff (https://github.com/kcsaff/getkey) 7""" 8 9# pylint doesn't see the C source 10# pylint: disable=c-extension-no-member, no-name-in-module, used-before-assignment 11 12from __future__ import annotations 13 14import os 15import signal 16import sys 17from codecs import getincrementaldecoder 18from contextlib import contextmanager 19from select import select 20from typing import ( 21 IO, 22 Any, 23 AnyStr, 24 Generator, 25 ItemsView, 26 KeysView, 27 Optional, 28 Union, 29 ValuesView, 30) 31 32from .exceptions import TimeoutException 33 34__all__ = ["Keys", "getch", "getch_timeout", "keys"] 35 36 37@contextmanager 38def timeout(duration: float) -> Generator[None, None, None]: 39 """Allows context to run for a certain amount of time, quits it once it's up. 40 41 Note that this should never be run on Windows, as the required signals are not 42 present. Whenever this function is run, there should be a preliminary OS check, 43 to avoid running into issues on unsupported machines. 44 """ 45 46 def _raise_timeout(*_, **__): 47 raise TimeoutException("The action has timed out.") 48 49 try: 50 # set the timeout handler 51 signal.signal(signal.SIGALRM, _raise_timeout) 52 signal.setitimer(signal.ITIMER_REAL, duration) 53 yield 54 55 except TimeoutException: 56 pass 57 58 finally: 59 signal.alarm(0) 60 61 62def _is_ready(file: IO[AnyStr]) -> bool: 63 """Determines if IO object is reading to read. 64 65 Args: 66 file: An IO object of any type. 67 68 Returns: 69 A boolean describing whether the object has unread 70 content. 71 """ 72 73 result = select([file], [], [], 0.0) 74 return len(result[0]) > 0 75 76 77class _GetchUnix: 78 """Getch implementation for UNIX systems.""" 79 80 def __init__(self) -> None: 81 """Initializes object.""" 82 83 if sys.stdin.encoding is not None: 84 self.decode = getincrementaldecoder(sys.stdin.encoding)().decode 85 else: 86 self.decode = lambda item: item 87 88 def _read(self, num: int) -> str: 89 """Reads characters from sys.stdin. 90 91 Args: 92 num: How many characters should be read. 93 94 Returns: 95 The characters read. 96 """ 97 98 buff = "" 99 while len(buff) < num: 100 char = os.read(sys.stdin.fileno(), 1) 101 102 try: 103 buff += self.decode(char) 104 except UnicodeDecodeError: 105 buff += str(char) 106 107 return buff 108 109 def get_chars(self) -> Generator[str, None, None]: 110 """Yields characters while there are some available. 111 112 Yields: 113 Any available characters. 114 """ 115 116 descriptor = sys.stdin.fileno() 117 old_settings = termios.tcgetattr(descriptor) 118 tty.setcbreak(descriptor) 119 120 try: 121 yield self._read(1) 122 123 while _is_ready(sys.stdin): 124 yield self._read(1) 125 126 finally: 127 # reset terminal state, set echo on 128 termios.tcsetattr(descriptor, termios.TCSADRAIN, old_settings) 129 130 def __call__(self) -> str: 131 """Returns all characters that can be read.""" 132 133 buff = "".join(self.get_chars()) 134 return buff 135 136 137class _GetchWindows: 138 """Getch implementation for Windows.""" 139 140 @staticmethod 141 def _ensure_str(string: AnyStr) -> str: 142 """Ensures return value is always a `str` and not `bytes`. 143 144 Args: 145 string: Any string or bytes object. 146 147 Returns: 148 The string argument, converted to `str`. 149 """ 150 151 if isinstance(string, bytes): 152 return string.decode("utf-8") 153 154 return string 155 156 def get_chars(self) -> str: 157 """Reads characters from sys.stdin. 158 159 Returns: 160 All read characters. 161 """ 162 163 # We need to type: ignore these on non-windows machines, 164 # as the library does not exist. 165 166 # Return empty string if there is no input to get 167 if not msvcrt.kbhit(): # type: ignore 168 return "" 169 170 char = msvcrt.getch() # type: ignore 171 if char == b"\xe0": 172 char = "\x1b" 173 174 buff = self._ensure_str(char) 175 176 while msvcrt.kbhit(): # type: ignore 177 char = msvcrt.getch() # type: ignore 178 buff += self._ensure_str(char) 179 180 return buff 181 182 def __call__(self) -> str: 183 """Returns all characters that can be read. 184 185 Returns: 186 All readable characters. 187 """ 188 189 buff = self.get_chars() 190 return buff 191 192 193class Keys: 194 """Class for easy access to key-codes. 195 196 The keys for CTRL_{ascii_letter}-s can be generated with 197 the following code: 198 199 ```python3 200 for i, letter in enumerate(ascii_lowercase): 201 key = f"CTRL_{letter.upper()}" 202 code = chr(i+1).encode('unicode_escape').decode('utf-8') 203 204 print(key, code) 205 ``` 206 """ 207 208 def __init__(self, platform_keys: dict[str, str], platform: str) -> None: 209 """Initialize Keys object. 210 211 Args: 212 platform_keys: A dictionary of platform-specific keys. 213 platform: The platform the program is running on. 214 """ 215 216 self._keys = { 217 "SPACE": " ", 218 "ESC": "\x1b", 219 # The ALT character in key combinations is the same as ESC 220 "ALT": "\x1b", 221 "TAB": "\t", 222 "ENTER": "\n", 223 "RETURN": "\n", 224 "CTRL_SPACE": "\x00", 225 "CTRL_A": "\x01", 226 "CTRL_B": "\x02", 227 "CTRL_C": "\x03", 228 "CTRL_D": "\x04", 229 "CTRL_E": "\x05", 230 "CTRL_F": "\x06", 231 "CTRL_G": "\x07", 232 "CTRL_H": "\x08", 233 "CTRL_I": "\t", 234 "CTRL_J": "\n", 235 "CTRL_K": "\x0b", 236 "CTRL_L": "\x0c", 237 "CTRL_M": "\r", 238 "CTRL_N": "\x0e", 239 "CTRL_O": "\x0f", 240 "CTRL_P": "\x10", 241 "CTRL_Q": "\x11", 242 "CTRL_R": "\x12", 243 "CTRL_S": "\x13", 244 "CTRL_T": "\x14", 245 "CTRL_U": "\x15", 246 "CTRL_V": "\x16", 247 "CTRL_W": "\x17", 248 "CTRL_X": "\x18", 249 "CTRL_Y": "\x19", 250 "CTRL_Z": "\x1a", 251 } 252 253 self.platform = platform 254 255 if platform_keys is not None: 256 for key, code in platform_keys.items(): 257 if key == "name": 258 self.name = code 259 continue 260 261 self._keys[key] = code 262 263 def __getattr__(self, attr: str) -> str: 264 """Gets attr from self._keys.""" 265 266 if attr == "ANY_KEY": 267 return attr 268 269 return self._keys.get(attr, "") 270 271 def get_name(self, key: str, default: Optional[str] = None) -> Optional[str]: 272 """Gets canonical name of a key code. 273 274 Args: 275 key: The key to get the name of. 276 default: The return value to substitute if no canonical name could be 277 found. Defaults to None. 278 279 Returns: 280 The canonical name if one can be found, default otherwise. 281 """ 282 283 for name, value in self._keys.items(): 284 if key == value: 285 return name 286 287 return default 288 289 def values(self) -> ValuesView[str]: 290 """Returns values() of self._keys.""" 291 292 return self._keys.values() 293 294 def keys(self) -> KeysView[str]: 295 """Returns keys() of self._keys.""" 296 297 return self._keys.keys() 298 299 def items(self) -> ItemsView[str, str]: 300 """Returns items() of self._keys.""" 301 302 return self._keys.items() 303 304 305_getch: Union[_GetchWindows, _GetchUnix] 306 307keys: Keys 308"""Instance storing platform specific key codes.""" 309 310try: 311 import msvcrt 312 313 # TODO: Add shift+arrow keys 314 _platform_keys = { 315 "ESC": "\x1b", 316 "LEFT": "\x1bK", 317 "RIGHT": "\x1bM", 318 "UP": "\x1bH", 319 "DOWN": "\x1bP", 320 "ENTER": "\r", 321 "RETURN": "\r", 322 "BACKSPACE": "\x08", 323 "F1": "\x00;", 324 "F2": "\x00<", 325 "F3": "\x00=", 326 "F4": "\x00>", 327 "F5": "\x00?", 328 "F6": "\x00@", 329 "F7": "\x00A", 330 "F8": "\x00B", 331 "F9": "\x00C", 332 "F10": "\x00D", 333 "F11": "\xe0\x85", 334 "F12": "\xe0\x86", 335 } 336 337 _getch = _GetchWindows() 338 keys = Keys(_platform_keys, "nt") 339 340except ImportError as import_error: 341 if not os.name == "posix": 342 raise NotImplementedError( 343 f"Platform {os.name} is not supported." 344 ) from import_error 345 346 import termios 347 import tty 348 349 _platform_keys = { 350 "name": "posix", 351 "UP": "\x1b[A", 352 "DOWN": "\x1b[B", 353 "RIGHT": "\x1b[C", 354 "LEFT": "\x1b[D", 355 "SHIFT_UP": "\x1b[1;2A", 356 "SHIFT_DOWN": "\x1b[1;2B", 357 "SHIFT_RIGHT": "\x1b[1;2C", 358 "SHIFT_LEFT": "\x1b[1;2D", 359 "ALT_UP": "\x1b[1;3A", 360 "ALT_DOWN": "\x1b[1;3B", 361 "ALT_RIGHT": "\x1b[1;3C", 362 "ALT_LEFT": "\x1b[1;3D", 363 "ALT_SHIFT_UP": "\x1b[1;4A", 364 "ALT_SHIFT_DOWN": "\x1b[1;4B", 365 "ALT_SHIFT_RIGHT": "\x1b[1;4C", 366 "ALT_SHIFT_LEFT": "\x1b[1;4D", 367 "CTRL_UP": "\x1b[1;5A", 368 "CTRL_DOWN": "\x1b[1;5B", 369 "CTRL_RIGHT": "\x1b[1;5C", 370 "CTRL_LEFT": "\x1b[1;5D", 371 "BACKSPACE": "\x7f", 372 "INSERT": "\x1b[2~", 373 "DELETE": "\x1b[3~", 374 "BACKTAB": "\x1b[Z", 375 "F1": "\x1b[11~", 376 "F2": "\x1b[12~", 377 "F3": "\x1b[13~", 378 "F4": "\x1b[14~", 379 "F5": "\x1b[15~", 380 "F6": "\x1b[17~", 381 "F7": "\x1b[18~", 382 "F8": "\x1b[19~", 383 "F9": "\x1b[20~", 384 "F10": "\x1b[21~", 385 "F11": "\x1b[23~", 386 "F12": "\x1b[24~", 387 } 388 389 _getch = _GetchUnix() 390 keys = Keys(_platform_keys, "posix") 391 392 393def getch(printable: bool = False, interrupts: bool = True) -> Any: 394 """Wrapper to call the platform-appropriate character getter. 395 396 Args: 397 printable: When set, printable versions of the input are returned. 398 interrupts: If not set, `KeyboardInterrupt` is silenced and `chr(3)` 399 (`CTRL_C`) is returned. 400 """ 401 402 try: 403 key = _getch() 404 405 # msvcrt.getch returns CTRL_C as a character, unlike UNIX systems 406 # where an interrupt is raised. Thus, we need to manually raise 407 # the interrupt. 408 if key == chr(3): 409 raise KeyboardInterrupt 410 411 except KeyboardInterrupt as error: 412 if interrupts: 413 raise KeyboardInterrupt("Unhandled interrupt") from error 414 415 key = chr(3) 416 417 if printable: 418 key = key.encode("unicode_escape").decode("utf-8") 419 420 return key 421 422 423def getch_timeout( 424 duration: float, default: str = "", printable: bool = False, interrupts: bool = True 425) -> Any: 426 """Calls `getch`, returns `default` if timeout passes before getting input. 427 428 No timeout is applied on Windows systems, as there is no support for `SIGALRM`. 429 430 Args: 431 timeout: How long the call should wait for input. 432 default: The value to return if timeout occured. 433 """ 434 435 if isinstance(_getch, _GetchWindows): 436 return getch() 437 438 with timeout(duration): 439 return getch(printable=printable, interrupts=interrupts) 440 441 return default
class
Keys:
194class Keys: 195 """Class for easy access to key-codes. 196 197 The keys for CTRL_{ascii_letter}-s can be generated with 198 the following code: 199 200 ```python3 201 for i, letter in enumerate(ascii_lowercase): 202 key = f"CTRL_{letter.upper()}" 203 code = chr(i+1).encode('unicode_escape').decode('utf-8') 204 205 print(key, code) 206 ``` 207 """ 208 209 def __init__(self, platform_keys: dict[str, str], platform: str) -> None: 210 """Initialize Keys object. 211 212 Args: 213 platform_keys: A dictionary of platform-specific keys. 214 platform: The platform the program is running on. 215 """ 216 217 self._keys = { 218 "SPACE": " ", 219 "ESC": "\x1b", 220 # The ALT character in key combinations is the same as ESC 221 "ALT": "\x1b", 222 "TAB": "\t", 223 "ENTER": "\n", 224 "RETURN": "\n", 225 "CTRL_SPACE": "\x00", 226 "CTRL_A": "\x01", 227 "CTRL_B": "\x02", 228 "CTRL_C": "\x03", 229 "CTRL_D": "\x04", 230 "CTRL_E": "\x05", 231 "CTRL_F": "\x06", 232 "CTRL_G": "\x07", 233 "CTRL_H": "\x08", 234 "CTRL_I": "\t", 235 "CTRL_J": "\n", 236 "CTRL_K": "\x0b", 237 "CTRL_L": "\x0c", 238 "CTRL_M": "\r", 239 "CTRL_N": "\x0e", 240 "CTRL_O": "\x0f", 241 "CTRL_P": "\x10", 242 "CTRL_Q": "\x11", 243 "CTRL_R": "\x12", 244 "CTRL_S": "\x13", 245 "CTRL_T": "\x14", 246 "CTRL_U": "\x15", 247 "CTRL_V": "\x16", 248 "CTRL_W": "\x17", 249 "CTRL_X": "\x18", 250 "CTRL_Y": "\x19", 251 "CTRL_Z": "\x1a", 252 } 253 254 self.platform = platform 255 256 if platform_keys is not None: 257 for key, code in platform_keys.items(): 258 if key == "name": 259 self.name = code 260 continue 261 262 self._keys[key] = code 263 264 def __getattr__(self, attr: str) -> str: 265 """Gets attr from self._keys.""" 266 267 if attr == "ANY_KEY": 268 return attr 269 270 return self._keys.get(attr, "") 271 272 def get_name(self, key: str, default: Optional[str] = None) -> Optional[str]: 273 """Gets canonical name of a key code. 274 275 Args: 276 key: The key to get the name of. 277 default: The return value to substitute if no canonical name could be 278 found. Defaults to None. 279 280 Returns: 281 The canonical name if one can be found, default otherwise. 282 """ 283 284 for name, value in self._keys.items(): 285 if key == value: 286 return name 287 288 return default 289 290 def values(self) -> ValuesView[str]: 291 """Returns values() of self._keys.""" 292 293 return self._keys.values() 294 295 def keys(self) -> KeysView[str]: 296 """Returns keys() of self._keys.""" 297 298 return self._keys.keys() 299 300 def items(self) -> ItemsView[str, str]: 301 """Returns items() of self._keys.""" 302 303 return self._keys.items()
Class for easy access to key-codes.
The keys for CTRL_{ascii_letter}-s can be generated with the following code:
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)
Keys(platform_keys: dict[str, str], platform: str)
209 def __init__(self, platform_keys: dict[str, str], platform: str) -> None: 210 """Initialize Keys object. 211 212 Args: 213 platform_keys: A dictionary of platform-specific keys. 214 platform: The platform the program is running on. 215 """ 216 217 self._keys = { 218 "SPACE": " ", 219 "ESC": "\x1b", 220 # The ALT character in key combinations is the same as ESC 221 "ALT": "\x1b", 222 "TAB": "\t", 223 "ENTER": "\n", 224 "RETURN": "\n", 225 "CTRL_SPACE": "\x00", 226 "CTRL_A": "\x01", 227 "CTRL_B": "\x02", 228 "CTRL_C": "\x03", 229 "CTRL_D": "\x04", 230 "CTRL_E": "\x05", 231 "CTRL_F": "\x06", 232 "CTRL_G": "\x07", 233 "CTRL_H": "\x08", 234 "CTRL_I": "\t", 235 "CTRL_J": "\n", 236 "CTRL_K": "\x0b", 237 "CTRL_L": "\x0c", 238 "CTRL_M": "\r", 239 "CTRL_N": "\x0e", 240 "CTRL_O": "\x0f", 241 "CTRL_P": "\x10", 242 "CTRL_Q": "\x11", 243 "CTRL_R": "\x12", 244 "CTRL_S": "\x13", 245 "CTRL_T": "\x14", 246 "CTRL_U": "\x15", 247 "CTRL_V": "\x16", 248 "CTRL_W": "\x17", 249 "CTRL_X": "\x18", 250 "CTRL_Y": "\x19", 251 "CTRL_Z": "\x1a", 252 } 253 254 self.platform = platform 255 256 if platform_keys is not None: 257 for key, code in platform_keys.items(): 258 if key == "name": 259 self.name = code 260 continue 261 262 self._keys[key] = code
Initialize Keys object.
Args
- platform_keys: A dictionary of platform-specific keys.
- platform: The platform the program is running on.
def
get_name(self, key: str, default: Optional[str] = None) -> Optional[str]:
272 def get_name(self, key: str, default: Optional[str] = None) -> Optional[str]: 273 """Gets canonical name of a key code. 274 275 Args: 276 key: The key to get the name of. 277 default: The return value to substitute if no canonical name could be 278 found. Defaults to None. 279 280 Returns: 281 The canonical name if one can be found, default otherwise. 282 """ 283 284 for name, value in self._keys.items(): 285 if key == value: 286 return name 287 288 return default
Gets canonical name of a key code.
Args
- key: The key to get the name of.
- default: The return value to substitute if no canonical name could be found. Defaults to None.
Returns
The canonical name if one can be found, default otherwise.
def
values(self) -> ValuesView[str]:
290 def values(self) -> ValuesView[str]: 291 """Returns values() of self._keys.""" 292 293 return self._keys.values()
Returns values() of self._keys.
def
getch(printable: bool = False, interrupts: bool = True) -> Any:
394def getch(printable: bool = False, interrupts: bool = True) -> Any: 395 """Wrapper to call the platform-appropriate character getter. 396 397 Args: 398 printable: When set, printable versions of the input are returned. 399 interrupts: If not set, `KeyboardInterrupt` is silenced and `chr(3)` 400 (`CTRL_C`) is returned. 401 """ 402 403 try: 404 key = _getch() 405 406 # msvcrt.getch returns CTRL_C as a character, unlike UNIX systems 407 # where an interrupt is raised. Thus, we need to manually raise 408 # the interrupt. 409 if key == chr(3): 410 raise KeyboardInterrupt 411 412 except KeyboardInterrupt as error: 413 if interrupts: 414 raise KeyboardInterrupt("Unhandled interrupt") from error 415 416 key = chr(3) 417 418 if printable: 419 key = key.encode("unicode_escape").decode("utf-8") 420 421 return key
Wrapper to call the platform-appropriate character getter.
Args
- printable: When set, printable versions of the input are returned.
- interrupts: If not set,
KeyboardInterrupt
is silenced andchr(3)
(CTRL_C
) is returned.
def
getch_timeout( duration: float, default: str = '', printable: bool = False, interrupts: bool = True) -> Any:
424def getch_timeout( 425 duration: float, default: str = "", printable: bool = False, interrupts: bool = True 426) -> Any: 427 """Calls `getch`, returns `default` if timeout passes before getting input. 428 429 No timeout is applied on Windows systems, as there is no support for `SIGALRM`. 430 431 Args: 432 timeout: How long the call should wait for input. 433 default: The value to return if timeout occured. 434 """ 435 436 if isinstance(_getch, _GetchWindows): 437 return getch() 438 439 with timeout(duration): 440 return getch(printable=printable, interrupts=interrupts) 441 442 return default
Calls getch
, returns default
if timeout passes before getting input.
No timeout is applied on Windows systems, as there is no support for SIGALRM
.
Args
- timeout: How long the call should wait for input.
- default: The value to return if timeout occured.
Instance storing platform specific key codes.