go语言实现终端里的倒计时
最近在更新系统的时候发现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命令。我们最常见的转义序列是颜色控制,让终端里的文字变成红色:\033[0;31m。其中\033[是转义序列的开始标志,0;31是命令m的两个参数,参数之间用空格分隔,最后一个参数紧贴着命令。
转义序列的支持程度要看终端和终端模拟器,好消息是我们需要用到的转义序列的被广泛支持的,我们要用它们来在行与行之间移动光标并绘制内容。
转义序列支持光标上下左右移动还支持直接清除整行的内容,这使得我们可以将终端当成一个画布:每个字符的位置相当于画布上的一个像素点(因此使用等宽字体效果显示会更好),坐标原点是程序运行开始后光标所在的位置,根据这个原点可以简单构建出一个平面坐标系,我们可以用一些特殊字符模拟点和线来绘制简单的图形。
我们要用的转义序列是这些:
\033[nF,将光标向上移动n行\033[nE,将光标向下移动n行\033[nC,将光标向后(右)移动n个字符\033[2K,清除光标所在行的整个内容(2以外的参数可以选择只清除光标前/后的内容)- 转义字符之间可以组合使用,比如
\033[nE\033[mC表示光标先向下移动n行然后再向右移动m个字符。
现在你应该明白那个倒计时是怎么画出来的了,核心技术点就是找到个合适的数字asciiart,然后根据每秒更新的内容在正确的位置上用上面的转义序列像画像素点一样把数字和分隔符画出来就行了。
说说其实一句话的事情,但做起来还是比较麻烦的,因为转义序列用的都是相对坐标,稍微算错一点相对位置显示效果就整个完蛋了,我也是调试了三四回才做到正确绘制的:
func (ar *ASCIIArtCharRender) RenderContent(duration time.Duration) {
if len(ar.chars) > 0 {
ar.chars = ar.chars[:0]
}
ar.chars = char.ConvertToChars(duration, char.ASCIIArtChars, ar.chars)
for i := 0; i < char.MaxASCIIArtCharHeight(); i++ {
util.CursorEraseEntireLine()
fmt.Print(ar.chars[0][i])
fmt.Print(" ")
fmt.Print(ar.chars[1][i])
fmt.Print(" ")
fmt.Print(char.ASCIIArtChars[char.ASCIIArtColonIdx][i])
fmt.Print(" ")
fmt.Print(ar.chars[2][i])
fmt.Print(" ")
fmt.Print(ar.chars[3][i])
fmt.Print(" ")
fmt.Print(char.ASCIIArtChars[char.ASCIIArtColonIdx][i])
fmt.Print(" ")
fmt.Print(ar.chars[4][i])
fmt.Print(" ")
fmt.Print(ar.chars[5][i])
fmt.Print("\n")
}
}
func (ar *ASCIIArtCharRender) RenderFlashing() {
util.CursorDownForward(1, 3+len(ar.chars[0][0])+1+len(ar.chars[1][0]))
fmt.Print(" ")
util.CursorForward(3 + len(ar.chars[2][0]) + 1 + len(ar.chars[3][0]) + 3)
fmt.Print(" ")
util.CursorDownForward(1, 2+len(ar.chars[0][0])+1+len(ar.chars[1][0]))
fmt.Print(" ")
util.CursorForward(2 + len(ar.chars[2][0]) + 1 + len(ar.chars[3][0]) + 2)
fmt.Print(" ")
util.CursorDownForward(2, 3+len(ar.chars[0][0])+1+len(ar.chars[1][0]))
fmt.Print(" ")
util.CursorForward(3 + len(ar.chars[2][0]) + 1 + len(ar.chars[3][0]) + 3)
fmt.Print(" ")
util.CursorDownForward(1, 2+len(ar.chars[0][0])+1+len(ar.chars[1][0]))
fmt.Print(" ")
util.CursorForward(2 + len(ar.chars[2][0]) + 1 + len(ar.chars[3][0]) + 2)
fmt.Print(" ")
// move to bottom
util.CursorDown(1)
}
第一个函数是绘制时间用的数字的,为了简单我已经提前把数字的asciiart保存进了二维数组并且做到了等高,这样画的时候只要知道需要什么数字就行,剩下的就是逐行输出“像素点”。
第二个函数是用来绘制电子时钟数字分隔符的闪烁效果的,这个看上去就更乱了,因为需要在终端画布上大范围移动。
所以会者不难,纯体力活。
完整的代码可以在这找到:https://github.com/apocelipes/ascii-count-down,欢迎各位大佬的改进或者功能增强。
总结
TUI还是挺有意思的,好玩能学到东西而且很能消磨无聊的时间。
另外我觉得在之间看源码对答案之前,可以先自己思考一下并动手做做试验比如像我那样最先异想天开用折行去实现多行进度条。这样虽然浪费了点时间,但可以加深自己对新知识的理解和记忆。
go语言实现终端里的倒计时的更多相关文章
- 在 Mac OS X 终端里使用 Solarized 配色方案
MacOS X 终端solarized配色 相信长期浸泡在终端和代码的小伙伴们都有一套自己喜爱的配色方案.以前一直在用简单.适合阅读的 Terminal.app 配色方案,换到 MacBook Pro ...
- 如何在 Arch Linux 的终端里设定 WiFi 网络
如果你使用的是其他 Linux 发行版 而不是 Arch CLI,那么可能会不习惯在终端里设置 WiFi.尽管整个过程有点简单,不过我还是要讲一下.在这篇文章里,我将带领新手们通过一步步的设置向导,把 ...
- 在终端里使用 Solarized 配色方案
在终端里使用 Solarized 配色方案 参考: 1.在 Mac OS X 终端里使用 Solarized 配色方案 2.solarized
- [转]在 Mac OS X 终端里使用 Solarized 配色方案
相信长期浸泡在终端和代码的小伙伴们都有一套自己喜爱的配色方案.以前一直在用简单.适合阅读的 Terminal.app 配色方案,换到 MacBook Pro with Retina display 后 ...
- script —— 终端里的记录器
当 你在终端或者控制台工作时,你可能想要记录在终端中所做的一切.这些记录可以用来当作史料,保存终端所发生的一切.比如说,你和一些Linux管理员们同 时管理着相同的机器,或者你让某人远程登陆到了你的服 ...
- Linux终端里的记录器
我们在调试程序的时候,免不了要去抓一些 log ,然后进行分析. 如果 log 量不是很大的话,那很简单,只需简单的复制粘贴就好. 但是如果做一些压力测试,产生大量 log ,而且系统内存又比较小(比 ...
- 读陈浩的《C语言结构体里的成员数组和指针》总结,零长度数组
原文链接:C语言结构体里的成员数组和指针 复制例如以下: 单看这文章的标题,你可能会认为好像没什么意思.你先别下这个结论,相信这篇文章会对你理解C语言有帮助.这篇文章产生的背景是在微博上,看到@Lar ...
- windows下R语言在终端的运行
在windows下可以有多种方式来运行R,R导论的这些章节给出一些详细的指导. 通常在环境变量离包含R的安装目录类似于R\R-3.1.2\bin\x64的情况下,就可以在CMD下运行R程序了 注意我这 ...
- 解决javac和java命令在Mac OSX终端里的乱码问题
转自:https://www.surfchen.org/archives/710 java和javac在简体中文的Mac OSX的终端(Terminal.app)环境下,默认是以GBK编码的中文输出各 ...
- mac 终端里进入mysql和退出
先在偏好设置里启动mysql服务 获取超级权限 在终端输入代码 sudo su 输入完后获取超级权限 终端显示 sh-3.2# 输入本机密码(Apple ID密码) 接着通过绝对路径登陆 代码 /us ...
随机推荐
- Object-relational impedance mismatch (转载)
http://www.agiledata.org/essays/impedanceMismatch.html Why does this impedance mismatch exist? The ...
- 【Python】【爬虫】【爬狼】004_正则规则模板及其应用
# 正则规则模板 与 应用(一) 先看这些视频,是在哪个div里面的 for datapage in soup.find_all("div", class_="lpic& ...
- kubernetes更改nodePort模式下的默认端口范围
使用nodePort模式,官方默认范围为30000-32767,详见Service官方文档. NodePort 类型如果将 type 字段设置为 NodePort,则 Kubernetes 控制平面将 ...
- tar 分卷压缩和解压缩
示例将 jdk1.8.0_221 文件夹按 98m 进行分卷压缩和解压缩压缩: tar -czvf - jdk1.8.0_221/ |split -b 98m - jdk1.8.0_221.tar.g ...
- Qt音视频开发38-USB摄像头解码linux方案
一.前言 做嵌入式linux上的开发很多年了,扳手指头算算,也起码9年了,陆陆续续做过很过诸如需要读取外接的USB摄像头或者CMOS摄像机的程序,实时采集视频,将图像传到前端,或者对图像进行人脸分析处 ...
- RL中on-policy和off-policy的本质区别/重要性采样
本随笔的图片都来自UCL强化学习课程lec5 Model-free prediction的ppt (Teaching - David Silver ). 回忆值函数的表达式: \[v_\pi(s) = ...
- Solution Set - “卷起击碎定论的漩涡”
目录 0.「CF 1788F」XOR, Tree, and Queries 1.「CF 1815F」OH NO1 (-2-3-4) 2.「CF 1787F」Inverse Transformation ...
- CDS标准视图:设备 I_Equipment
视图名称:I_Equipment 视图类型:基础视图 视图内容: 设备编码和设备内容 设备来源及详细信息 有效期 事务代码: IE03,IH08 视图代码 点击查看代码 @EndUserText.la ...
- x86平台SIMD编程入门(5):提示与技巧
1.提示与技巧 访问内存的成本非常高,一次缓存未命中可能会耗费100~300个周期.L3缓存加载需要40~50个周期,L2缓存大约需要10个周期,即使L1缓存的访问速度也明显慢于寄存器.所以要尽量保持 ...
- c# 免注册调用大漠插件100%完美识别文字
c# 免注册调用大漠插件100%完美识别文字 下载:https://download.csdn.net/download/xxq931123/10875122 绑定 模式:http://zy.anji ...