11

I want to get the cursor's position in a terminal window. I know I can echo -e "\033[6n" and read the output -s silently as in this answer, but how can I do this in Python?

I've tried this contextmanager like this:

with Capturing() as output:
    sys.stdout.write("\e[6n")
print(output)

but it only captures the \e[6n ('\x1b[6n') escape sequence I write, not the ^[[x;yR1 sequence I need.

I've also tried spawning a subprocess and getting its output, but again, only the escape sequence I write is captured:

output = subprocess.check_output(["echo", "\033[6n"], shell=False)
print(output)

shell=True makes the output be a list of empty strings.

Avoiding curses (because this is supposed to be a simple, poor man's cursor pos getter), how can I get the escape sequence returned by printing \e[6n?

Community
  • 1
  • 1
cat
  • 3,888
  • 5
  • 32
  • 61
  • Maybe you should be using curses instead. https://docs.python.org/2/library/curses.html – jsbueno Feb 20 '16 at 16:52
  • 3
    @jsbueno did you read the question? No curses, please. – cat Feb 20 '16 at 16:53
  • 1
    That is why I did not try to answer with curses. :-) I am still thinking about your question - not sure if feasible without low level access to the system charcter devices themselves. For which Operating Systems you want this? – jsbueno Feb 20 '16 at 16:55
  • @jsbueno POSIX, (Unix + Linux) mostly, because I can have an ANSI term in Windows if i use `colorama`, which is less huge than curses – cat Feb 20 '16 at 16:56
  • Where are you viewing your output from stdout? using `\e[6n` should return the proper value `^[[76;5R` if run in terminal. – l'L'l Feb 20 '16 at 17:01
  • I think the way to go is to use ANSI to clear the screen, and account for the cursor position on your program side. That would not prevent your users from typing in and changing the cursor position, though. – jsbueno Feb 20 '16 at 17:01
  • BTW, there is a reason curses is big - it it is taht this kindo f thing is not trivial to do. Maybe you should also think about moving your application to a graphic UI or to to a web interface, if this is getting your work stuck. – jsbueno Feb 20 '16 at 17:03
  • @jsbueno Actually, this *is* the work. see https://github.com/catb0t/whereami and https://github.com/catb0t/input_constrain – cat Feb 20 '16 at 17:07
  • @l'L'l I'm just printing it in the terminal. What's your Python version and terminal emulator? – cat Feb 20 '16 at 17:08

2 Answers2

18

While the question here on Stack Overflow is old, it's certainly not outdated, and as such I wrote a complete example of how to do this.

The basic approach is:

  1. Enable processing of ANSI escape sequences on stdout.
  2. Disable ECHO and line mode on stdin.
  3. Send the ANSI sequence to query cursor position on stdout.
  4. Read the reply on stdin.
  5. Restore the settings for stdin and stdout.

For step 1, under Linux handling of ANSI escape sequences on stdout should be enabled by default, but under Windows they aren't, at least at the moment, which is why the example below uses SetConsoleMode to enable those. With regards to the kernel32.GetStdHandle() - calls, the Windows standard handle for stdin is -10 and for stdout it's -11, and we are just getting the file descriptors for those. These are Windows-only functions.

As for Linux, we can use termios to disable/enable ECHO and line mode. Of note is that termios isn't available under Windows.

For step 2, any input on stdin is buffered and only sent forward line-by-line, but we want to read all input on stdin as soon as possible. We also want to disable ECHO, so the reply to step 3 doesn't get printed out to the console.

Just for a good measure, the example below will give a result of (-1, -1) if something went wrong, so your code could e.g. try again.

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}")

The resulting output should be akin to this:

Cursor x: 1, y: 30

Additional links that may be of use, if one wishes to dig deeper into all this and e.g. expand on the functionality here: Man-page for Linux's termios, Windows SetConsoleMode, Windows GetConsoleMode, Wikipedia-entry for ANSI escape sequences

WereCatf
  • 321
  • 2
  • 4
  • Why that `while not (_ := _ + sys.stdin.read(1)).endswith('R'): True`? Wouldn't it be easier and simpler to read `while not.... :pass`? – Adam Jenča Dec 09 '21 at 19:37
  • If you're referring to my use of "True" there, we're talking about a single word: I don't see how it'd be any easier or simpler to read. It'd be more pythonic to use "pass", yes, I'll grant you that. There is no real reason why I used "True"; I just simply wasn't thinking and I'm so used to doing loops in shell-scripts that it slipped in there. – WereCatf Dec 11 '21 at 00:56
7

You can simply read sys.stdin yourself to get the value. I found the answer in a question just like yours, but for one trying to do that from a C program:

http://www.linuxquestions.org/questions/programming-9/get-cursor-position-in-c-947833/

So, when I tried something along that from the Python interactive terminal:

>>> import sys
>>> sys.stdout.write("\x1b[6n");a=sys.stdin.read(10)
]^[[46;1R
>>>
>>> a
'\x1b[46;1R'
>>> sys.stdin.isatty()
True   

You will have to use other ANSI tricks/position/reprint to avoid the output actually showing up on the terminal, and prevent blocking on stdin read - but I think it can be done with some trial and error.

user673679
  • 1,327
  • 1
  • 16
  • 35
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • That works perfectly; I don't know why I didn't try reading `stdin`. – cat Feb 20 '16 at 17:14
  • Is it possible to get the maximal allowed position, ie the terminal's "resolution"? – Jay May 29 '20 at 09:07
  • Yes, but not through an ANSI sequence (or maybe, even as well through an ANSI sequence, these things are complex) - but Python has a call in the `os` module: `os.terminal_size` will return a named-tuple with the number of columns and lines. – jsbueno May 29 '20 at 18:22
  • 1
    It nicely prints to the terminal, but does not read anything. Proper solution is WereCatf's! – TrueY Jan 30 '22 at 14:28