Python 非阻塞非干扰我的 TTY 的键盘按键检测

3 投票
2 回答
2370 浏览
提问于 2025-04-18 12:10

我有一个循环,它在不停地做一些工作,并且会打印很多信息到标准输出(就是屏幕上)。这个循环一直在重复。现在我想要实现的是,当用户按下某个键(可以是方向键、回车键或者字母键)时,能够检测到这个动作,并在发生时执行一些操作。

本来这应该是个很简单的小任务,但我花了四个小时尝试不同的方法,结果却没有什么进展。

这个功能只需要在Linux系统上运行。

我目前能做到的就是下面这个代码,但它只部分有效,只有在0.05秒内才能捕捉到按键。

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)

2 个回答

1

这是我想到的一个解决方案。虽然不是完美的,因为它依赖于超时,有时候只能捕捉到一半的转义序列,如果按键在超时到期前的毫秒(微秒?纳秒?)内被按下的话。不过,这是我能想到的最不糟糕的解决办法。真让人失望……

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

使用方法:

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')
4

这个问题之前有人问过。有人分享了一个简洁、经过改进的解决方案

在这里重新发布一下

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

撰写回答