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

新的更新进度界面能同时显示多个进度条,而且并没有依靠ncurses这个传统的TUI库。为啥我能断定没有用ncurses呢,因为用过这个库的人都会发现程序在绘制界面的时候会用背景色清屏,且退出后终端的内容会恢复成运行程序前的样子,而上述表现都不存在。

不借助专用的库却又能绘制出比较生动的效果,这难道不吸引人吗?

所以带着好奇心,我简单探索了实现的原理,并且用相同的原理做了个新东西:

这是一个在终端中显示倒计时的小玩具,原理和pacman的进度条是一样的,我并没有一比一去复现pacman的效果,那样其实和对着范本写作文一样略显无聊,所以我选择活用知识做个新玩具。

好了,我们先来复习下单个终端命令行的进度条是怎么实现的。

单个进度条的原理其实很简单,几乎所有的终端和终端模拟器都支持一些特殊的控制字符,比如\n表示新加一个空白行并把光标移动到这个新行的最左侧也就是开头处;\r则是将光标移动到当前行的开头处。

所以单个进度条的绘制过程一共只要两步:

  1. 根据进度计算出当前进度条的样子,然后用打印函数输出,注意不能输出换行符\n
  2. 输出\r让光标回到行首,等待一段时间,重复步骤1,新的输出内容会覆盖掉老的。
  3. 进度到了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的两个参数,参数之间用空格分隔,最后一个参数紧贴着命令。

转义序列的支持程度要看终端和终端模拟器,好消息是我们需要用到的转义序列的被广泛支持的,我们要用它们来在行与行之间移动光标并绘制内容。

转义序列支持光标上下左右移动还支持直接清除整行的内容,这使得我们可以将终端当成一个画布:每个字符的位置相当于画布上的一个像素点(因此使用等宽字体效果显示会更好),坐标原点是程序运行开始后光标所在的位置,根据这个原点可以简单构建出一个平面坐标系,我们可以用一些特殊字符模拟点和线来绘制简单的图形。

