在Python 3中使用ANSI序列确定终端光标位置

4

我希望写一个小脚本,它可以使用/usr/lib/w3mimgdisplay在终端中打印图片(就像mac osx中的lsi)。因此,在脚本启动时,我需要实际光标位置(或插入符号位置)。到目前为止,我已经通过ANSI序列在shell中获取了光标位置:

$ echo -en "\e[6n"
^[[2;1R$ 1R

这就是 ANSI 序列的响应样式(urvxt 和 bash - 不知道这是否重要)。所以这个序列会立即打印出结果 (^[[2;1R)。这就是我不理解的地方。这是如何完成的?如果我写一个非常简单的 shell 脚本,只有这个指令并使用 strace 跟踪脚本,它也无法解决问题。然后我尝试通过查看 terminfo 手册来了解发生了什么。但在这里找不到它(也许我没有足够努力)。此时,我对这个概念感到非常困惑。终端是否将光标位置甚至写入标准输出?
#!/bin/bash
echo -en "\e[6n"

$ strace sh curpos.sh
[...]
read(255, "#!/bin/bash\necho -en \"\\e[6n\"\n", 29) = 29
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 8), ...}) = 0
write(1, "\33[6n", 4)                   = 4
^[[54;21Rread(255, "", 29)                       = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

Python

一开始我尝试使用subprocess.check_output,但它只返回了我回显的字符串。那么如何捕获这个ANSI序列的响应呢?

>>> subprocess.check_output(["echo", "-en", "\x1b[6n"])
b"\x1b[6n"

我还尝试了很多其他方法,比如读取stdin和stdout!使用线程或不使用线程都试过,但这些都更像是猜测和摸索而不是真正知道该怎么做。我也在互联网上搜索了相当长时间,希望能找到一个关于如何做这个的例子,但没有运气。我发现了与同样问题有关的答案:https://dev59.com/Y5Tfa4cB1Zd3GeqPOkqU#35526389,但这行不通。实际上,我不知道这是否曾经起作用,因为在这个答案中,在读取stdin之前将ANSI序列写入stdout?在这里我再次意识到我不理解这些ANSI序列的概念/机制。所以在这一点上,任何可以澄清事情的解释都非常受欢迎。我发现最有帮助的帖子是这个:https://www.linuxquestions.org/questions/programming-9/get-cursor-position-in-c-947833/。在这个帖子里,有人发布了这个bash脚本:

#!/bin/bash
# Restore terminal settings when the script exits.
termios="$(stty -g)"
trap "stty '$termios'" EXIT
# Disable ICANON ECHO. Should probably also disable CREAD.
stty -icanon -echo
# Request cursor coordinates
printf '\033[6n'
# Read response from standard input; note, it ends at R, not at newline
read -d "R" rowscols
# Clean up the rowscols (from \033[rows;cols -- the R at end was eaten)
rowscols="${rowscols//[^0-9;]/}"
rowscols=("${rowscols//;/ }")
printf '(row %d, column %d) ' ${rowscols[0]} ${rowscols[1]}
# Reset original terminal settings.
stty "$termios"

在这里,我们可以看到响应似乎神奇地出现在屏幕上 :).这就是为什么这个脚本会在终端上禁用回显,并且在读取响应后通过 stty 重置原始终端设置的原因。

看起来相关:远程终端将在位置上“键入”,就像用户键入一样。要发生这种情况,您需要实际发送它:https://dev59.com/KGsy5IYBdhLWcg3wvgr7 - Max
@max 但是如何读取这些按键呢?我猜是 sys.stdin.read()。但是 subprocess.check_output(["echo", "-en", "\x1b[6n"]);curpos=sys,stdin.read(10) 不起作用。这是因为竞争条件问题还是 Python shell 忽略了它? - netzego
1个回答

6
这是一个POC代码片段,演示如何通过ansi/vt100控制序列读取当前光标位置。

curpos.py

import os, re, sys, termios, tty

def getpos():

    buf = ""
    stdin = sys.stdin.fileno()
    tattr = termios.tcgetattr(stdin)

    try:
        tty.setcbreak(stdin, termios.TCSANOW)
        sys.stdout.write("\x1b[6n")
        sys.stdout.flush()

        while True:
            buf += sys.stdin.read(1)
            if buf[-1] == "R":
                break

    finally:
        termios.tcsetattr(stdin, termios.TCSANOW, tattr)

    # reading the actual values, but what if a keystroke appears while reading
    # from stdin? As dirty work around, getpos() returns if this fails: None
    try:
        matches = re.match(r"^\x1b\[(\d*);(\d*)R", buf)
        groups = matches.groups()
    except AttributeError:
        return None

    return (int(groups[0]), int(groups[1]))

if __name__ == "__main__":
    print(getpos())

示例输出

$ python ./curpos.py
(2, 1)

警告

这并不完美。为了使其更加健壮,最好编写一个程序来从stdin读取用户按键。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接