首页 > 解决方案 > 读取用户的击键

问题描述

我想从用户那里读取一个:字母、数字和诸如Escor之类Del的东西,以及箭头键。

到目前为止,我一直在使用名为 readchar 的第 3 方模块。这里讨论了一些完成任务的方法:如何从用户那里读取单个字符?. 他们沿着这些路线运行:

import termios, sys, tty
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
    tty.setraw(fd)
    ch = sys.stdin.read(1)
finally:
    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

不幸的是,这只返回单个字符,例如,\x1b而箭头将是多个字符,例如\x1b\x5b\x41. 模块 readchar 试图通过提供一个函数readkey来解决这个问题,该函数读取字符直到读取完整的键,然后返回它。问题是它会在Esc按下键时挂起,因为它预计会有更多字符。

如何编写一个函数,该函数将立即返回按下的键EscDel其他键?

标签: pythonlinux

解决方案


如果您在终端中执行此操作,您将需要代码中的自定义逻辑来处理它。

背景:据您的程序所知,它可能连接到具有每秒 300 位传输速率的老式硬件终端。该终端可能有一个向上箭头键^[,当被按下[时会自动发送字符行为(+并且都发送字符)。或者它可能是不同型号的终端,在按下时发送不同的字符序列。如果您在 Python 中使用,操作系统会负责从Aup arrow^[esc^[up arrowinput()stdin,识别远程终端(或由用户终端应用程序模拟的终端),并将这些序列转换为命令行上的适当编辑操作。但是,如果您直接读取stdin,则每当用户按下特殊键时,您都会得到一些任意的字符序列(并且可能会根据他们所模拟的终端型号而有所不同)。

在我的终端中,向上箭头发送字符序列^[ [ A(您可以通过运行来检查它cat,然后按下键并查看显示的内容)。因此,如果您正在逐个字符地读取终端字符,则无法判断用户是刚刚按下esc还是按下了其他发出字符序列的键。

假设您有一种将字符序列转换回击键的好方法,处理此问题的一种方法是读取当前可用的所有内容sys.stdin(使用sys.stdin.read()而不是sys.stdin.read(1)),然后处理您在缓冲区中获得的所有内容。有时这会起作用,因为终端会将所有字符同时推送到缓冲区中,而您的应用程序将同时获取所有字符。但这并不是 100% 保证的——read原则上可以以任何感觉的方式分解流。通过缓慢的终端连接,您的应用程序很可能有时间在下一个待处理的字符出现之前处理每个字符。

您可以通过在没有新输入的情况下等待某个安静时间段来解决此问题,然后处理您到目前为止所拥有的任何内容。但是静默期的长短取决于连接速度、处理器负载等,所以这是不确定的。此外,延迟时间越长,您的程序就越可靠,但也越滞后。

在某种程度上,您正在尝试重新定义什么才是您的程序的输入。所以也许你应该一路走下去?我的意思是,命令行应用程序通常不会自行响应esc字符。相反,他们接受它并等待接下来的任何事情。因此,用户可以按自己喜欢的速度键入esc[, [A应用程序将依次处理每一个并最终确定它具有完整的序列,然后执行up arrow. 如果您的应用程序应该响应esc,那么也许您应该这样做,并记住大多数人的终端会^[在他们按下时发送后跟一堆额外的字符up arrow. 因此,如果它不重要,您可以扔掉其他东西,或者等待并像其他终端程序一样对整个序列采取行动。

另一种选择可能是使用 Pythoncurses库更直接地与终端交互,但我对此不是很熟悉。

我认为这个总结版本是:当用户按下或其他各种终端特定的键时,不可避免地stdin会给你一个字符。区分按下哪个键的唯一方法是在 之后开始检查其他字符,可能允许每个字符有短暂的延迟(可能是 0.1 秒,使用)。如果您获得与特定键对应的字符序列,那么您就可以使用它。如果有足够长的延迟而没有额外的字符,那么你放弃并处理你到目前为止所拥有的(可能只是,可能是部分转义序列)。如果这还不够好,那么您需要使用与本地硬件更直接交互的库。^[esc^[select.select^[

curses 库提供了这些行为——超时、组合序列、使用 terminfo 数据库转换为通用击键。可能有一些重量更轻的解决方案,但我还没有看到。

只是为了完成讨论:一些终端允许您将转义序列打印到控制台,这将重新编程功能键,包括箭头键,以发送不同的字符或字符序列。例如,有一个用于 VT100 终端和仿真器的 DECPFK 命令。这可能比使用更简单curses(只需编程箭头键以发送<>/或其他任何东西——除了 之外的任何东西^[)。但是顺序可能因终端而异,因此您需要在 terminfo 数据库中查找它(如果有的话)。我怀疑这是否值得麻烦。

其中一些可能提供有用的背景:


推荐阅读