阅读设备状态报告 ANSI 转义序列回复。

18

我正在尝试使用以下代码在VT100终端中检索光标的坐标:

void getCursor(int* x, int* y) {
  printf("\033[6n");
   scanf("\033[%d;%dR", x, y);
}

我正在使用以下 ANSI 转义序列:

设备状态报告 - ESC[6n

报告光标位置到应用程序,就像在键盘上键入 ESC[n;mR 一样,其中 n 是行数,m 是列数。

代码编译成功并发送了 ANSI 序列,但是在接收到它之后,终端将 ^[[x;yR 字符串打印到了 stdout 而非 stdin,使我无法从程序中检索它:

terminal window

显然,该字符串是指定给程序的,所以我肯定做错了什么。有人知道吗?

4个回答

9

我请求获取光标位置。如果100毫秒后(这是任意的)没有得到回复,我就认为控制台不支持ANSI。

/* This function tries to get the position of the cursor on the terminal. 
It can also be used to detect if the terminal is ANSI.
Return 1 in case of success, 0 otherwise.*/

int console_try_to_get_cursor_position(int* x, int *y)
{
    fd_set readset;
    int success = 0;
    struct timeval time;
    struct termios term, initial_term;

    /*We store the actual properties of the input console and set it as:
    no buffered (~ICANON): avoid blocking 
    no echoing (~ECHO): do not display the result on the console*/
    tcgetattr(STDIN_FILENO, &initial_term);
    term = initial_term;
    term.c_lflag &=~ICANON;
    term.c_lflag &=~ECHO;
    tcsetattr(STDIN_FILENO, TCSANOW, &term);

    //We request position
    print_escape_command("6n");
    fflush(stdout);

    //We wait 100ms for a terminal answer
    FD_ZERO(&readset);
    FD_SET(STDIN_FILENO, &readset);
    time.tv_sec = 0;
    time.tv_usec = 100000;

    //If it success we try to read the cursor value
    if (select(STDIN_FILENO + 1, &readset, NULL, NULL, &time) == 1) 
      if (scanf("\033[%d;%dR", x, y) == 2) success = 1;

    //We set back the properties of the terminal
    tcsetattr(STDIN_FILENO, TCSADRAIN, &initial_term);

    return success;
}

5
你的程序可以运行,但需要等待EOL字符。
scanf是面向行的,所以在处理之前需要等待一个新行。尝试运行程序,然后按回车键。
解决方案是使用其他不需要新行读取输入的东西,然后使用sscanf解析值。
您还需要使stdin非阻塞,否则直到缓冲区满或stdin关闭才会收到输入。请参见此问题Making stdin non-blocking 在printf之后,您还应调用fflush(stdout);以确保它实际上已被写入(printf通常是行缓冲,因此没有换行符可能不会刷新缓冲区)。

2
有没有办法让 ANSI 命令完全不向终端输出 ^[[x;yR?我想在不影响终端屏幕的情况下静默地获取坐标。但是这会向终端写入内容(在创建文本 GUI 时不希望出现),从而更改光标的坐标(使其变得完全无用)。 - Witiko
这就是为什么 curses(和 ncurses)存在的原因,因此你不必担心所有这些细节。 - Craig
2
当然可以,但ncurses是完全的臃肿软件。这就是为什么我正在编写自己的轻量级ANSI转义序列库,它仅支持VT100。但是,看起来我只能下载ncurses并尝试反向工程来找到解决此问题的方法。 - Witiko
我认为ncurses要么在内部跟踪光标位置,要么在输出之前将其移动到已知位置。这是必须的,因为有些终端无法报告当前位置。 - Craig
发现在MSYS2/mintty上,fflush(stdin)也能解决问题。 - Keith

3

我相信您在stdin中真正得到了预期的响应。但是想象一下实际发生了什么:

  • 您将请求作为转义序列发送到stdout
  • 终端接收它并制定相应的答案同样作为转义序列
  • 答案被发送到stdin
  • 调用scanf,并通过shell重定向stdin,其中使用readline库进行交互和可编辑用户输入
  • readline 捕获 转义序列而不是将其传递给终端
  • readline重新制定它,没有ESC字符以防止控制序列的执行,而是只使用可打印字符使其可读
  • 修复后的答案到达scanf,但已经太晚了
  • 修复后的答案也会回显到stdout,以便用户可以立即看到自己输入的内容。

为了避免这种情况,请改用getc()==fgetc(stdin))循环。如果遇到ESC(0x1B),则将以下字符转储到字符串中,直到找到ESC序列的最终分隔符(在您的情况下为'n')。之后,您可以使用sscanf(esqString, formatString, ...)

但是在遇到循环之前,您需要使用termios更改为raw模式(请参阅下面的代码示例)。否则,什么也不会有所不同。


0
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>

void readCoord(void* row, void* col){
    int i = 0;
    char* input = malloc(10);
    printf("\e[6n");
    while(!_kbhit()) _sleep(1);
    while(_kbhit()) *(input + (i++)) = getch();
    *(input + i) = '\0';
    sscanf(input, "\e[%d;%dR", row, col);
}

void main(void){
    int i = 0, r, c;
    char* coord = malloc(10);
    printf("Hello");
    readCoord(&r , &c);
    printf("\nYour coordinate is (%d, %d)", c, r);
}

_kbhit() 用于检测输入(将DSR视为键盘键入),而getch() 用于从标准输入读取字符并将其删除。

该程序依赖于 conio.h,它不是一个标准库,因此不建议在可移植的C程序中使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接