在命令行中不是所有的输入字符都会被当成字符处理,其中有一些字符会被识别为控制指令,在很多控制指令中以ESC(ASCII码表第27个字符)开头的这部分就叫做转义序列(escape codes),监听鼠标的事件就在其中。
ESC字符在各种编程语言中可能需要用不同的形式输出:
- 八进制:
\033
- 特殊字符:
\e
- Unicode:
\u001b
- 十六进制:
\x1B
ANSI 转义序列是一种带内信号标准,用于控制光标位置、颜色、字体样式以及视频文本终端和终端模拟器上的其他选项。某些字节序列(多数以 ASCII 转义字符和括号字符开头)被嵌入到文本中。终端将这些序列解释为命令,而不是逐字显示的文本。 ANSI 序列于 20 世纪 70 年代推出,以取代供应商特定的序列,并在 20 世纪 80 年代初开始在计算机设备市场上普及。虽然硬件文本终端在 21 世纪已越来越少,但 ANSI 标准的相关性依然存在,因为绝大多数终端仿真器和命令控制台至少都能解释 ANSI 标准的一部分。
转义序列根据ESC后面接的字符,还能分为很多的子序列,负责不同的功能
代码 | 缩写 | 全称 | 描述 |
---|---|---|---|
ESC N | SS2 | Single Shift Two | SS2用于在多字节字符集环境中临时切换到另一字符集。如在某些终端中,使用SS2可以临时调用第二字符集以显示特殊字符。 |
ESC O | SS3 | Single Shift Three | SS3类似于SS2,但用于调用第三字符集。如在多字节字符环境中,SS3可以用于访问第三字符集的符号或字符。 |
ESC P | DCS | Device Control String | DCS用于发送控制字符串到设备,以执行特定的设备功能。如一些打印机可以通过DCS接收定制的打印命令。 |
ESC [ | CSI | Control Sequence Introducer | CSI引入一系列控制命令,用于光标移动、字符渲染等。如\033[31m 将文本颜色设置为红色。 |
ESC \ | ST | String Terminator | ST用于标记字符串的结束。 |
ESC ] | OSC | Operating System Command | OSC用于与操作系统进行通信,通常用来改变终端标题或配色方案。如\033]0;New Title\ 可以将终端窗口标题设置为“New Title”。 |
ESC X | SOS | Start of String | SOS标记一个字符串的开始,通常在特定协议中使用。 |
ESC ^ | PM | Privacy Message | PM用于传输隐私信息,通常不被显示。 |
ESC _ | APC | Application Program Command | APC用于向应用程序发送命令。 在一些复杂的终端应用中,APC用于触发特定的应用功能或脚本。 |
更多细节可以参考Fe_Escape_sequences - Wikipedia,本篇博客我们将重点关注CSI 序列
Note:
windows端cmd的echo指令默认不支持ansi转义序列(需要手动开启),可使用python的print或者sys.stdout.write,linux端bash中可以使用echo -e 指令
CSI序列
ANSI转义序列中以 ESC [
开头的叫作 Control Sequence Introducer
,简写为 CSI。以 CSI 开头的指令有很多,大致可分四类:
-
光标移动指令
代码 描述 作用 CSI n A 光标上移(Cursor Up) 如 \033[2A
光标上移2行,如果在边界就无效。CSI n B 光标下移(Cursor Down) 如 \033[2B
光标下移2行,如果在边界就无效。CSI n C 光标前(右)移(Cursor Forward) 如 \033[2C
光标右移2列,如果在边界就无效。CSI n D 光标后(左)移(Cursor Back) 如 \033[2D
光标左移2列,如果在边界就无效。CSI n E 光标移下移,置于行首(Cursor Next Line) 如 \033[2E
光标下移2行,置于行起始位置。CSI n F 光标移上移,置于行首(Cursor Previous Line) 如 \033[2F
光标上移2行,置于行起始位置。CSI n G 设置光标列坐标(Cursor Horizontal Absolute) 如 \033[2G
光标行位置不变,列位置变为2。CSI n;mH 设置光标的行列坐标(Cursor Position) 如 \033[2;3H
位置为2行,3列(左上角为0行0列)。CSI s 保存当前光标的位置(Save Cursor Position) 如当前光标坐标为2行3列,使用 \033[s
后会保存这个位置。CSI u 恢复光标位置(Restore Cursor Position) 恢复光标到之前保存的坐标。 -
清屏指令
代码 描述 作用 CSI n J 擦除显示(Erase in Display),细节由n的取值控制。 清除屏幕的部分区域。如果是0(或缺失),则清除从光标位置到屏幕末尾的部分。如果是1,则清除从光标位置到屏幕开头的部分。如果是2,则清除整个屏幕(在D0SANSI.SYS中,光标还会向左上方移动)。如果是3,则清除整个屏幕,并删除回滚缓存区中的所有行(这个特性是xterm添加的,其他终端应用程序也支持)。 CSI n K 擦除行(Erase in Line),细节由n的取值控制。 清除行内的部分区域。如果是0(或缺失),清除从光标位置到该行末尾的部分。如果是1,清除从光标位置到该行开头的部分。如果是2,清除整行。光标位置不变。 -
字符渲染指令(SGR - Select Graphic Rendition)
用来渲染字符,如设置字符的颜色或背景色,添加下滑线,加粗等,格式为
CSI n m
其中n为[1, 107]的数字,随着支持的颜色的增多后被拓展成 多个用;
隔开的数组。目前很多终端都能渲染24bit颜色(使用指令\033[38;2;⟨r⟩;⟨g⟩;⟨b⟩m
其中r、g、b每个8位范围0-255和图片的像素一样)。在命令行可以打印环境变量COLORTERM或TERM判断支持的颜色。如果包含24bit或者truecolor即支持24bit颜色,如果包含256则支持8bit颜色,否则为4bit。
关于更多渲染颜色的细节可以参考ANSI escape code - Wikipedia
-
终端控制指令
关于鼠标事件的指令格式为
CSI ? n h
,其中n为可选数字:- 1000 模式:报告鼠标点击和按钮释放事件。
- 1002 模式:报告鼠标所有按键的点击、释放和拖动事件(即按下按钮移动)。
- 1003 模式:报告所有鼠标移动事件(无论是否按下按钮)和所有按键的点击事件。
- 1004 模式:报告焦点事件(当终端获得或失去焦点)。
- 1005 模式:启用 UTF-8 编码来报告鼠标位置,适用于大于 223 的坐标。
- 1006 模式:使用 SGR 编码格式(
\033[<b>;<x>;<y>M
或\033[<b>;<x>;<y>m
)报告鼠标事件,支持更高的坐标范围。- 其中b为按键事件,左键按下时值为0(一帧,可以用来判断点击事件)然后变为32,右键按下时值为2然后变为34,滚轮键按下时值为1然后变为33,滚轮向下滚动值为64,向上滚动值为65,按键都未按下时值为35
- x为列坐标
- y为行坐标
- 1015 模式:使用 URXVT 编码格式(
\033[<b>;<x>;<y>R
)报告鼠标事件。
要监听鼠标事件可以使用这条序列
\033[?1003h\033[?1015h\033[?1006h
,当我们在bash终端输入时会发现鼠标的每个动作都会在终端输出一个SGR编码格式(1006模式)的序列(windows端需要开启虚拟终端才能使用),如果需要关闭,就输入一串相同的序列把其中的h换成l。接下来只要关注如何在程序中捕获这种包含鼠标信息的响应序列。
Note: 传统上,Windows控制台不支持ANSI转义序列,而是使用自己的API进行格式化和控制。然而,许多跨平台应用程序(如那些在Unix系统上运行的程序)依赖于ANSI序列来实现控制台输出的格式化。为了提高跨平台兼容性,Microsoft在Windows 10中引入了对虚拟终端的支持。windows端开启虚拟终端的代码为:
#include "windows.h" HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); DWORD dwMode = 0; GetConsoleMode(hInput, &dwMode); dwMode |= ENABLE_VIRTUAL_TERMINAL_INPUT; // 增加控制台模式的选项:启用虚拟终端 SetConsoleMode(hInput, dwMode);
获取响应序列
首先我们需要解决两个问题:
-
行缓冲
默认开启,该模式下命令行需要等待用户敲下回车后才把输入发送给程序,对于实时捕获鼠标移动的场景不合适。
-
回显
默认开启,该模式下用户输入的字符都会显示在命令行上,鼠标的响应序列也会显示,如上面的动图所示,应该禁止。
在Unix系统(linux和mac)中可以使用termios。
#include "termios.h"
termios raw = orig_termios;
raw.c_lflag &= ~(ICANON | ECHO); // 禁用标准模式(行缓冲)和回显
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
在windows端使用windows API。
#include "windows.h"
HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
DWORD dwMode = 0;
GetConsoleMode(hInput, &dwMode);
dwMode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT); // 禁用标准模式(行缓冲)和回显
SetConsoleMode(hInput, dwMode);
接下来可以直接使用getchar获取到响应序列
std::string csi;
while (true) {
char ch = getchar();
csi += (ch == '\033') ? 'E' : ch;
// 序列以m或M结尾
if (ch == 'm' || ch == 'M') {
std::cout << "CSI: " << csi << std::endl;
// 解析序列获取 x,y,按键 等信息
// auto info = get_mouse_info(csi);
csi = "";
}
if (ch == 'q') {
break;
}
}
然后就可以解析响应序列。
std::tuple get_mouse_info(std::string csi) {
std::regex re("\\<(\\d+);(\\d+);(\\d+)([m|M])");
std::smatch match;
if (std::regex_search(csi, match, re)) {
int b = std::stoi(match[1]);
int x = std::stoi(match[2]);
int y = std::stoi(match[3]);
return std::make_tuple(x, y, b);
} else {
return std::make_tuple(-1, -1, -1);
}
}
//b的取值
#define MOUSE_EVENT_BUTTON_LEFT_PRESS (32)
#define MOUSE_EVENT_BUTTON_LEFT_CLICK (0)
#define MOUSE_EVENT_BUTTON_RIGHT_PRESS (34)
#define MOUSE_EVENT_BUTTON_RIGHT_CLICK (2)
#define MOUSE_EVENT_BUTTON_MIDDLE_PRESS (33)
#define MOUSE_EVENT_BUTTON_MIDDLE_CLICK (1)
#define MOUSE_EVENT_BUTTON_SCROLL_UP (65)
#define MOUSE_EVENT_BUTTON_SCROLL_DOWN (64)
获取到鼠标坐标后,可以做的操作就非常多,比如我们可以做一个画板,当鼠标左键按下后就把光标移动到鼠标位置然后输出字符。
参考
ArthurSonzogni/FTXUI: :computer: C++ Functional Terminal User Interface. :heart:
linux - how to get MouseMove and MouseClick in bash? - Stack Overflow
Comments | NOTHING