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 keys(self) -> KeysView[str]:
297    def keys(self) -> KeysView[str]:
298        """Returns keys() of self._keys."""
299
300        return self._keys.keys()

Returns keys() of self._keys.

def items(self) -> ItemsView[str, str]:
302    def items(self) -> ItemsView[str, str]:
303        """Returns items() of self._keys."""
304
305        return self._keys.items()

Returns items() 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 and chr(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.