Since this thing is on the innerwebs I suppose it should have a
[python_utils.git] / input_utils.py
1 #!/usr/bin/env python3
2
3 # © Copyright 2021-2022, Scott Gasch
4
5 """Utilities related to user input."""
6
7 import logging
8 import signal
9 import sys
10 from typing import List
11
12 import readchar  # type: ignore
13
14 import exceptions
15
16 logger = logging.getLogger(__file__)
17
18
19 def single_keystroke_response(
20     valid_responses: List[str],
21     *,
22     prompt: str = None,
23     default_response: str = None,
24     timeout_seconds: int = None,
25 ) -> str:
26     """Get a single keystroke response to a prompt."""
27
28     def _handle_timeout(signum, frame) -> None:
29         raise exceptions.TimeoutError()
30
31     def _single_keystroke_response_internal(
32         valid_responses: List[str], timeout_seconds=None
33     ) -> str:
34         os_special_keystrokes = [3, 26]  # ^C, ^Z
35         if timeout_seconds is not None:
36             signal.signal(signal.SIGALRM, _handle_timeout)
37             signal.alarm(timeout_seconds)
38
39         try:
40             while True:
41                 response = readchar.readchar()
42                 logger.debug('Keystroke: 0x%x', ord(response))
43                 if response in valid_responses:
44                     break
45                 if ord(response) in os_special_keystrokes:
46                     break
47             return response
48         finally:
49             if timeout_seconds is not None:
50                 signal.alarm(0)
51
52     if prompt is not None:
53         print(prompt, end="")
54         sys.stdout.flush()
55     try:
56         response = _single_keystroke_response_internal(valid_responses, timeout_seconds)
57         if ord(response) == 3:
58             raise KeyboardInterrupt('User pressed ^C in input_utils.')
59
60     except exceptions.TimeoutError:
61         if default_response is not None:
62             response = default_response
63     if prompt is not None:
64         print(response)
65     return response
66
67
68 def yn_response(prompt: str = None, *, timeout_seconds=None) -> str:
69     """Get a Y/N response to a prompt."""
70
71     return single_keystroke_response(
72         ["y", "n", "Y", "N"], prompt=prompt, timeout_seconds=timeout_seconds
73     ).lower()
74
75
76 def keystroke_helper() -> None:
77     """Misc util to watch keystrokes and report what they were."""
78
79     print("Watching for keystrokes; ^C to quit.")
80     while True:
81         key = readchar.readkey()
82         if len(key) == 1:
83             print(f'That was "{key}" ({ord(key)}).')
84             if ord(key) == 3:
85                 return
86         else:
87             print(f'That was sequence "{key}" (', end="")
88             for _ in key:
89                 print(f" {ord(_)} ", end="")
90             print(")")