VT100/ANSI转义序列概述——在终端实现优雅的交互
搜索如何实现在控制台输出彩色文本,我们会见到输出代码中出现\033[
或者\x1b[
这样的形式,这其实就是VT100控制码的前缀,\033
或\x1b
对应ANSI
编码中的ESC控制字符。这篇文章,我将粗略介绍一下VT100/ANSI转义序列是什么,以及如何使用VT100/ANSI转义序列。
使用ANSI转义序列输出的像素画(我的mc皮肤大头)和DNA序列,代码在文末。
封面图来自rich库的展示图。
0 写在前面
如果只需要实现输出效果,各语言(比如python的rich库)几乎都有现成的库可用,相比自己书写转义序列,更推荐使用库实现输出效果。
1 VT100是什么
图为VT100实机图,来自oldcomputr.com
内核(Kernal):与硬件交互,管理硬件资源,为上层软件提供接口。
壳(Shell):访问内核提供的服务。
终端(Terminal):人机交互设备,其中最有名的属DEC公司的VT100智能终端,第一个采用了ANSI标准。
控制台(Console):特殊的终端,用于系统管理,相比终端一般拥有更高的权限,但现已基本不与终端作区分。
终端模拟器(Terminal Emulator):在现代,模拟传统终端的软件。
ANSI转义序列(ANSI Escape Code): 由ANSI标准化的转义序列,几乎所有终端模拟器都参考该标准。
有关区分这些概念的内容,可以查看知乎大佬的文章。有关ANSII的内容,网上有很多介绍,在此也不做赘述。
可以参考:VT100用户手册,WIKI-ANSI转义序列,ECMA-48,XtermControlSequence。
在接下来的内容中,提到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'))