虚拟终端序列


Update 2021/11/24 : 针对 Windows 11 进行部分微调

注意:本文已停止更新,不再保证内容正确性!

零. 简介

  1. 什么是虚拟终端序列?

虚拟终端序列是控制字符序列,当写入输出流时,可以控制光标移动、颜色/字体模式和其他操作。 在输入流上也可以接收序列,以响应输出流查询信息序列,或在设置适当模式时作为用户输入的编码。 ---控制台虚拟终端序列

一. 前置操作

对于 Windows, 需要使用以下代码以启用虚拟终端序列(注意:较老版本 Windows 不支持虚拟终端序列)

// 需要包含头文件 Windows.h
DWORD outmode, errmode, inmode;

// 全局变量, 类型为 HANDLE
g_hstdin = GetStdHandle(STD_INPUT_HANDLE);
g_hstdout = GetStdHandle(STD_OUTPUT_HANDLE);
g_hstderr = GetStdHandle(STD_ERROR_HANDLE);

GetConsoleMode(g_hstdin, &g_old_inmode);
GetConsoleMode(g_hstdout, &g_old_outmode);
GetConsoleMode(g_hstderr, &g_old_errmode);

inmode = g_old_inmode | ENABLE_VIRTUAL_TERMINAL_INPUT; // 启用序列终端序列
//inmode &= ~(ENABLE_PROCESSED_INPUT); // 关闭由系统响应 Ctrl+C 的选项
//inmode |= ENABLE_MOUSE_INPUT; // 响应鼠标输入 (注: Windows 11 开始如果启用了虚拟终端序列则该选项不可用, 需要使用虚拟终端序列来启用)
//inmode &= ~(ENABLE_QUICK_EDIT_MODE); // 关闭快速编辑模式以收到鼠标操作
outmode = g_old_outmode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
errmode = g_old_errmode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;

SetConsoleMode(g_hstdin, inmode);
SetConsoleMode(g_hstdout, outmode);
SetConsoleMode(g_hstderr, errmode);

推荐关闭 行输入模式(行输入模式下, 调用读入函数后会一直等待换行符, 通过下面代码可以关闭/开启行输入模式)

static void set_input_line_mode(int in_flag)
{
#ifdef _WIN32
    DWORD mode;
    GetConsoleMode(g_hstdin, &mode);
    if (in_flag)
    {
        mode |= ENABLE_LINE_INPUT;
    }
    else
    {
        mode &= ~ENABLE_LINE_INPUT;
    }
    SetConsoleMode(g_hstdin, mode);
#else
    struct termios oldt;
    tcgetattr(STDIN_FILENO, &oldt);
    if (in_flag)
    {
        oldt.c_lflag |= ICANON;
    }
    else
    {
        oldt.c_lflag &= ~ICANON;
    }
    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
#endif
}

推荐关闭 输入内容回显(默认情况下你输入的内容会显示在屏幕上, 使用下面的代码以关闭/开启回显)

static void set_input_echo(int in_flag)
{
#ifdef _WIN32
    DWORD mode;
    GetConsoleMode(g_hstdin, &mode);
    if (in_flag)
    {
        mode |= ENABLE_ECHO_INPUT;
    }
    else
    {
        mode &= ~ENABLE_ECHO_INPUT;
    }
    SetConsoleMode(g_hstdin, mode);
#else
    struct termios oldt;
    tcgetattr(STDIN_FILENO, &oldt);
    if (in_flag)
    {
        oldt.c_lflag |= ECHO;
    }
    else
    {
        oldt.c_lflag &= ~ECHO;
    }
    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
#endif
}

( getch 不是跨平台的, 我们关闭行输入模式和回显, 这样用 getchar 就可以起到类似 getch 的效果)

二. 常用序列

注意 : 这里所说的行是指输出在屏幕上的行,而不是以换行符为结尾的行.

