如何在ANSI终端中获取光标的位置?

12
我想要获得终端窗口中光标的位置。我知道可以像这个答案一样使用echo -e "\033[6n"并且以-s静默读取输出,但是我该如何在Python中实现?
我尝试了这个上下文管理器,像这样:
with Capturing() as output:
    sys.stdout.write("\e[6n")
print(output)

但是它只捕获我写的\e[6n('\x1b[6n')转义序列,而不是我需要的^[[x;yR1序列。
我还尝试了生成子进程并获取其输出,但同样只捕获我写的转义序列:
output = subprocess.check_output(["echo", "\033[6n"], shell=False)
print(output)

shell=True 让输出成为一个空字符串列表。

避免使用 curses (因为这应该是一个简单的、穷人的光标位置获取器),我怎样才能得到打印 \e[6n 返回的转义序列呢?


也许你应该使用curses。https://docs.python.org/2/library/curses.html - jsbueno
3
@jsbueno,你有没有看到这个问题?请不要使用诅咒。 - cat
1
这就是为什么我没有试图用咒语回答。 :-) 我还在思考你的问题 - 不确定是否可以在没有对系统字符设备进行低级访问的情况下实现。你想要哪些操作系统的支持? - jsbueno
我认为最好的方法是使用ANSI清除屏幕,并在程序中考虑光标位置。但这不会防止用户输入和更改光标位置。 - jsbueno
顺便说一句,curses 之所以如此重要,是因为这种东西并不容易实现。如果这让你的工作陷入困境,也许你应该考虑将应用程序移植到图形用户界面或 Web 界面。 - jsbueno
显示剩余4条评论
2个回答

19

虽然 Stack Overflow 上的问题很老,但它肯定不过时,因此我写了一个完整的示例来说明如何做这个。

基本方法是:

  1. 启用 stdout 上 ANSI 转义序列的处理。
  2. 禁用 stdin 上的 ECHO 和行模式。
  3. 发送 ANSI 序列来查询光标位置到 stdout
  4. stdin 上读取回复。
  5. 恢复 stdinstdout 的设置。

对于第一步,在 Linux 下,ANSI 转义序列的处理应该是默认启用的,但在 Windows 下它们没有被启用,至少目前是这样的,这就是为什么下面的示例使用 SetConsoleMode 来启用它们。关于 kernel32.GetStdHandle() - 调用,Windows 标准句柄的 stdin 是 -10,而 stdout 则是 -11,我们只是获取了这些文件描述符。这些都是 Windows 特有的函数。

至于 Linux,我们可以使用 termios 来禁用/启用 ECHO 和行模式。值得注意的是,termios 在 Windows 下不可用。

对于第二步,stdin 上的任何输入都会被缓冲,并且只会逐行发送,但我们希望尽快读取 stdin 上的所有输入。我们还希望禁用 ECHO,这样第三步的回复就不会被打印到控制台上。

为了保险起见,下面的示例将在出现问题时给出 (-1, -1) 的结果,因此你的代码可以再次尝试。

import sys, re
if(sys.platform == "win32"):
    import ctypes
    from ctypes import wintypes
else:
    import termios

def cursorPos():
    if(sys.platform == "win32"):
        OldStdinMode = ctypes.wintypes.DWORD()
        OldStdoutMode = ctypes.wintypes.DWORD()
        kernel32 = ctypes.windll.kernel32
        kernel32.GetConsoleMode(kernel32.GetStdHandle(-10), ctypes.byref(OldStdinMode))
        kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 0)
        kernel32.GetConsoleMode(kernel32.GetStdHandle(-11), ctypes.byref(OldStdoutMode))
        kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
    else:
        OldStdinMode = termios.tcgetattr(sys.stdin)
        _ = termios.tcgetattr(sys.stdin)
        _[3] = _[3] & ~(termios.ECHO | termios.ICANON)
        termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, _)
    try:
        _ = ""
        sys.stdout.write("\x1b[6n")
        sys.stdout.flush()
        while not (_ := _ + sys.stdin.read(1)).endswith('R'):
            True
        res = re.match(r".*\[(?P<y>\d*);(?P<x>\d*)R", _)
    finally:
        if(sys.platform == "win32"):
            kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), OldStdinMode)
            kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), OldStdoutMode)
        else:
            termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, OldStdinMode)
    if(res):
        return (res.group("x"), res.group("y"))
    return (-1, -1)

x, y = cursorPos()
print(f"Cursor x: {x}, y: {y}")

得到的输出应类似于这个:

Cursor x: 1, y: 30

以下链接可能会有所帮助,如果您想深入研究所有这些,并扩展这里的功能:Linux的termios手册页Windows SetConsoleModeWindows GetConsoleMode维基百科的ANSI转义序列条目


1
为什么要写 while not (_ := _ + sys.stdin.read(1)).endswith('R'): True?用 while not.... :pass 不是更容易和简单吗? - Adam Jenča
如果你在谈论我使用“True”的地方,我们只是在谈论一个单词:我不认为使用其他方式会更容易或更简单阅读。使用“pass”更符合Python的风格,这一点我可以承认。我使用“True”并没有什么实际原因,只是因为我没有思考,而且我习惯于在shell脚本中编写循环,所以它就出现在那里了。 - WereCatf

8
您只需读取sys.stdin以获取值即可。我在一个类似您的问题的问题中找到了答案,但针对试图从C程序中执行此操作的用户:http://www.linuxquestions.org/questions/programming-9/get-cursor-position-in-c-947833/。因此,当我尝试从Python交互终端执行某些操作时:
>>> import sys
>>> sys.stdout.write("\x1b[6n");a=sys.stdin.read(10)
]^[[46;1R
>>>
>>> a
'\x1b[46;1R'
>>> sys.stdin.isatty()
True   

您将需要使用其他ANSI技巧/位置/重新打印来避免输出实际显示在终端上,并防止stdin读取时阻塞 - 但我认为可以通过一些试错来完成。


那个完美地运行了;我不知道为什么我没有尝试读取“stdin”。 - cat
能否获取最大允许的位置,即终端的“分辨率”? - Jay
有,但不是通过 ANSI 序列(或者说,甚至可能是通过 ANSI 序列,这些东西很复杂) - 但是 Python 在 os 模块中提供了一个调用:os.terminal_size 将返回一个命名元组,其中包含列和行的数量。 - jsbueno
1
它可以很好地打印到终端,但无法读取任何内容。正确的解决方案是WereCatf的! - TrueY

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