需要与粘贴和 ANSI 转义序列良好交互的逐字符键盘输入

Need character-by-character keyboard input that interacts well with paste and ANSI escape sequences

我的程序(一个"TRAC Processor") uses character-by-character input. I am implementing readline-like input features for strings which are terminated with characters other than enter (usually ') and may themselves be multi-line. So I output terminal escape sequences between input characters, including escape sequences which query the terminal emulator (cursor position and screen size). To do cross-platform single-character input, I used http://code.activestate.com/recipes/134892/,很有帮助。

粘贴时效果很好...直到我需要在粘贴的第一个字符后获得终端响应。粘贴的文本似乎与对转义序列的响应混合在一起。我想我会在启动转义序列查询之前通过刷新输入缓冲区来修复它:等待 10ms,如果没有输入则继续;如果有输入,缓冲它并再次等待直到没有输入。基于 this post,我尝试使用 select() 来轮询标准输入。好主意,但没有奏效,而且产生了非常奇怪的行为。我 post 在这个问题的原始版本中编辑了那个奇怪的行为,以为我误解了 select 并且有一种方法可以解决它。似乎没有,但我找到了另一种刷新(并保存)输入流的方法。我决定保留这个问题,post那个方法作为答案。

select() 的问题已得到解释 here。在粘贴的第一个字符之后,其他字符已经被缓冲,并且 select 只有 returns 新输入时,新的输入超出了已经缓冲的内容。我无法让自己删除我为此行为产生的 MWE,所以你可以在下面看到它。

不幸的是,post 中提出的答案要么对我不起作用,要么需要更多解释。 @slowdog 建议使用无缓冲输入(os.read(stdin.fileno(), 1) 而不是 stdin.read(1))。这解决了 select 问题,但它破坏了粘贴:似乎第一个之后的粘贴的所有字符都被缓冲 无论如何 ,所以你永远看不到它们。它似乎也不能很好地处理转义序列响应,这些响应似乎也得到了缓冲。这也很烦人,因为您需要刷新输出缓冲区,但这并没有那么糟糕。 @Omnafarious 在评论中说 "Though, another way to handle the Python buffering issue to to simply do a no-parameter read, which should read everything currently available." 这就是我最终所做的,如下 posted,但 "simply" 结果并不是那么简单。还有另一种解决方案 here,但我认为 必须 是一种无需线程即可执行此操作的方法。

顺便说一句,有一个相对简单的解决方法,因为事实证明粘贴不是随机穿插对转义序列的响应。粘贴的全部剩余部分在转义序列响应之前被读取,因此当您寻找转义序列响应(它本身以转义开始)时,您可以只缓冲您在转义之前读取的所有字符,然后处理他们以后。如果您可能在终端输入 ESC 字符,这只会失败。无论如何,此时我已经非常着迷于解决这个问题,我认为其他人可能会发现答案很有价值。

无论如何,FWIW 是我针对 select 问题的 MWE,它只是回显文本而不是缓冲文本:

def flush():
    import sys, tty, termios
    from select import select
    tty.setraw(sys.stdin.fileno())
    while True:
        rlist, wlist, xlist = select([sys.stdin], [], [], 1)
        if rlist == []: return
        sys.stdout.write(sys.stdin.read(1))

将其粘贴到 Python 提示符 (2.7.9) 中,并在末尾放置另一个空行。如果您调用 flush() 并以超过每秒一个字母的速度键入一些文本,它会返回给您。例如,我输入 "hello" 然后暂停,得到这个结果:

>>> flush()
hello>>> 

在 OSX 终端应用程序中(至少),如果您将单词 text 复制到剪贴板,调用该函数并在一秒钟内点击粘贴,这就是您得到的结果:

>>> flush()
t>>> 

奇怪!只有第一个字母。再试一次,不要输入任何内容:

>>> flush()
>>> 

它停了一秒钟,什么也没做,就像没有输入等待,对吧?再试一次,然后点击 ?:

>>> flush()
ext?>>> 

? 之前,你得到剩下的粘贴,保存起来!另外,奇怪的是,在输入我不理解的 ? 之前有 1 秒的停顿。如果此时再试一次,它的行为与正常一样。

好的,我们再试一次,先粘贴text,然后粘贴WTF,然后输入!:

>>> flush()
t>>> flush()
extW>>> flush()
TF!>>> 

所以再次粘贴仅给出第一个字母,并将其他字母保存在输入缓冲区中,并在 W! 之前暂停一秒钟。还有一件奇怪的事情:缓冲的字符没有在 Python >>> 提示符下输入。

一个挥之不去的问题:为什么在回显下一个字母之前会有额外的 1 秒停顿? Select并不总是等待整个时间段...

"no-parameter read" 经常被引用为一种读取所有可用字节的方法,这听起来非常适合此应用程序。不幸的是,当您查看 documentation 时,read() 与 readall() 相同,后者阻塞直到 EOF。所以需要设置stdin为非阻塞模式。

执行此操作后,您将开始获得:

IOError: [Errno 35] Resource temporarily unavailable

当你google这个的时候,绝大多数的回复都说这个问题的解决方案是摆脱非阻塞模式......所以这在这里没有帮助。然而,this post 解释说这就是非阻塞 read() 在 return.

没有字符时所做的事情

这是我的 flush() 函数,其中大部分是从 post:

复制而来的
def flush():
    import sys, tty, termios, fcntl, os
    fd = sys.stdin.fileno()
    old_attr = termios.tcgetattr(fd)
    old_fl = fcntl.fcntl(fd, fcntl.F_GETFL)
    try:
        tty.setraw(fd)
        fcntl.fcntl(fd, fcntl.F_SETFL, old_fl | os.O_NONBLOCK)
        inp = sys.stdin.read()
    except IOError, ex1:  #if no chars available generates exception
        try: #need to catch correct exception
            errno = ex1.args[0] #if args not sequence get TypeError
            if errno == 35:
                return '' #No characters available
            else:
                raise #re-raise exception ex1
        except TypeError, ex2:  #catch args[0] mismatch above
            raise ex1 #ignore TypeError, re-raise exception ex1
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_attr)
        fcntl.fcntl(fd, fcntl.F_SETFL, old_fl)
    return inp

希望对大家有所帮助!