#!/usr/bin/env python3 # © Copyright 2021-2022, Scott Gasch """Utilities related to user input.""" import logging import signal import sys from typing import List, Optional import readchar # type: ignore import exceptions logger = logging.getLogger(__file__) def single_keystroke_response( valid_responses: Optional[List[str]], # None = accept anything *, prompt: str = None, default_response: str = None, timeout_seconds: int = None, ) -> Optional[str]: # None if timeout w/o keystroke """Get a single keystroke response to a prompt and returns it. Args: valid_responses: a list of strings that are considered to be valid keystrokes to be accepted. If None, we accept anything. prompt: the prompt to print before watching keystrokes. If None, skip this. default_response: the response to return if the timeout expires. If None, skip this. timeout_seconds: number of seconds to wait before timing out and returning the default_response. If None, wait forever. Returns: The keystroke the user pressed. If the user pressed a special keystroke like ^C or ^Z, we raise a KeyboardInterrupt exception. """ def _handle_timeout(signum, frame) -> None: raise exceptions.TimeoutError() def _single_keystroke_response_internal( valid_responses: Optional[List[str]], timeout_seconds: int = None ) -> str: os_special_keystrokes = [3, 26] # ^C, ^Z if timeout_seconds is not None: signal.signal(signal.SIGALRM, _handle_timeout) signal.alarm(timeout_seconds) try: while True: response = readchar.readchar() logger.debug('Keystroke: 0x%x', ord(response)) if not valid_responses or response in valid_responses: break if ord(response) in os_special_keystrokes: break return response finally: if timeout_seconds is not None: signal.alarm(0) response = None if prompt is not None: print(prompt, end="") sys.stdout.flush() try: response = _single_keystroke_response_internal(valid_responses, timeout_seconds) if ord(response) == 3: raise KeyboardInterrupt('User pressed ^C in input_utils.') except exceptions.TimeoutError: if default_response is not None: response = default_response if prompt and response: print(response) return response def yn_response(prompt: str = None, *, timeout_seconds=None) -> Optional[str]: """Get a Y/N response to a prompt. Args: prompt: the user prompt or None to skip this timeout_seconds: the number of seconds to wait for a response or None to wait forever. Returns: A lower case 'y' or 'n'. Or None if the timeout expires with no input from the user. Or raises a KeyboardInterrupt if the user pressed a special key such as ^C or ^Z. """ yn = single_keystroke_response( ["y", "n", "Y", "N"], prompt=prompt, timeout_seconds=timeout_seconds ) if yn: yn = yn.lower() return yn def press_any_key( prompt: str = "Press any key to continue...", *, timeout_seconds=None ) -> Optional[str]: """Press any key to continue...""" return single_keystroke_response(None, prompt=prompt, timeout_seconds=timeout_seconds) def up_down_enter() -> Optional[str]: """Respond to UP, DOWN or ENTER events for simple menus without the need for curses.""" os_special_keystrokes = [3, 26] # ^C, ^Z while True: key = readchar.readkey() if len(key) == 1: if ord(key) in os_special_keystrokes: return None if ord(key) == 13: return 'enter' elif len(key) == 3: if ord(key[0]) == 27 and ord(key[1]) == 91: if ord(key[2]) == 65: return "up" elif ord(key[2]) == 66: return "down" def keystroke_helper() -> None: """Misc util to watch keystrokes and report what they were.""" print("Watching for keystrokes; ^C to quit.") while True: key = readchar.readkey() if len(key) == 1: print(f'That was "{key}" ({ord(key)}).') if ord(key) == 3: return else: print(f'That was sequence "{key}" (', end="") for _ in key: print(f" {ord(_)} ", end="") print(")")