文章

VT100/ANSI转义序列概述——在终端实现优雅的交互

搜索如何实现在控制台输出彩色文本,我们会见到输出代码中出现\033[ 或者\x1b[ 这样的形式,这其实就是VT100控制码的前缀,\033\x1b对应ANSI编码中的ESC控制字符。这篇文章,我将粗略介绍一下VT100/ANSI转义序列是什么,以及如何使用VT100/ANSI转义序列。

使用ANSI转义序列输出的像素画(我的mc皮肤大头)和DNA序列,代码在文末。

封面图来自rich库的展示图。

0 写在前面

如果只需要实现输出效果,各语言(比如python的rich库)几乎都有现成的库可用,相比自己书写转义序列,更推荐使用库实现输出效果。

1 VT100是什么

VT100图

图为VT100实机图,来自oldcomputr.com

--- title 一张简单的关系图 --- classDiagram 内核Kernal <|-- 壳Shell 壳Shell <|-- 控制台Console 壳Shell <|-- 终端Terminal 壳Shell <|-- 终端模拟器TerminalEmulator class 内核Kernal { 宏内核(Monolithic kernel) 微内核(Microkernel) 混合内核(Hybrid kernel) 外内核(Exokernel) } class 壳Shell { 图形用户界面(GUI) 命令行界面(CLI) } class 控制台Console { } class 终端Terminal { VT52 VT100 Tektronix 4014 哑终端(Dumb Terminal) 智能终端(Intelligent Terminal) 图形终端(Graphical Terminal) } class 终端模拟器TerminalEmulator { }

内核(Kernal):与硬件交互,管理硬件资源,为上层软件提供接口。

壳(Shell):访问内核提供的服务。

终端(Terminal):人机交互设备,其中最有名的属DEC公司的VT100智能终端,第一个采用了ANSI标准。

控制台(Console):特殊的终端,用于系统管理,相比终端一般拥有更高的权限,但现已基本不与终端作区分。

终端模拟器(Terminal Emulator):在现代,模拟传统终端的软件。

ANSI转义序列(ANSI Escape Code): 由ANSI标准化的转义序列,几乎所有终端模拟器都参考该标准。

有关区分这些概念的内容,可以查看知乎大佬的文章。有关ANSII的内容,网上有很多介绍,在此也不做赘述。

可以参考:VT100用户手册WIKI-ANSI转义序列ECMA-48XtermControlSequence

在接下来的内容中,提到VT100均指VT100控制码。

2 ANSI转义序列

2.1 ANSI转义序列的组成

在ANSII中ESC 控制字符八进制\033[ ,十六进制\x1b[

ANSI转义序列区分大小写。

一个序列通常是这样的:ESC [ 参数 ; 参数 ; 指令 ,如\033[10;20H 执行H(10,20) (该表述方法没有出现在官方文档中),将光标移动到第10行,第20列。

以下介绍的是常用的序列,有些没有在VT100的手册定义,有些没有在ECMA48定义,括号中的简写均为ECMA48中定义的简写(Acronym)。

2.2 光标位置控制

  • 光标上移 (CUU): \033[nA 移动光标向上n行

  • 光标下移 (CUD): \033[nB 移动光标向下n(1)行

  • 光标右移 (CUF): \033[nC 移动光标向右n(1)列

  • 光标左移 (CUB): \033[nD 移动光标向左n(1)列

  • 光标定位 (CUP): \033[y;xH\033[y;xf将光标移动到第y(1)行、第x(1)列

例如,ESC [ 10 ; 20 H 把光标移动到第 10 行的第 20 列位置。

下面这些不太常见:

  • 光标下移: \033D 移动光标向下移动1行,但不改变光标列位置

  • 光标下移: \033E 移动光标向下移动1行,并到行的起始位置

  • 光标上移: \033M 移动光标向上移动1行,但不改变光标列位置

  • 光标保存: \0337 保存当前光标位置

  • 光标返回: \0338 回到DECSC保存的光标位置

以及不在AT100中的两个序列:

  • 光标隐藏: \033[?25l 隐藏光标

  • 光标显示: \033[?25h 显示光标

2.3 擦除

本节所有的“光标”均包括光标字符

  • 页擦除 (ED): \033[sJ 擦除终端上的所有或部分内容,s(0)可选值如下:

    • 0: 从光标到终端末尾

    • 1: 从起始位置到光标

    • 2: 所有

  • 行擦除 (EL): \033[sK 擦除终端当前列的所有或部分内容,s(0)可选值如下:

    • 0: 从光标到行尾

    • 1: 从起始位置到光标

    • 2: 整行

2.4 字符属性

在这节中,ANSI转义序列和VT100相比发生了许多变化,在此不再标注ANSI转义序列与VT100的异同。

  • 选择图形渲染(SGR): \033[s1;s2;...;sNm 控制字符的渲染方式,s(0)可以传递多个,将依照参数的传递顺序改变渲染方式,s为0时将重置所有SGR设置的字符属性。s的具体含义在以下几节进行介绍

2.4.1 字体修饰

添加*号的参数代表该选项混合其他参数有不同效果。

  • 1: *加粗;。

  • 2: *弱化;

  • 3: 斜体

  • 4: 下划线

  • 5: *缓慢闪烁

  • 6: 快速闪烁

  • 7: 反显(交换前景色与背景色)

  • 8: 隐藏

  • 9: 删除线

  • 53: 上划线

以上每个效果几乎都有对应的关闭参数,但不常用,通常直接用0重置所有,在此不作介绍。

2.4.2 前景色

部分终端在将此参数和加粗共同使用时,显示为加粗的同时,使用更明亮的前景色代替。

  • 30: 黑色

  • 31: 红色

  • 32: 绿色

  • 33: 黄色

  • 34: 蓝色

  • 35: 紫色

  • 36: 青色

  • 37: 白色

  • 38: 256色/24位真彩色,具体如下:

    • 5: 256色,下一个参数代表具体的颜色值,形如 \033[..;38;5;n;..m,这里有一份256色值表

    • 2: rgb真彩色,下三个参数代表具体的rgb值,形如 \033[..;38;2;r;g;b;..m

  • 39: 重置颜色(由具体实现定义)

2.4.3 背景色

  • 40: 黑色

  • 41: 红色

  • 42: 绿色

  • 43: 黄色

  • 44: 蓝色

  • 45: 紫色

  • 46: 青色

  • 47: 白色

  • 48: 256色/24位真彩色,具体如下:

    • 5: 256色,下一个参数代表具体的颜色值,形如 \033[..;38;5;n;..m,这里有一份256色值表

    • 2: rgb真彩色,下三个参数代表具体的rgb值,形如 \033[..;38;2;r;g;b;..m

  • 49: 重置颜色(由具体实现定义)

2.5 报告

报告内容将以输入流的方式返回

  • 光标位置报告(DSR): \033[6n 获取终端光标的位置,返回示例: \033[30;48R (48,30)

  • 设备属性(DA): \033[c 获取终端的属性,具体含义并没有统一的定义,以下是几个返回示例:

    • \033[?61;6;7;21;22;23;24;28;32;42c

    • \033[?65;1;2;6;9;15;16;18;21;22;28;29c

    • \033[?1;0c

3 一些案例

3.1 DNA

import random

def print_dna(length):

    ESC = "\x1b["
    RESET = ESC + "0m"
    RED = ESC + "38;5;196m"
    GREEN = ESC + "38;5;46m"
    YELLOW = ESC + "38;5;226m"
    BLUE = ESC + "38;5;21m"

    # 定义碱基对
    base_pairs = [
        (RED + "A" + RESET, GREEN + "T" + RESET),
        (GREEN + "T" + RESET, RED + "A" + RESET),
        (YELLOW + "G" + RESET, BLUE + "C" + RESET),
        (BLUE + "C" + RESET, YELLOW + "G" + RESET)
    ]

    blank = ' ' + RESET
    h = '-' + RESET

    # 生成DNA双螺旋
    for i in range(length):
        # 选择随机的碱基对
        left_base, right_base = random.choice(base_pairs)
        # 控制连字符长度以模拟双螺旋旋转
        if i % 4 == 0:
            spaces = 6
        elif i % 4 == 1:
            spaces = 8
        elif i % 4 == 2:
            spaces = 10
        else:
            spaces = 8
        print(blank * (10 - spaces // 2) + left_base + h * spaces + right_base + blank * (10 - spaces // 2))

# 输出30行的DNA双螺旋图形
print_dna(30)

3.2 转换像素画

import numpy as np
from PIL import Image

def image_to_ascii(image_path):
    # 加载图像
    img = Image.open(image_path)
    # 将图像转换为RGBA格式
    img = img.convert("RGBA")
    # 缩放图像
    img = img.resize((32, 32), Image.LANCZOS)
    
    # 转换为numpy数组
    pixels = np.array(img)
    
    # 创建一个空字符串来存储字符画
    ascii_image = ""
    
    # 遍历像素
    for i in range(pixels.shape[0]):
        for j in range(pixels.shape[1]):
            # 获取RGBA值
            r, g, b, a = pixels[i, j]
            # 如果透明度小于128,则认为是透明的
            if a < 128:
                ascii_image += ' '
            else:
                # 计算亮度
                brightness = 0.299 * r + 0.587 * g + 0.114 * b
                char = '██'
                # 添加带颜色的字符
                ascii_image += f'\033[38;2;{r};{g};{b}m{char}'
        ascii_image += '\033[0m\n'  # 重置颜色并换行
    
    return ascii_image

# 使用示例
print(image_to_ascii('img.png'))