2

(Note: Applied to Windows 10 and Python 3+)

I have already read a couple of postings regarding how to get cursor position using ANSI escape codes, but they offer no solution, at least not for Python. Wikipedia (https://en.wikipedia.org/wiki/ANSI_escape_code) includes the following ANSI seq: in its list of cursor management:

ESC 6n: DSR (Device Status Report) Reports the cursor position (CPR) to the application as (as though typed at the keyboard) ESC[n;mR, where n is the row and m is the column.)

So, if you apply this, using print("\033[6n"), what happens is that the response is "pushed" to keyboard and only after the script terminates, you get the following at the MS DOS prompt: ^[[7;11R (Numbers depend on the case).

Now, I really don't see what does this serve for! So, my question is "Can we 'finally' get information about the cursor position using the mentioned conditions (Windows, ANSI codes and Python)?" ... So that I can close this chapter once for all!

Thomas Dickey
  • 51,086
  • 7
  • 70
  • 105
Apostolos
  • 3,115
  • 25
  • 28
  • No it isn't possible with ANSI escape codes. You'd have to use the [`curses.getsyx`](https://docs.python.org/3.8/library/curses.html#curses.getsyx) method instead after installing the [`wheel`](https://stackoverflow.com/questions/32417379/what-is-needed-for-curses-in-python-3-4-on-windows7) and `curses` package. – blhsing Sep 23 '20 at 16:10
  • I know that well. That's why I stressed "**using the mentioned conditions**". How more could I stress it? – Apostolos Sep 24 '20 at 09:48

2 Answers2

2

I found a solution. You'll have to use msvcrt, which is part of Python standard library.
I have tried this on Windows 10 version 2004, using Python 3.8.7.
This method does not output the cursor position to the keyboard input buffer. Instead it allows you to save it to a variable for further manipulation.

import msvcrt

# Added "\033[A" to move cursor up one line since 'print("\033[6n")' outputs a blank line
print("\033[A\033[6n")
buff = ""

keep_going = True
while keep_going:

    # getch() reads the keypress one character at a time,
    # since ANSI ESC[6n sequence fills the keyboard input buffer
    # 'decode("utf-8")' also works.
    buff += msvcrt.getch().decode("ASCII")

    # kbhit() returns True if a keypress is waiting to be read.
    # Once all the characters in the keyboard input buffer are read, the while loop exits
    keep_going = msvcrt.kbhit()

# Printing 'buff' variable outputs a blank line and no cursor position.
# By removing "\x1b" from the string in 'buff' variable,
# it will allow you to save the remaining string,
# containing the cursor position, to a variable for later use
newbuff = buff.replace("\x1b[", "")
print(newbuff)

Result:

1;1R
howdoicode
  • 779
  • 8
  • 16
  • What does this have to do with getting cursor position??? – Apostolos Sep 02 '21 at 09:11
  • ... After a long time. It **does** have to do with cursor position! The output means row 1, column 1! (I din't "decipher" it the first time. Sorry.) Great solution! And it can be simplified. Thank you. If this means something after that long time ... – Apostolos Jul 12 '22 at 11:50
0

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.

clartaq
  • 5,320
  • 3
  • 39
  • 49
  • It's never late in programming! One can always find better, more efficient solutions. Only that this is not the case. @howdoicode's solution, also using keyboard reading, is much more simple. And I have simplified it even more! Anyway, thank you very much for coming in to help after that long. A really appreciated it. – Apostolos Jul 12 '22 at 11:46