终端,现在也叫命令行。但在历史上,确实有一种设备叫终端。其中最为著名的,可能就是 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() {
fmt.Print(
"x1b[8C", // 向前移动 8 列,跑到屏幕中间
"ab", // 打印 ab
"x1b[1B", // 向下移动 1 行
"x1b[2D", // 后退 2 列,使光标移动到 a 下面
"cd", // 打印 cd
"x1b[1D", // 后退 1 列
)
for {
fmt.Print("x1b[1D") // 后退 1 列,移动到 c 上
time.Sleep(200 * time.Millisecond)
fmt.Print("x1b[1A") // 上移 1 行,移动到 a 上
time.Sleep(200 * time.Millisecond)
fmt.Print("x1b[1C") // 前进 1 列,移动到 b 上
time.Sleep(200 * time.Millisecond)
fmt.Print("x1b[1B") // 下移 1 行,移动到 d 上
time.Sleep(200 * time.Millisecond)
}
}
再来一个复杂一点的进度条示例
同样是 Go 代码:
package main
import (
"fmt"
"strings"
"time"
)
func main() {
// 进度条最左边模拟旋转的 -
s := []string{
"-", // 0/180 度
"", // 45 度
"|", // 90 度
"/", // 135 度
}
for i := 0; ; i++ {
k := i % 4 // 控制旋转角度
l := i % 20 // 控制进度条长度
if l == 0 {
// 如果进度条超过长度则清空进度条重新绘制
fmt.Print("x1b[2K")
}
bar := strings.Repeat("=", l) + ">"
// 进度条一共有 l + 1 + 1 个字符
// 光标在 > 后面,需要向后移动 l+2 列才能回到第 1 列
// 为下次重绘做好准备
back := fmt.Sprintf("x1b[%dD", l+2)
fmt.Print(
s[k], // 绘制最左边的 -
bar, // 绘制进度条
back, // 将光标移动到第 1 列
)
time.Sleep(200 * time.Millisecond)
}
}
我们在终端下看到的移动效果大都是这样实现的。接下来说一下字符渲染指令。
字符渲染指令
字符渲指令全称 Select Graphic Rendition,简写为 SGR。其格式为 CSI n m
,以数字开头,并以 m
结尾,n
的取值范围是 0-107
。又可以分成两类,一类控制字符显示样式,另一类控制显示颜色。
控制显示样式的主要指令有:
- 0 重置所有显示效果
- 1 显示粗体
- 3 显示斜体
- 4 显示下划线
- 22 关闭粗体效果
- 23 关闭斜体效果
- 24 关闭下划线效果
例如执行下面的脚本:
# echo 可以使用 e 表示 0x1b
echo -e "e[1mbolde[0m"
echo -e "e[3mitalice[0m"
echo -e "e[4munderlinee[0m"
# 注意,这里可以组合几种不同的效果
echo -e "e[1;3;4malle[0m"
可以得到如下输出:
控制颜色的指令有:
- 30–37 设置字符颜色
- 38 设置字符颜色
- 39 恢复默设字符颜色
- 40–47 设置背景颜色
- 48 设置背景颜色
- 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[31mrede[0m"
# 使用 256 色格式编码
echo -e "e[38;5;1mrede[0m"
# 使用 24-bit 真彩色编码
echo -e "e[38;2;255;0;0mrede[0m"
# 上面几个例子都会输出红色的 red
# 这是个综合性的例子,会输出
# 黄底线字,加粗、加下划线线、斜体的 red
echo -e "e[1;3;4;38;2;255;0;0;48;2;0;255;0mrede[0m"