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
#   class Keys:
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)
#   Keys(platform_keys: dict[str, str], platform: str)
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.
#   def get_name(self, key: str, default: Optional[str] = None) -> Optional[str]:
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.

#   def values(self) -> ValuesView[str]:
View Source
291    def values(self) -> ValuesView[str]:
292        """Returns values() of self._keys."""
293
294        return self._keys.values()

Returns values() of self._keys.

#   def keys(self) -> KeysView[str]:
View Source
296    def keys(self) -> KeysView[str]:
297        """Returns keys() of self._keys."""
298
299        return self._keys.keys()

Returns keys() of self._keys.

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

Returns items() of self._keys.

#   def getch(printable: bool = False, interrupts: bool = True) -> Any:
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 and chr(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.