Late to the party, I know. But I came across this while searching for something similar. It's in Scheme - I don't know Python. In case you are still interested, here's how I do it.
;; Return a list obtained by reading characters from the input up to and
;; including an instance of the terminating character. The result does not
;; include the termination character. If the termination character is not
;; read before the `eof` object, returns a list composed of all of the
;; characters read to that point.
(define (read-to-terminating-char terminator input-port)
(let loop ()
(let ([c (read-char input-port)])
(if (or (eof-object? c) (char=? terminator c))
'()
(cons c (loop))))))
;; Return a version of the list without the first `n` elements.
(define (drop lst n)
(let loop ([my-lst lst]
[num n])
(if (zero? num)
my-lst
(loop (cdr my-lst) (- num 1)))))
;; Return a list containing all of the digits at the start of
;; the input.
(define (get-leading-digits lst)
(let loop ([my-lst lst])
(if (or (null? my-lst) (not (char-numeric? (car my-lst))))
'()
(cons (car my-lst) (loop (cdr my-lst))))))
;; Request the position of the cursor in the terminal. Return a list
;; containing the cursor row and column positions (integers).
(define (request-cursor-position)
(display "\x1b;[6n")
;; The response will be of the form "\esc[rr;cccR" where there are
;; typically 1 or 2 digits representing the row and 1 to 3 digits
;; representing the column.
(let* ([resp (read-to-terminating-char #\R (current-input-port))]
[no-prefix (cddr resp)] ;; Remove the "\esc[" from the response.
[row-lst (get-leading-digits no-prefix)]
[row (string->number (list->string row-lst))]
[last-part (drop no-prefix (+ 1 (length row-lst)))]
[cols-digits (get-leading-digits last-part)]
[col (string->number (list->string cols-digits))])
(list row col)))
The request-cursor-position
procedure uses the same ANSI command as you tried (\x1b;
is the Scheme way of writing the ESCAPE
character.) The response, containing the row and column of the cursor location, is returned by the terminal in the form you observed. That's what the read-to-terminating-character
procedure does. In this case, the response is terminated by an R
character, as you saw.
From there, it is just a matter of parsing the row and column numbers from the response. My procedure returns the results in a list containing the row and column, in that order.
For me to get these commands to work reliably, I have to set the terminal up in "raw mode". The standard "cooked mode" just gets messed up by the control strings.
Here's a little test program that shows how to use the procedure.
(import
(scheme)
(srfi s27 random-bits)
(ansi-terminal))
(define (random-in-range min max)
(+ min (random-integer (+ 1 (- max min)))))
(define (test-one-position row col)
(move-cursor-to row col)
(let ([pos-list (request-cursor-position)])
(when (or (not (= row (car pos-list)))
(not (= col (cadr pos-list))))
(display "\rError: output: ") (display pos-list)
(display " not equal to input ")
(display (list row col)) (display "\r") (newline)
(exit 0))))
(define (test-random-positions rows cols)
(let loop ([num-tests 100]
[test-row (random-in-range 1 rows)]
[test-col (random-in-range 1 cols)])
(when (not (zero? num-tests))
(test-one-position test-row test-col)
(loop (- num-tests 1)
(random-in-range 1 rows)
(random-in-range 1 cols)))))
(define (run-tests)
(dynamic-wind
(lambda ()
(enable-raw-mode))
(lambda ()
(display "Testing...")
(random-source-randomize! default-random-source)
(let ([term-size (window-size)]
[pos (request-cursor-position)])
(test-random-positions (car term-size) (cadr term-size))
(move-cursor-to (+ 1 (car pos)) 1)
(display "All tests successful!")))
(lambda ()
(disable-raw-mode))))
Running the program produces results like this:
on branch: (main) ! chez
Chez Scheme Version 9.5.8
Copyright 1984-2022 Cisco Systems, Inc.
> (load "test-ansi-pos.ss")
> (run-tests)
Testing...
All tests successful!
>
Hope this helps even though it isn't in Python.