我们要用的转义序列是这些:

  1. \033[nF,将光标向上移动n行
  2. \033[nE,将光标向下移动n行
  3. \033[nC,将光标向后(右)移动n个字符
  4. \033[2K,清除光标所在行的整个内容(2以外的参数可以选择只清除光标前/后的内容)
  5. 转义字符之间可以组合使用,比如\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语言实现终端里的倒计时的更多相关文章

  1. 在 Mac OS X 终端里使用 Solarized 配色方案

    MacOS X 终端solarized配色 相信长期浸泡在终端和代码的小伙伴们都有一套自己喜爱的配色方案.以前一直在用简单.适合阅读的 Terminal.app 配色方案,换到 MacBook Pro ...

  2. 如何在 Arch Linux 的终端里设定 WiFi 网络

    如果你使用的是其他 Linux 发行版 而不是 Arch CLI,那么可能会不习惯在终端里设置 WiFi.尽管整个过程有点简单,不过我还是要讲一下.在这篇文章里,我将带领新手们通过一步步的设置向导,把 ...

  3. 在终端里使用 Solarized 配色方案

    在终端里使用 Solarized 配色方案 参考: 1.在 Mac OS X 终端里使用 Solarized 配色方案 2.solarized

  4. [转]在 Mac OS X 终端里使用 Solarized 配色方案

    相信长期浸泡在终端和代码的小伙伴们都有一套自己喜爱的配色方案.以前一直在用简单.适合阅读的 Terminal.app 配色方案,换到 MacBook Pro with Retina display 后 ...

  5. script —— 终端里的记录器

    当 你在终端或者控制台工作时,你可能想要记录在终端中所做的一切.这些记录可以用来当作史料,保存终端所发生的一切.比如说,你和一些Linux管理员们同 时管理着相同的机器,或者你让某人远程登陆到了你的服 ...

  6. Linux终端里的记录器

    我们在调试程序的时候,免不了要去抓一些 log ,然后进行分析. 如果 log 量不是很大的话,那很简单,只需简单的复制粘贴就好. 但是如果做一些压力测试,产生大量 log ,而且系统内存又比较小(比 ...

  7. 读陈浩的《C语言结构体里的成员数组和指针》总结,零长度数组

    原文链接:C语言结构体里的成员数组和指针 复制例如以下: 单看这文章的标题,你可能会认为好像没什么意思.你先别下这个结论,相信这篇文章会对你理解C语言有帮助.这篇文章产生的背景是在微博上,看到@Lar ...

  8. windows下R语言在终端的运行

    在windows下可以有多种方式来运行R,R导论的这些章节给出一些详细的指导. 通常在环境变量离包含R的安装目录类似于R\R-3.1.2\bin\x64的情况下,就可以在CMD下运行R程序了 注意我这 ...

  9. 解决javac和java命令在Mac OSX终端里的乱码问题

    转自:https://www.surfchen.org/archives/710 java和javac在简体中文的Mac OSX的终端(Terminal.app)环境下,默认是以GBK编码的中文输出各 ...

  10. mac 终端里进入mysql和退出

    先在偏好设置里启动mysql服务 获取超级权限 在终端输入代码 sudo su 输入完后获取超级权限 终端显示 sh-3.2# 输入本机密码(Apple ID密码) 接着通过绝对路径登陆 代码 /us ...

随机推荐

  1. Uninstall or delete MariaDB completely for re-installation

    I am new to this forum so pse forgive me if I am asking a question which already has been answered. ...

  2. 移动端弱网优化专题(十四):携程APP移动网络优化实践(弱网识别篇)

    本文由携程技术团队Aaron分享,原题"干货 | 携程弱网识别技术探索",下文进行了排版和内容优化. 1.引言 网络优化一直是移动互联网时代的热议话题,弱网识别作为移动端弱网优化的 ...

  3. SpringBoot原理深入及源码剖析(二) 自定义Starter及SpringBoot执行原理

    自定义Starter SpringBoot starter机制 SpringBoot由众多starter组成(一系列的自动化配置的starter插件),SpringBoot之所以流行,也是因为star ...

  4. 百度高效研发实战训练营-Step1

    百度高效研发实战训练营-Step1 1 设计方法与实践介绍 1.1. 软件设计原则 (1)软件设计的目的 软件设计是为了使软件在长期范围内能够容易的进行变化. 变化:软件不是一成不变的,无论是软件本身 ...

  5. JS 实现在指定的时间点播放列表中的视频

    为了实现在指定的时间点播放列表中的视频,你可以使用JavaScript中的setTimeout或setInterval结合HTML5的<video>元素.但是,由于你需要处理多个时间点,并 ...

  6. Peewee:Python 简洁强大的 ORM 框架

    在 Python 的开发世界中,数据库操作是至关重要的一环. 今天介绍的 Peewee 作为一款简洁且功能强大的 ORM(对象关系映射)框架,为开发者提供了高效便捷的数据库交互方式. 1. Peewe ...

  7. Spaghetti pg walkthrough Intermediate

    nmap ┌──(root㉿kali)-[~] └─# nmap -p- -A 192.168.170.160 Starting Nmap 7.94SVN ( https://nmap.org ) a ...

  8. OSI七层经典模型架构以及网络的基本概念

    在大.中型网络中,通常通过模块化方式将网络功能结构进行分解.但是在各个模块内部,还是存在结构的扩展和弹性问题. 譬如一个园区网络需要接入大量用户等,这个问题一般通过网络的层次化来解决. 传统的网络采用 ...

  9. 支持S3协议的S3cmd工具简单使用

    本文分享自天翼云开发者社区<支持S3协议的S3cmd工具简单使用>,作者:付****健 一:安装方法 #wget http://nchc.dl.sourceforge.net/projec ...

  10. 大数据HDFS集群相关概念

    一.Zookeeper服务 端口 描述 配置路径 2181 主要使用端口,对cline端提供服务.连接方式jdbc:hive2://ip:2181 conf/zoo.cfg中clientPort 21 ...