-1

I have an endless while or for loop, and wish to break when a keyboard key is pressed.

while True:
    time.sleep(5)
    if any key is pressed:
        break

How can I do that?


Q&A Inspired by:

Breaking out of an infinite FOR loop in python without interupting the loop

Bharel
  • 23,672
  • 5
  • 40
  • 80
  • 2
    What is the use case for "any key" exactly? `SIGINT` exists for exactly cases like this when you want to interrupt whatever process is doing, which in this case would be interpreted as a signal to interrupt this loop. All you have to do is catch `KeyboardInterrupt` and break out of the loop. This not only makes it less likely that you will missclick and interrupt the loop when you didn't mean to, but is also standard way that is obvious to everyone. –  Jan 11 '22 at 09:09
  • @Sahsahae Plenty of use cases, starting from improper signal handling, multithreading where signals are not caught by correct thread, text program for users who aren't tech-savvys, capturing all keys for monitoring or refresh programs. I found multiple differently worded questions where the end goal was doing something like this. – Bharel Jan 11 '22 at 09:28
  • "I found multiple differently worded questions where the end goal was doing something like this." Why didn't they solve your problem, then? – MisterMiyagi Jan 11 '22 at 09:29
  • @MisterMiyagi it's a Q&A. Most of the solutions were containing specific code relevant to the OP, had lacking solutions or were closed as they were unclear. I chose to answer the general question to help other users while they're searching. – Bharel Jan 11 '22 at 09:31
  • @Bharel "non-techsavvy" users will complain about having to use a terminal in the first place. There's nothing tech-savvy about pressing ctrl-c to begin with. Also you shouldn't be receiving input from multiple threads at a time to begin with. –  Jan 11 '22 at 09:34
  • @Sahsahae I'm sorry but I disagree with you. I have encountered the demand before numerous times. – Bharel Jan 11 '22 at 09:41
  • @Tomerikoo it's quite close. Shall I move my solution over there? (All of the others are using external libraries or platform specific code) – Bharel Jan 11 '22 at 09:44
  • 1
    @Bharel Your answer is nice and it seems like you put some time to it. You should post it in one of the many existing duplicates of this problem instead of asking a new one (and probably delete this one) – Tomerikoo Jan 11 '22 at 09:44
  • @Bharel it is quite curious to me how you can control which thread you interrupt by simply pressing "any key", which the opposite of pinpointing anything, including semantic meaning of the input. The matter of fact is that providing real meaningful input to a single controlling thread is objectively the only correct way. There's a huge difference between `press any key` and `type stop doing x to stop doing x but leave y alone` and that's not even a matter of opinion if we're talking about a parallel system that is allowed to be controlled by an user. Otherwise I have no idea what you're on. –  Jan 11 '22 at 09:49
  • 1
    @Sahsahae if the controlling or UI thread is not the main thread, which is hardly a must, Python by default throws the exception in the main thread. This is a much easier solution than redirecting the signal using any sort of side channel, like setting thread-global variables, state or signalling. The answer I provided also allows a specific key to be captured. – Bharel Jan 11 '22 at 09:54
  • @Tomerikoo I've moved the answer accordingly, but believe a closed duplicate would be beneficial in this scenario for other users searching. – Bharel Jan 11 '22 at 09:56

1 Answers1

1

Here is a cross-platform solution:

import contextlib as _contextlib

try:
    import msvcrt as _msvcrt

    # Length 0 sequences, length 1 sequences...
    _ESCAPE_SEQUENCES = [frozenset(("\x00", "\xe0"))]

    _next_input = _msvcrt.getwch

    _set_terminal_raw = _contextlib.nullcontext

    _input_ready = _msvcrt.kbhit

except ImportError:  # Unix
    import sys as _sys, tty as _tty, termios as _termios, \
        select as _select, functools as _functools

    # Length 0 sequences, length 1 sequences...
    _ESCAPE_SEQUENCES = [
        frozenset(("\x1b",)),
        frozenset(("\x1b\x5b", "\x1b\x4f"))]

    @_contextlib.contextmanager
    def _set_terminal_raw():
        fd = _sys.stdin.fileno()
        old_settings = _termios.tcgetattr(fd)
        try:
            _tty.setraw(_sys.stdin.fileno())
            yield
        finally:
            _termios.tcsetattr(fd, _termios.TCSADRAIN, old_settings)

    _next_input = _functools.partial(_sys.stdin.read, 1)

    def _input_ready():
        return _select.select([_sys.stdin], [], [], 0) == ([_sys.stdin], [], [])

_MAX_ESCAPE_SEQUENCE_LENGTH = len(_ESCAPE_SEQUENCES)

def _get_keystroke():
    key = _next_input()
    while (len(key) <= _MAX_ESCAPE_SEQUENCE_LENGTH and
           key in _ESCAPE_SEQUENCES[len(key)-1]):
        key += _next_input()
    return key

def _flush():
    while _input_ready():
        _next_input()

def key_pressed(key: str = None, *, flush: bool = True) -> bool:
    """Return True if the specified key has been pressed

    Args:
        key: The key to check for. If None, any key will do.
        flush: If True (default), flush the input buffer after the key was found.
    
    Return:
        boolean stating whether a key was pressed.
    """
    with _set_terminal_raw():
        if key is None:
            if not _input_ready():
                return False
            if flush:
                _flush()
            return True

        while _input_ready():
            keystroke = _get_keystroke()
            if keystroke == key:
                if flush:
                    _flush()
                return True
        return False

def print_key() -> None:
    """Print the key that was pressed
    
    Useful for debugging and figuring out keys.
    """
    with _set_terminal_raw():
        _flush()
        print("\\x" + "\\x".join(map("{:02x}".format, map(ord, _get_keystroke()))))

def wait_key(key=None, *, pre_flush=False, post_flush=True) -> str:
    """Wait for a specific key to be pressed.

    Args:
        key: The key to check for. If None, any key will do.
        pre_flush: If True, flush the input buffer before waiting for input.
        Useful in case you wish to ignore previously pressed keys.
        post_flush: If True (default), flush the input buffer after the key was
        found. Useful for ignoring multiple key-presses.
    
    Returns:
        The key that was pressed.
    """
    with _set_terminal_raw():
        if pre_flush:
            _flush()

        if key is None:
            key = _get_keystroke()
            if post_flush:
                _flush()
            return key

        while _get_keystroke() != key:
            pass
        
        if post_flush:
            _flush()

        return key

Use key_pressed() inside your while loop:

while True:
    time.sleep(5)
    if key_pressed():
        break

You can also check for a specific key:

while True:
    time.sleep(5)
    if key_pressed("\x00\x48"):  # Up arrow key on Windows.
        break

And find out special keys using print_key():

>>> print_key()
# Press up key
\x00\x48
Bharel
  • 23,672
  • 5
  • 40
  • 80