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