summaryrefslogtreecommitdiff
path: root/input_utils.py
blob: 5e36db1ba853884636a2df7a5007004aa086760b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/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."""

    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."""

    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 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(")")