3

I am trying to get cursor position from a little c program so after googling I found this ANSI code \x1b[6n. It should return x and y location of cursor ( if I am not wrong) So printf("\x1b[6n"); is giving me output : ;1R I can't understand the output in terms of x and y location.

Edit : Platform is Linux (xterm)

Thomas Dickey
  • 51,086
  • 7
  • 70
  • 105
Aux
  • 85
  • 2
  • 3
  • 10
  • There's a lot more to getting the cursor position in C than you anticipated. Since the C standard doesn't have any concept of a mouse cursor, the answer to this question will depend on what environment and operating system you're using. If Linux/Unix/BSD with the X Window System, see [this](https://stackoverflow.com/q/44078725/3258851). Otherwise please specify. – Marc.2377 Jun 16 '18 at 03:32
  • For MS Windows, see [`GetCurPos()`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms648390%28v=vs.85%29.aspx). – Marc.2377 Jun 16 '18 at 03:35
  • See also [How can I get the current mouse (pointer) position co-ordinates in X](https://stackoverflow.com/q/3585871/3258851). – Marc.2377 Jun 16 '18 at 03:48
  • Various pages tell you about ANSI codes. Here is one: https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences – Arndt Jonasson Jun 16 '18 at 04:06
  • I know about ANSI codes but I am asking about output which is giving me headache because I am unable to understand it – Aux Jun 16 '18 at 05:11
  • I have this esc. seq. in my old code working properly, but i can't make small example :D Sorry. – purec Jun 16 '18 at 12:33
  • Which code ? Can you post on pastebin ? – Aux Jun 16 '18 at 12:47
  • I'll try to post an answer. In short, you need to turn off echo, and parse this output. – purec Jun 16 '18 at 13:22

2 Answers2

9

On some terminals, such as DEC VT102 and later VTs, and on many terminal emulators, notably XTerm and its many imitations, sending Esc [ 6 n will make the terminal respond with Esc [ row ; column R, where row and column are decimal representations of the position of the text cursor.

So your terminal emulator is not replying with ;1R; it is replying correctly, but the readline routines are eating Esc [ and the decimal digits up to ; (and flash the screen or beep the bell, depending on configuration).

Here is a nice Bash command to illustrate:

out=''; \
echo $'\e[6n'; \
while read -n 1 -s -t 1; do out="$out$REPLY"; done < /dev/tty; \
echo -n "$out" | od -A x -t x1z -v

Running this gives:

$ out=''; \
> echo $'\e[6n'; \
> while read -n 1 -s -t 1; do out="$out$REPLY"; done < /dev/tty; \
> echo -n "$out" | od -A x -t x1z -v

000000 1b 5b 31 36 3b 31 52                             >.[16;1R<
000007

Note that the answer does not necessarily come on standard input: the answer comes from the terminal even if standard input is redirected.

At the inquirer's request, here is a small C program which partially duplicates the functionality of the scriptlet above. Note that the program does not handle setting the terminal in raw mode and back to cooked mode; this must be handled outside the program, as indicated below.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main (void)

{
  int ttyfd = open ("/dev/tty", O_RDWR);
  if (ttyfd < 0)
    {
      printf ("Cannot open /devv/tty: errno = %d, %s\r\n",
        errno, strerror (errno));
      exit (EXIT_FAILURE);
    }

  write (ttyfd, "\x1B[6n\n", 5);

  unsigned char answer[16];
  size_t answerlen = 0;
  while (answerlen < sizeof (answer) - 1 &&
         read (ttyfd, answer + answerlen, 1) == 1)
    if (answer [answerlen ++] == 'R') break;
  answer [answerlen] = '\0';

  printf ("Answerback = \"");
  for (size_t i = 0; i < answerlen; ++ i)
    if (answer [i] < ' ' || '~' < answer [i])
      printf ("\\x%02X", (unsigned char) answer [i]);
    else
      printf ("%c", answer [i]);
  printf ("\"\r\n");

  return EXIT_SUCCESS;
}

Assuming this little program is answerback.c:

$ gcc -Wall -Wextra answerback.c -o answerback
$ stty raw -echo; ./answerback; stty sane

Answerback = "\x1B[24;1R"
$ _
AlexP
  • 4,370
  • 15
  • 15
  • I am not familiar with bash so your illustration is not helping me can you give same in C. – Aux Jun 16 '18 at 12:39
  • 1
    @Aux, added C programlet. – AlexP Jun 16 '18 at 13:51
  • I doubt you have to do that loop to read from the terminal. simply reading from the terminal should best. read(tty,answer,4096); –  Jul 26 '19 at 12:39
  • @AlexP I added a comment to the answer above about a problem, any help would be greatly appreciated :D – Koen Mar 27 '21 at 20:24
8
#include <stdio.h>
#include <termios.h>

int
main() {
 int x = 0, y = 0;
 get_pos(&y, &x);
 printf("x:%d, y:%d\n", x, y);
 return 0;
}

int
get_pos(int *y, int *x) {

 char buf[30]={0};
 int ret, i, pow;
 char ch;

*y = 0; *x = 0;

 struct termios term, restore;

 tcgetattr(0, &term);
 tcgetattr(0, &restore);
 term.c_lflag &= ~(ICANON|ECHO);
 tcsetattr(0, TCSANOW, &term);

 write(1, "\033[6n", 4);

 for( i = 0, ch = 0; ch != 'R'; i++ )
 {
    ret = read(0, &ch, 1);
    if ( !ret ) {
       tcsetattr(0, TCSANOW, &restore);
       fprintf(stderr, "getpos: error reading response!\n");
       return 1;
    }
    buf[i] = ch;
    printf("buf[%d]: \t%c \t%d\n", i, ch, ch);
 }

 if (i < 2) {
    tcsetattr(0, TCSANOW, &restore);
    printf("i < 2\n");
    return(1);
 }

 for( i -= 2, pow = 1; buf[i] != ';'; i--, pow *= 10)
     *x = *x + ( buf[i] - '0' ) * pow;

 for( i-- , pow = 1; buf[i] != '['; i--, pow *= 10)
     *y = *y + ( buf[i] - '0' ) * pow;

 tcsetattr(0, TCSANOW, &restore);
 return 0;
}
Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
purec
  • 308
  • 2
  • 6
  • 1
    one question , in which case `if(i<2)` will be true in your code – Aux Jun 17 '18 at 16:25
  • 2
    I don't remember. – purec Jun 17 '18 at 18:15
  • 1
    I mean Whats the reason or logic you thought before writing this line `if(i<2)` – Aux Jun 18 '18 at 02:16
  • This doesn't work if there's another thread reading stdin, so how can I get position if another thread is doing fgets()? – zhao Jan 12 '19 at 16:14
  • @zhao, AlexP's answer provides a good solution for that case. Use another connection to your `tty`. However, AlexP example does not include the `ICANON` & `ECHO` removal. You will still need those. – Alexis Wilke Jun 25 '19 at 09:43
  • @Aux, the first `for()` has `i -= 2`, so he probably wanted to make sure that would not blow up everything. The truth is he should be checking `i >= 0` before doing `buf[i]`. His code has very strong assumptions which if wrong will crash your software possibly badly... – Alexis Wilke Jun 25 '19 at 09:45
  • I have a little question/problem with this, this solution works perfectly... until I added another program that pipes its output into my program. I also tried it with AlexP solution below, but this still did not fix it, even though I don't use stdin anymore to get the location right? (since I assume that is the problem after adding the pipe). The behaviour after piping input in the program is that "^[[7;1R" (or similar) is displayed on the screen, and my ```fgetc``` call after this method doesn't read any input anymore. Any ideas @purec maybe? – Koen Mar 27 '21 at 20:21
  • After testing it seems that setting raw mode in code might be the problem? running with ```stty raw -echo``` seems to work, but using the code in this answer to set raw mode does not with a pipe – Koen Mar 27 '21 at 20:32
  • @KoenduBuf: "Pipe"? Pipes are not terminals. What are you trying to do with a pipe? – AlexP Mar 27 '21 at 22:34
  • @AlexP I mean that running ```./compiled``` prints ```Answerback = ...``` as by your solution, but running ```echo "hey" | ./compiled``` prints the right answer, but not ```Answerback = ...``` and also doesnt terminate (so I guess the read call is not getting input anymore). On the other hand, again, running ```stty raw -echo; echo "test" | ./compiled; stty sane``` does print ```Answerback = ...``` so my current guess is that ```term.c_lflag &= ~(ICANON|ECHO); tcsetattr(0, TCSANOW, &term);``` doesnt work like stty raw -echo, but I don't know.. (https://wtools.io/paste-code/b4uc) – Koen Mar 28 '21 at 14:05
  • 1
    @KoenduBuf: Why does it surprise you? It is the expected behavior. **Pipes are not terminals**; pipes not being terminals, they don't have a notion of raw vs. cooked modes. When you run `··· | ./compiled`, the file descriptor 0 of `./compiled` is a pipe; the calls to `tcgetattr()` and `tcsetattr()` will fail. – AlexP Mar 28 '21 at 14:56
  • @AlexP ahhh, I did not think about the difference in file descriptors, that explains it, thanks! Is there any way to make this work still? My goal is to pipe input into my program, which then gets parsed and displayed as a sort of animation, and then I would like a user to be able to press a key to pause the animation (for example), I only now realize that user input and the pipe would both use stdin, any way around this? The only thing I can come up with is getting data input on a different port, but that would be less user friendly (would be nice to just do ```data | myprogram```) – Koen Mar 28 '21 at 15:37
  • 1
    @KoenduBuf: If stdout is a terminal, you can use file descriptor 1. If stderr is a terminal, you can use file descriptor 2. (Use `isatty()` to find out.) If the program has a controlling terminal, that's `/dev/tty`. – AlexP Mar 28 '21 at 15:49
  • Thanks! That solved my problem and got me a whole new understanding of file descriptors :D – Koen Mar 28 '21 at 16:09