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
0""" 1File providing the getch() function to easily read character inputs. 2 3Credits: 4- Original getch implementation: Danny Yoo (https://code.activestate.com/recipes/134892) 5- Modern additions & idea: kcsaff (https://github.com/kcsaff/getkey) 6""" 7 8# pylint doesn't see the C source 9# pylint: disable=c-extension-no-member, no-name-in-module, used-before-assignment 10 11from __future__ import annotations 12 13import os 14import sys 15import signal 16 17from typing import ( 18 IO, 19 Any, 20 Union, 21 AnyStr, 22 Optional, 23 KeysView, 24 Generator, 25 ItemsView, 26 ValuesView, 27) 28 29from select import select 30from contextlib import contextmanager 31from codecs import getincrementaldecoder 32 33from .exceptions import TimeoutException 34 35__all__ = ["Keys", "getch", "getch_timeout", "keys"] 36 37 38@contextmanager 39def timeout(duration: float) -> Generator[None, None, None]: 40 """Allows context to run for a certain amount of time, quits it once it's up. 41 42 Note that this should never be run on Windows, as the required signals are not 43 present. Whenever this function is run, there should be a preliminary OS check, 44 to avoid running into issues on unsupported machines. 45 """ 46 47 def _raise_timeout(*_, **__): 48 raise TimeoutException("The action has timed out.") 49 50 try: 51 # set the timeout handler 52 signal.signal(signal.SIGALRM, _raise_timeout) 53 signal.setitimer(signal.ITIMER_REAL, duration) 54 yield 55 56 except TimeoutException: 57 pass 58 59 finally: 60 signal.alarm(0) 61 62 63def _is_ready(file: IO[AnyStr]) -> bool: 64 """Determines if IO object is reading to read. 65 66 Args: 67 file: An IO object of any type. 68 69 Returns: 70 A boolean describing whether the object has unread 71 content. 72 """ 73 74 result = select([file], [], [], 0.0) 75 return len(result[0]) > 0 76 77 78class _GetchUnix: 79 """Getch implementation for UNIX systems.""" 80 81 def __init__(self) -> None: 82 """Initializes object.""" 83 84 if sys.stdin.encoding is not None: 85 self.decode = getincrementaldecoder(sys.stdin.encoding)().decode 86 else: 87 self.decode = lambda item: item 88 89 def _read(self, num: int) -> str: 90 """Reads characters from sys.stdin. 91 92 Args: 93 num: How many characters should be read. 94 95 Returns: 96 The characters read. 97 """ 98 99 buff = "" 100 while len(buff) < num: 101 char = os.read(sys.stdin.fileno(), 1) 102 103 try: 104 buff += self.decode(char) 105 except UnicodeDecodeError: 106 buff += str(char) 107 108 return buff 109 110 def get_chars(self) -> Generator[str, None, None]: 111 """Yields characters while there are some available. 112 113 Yields: 114 Any available characters. 115 """ 116 117 descriptor = sys.stdin.fileno() 118 old_settings = termios.tcgetattr(descriptor) 119 tty.setcbreak(descriptor) 120 121 try: 122 yield self._read(1) 123 124 while _is_ready(sys.stdin): 125 yield self._read(1) 126 127 finally: 128 # reset terminal state, set echo on 129 termios.tcsetattr(descriptor, termios.TCSADRAIN, old_settings) 130 131 def __call__(self) -> str: 132 """Returns all characters that can be read.""" 133 134 buff = "".join(self.get_chars()) 135 return buff 136 137 138class _GetchWindows: 139 """Getch implementation for Windows.""" 140 141 @staticmethod 142 def _ensure_str(string: AnyStr) -> str: 143 """Ensures return value is always a `str` and not `bytes`. 144 145 Args: 146 string: Any string or bytes object. 147 148 Returns: 149 The string argument, converted to `str`. 150 """ 151 152 if isinstance(string, bytes): 153 return string.decode("utf-8") 154 155 return string 156 157 def get_chars(self) -> str: 158 """Reads characters from sys.stdin. 159 160 Returns: 161 All read characters. 162 """ 163 164 # We need to type: ignore these on non-windows machines, 165 # as the library does not exist. 166 167 # Return empty string if there is no input to get 168 if not msvcrt.kbhit(): # type: ignore 169 return "" 170 171 char = msvcrt.getch() # type: ignore 172 if char == b"\xe0": 173 char = "\x1b" 174 175 buff = self._ensure_str(char) 176 177 while msvcrt.kbhit(): # type: ignore 178 char = msvcrt.getch() # type: ignore 179 buff += self._ensure_str(char) 180 181 return buff 182 183 def __call__(self) -> str: 184 """Returns all characters that can be read. 185 186 Returns: 187 All readable characters. 188 """ 189 190 buff = self.get_chars() 191 return buff 192 193 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() 304 305 306_getch: Union[_GetchWindows, _GetchUnix] 307 308keys: Keys 309"""Instance storing platform specific key codes.""" 310 311try: 312 import msvcrt 313 314 # TODO: Add shift+arrow keys 315 _platform_keys = { 316 "ESC": "\x1b", 317 "LEFT": "\x1bK", 318 "RIGHT": "\x1bM", 319 "UP": "\x1bH", 320 "DOWN": "\x1bP", 321 "ENTER": "\r", 322 "RETURN": "\r", 323 "BACKSPACE": "\x08", 324 "F1": "\x00;", 325 "F2": "\x00<", 326 "F3": "\x00=", 327 "F4": "\x00>", 328 "F5": "\x00?", 329 "F6": "\x00@", 330 "F7": "\x00A", 331 "F8": "\x00B", 332 "F9": "\x00C", 333 "F10": "\x00D", 334 "F11": "\xe0\x85", 335 "F12": "\xe0\x86", 336 } 337 338 _getch = _GetchWindows() 339 keys = Keys(_platform_keys, "nt") 340 341except ImportError as import_error: 342 if not os.name == "posix": 343 raise NotImplementedError( 344 f"Platform {os.name} is not supported." 345 ) from import_error 346 347 import termios 348 import tty 349 350 _platform_keys = { 351 "name": "posix", 352 "UP": "\x1b[A", 353 "DOWN": "\x1b[B", 354 "RIGHT": "\x1b[C", 355 "LEFT": "\x1b[D", 356 "SHIFT_UP": "\x1b[1;2A", 357 "SHIFT_DOWN": "\x1b[1;2B", 358 "SHIFT_RIGHT": "\x1b[1;2C", 359 "SHIFT_LEFT": "\x1b[1;2D", 360 "ALT_UP": "\x1b[1;3A", 361 "ALT_DOWN": "\x1b[1;3B", 362 "ALT_RIGHT": "\x1b[1;3C", 363 "ALT_LEFT": "\x1b[1;3D", 364 "ALT_SHIFT_UP": "\x1b[1;4A", 365 "ALT_SHIFT_DOWN": "\x1b[1;4B", 366 "ALT_SHIFT_RIGHT": "\x1b[1;4C", 367 "ALT_SHIFT_LEFT": "\x1b[1;4D", 368 "CTRL_UP": "\x1b[1;5A", 369 "CTRL_DOWN": "\x1b[1;5B", 370 "CTRL_RIGHT": "\x1b[1;5C", 371 "CTRL_LEFT": "\x1b[1;5D", 372 "BACKSPACE": "\x7f", 373 "INSERT": "\x1b[2~", 374 "DELETE": "\x1b[3~", 375 "BACKTAB": "\x1b[Z", 376 "F1": "\x1b[11~", 377 "F2": "\x1b[12~", 378 "F3": "\x1b[13~", 379 "F4": "\x1b[14~", 380 "F5": "\x1b[15~", 381 "F6": "\x1b[17~", 382 "F7": "\x1b[18~", 383 "F8": "\x1b[19~", 384 "F9": "\x1b[20~", 385 "F10": "\x1b[21~", 386 "F11": "\x1b[23~", 387 "F12": "\x1b[24~", 388 } 389 390 _getch = _GetchUnix() 391 keys = Keys(_platform_keys, "posix") 392 393 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 422 423 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
View Source
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()
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)
View Source
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
Initialize Keys object.
Args
- platform_keys: A dictionary of platform-specific keys.
- platform: The platform the program is running on.
View Source
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
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.
View Source
Returns values() of self._keys.
View Source
Returns keys() of self._keys.
View Source
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
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:
View Source
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
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.