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 ...
随机推荐
- rabbitmq3.7.3 发布了一个新的 exchange x-random
direct exchange 同一个 routing key 可以绑定多个 queue,当给这个routing key发消息时,所有 queue 都会投递.这个行为对于一些场景不适用,有时我们希望只 ...
- jmeter接口测试 -- 连接数据库(MySQL)
三个步骤 一.下载MySQL的连接驱动 1.先查看MySQL的版本 1)服务器上查看:mysql --version 2)在连接工具上查看 2.下载连接驱动,下载地址:https://dev.mysq ...
- Netty有关
https://www.baeldung.com/tag/netty/ https://github.com/eugenp/tutorials https://stackoverflow.com/qu ...
- Archlinux常用软件推荐 更新于2022年5月
必装软件# xdg-user-dirs-gtk 执行 xdg-user-dirs-gtk-update 可将更新Home目录路径 包管理工具# yay 代替pacman的包管理 downgrade p ...
- docker-entrypoint.sh 文件的用处
参考出处很多著名库的 Dockerfile 文件中,通常都是 ENTRYPOINT 字段会是这样: ENTRYPOINT ["docker-entrypoint.sh"]这里我们参 ...
- Qt音视频开发07-合并音视频文件
一.前言 之前已经把音视频分开存储了对应的文件,因为这个需求特别少,当然确实有部分用户是需要把音视频分开存储,但是毕竟是很少数,绝大部分的用户都是音视频合并到一个MP4文件,所以如果要合并到一个文件, ...
- 修改leds-gpio.c 让GPIO LED在kernel启动时就开始闪烁
内容提要: 客户需要在开机时就闪烁LED,并要求越快越好 diff --git a/drivers/leds/leds-gpio.c b/drivers/leds/leds-gpio.c index ...
- C#中如何使用异步编程
在 C# 中,异步编程主要通过 async 和 await 关键字来实现.异步编程的目的是让程序在执行耗时操作(如 I/O 操作.网络请求等)时不会阻塞主线程,从而提高程序的性能. 1. 异步编程的核 ...
- 前端面试100-copy
1.一些开放性题目 1.自我介绍:除了基本个人信息以外,面试官更想听的是你与众不同的地方和你的优势. 2.项目介绍 3.如何看待前端开发? 4.平时是如何学习前端开发的? 5.未来三到五年的规划是怎样 ...
- ClickHouse介绍-示例
示例 GitHub 事件数据集 数据集包含了GitHub上从2011年到2020年12月6日的所有事件,大小为31亿条记录.下载大小为75 GB,如果存储在使用lz4压缩的表中,则需要多达200 GB ...