python non-blocking non-messing-my-tty key press d

2019-05-18 17:01发布

问题:

I have a loop that does some work and prints a lot of info to stdout. Over and over again (it's a loop...) What I'd like to do is to detect when / if user presses a key (it can be an arrow, enter, or a letter), and do some work when that happens.

This should have been a very simple subsubtask, but I've spent last four hours trying different approaches and getting pretty much nowhere.

This needs only work in Linux.

Best I could get is something like this below. But that works partially, catching the keys only if within the 0.05 sec.

import sys,tty,termios
class _Getch:
    def __call__(self, n=1):
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(n)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch


def getch(timeout=0.2):
    inkey = _Getch()
    k = ''
    start_sec = time()
    while(time() - start_sec < timeout):
        if k == '':
            k = timeout_call(inkey, timeout_duration=timeout - (time() - start_sec))
    if k == u'\x1b':
        k += inkey(2)
        if k == u'\x1b[A':
            return "up"
        if k == u'\x1b[B':
            return "down"
        if k == u'\x1b[C':
            return "right"
        if k == u'\x1b[D':
            return "left"
    elif k == "q":
        return 'q'
    elif k == "\n":
        return 'enter'
    else:
        return None


while True:
    do_some_work_that_lasts_about_0_2_seconds()
    key = getch(0.05)
    if key:
        do_something_with_the(key)

回答1:

This has been asked before. Someone posted a nice, short, refactored solution

Reposted here

import sys
import select
import tty
import termios

class NonBlockingConsole(object):

    def __enter__(self):
        self.old_settings = termios.tcgetattr(sys.stdin)
        tty.setcbreak(sys.stdin.fileno())
        return self

    def __exit__(self, type, value, traceback):
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)


    def get_data(self):
        if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
            return sys.stdin.read(1)
        return False


if __name__ == '__main__':
    # Use like this
    with NonBlockingConsole() as nbc:
        i = 0
        while 1:
            print i
            i += 1

            if nbc.get_data() == '\x1b':  # x1b is ESC
                break


回答2:

Here is a solution I came up with. Not perfect, because it relies on timeouts and sometimes can catch only half of escape sequences, if the key is pressed mili(micro? nano?)seconds before timeout expires. But it's the least bad solution I could come up with. Disappointing...

def timeout_call(func, args=(), kwargs=None, timeout_duration=1.0, default=None):
    if not kwargs:
        kwargs = {}
    import signal

    class TimeoutError(Exception):
        pass

    def handler(signum, frame):
        raise TimeoutError()

    # set the timeout handler
    signal.signal(signal.SIGALRM, handler)
    signal.setitimer(signal.ITIMER_REAL, timeout_duration)
    try:
        result = func(*args, **kwargs)
    except TimeoutError as exc:
        result = default
    finally:
        signal.alarm(0)

    return result


class NonBlockingConsole(object):

    def __enter__(self):
        self.old_settings = termios.tcgetattr(sys.stdin)
        tty.setcbreak(sys.stdin.fileno())
        return self

    def __exit__(self, type, value, traceback):
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)

    def get_data(self):
        k = ''
        while True:
            c = timeout_call(sys.stdin.read, args=[1], timeout_duration=0.05)
            if c is None:
                break
            k += c

        return k if k else False

Usage:

with NonBlockingConsole() as nbc:
    while True:
        sleep(0.05)  # or longer, but not shorter, for my setup anyways...
        data = nbc.get_data()
        if data:
            print data.encode('string-escape')


标签: python stdin