Determine the terminal cursor position with an ANSI sequence in Python 3

Question:

I want to write a little script which prints images to the terminal with /usr/lib/w3mimgdisplay(like in mac osx lsi). Therefore i need the the actual cursor position (or caret position) when the script has started. So far i figured out to get the cursor position in a shell with an ANSI sequence:

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

This is how the response of this ANSI sequence look like (urvxt and bash – don’t know if that is important). So this sequence prints out the result (^[[2;1R) immediately. And this is what i don’t understand. How is this done? If i write a very simple shell script, just with that instruction and strace the script, this doesn’t clear up things ether. What the? Then i try to figure out how this happens by looking in the terminfo manpage. Can’t find it here (maybe i didn’t try hard enough). At this point i find myself very confused about this concept. Does the terminal write the position even to stdout?

Terminal

#!/bin/bash
echo -en "e[6n"

$ strace sh curpos.sh
[...]
read(255, "#!/bin/bashnecho -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

First i tried to use subprocess.check_output which of course just returns the the string i’ve echoed. How do i capture the response to this ANSI sequence?

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

I also tried a lot of other things, like reading the stdin and stdout!? With and without threads, but all of that was more a guessing and mocking around rather than knowing what to do. I also search the Internet for quite a while, hoping to find an example of how to do this, but whit no luck. I’ve found this answer to the same question: https://stackoverflow.com/a/35526389/2787738 but this don’t work. Actually i don’t know if this has ever worked, because in this answer the ANSI sequence is written to stdout before it starts reading from stdin? Here i realised again that i don’t understand the concept/mechanism how these ANSI sequences really work. So at this point every explanation which clears things a very much appreciated. The most helpful post i found was this one: https://www.linuxquestions.org/questions/programming-9/get-cursor-position-in-c-947833/. In this thread someone posted this bash script:

#!/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 '33[6n'
# Read response from standard input; note, it ends at R, not at newline
read -d "R" rowscols
# Clean up the rowscols (from 33[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"

Here we can see that indeed the response is somehow magically appears one the screen :). This is why this script disables echoing on the terminal and after reading the response it resets the original terminal settings via stty.

Asked By: netzego

||

Answers:

Here is a POC snippet, how to read the current cursor position via an ansi/vt100 controll sequence.

I suggest you to put the following code inside a curpos.py file and import this file in your code (import curpos).

import re, sys, termios, tty

def getpos():
  buff = ''
  stdin = sys.stdin.fileno()
  tattr = termios.tcgetattr(stdin)

  try:
    tty.setcbreak(stdin, termios.TCSANOW)
    sys.stdout.write('33[6n')
    sys.stdout.flush()

    while True:
      buff += sys.stdin.read(1)

      if buff[-1] == 'R':
        break
  finally:
    termios.tcsetattr(stdin, termios.TCSANOW, tattr)

  matches = re.match(r'^33[(d*);(d*)R', buff)
  if matches == None: return None

  groups = matches.groups()
  return (int(groups[0]), int(groups[1]))

An example of how your code should/could be:

import curpos

res = getpos()

if res == None:
  print('Problem, I cannot get the cursor possition')
else:
  print(f'The cursor position is: {res}')

warning

This is not perfect. To make it more robust, a routine which sorts out keystrokes from the user while reading from stdin would be nice.

Answered By: netzego