最近在更新系统的时候发现pacman的命令行界面变了,我有很久没更新过设备上的Linux系统了,所以啥时候变的不好说。但这一变化成功勾起了我的好奇心。新版的更新进度界面如下:

新的更新进度界面能同时显示多个进度条,而且并没有依靠ncurses这个传统的TUI库。为啥我能断定没有用ncurses呢,因为用过这个库的人都会发现程序在绘制界面的时候会用背景色清屏,且退出后终端的内容会恢复成运行程序前的样子,而上述表现都不存在。
不借助专用的库却又能绘制出比较生动的效果,这难道不吸引人吗?
所以带着好奇心,我简单探索了实现的原理,并且用相同的原理做了个新东西:

这是一个在终端中显示倒计时的小玩具,原理和pacman的进度条是一样的,我并没有一比一去复现pacman的效果,那样其实和对着范本写作文一样略显无聊,所以我选择活用知识做个新玩具。
好了,我们先来复习下单个终端命令行的进度条是怎么实现的。
单个进度条的原理其实很简单,几乎所有的终端和终端模拟器都支持一些特殊的控制字符,比如n表示新加一个空白行并把光标移动到这个新行的最左侧也就是开头处;r则是将光标移动到当前行的开头处。
所以单个进度条的绘制过程一共只要两步:
- 根据进度计算出当前进度条的样子,然后用打印函数输出,注意不能输出换行符
n; - 输出
r让光标回到行首,等待一段时间,重复步骤1,新的输出内容会覆盖掉老的。 - 进度到了100%之后就可以输出一个换行符
n结束进度条的打印了。
最关键的地方也只有一处,新的输出内容的长度要大于或者等于老内容,否则老内容会残留在终端里。
人眼的要求很低,所以你甚至可以不必做到每秒xx次刷新,只要在一秒或几秒里更新几次就能让人觉得你的进度条动起来了。
所以一个最简单的例子可以是这样的:
package main import ( "bytes" "fmt" "time" ) const width = 50 func main() { bar := bytes.Repeat([]byte{' '}, width) fmt.Println() for i := range 50 { bar[i] = '=' fmt.Printf("[%s] % 3d%%r", bar, (i+1)*2) time.Sleep(100 * time.Millisecond) } fmt.Println() fmt.Println("end") }
这是效果:

但r有个缺点,它只能回溯当前行,而且这个“行”是以终端显示为准的——即使你的输出并没有包含换行符但它的长度超过了终端显示的宽度导致需要“折行”,那么新折行出来的那行在终端显示中会被认为是一个新行,r只会将光标放到这个新行的开头。
其实我最开始想利用折行加r字符实现多行进度条,但很快就发现这条路是走不通的。显然pacman并没有使用r或者说它还利用了一些其他的东西。
看源代码是最快的,而且简单搜索一下“progressbar”很快就能找到答案。我就不卖关子了,pacman实现多行进度条效果是利用了ASNI转义序列。
ANSI转义序列(ANSI escape sequences)是一种带内信号的转义序列标准,用于控制视频文本终端上的光标位置、颜色和其他选项。在文本中嵌入确定的字节序列,大部分以ESC转义字符和"["字符开始,终端会把这些字节序列解释为相应的指令,而不是普通的字符编码。
简单的说,转义序列就像一些命令,可以控制光标和终端的各种行为。
具体格式是:转义序列开始字符参数1;参数2;...;参数N命令。我们最常见的转义序列是颜色控制,让终端里的文字变成红色: