Press any key to continue.
[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, Optional
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: Optional[List[str]],  # None = accept anything
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: Optional[List[str]], timeout_seconds: int = 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 not valid_responses or 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 press_any_key(prompt: str = "Press any key to continue...", *, timeout_seconds=None) -> str:
77     return single_keystroke_response(None, prompt=prompt, timeout_seconds=timeout_seconds)
78
79
80 def keystroke_helper() -> None:
81     """Misc util to watch keystrokes and report what they were."""
82
83     print("Watching for keystrokes; ^C to quit.")
84     while True:
85         key = readchar.readkey()
86         if len(key) == 1:
87             print(f'That was "{key}" ({ord(key)}).')
88             if ord(key) == 3:
89                 return
90         else:
91             print(f'That was sequence "{key}" (', end="")
92             for _ in key:
93                 print(f" {ord(_)} ", end="")
94             print(")")