注意2 : 本文中 ESC 指代的是特殊字符 \033(\x1b)

  1. [ 表 0-0 ] 用于操作光标的序列 (部分序列移至 [ 表 1-0-2 ])

    序列 行为 默认值 备注
    ESC[nA 光标上移 n n=1
    ESC[nB 光标下移 n n=1
    ESC[nC 光标前(左)移 n n=1
    ESC[nD 光标后(右)移 n n=1
    ESC[nE 光标下移 n 行后移动到开头 n=1
    ESC[nF 光标上移 n 行后移动到开头 n=1
    ESC[nG 光标移动到当前行的第 n n=1
    ESC[nd 光标移动到当前列的第 n n=1
    ESC[x;yH 光标移动到第 x 行, 第 y x=1; y=1
    ESC[x;yf 同上 同上 推荐使用上方序列
    ESC[sESC7 保存光标位置
    ESC[uESC8 移动光标到上一次保存的位置 如果之前没有保存过, 一般会移动到第一行第一列
  2. [ 表 1-0 ] 用于控制显示的序列

    序列 行为 默认值 备注
    ESCH 将制表符宽度设为第一列到当前光标所在列的距离
    ESC[nJ 根据 n 擦除当前显示屏幕上的部分内容 n=0 对于 n ,为 0 表示擦除从当前光标位置(含)到 屏幕显示区域/行 的末尾; 为 1 表示擦除从 屏幕显示区域/行 的开始到当前光标位置(含); 为 2 表示擦除 整个屏幕显示区域/整行
    ESC[nK 根据 n 擦除当前行的部分内容 n=0 见上
    ESC[argsm 根据 args 的内容设置显示效果 args="0" args 应为多个由 数字+;(最后一个数字不加;)组成的字符串. 数字所代表的含义见 [ 表 1-0-0 ]
    ESC]4;s;rgb:r/g/bESC\ 将当前终端颜色表中索引为 s 的颜色设为 RGB(r,g,b) 此处的 r,g,b 为 0-255 的 16 进制数(即 0-ff)
    ESC(0 指定字符集为 DEC 线条绘制 详见 [表 1-0-1]
    ESC(B 指定字符集为US ASCII 默认的字符集
    ESC]0; string ESC\ESC]2; string ESC\ 设置窗口标题为string 字符串长度不应超过 255
    ESC[?nh 启用 n 详见 [ 表 1-0-2 ]
    ESC[?nl 关闭 n 同上
    • [ 表 1-0-0 ] ESC[argsmargs 所填数字的含义: (带*表示该选项部分终端不支持)

      数字 说明 关闭该属性的数字
      0 恢复到默认值
      1 粗体/提高亮度 22
      2* 低亮度 22
      3* 斜体 23
      4 下划线 24
      5* 以较慢的速度闪烁 25
      6* 以较快的速度闪烁 25
      7 反显(以背景色显示文字, 以前景色显示背景) 27
      8* 隐藏 28
      9* 划掉 29
      10-20* 将终端字体设置为第 10-20 个代替字体(几乎无支持)
      21* 双下划线 24
      26* 保留, 部分终端将其解释为设置比例间距 (这里不详讲) 50
      30 设置前景色为黑色 (该值 +10 为设置背景色; +60 为设置为该颜色的亮色版本; +10+60 为设置背景色为该颜色的 亮色版本* ) 这些颜色的具体呈现效果取决于当前终端的颜色表
      31 红色
      32 绿色
      33 黄色
      34 蓝色
      35 品红
      36 蓝绿色
      37 白色
      38 保留给未来的标准, 现标准规定表示使用扩展颜色(详见 [ 表 1-0-0-0 ])
      39 默认前景色(使用亮色版本将不起作用)
      51* 框架 (framed 直译, 不太清楚什么意思) 54
      52* 包围 54
      53* 上划线 55
      56-59* 保留给未来的标准
      60* 表意文字下划线或右边线 65
      61* 表意文字双下划线或双右边线 65
      62* 表意文字上划线或左边线 65
      63* 表意文字双上划线或双左边线 65
      64* 表意文字重音符号 65
    • [ 表 1-0-0-0 ][ 表 1-0-0 ] 中 38/48 的解释:

      序列 说明
      38;2;r;g;b 将前景色设为 RGB 颜色 RGB(r,g,b)
      48;2;r;g;b 将背景色设为 RGB 颜色 RGB(r,g,b)
      38;5;s 将前景色设为 88 或 256 颜色表中索引为 s 的颜色(一般为 255 色)
      48;5;s 将背景色设为 88 或 256 颜色表中索引为 s 的颜色(一般为 255 色)
    • [ 表 1-0-0-1 ] 255 颜色表的说明
      Windows 下某一终端的 255 颜色表
      win_eg_255_col_table
      该图仅供参考, 实际颜色以具体终端的实现为准.

      255 颜色表的索引通常含义如下: (一些终端使用特定颜色表, 一些将 RGB 颜色线性映射到索引表上, 一些...)

    • [表 1-0-0-2]

      索引范围 说明
      0-15 16 种基础颜色
      16-231 6*6*6=216 种颜色. (HTML中也被叫做网页安全色) 一般来说, RGB(r,g,b) 的索引值为 16+(r*36/51)+(g*6/51)+b/51 .
      232-255 24 种灰度颜色. 一般来说, 对于 r,g,b 相等的颜色, 其索引值为 232+(r-8)*24/247
    • [ 表 1-0-1 ] 设置为 DEC 线条绘制后, 下列字符的输出会被替换 (带*表示部分终端无法正常显示)

      字符 替换为
      ` * ?
      a * ?
      b * ?
      c * ?
      d * ?
      e * ?
      f * °
      g * ±
      h * (NL)
      i * ?
      j
      k
      l
      m
      n
      o * ?
      p * ?
      q
      r * ?
      s * ?
      t
      u
      v
      w
      x
      y
      z
      { π
      |
      }
      ~ * ·
    • [ 表 1-0-2 ] n 的说明 (部分常见属性)

      n 说明 备注
      9 在按下鼠标时发出序列 (使用较少, 这里不详讲)
      12 开始光标闪烁
      25 显示光标 终端窗口大小被改变时可能会重新设置为显示
      1000 在鼠标按下时生成序列 对于 Windows 的大部分终端, 该表中有关鼠标的选项绝大多数是无效的. (Windows 10 下鼠标操作生成的序列一般是 SGR 模式) (Windows 11 开始允许通过这些序列启用鼠标, 默认序列模式是 UTF-8 模式)
      1003 鼠标有任意操作时生成序列
      1004* 当终端窗口失去焦点时生成序列
      1005 设置生成的鼠标序列为 UTF-8 模式
      1006 设置生成的鼠标序列为 SGR 模式
      1047 使用备用缓冲区但不恢复光标属性 在备用缓冲区上的操作不会影响到主缓冲区.(常见于需要调用终端的程序, 比如 vim) 注意: 在部分 Windows 平台下切换到该缓冲区后如果在窗口大小改变时同时进行大规模输出有可能导致崩溃. (原因未知, 猜测是因为窗口线程接收到改变大小的信息为缓冲区重新分配内存, 同时程序又一直在写入, 导致的内存错误. Windows 11 貌似修复了这个错误)
      1049 使用备用缓冲区并恢复光标属性 同上
  3. 从输入缓冲区获取到的序列

    • [ 表 2-0 ] 键盘按键

      按键 序列
      对应字符的 ASCII 码在 0-127 范围的按键 对应值
      Up (Up 及以下方均为控制按键) ESC[A
      Down ESC[B
      Right ESC[C
      Left ESC[D
      End ESC[F
      Home ESC[H
      Insert ESC[2~
      Delete ESC[3~
      Page Up ESC[5~
      Page Down ESC[6~
      F1 ESCOP
      F2 ESCOQ
      F3 ESCOR
      F4 ESCOS
      F5 ESC[15~
      F6 ESC[17~
      F7 ESC[18~
      F8 ESC[19~
      F9 ESC[20~
      F10 ESC[21~
      F11 ESC[23~
      F12 ESC[24~

      如果按下按键同时额外按下了 Ctrl/Alt/Shift 键, 序列为有下列变化:

      • 在 Windows 下, Enter 键会产生 \r 字符, 使用 Ctrl+Enter 会产生 \n 字符. (在关闭行输入模式的情况下)

      • Ctrl+Space 会生成 \0(^@) 字符, 但一些终端貌似不会收到该字符

      • Ctrl+A->Z会产生值为 1-26 的字符 (A-Z 不区分大小写), Ctrl+[, +\, +], +^, +_ 会产生值为 27-31 的字符 (当按下该部分字符的同时按下 Alt 不会产生任何序列)

      • Alt+ (ASCII 码在 0-127 的字符) 会产生 ESC字符(如按下 Ctrl+Alt+A 会产生 ESC\1, Alt+a 会产生 ESCa)

      • 对于控制按键, 如果按键是 Up/Down/Right/Left/End/Home/F1-F4 则先将原先对应序列中的 ESC[/ESCO 替换为 ESC[1; . 然后在序列中最后一个数字后加上 ;n

        [ 表 2-0-0 ] n 应设为的值

        额外按下的键 n n-1 (通过 n-1 可以较快的判断按键情况)
        Shift 2 1=1<<0
        Alt 3 2=1<<1
        Alt+Shift 4 3=1|2
        Ctrl 5 4=1<<2
        Ctrl+Shift 6 5=4|1
        Ctrl+Alt 7 6=4|2
        Ctrl+Alt+Shift 8 7=4|2|1

        (如按下 Ctrl+Alt+F1 会产生 ESC[1;7P , 按下 Ctrl+F12 会产生 ESC[24;5~)

      • 如果是 Esc 键与这些键同时按下, 除了单独按下 Shift+Esc 会产生序列 ESC ,其他操作不会产生任何序列

    • 鼠标操作 (这里的序列为 SGR 模式)

      如果启用了鼠标操作, 当鼠标在当前窗口显示范围内进行操作时, 会产生序列 ESC[<flag;line;column state .

      • line , column 分别表示鼠标进行该操作时所在的行和列

      • statem 表示该操作时鼠标没有按下, 为 M 表示该操作时鼠标被按下 (如果是滚轮滚动, 无论滚轮是否按下该值均为 M)

      • flag 描述该操作的具体信息, 在二进制意义下(这里规定最低位为第一位),

        前 2 位 如果 第 7 位 为 0 时描述该操作是由鼠标的那个按键产生的, 0 为左键, 1 为 滚轮, 2 为 右键, 3 表示没有键按下; 第 7 位 为 1 时描述滚动方向, 0 为向前, 1 为 向后

        第 3 位 标识该操作产生时 Shift 键是否按下 (Windows 下按下 Shift 对终端进行鼠标操作会被认为是选择文本, 所以该位总为0)

        第 4 位 标识该操作产生时 Alt 键是否按下

        第 5 位 标识该操作产生时 Ctrl 键是否按下

        第 6 位 标识该操作是否为鼠标移动操作

        第 7 位 标识该操作是否为鼠标滚轮操作

      注: Windows 下部分终端无法接受到该序列, 需要使用 ReadConsoleInput 来读取鼠标操作, 相关信息可以参考我的另一篇博客(咕了,可能短时间内不会写)


参考资料:

  • https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
  • Console Virtual Terminal Sequences - Windows Console | Microsoft Docs
  • ECMA-48 - Ecma International (ecma-international.org)