Since this thing is on the innerwebs I suppose it should have a
[python_utils.git] / input_utils.py
index 913146a313608398d902a03eef7fe824399cd6fc..77094daaa7a70cd83ebf0e2b5ba0adac311d2b01 100644 (file)
@@ -1,12 +1,20 @@
 #!/usr/bin/env python3
 
+# © Copyright 2021-2022, Scott Gasch
+
 """Utilities related to user input."""
 
-import readchar  # type: ignore
+import logging
 import signal
 import sys
 from typing import List
 
+import readchar  # type: ignore
+
+import exceptions
+
+logger = logging.getLogger(__file__)
+
 
 def single_keystroke_response(
     valid_responses: List[str],
@@ -15,15 +23,15 @@ def single_keystroke_response(
     default_response: str = None,
     timeout_seconds: int = None,
 ) -> str:
-    class TimeoutError(Exception):
-        pass
+    """Get a single keystroke response to a prompt."""
 
     def _handle_timeout(signum, frame) -> None:
-        raise TimeoutError()
+        raise exceptions.TimeoutError()
 
     def _single_keystroke_response_internal(
         valid_responses: List[str], timeout_seconds=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)
@@ -31,8 +39,11 @@ def single_keystroke_response(
         try:
             while True:
                 response = readchar.readchar()
+                logger.debug('Keystroke: 0x%x', ord(response))
                 if response in valid_responses:
                     break
+                if ord(response) in os_special_keystrokes:
+                    break
             return response
         finally:
             if timeout_seconds is not None:
@@ -42,10 +53,11 @@ def single_keystroke_response(
         print(prompt, end="")
         sys.stdout.flush()
     try:
-        response = _single_keystroke_response_internal(
-            valid_responses, timeout_seconds
-        )
-    except TimeoutError:
+        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 is not None:
@@ -54,12 +66,16 @@ def single_keystroke_response(
 
 
 def yn_response(prompt: str = None, *, timeout_seconds=None) -> str:
+    """Get a Y/N response to a prompt."""
+
     return single_keystroke_response(
         ["y", "n", "Y", "N"], prompt=prompt, timeout_seconds=timeout_seconds
     ).lower()
 
 
 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()