pytermgui.input

File providing the getch() function to easily read character inputs.

Credits:

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

Returns keys() of self._keys.

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

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