多彩的终端
涛叔终端,现在也叫命令行。但在历史上,确实有一种设备叫终端。其中最为著名的,可能就是 vt100 系列了。我们现在能看到的 terminal 软件都是终端设备的模拟器。虽说终端设备已经作古,但终端的通信控制协议依然有效。我们可以在命令行下显示粗体、斜体、下划线字符,也可以显示不同的颜色,甚至还能显示简单的动画,这些功能依然使用几十年前终端设备通信协议。今天就给大家说说这种协议。
不过在开始之前你得先准备好一个支持 24-bit 真彩色的终端模拟软件,我在 mac 下用的是 iTerm。
学过编程的同学对转义一定不会陌生。转义可以理解成转换含义。比如,编程语言中一般用 "\n"
表示 0x0a
换行符。\
和 n
都是普通字符,但组合到一起却表示另外一个字符,这就是转义。这里的 \
就是转义字符。终端也使用一套转义规则,这种规则后来被标准化为 ANSI Escape Sequences。
终端使用 ESC
也就是 0x1b
作为转义字符。为开头,紧接着是一个字节,取值范围是 @A–Z[\]^_
。不同的字母表示不同的含义,其中 0x1b[
,叫作 Control Sequence Introducer,简写为 CSI
。其他的基本都是用来控制终端设备的,现在很少用到了。为行文方便,我们统称转义序列为指令。以 CSI 开头的指令有很多,大致可分四类:光标移动指令、清屏指令、字符渲染(Graphic Rendition)指令和终端控制指令。我们只说前三类。
光标移动指令
CSI n A
表示将光标向上移动 n 行,如0x1b[1A
表示向上移动一行。CSI n B
表示将光标向下移动 n 行,如0x1b[1B
表示向上移动一行。CSI n C
表示将光标向前移动 n 列,如0x1b[1C
表示向前移动一列。CSI n D
表示将光标向后移动 n 列,如0x1b[1D
表示向后移动一列。
清屏指令
CSI n J
表示清空屏幕- n = 0 清空光标以下区域
- n = 1 清空光标以上区域
- n = 2 清空全部区域
CSI n K
表示清空光标所在行- n = 0 清空光标到行尾的内容
- n = 1 清空光标到行首的内容
- n = 2 清空全部区域
牛刀小试
了解了光标移动指令和清屏指令,我们就可以做点有意思的事情了。
首先在终端上打印 abcd 四个字母,然后控制光标逆时针移动。
Go 代码如下:
package main
import (
"fmt"
"time"
)
func main() {
.Print(
fmt"\x1b[8C", // 向前移动 8 列,跑到屏幕中间
"ab", // 打印 ab
"\x1b[1B", // 向下移动 1 行
"\x1b[2D", // 后退 2 列,使光标移动到 a 下面
"cd", // 打印 cd
"\x1b[1D", // 后退 1 列
)
for {
.Print("\x1b[1D") // 后退 1 列,移动到 c 上
fmt.Sleep(200 * time.Millisecond)
time.Print("\x1b[1A") // 上移 1 行,移动到 a 上
fmt.Sleep(200 * time.Millisecond)
time.Print("\x1b[1C") // 前进 1 列,移动到 b 上
fmt.Sleep(200 * time.Millisecond)
time.Print("\x1b[1B") // 下移 1 行,移动到 d 上
fmt.Sleep(200 * time.Millisecond)
time}
}
再来一个复杂一点的进度条示例
同样是 Go 代码:
package main
import (
"fmt"
"strings"
"time"
)
func main() {
// 进度条最左边模拟旋转的 -
:= []string{
s "-", // 0/180 度
"\\", // 45 度
"|", // 90 度
"/", // 135 度
}
for i := 0; ; i++ {
:= i % 4 // 控制旋转角度
k := i % 20 // 控制进度条长度
l
if l == 0 {
// 如果进度条超过长度则清空进度条重新绘制
.Print("\x1b[2K")
fmt}
:= strings.Repeat("=", l) + ">"
bar
// 进度条一共有 l + 1 + 1 个字符
// 光标在 > 后面,需要向后移动 l+2 列才能回到第 1 列
// 为下次重绘做好准备
:= fmt.Sprintf("\x1b[%dD", l+2)
back
.Print(
fmt[k], // 绘制最左边的 -
s, // 绘制进度条
bar, // 将光标移动到第 1 列
back)
.Sleep(200 * time.Millisecond)
time}
}
我们在终端下看到的移动效果大都是这样实现的。接下来说一下字符渲染指令。
字符渲染指令
字符渲指令全称 Select Graphic Rendition,简写为 SGR。其格式为 CSI n m
,以数字开头,并以 m
结尾,n
的取值范围是 0-107
。又可以分成两类,一类控制字符显示样式,另一类控制显示颜色。
控制显示样式的主要指令有:
n 取值 | 效果 | 备注 |
---|---|---|
0 | 重置所有显示效果 | 0x1b[m |
1 | 显示粗体 | |
3 | 显示斜体 | |
4 | 显示下划线 | |
22 | 关闭粗体效果 | |
23 | 关闭斜体效果 | |
24 | 关闭下划线效果 |
# echo 可以使用 \e 表示 0x1b
echo -e "\e[1mbold\e[0m"
echo -e "\e[3mitalic\e[0m"
echo -e "\e[4munderline\e[0m"
# 注意,这里可以组合几种不同的效果
echo -e "\e[1;3;4mall\e[0m"
控制颜色的指令有:
n 取值 | 效果 | 备注 |
---|---|---|
30–37 | 设置字符颜色 | |
38 | 设置字符颜色 | 5;n 表示 256 色;2;r;g;b 表示 24-bit 真彩色 |
39 | 恢复默设字符颜色 | |
40–47 | 设置背景颜色 | |
48 | 设置背景颜色 | 5;n 表示 256 色;2;r;g;b 表示 24-bit 真彩色 |
49 | 恢复默认背景颜色 | |
90–97 | 设置字符颜色 | 高亮色 |
100–107 | 设置背景颜色 | 高亮色 |
最早的终端只支持 8 种颜色,分别是黑、红、绿、黄、蓝、洋红、青、白,因为数量很少,所以字符颜色直接使用 30-37 编码,背景色则使用 40-47。后来有厂商在此基础上又引入了 8 种对应亮度稍高的颜色,分别使用 90-97 和 100-107 编码。一共 16 种颜色。
随着硬件成本的不断降低,人们又生产出了可以显示 256 种颜色的终端。这次没有像 16 色那样再给 256 种颜色直接编码。而是引入了对应的 38 和 48 指令再配合扩展参数来表示 256 颜色。字符颜色:0x1b[38;5;<n>m
,背景颜色:0x1b[48;5;<n>m
,其中:
- n 取 0-7 时表示原来的 30-37 标准色
- n 取 8-15 时表示原来的 90-97 高亮标准色
- n 取 16-231 时表示 216 种颜色,计算公式为:
16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
- n 取 232-255 时表示 24 级灰度。
到现在,显卡可以显示 24-bit 真彩色了。终端指令进一步得到扩充。这次真得没法为每一种颜色直接编码了,索性直接使用颜色的 rgb 分量来表示。所以,24-bit 真彩色指令为 0x1b[38;2;<r>;<g>;<b>m
,背景色对应的则是 0x1b[48;2;<r>;<g>;<b>m
,这里用到了 2;<r>;<g>;<b>
扩展参数表示颜色。
最后,给出几个颜色示例:
# 使用 16 色格式编码
echo -e "\e[31mred\e[0m"
# 使用 256 色格式编码
echo -e "\e[38;5;1mred\e[0m"
# 使用 24-bit 真彩色编码
echo -e "\e[38;2;255;0;0mred\e[0m"
# 上面几个例子都会输出红色的 red
# 这是个综合性的例子,会输出
# 黄底线字,加粗、加下划线线、斜体的 red
echo -e "\e[1;3;4;38;2;255;0;0;48;2;0;255;0mred\e[0m"