3 # © Copyright 2021-2022, Scott Gasch
5 """Utilities related to user input."""
10 from typing import List, Optional
12 import readchar # type: ignore
16 logger = logging.getLogger(__file__)
19 def single_keystroke_response(
20 valid_responses: Optional[List[str]], # None = accept anything
23 default_response: str = None,
24 timeout_seconds: int = None,
25 ) -> Optional[str]: # None if timeout w/o keystroke
26 """Get a single keystroke response to a prompt and returns it.
29 valid_responses: a list of strings that are considered to be
30 valid keystrokes to be accepted. If None, we accept
32 prompt: the prompt to print before watching keystrokes. If
34 default_response: the response to return if the timeout
35 expires. If None, skip this.
36 timeout_seconds: number of seconds to wait before timing out
37 and returning the default_response. If None, wait forever.
40 The keystroke the user pressed. If the user pressed a special
41 keystroke like ^C or ^Z, we raise a KeyboardInterrupt exception.
44 def _handle_timeout(signum, frame) -> None:
45 raise exceptions.TimeoutError()
47 def _single_keystroke_response_internal(
48 valid_responses: Optional[List[str]], timeout_seconds: int = None
50 os_special_keystrokes = [3, 26] # ^C, ^Z
51 if timeout_seconds is not None:
52 signal.signal(signal.SIGALRM, _handle_timeout)
53 signal.alarm(timeout_seconds)
57 response = readchar.readchar()
58 logger.debug('Keystroke: 0x%x', ord(response))
59 if not valid_responses or response in valid_responses:
61 if ord(response) in os_special_keystrokes:
65 if timeout_seconds is not None:
69 if prompt is not None:
73 response = _single_keystroke_response_internal(valid_responses, timeout_seconds)
74 if ord(response) == 3:
75 raise KeyboardInterrupt('User pressed ^C in input_utils.')
77 except exceptions.TimeoutError:
78 if default_response is not None:
79 response = default_response
80 if prompt and response:
85 def yn_response(prompt: str = None, *, timeout_seconds=None) -> Optional[str]:
86 """Get a Y/N response to a prompt.
89 prompt: the user prompt or None to skip this
90 timeout_seconds: the number of seconds to wait for a response or
94 A lower case 'y' or 'n'. Or None if the timeout expires with
95 no input from the user. Or raises a KeyboardInterrupt if the
96 user pressed a special key such as ^C or ^Z.
98 yn = single_keystroke_response(
99 ["y", "n", "Y", "N"], prompt=prompt, timeout_seconds=timeout_seconds
107 prompt: str = "Press any key to continue...", *, timeout_seconds=None
109 """Press any key to continue..."""
111 return single_keystroke_response(None, prompt=prompt, timeout_seconds=timeout_seconds)
114 def up_down_enter() -> Optional[str]:
115 """Respond to UP, DOWN or ENTER events for simple menus without
116 the need for curses."""
118 os_special_keystrokes = [3, 26] # ^C, ^Z
120 key = readchar.readkey()
122 if ord(key) in os_special_keystrokes:
127 if ord(key[0]) == 27 and ord(key[1]) == 91:
128 if ord(key[2]) == 65:
130 elif ord(key[2]) == 66:
134 def keystroke_helper() -> None:
135 """Misc util to watch keystrokes and report what they were."""
137 print("Watching for keystrokes; ^C to quit.")
139 key = readchar.readkey()
141 print(f'That was "{key}" ({ord(key)}).')
145 print(f'That was sequence "{key}" (', end="")
147 print(f" {ord(_)} ", end="")