Golang库推荐: tcell实现对终端的坐标式绘图
引入话题
vim无疑是linux玩家必备的编辑工具, 即使在命令行模式下(假如你现在在使用linux桌面版, 现在你可以按下CTRL+ALT+F2进入命令行模式, 按CTRL+ALT+F1切回), 也能使用这样的编辑工具。它能够接管整个屏幕, 并将文字显示在上面。那么怎么实现这种”接管”整个终端屏幕的软件呢?在golang下就提供了tcell这样方便的工具, 使得我们能以坐标系的方式操纵整个终端屏幕。
这个库是跨平台的, 不同于c语言的ncurses
, 它的api更友好, 且跨平台支持效果很好。
go的ncurses移植(github.com/rthornton128/goncurses
)支持很垃圾, 究其原因, 是因为go接管了所有信号, 而这个c库设置了信号处理函数, 导致此库的某些接口效果很诡异。因此在任何情况下都不要使用goncurses。
快速体验
1 | package main |
这个程序将会接管整个终端屏幕, 并在屏幕左上角打印出你好世界
, 并且在三秒后程序结束。Show
代表刷新屏幕, 使之前的更改展示到屏幕上。Init
将修改终端的一些属性, 使得我们的程序能够接管整个屏幕。Fini
(finalize的缩写)将还原之前覆写的终端属性, 必须得调用这个, 否则在程序结束后, 终端的行为会不正常。
关键函数是SetContent
, 它能以坐标的形式指定某个单元格的内容。函数签名如下:
1 | func (tcell.Screen).SetContent(x int, y int, mainc rune, combc []rune, style tcell.Style) |
其中左上角第一个单元格的坐标被定义为(0,0), 关于计算机屏幕的坐标系一般都是这样定义的
mainc
代表要设置的字符, 可以是任何一个utf8字符(包括占用两个宽度的东亚字符)。combc
全称combining character
, 这个字符涉及到unicode的细节, 在大多数情况下用不着, 不提它了。
值得一提的是, 东亚字符占两个宽度, 一个ASCII字符只占一个宽度, 这就是为什么上面的代码设置每个字符都要间隔两个单元。如果要判定一个rune是否为宽字符, 可以用runewidth
库。
程序员要保证不要出现宽字符”交叉”的情况。如果这么写, 是未定义的:
1 | screen.SetContent(0, 0, '你', nil, tcell.StyleDefault) |
默认不显示光标, 如果要操作光标, 调用screen.ShowCurson
和screen.HideCurson
。
丰富的色彩和字符特效
现在的终端效果很厉害, 能让终端产生有颜色的字符, 可以让它带有下划线, 可以让它能够闪烁, 可以粗体/斜体。通过SetContent的style参数就能让它有各种各样的效果:
1 | screen.SetContent(0, 0, '你', nil, tcell.StyleDefault.Underline(true)) |
tcell设计了一个Style结构体, 能够链式地设置效果。tcell.StyleDefault是终端的默认显示配置, 在它的基础上修改需求是很好的实践。
tcell的事件队列机制
我们需要能响应终端的一些事件, 比如用户按下按键是最常见的事件, tcell提供了基于事件的消息传递, 需要用户做一个事件循环, 不断poll获取事件
下面的程序会在用户按下CTRL+C时退出程序, 按下普通的字符时将其显示到(0,0)处:
1 | package main |
PollEvent
会阻塞等待一个事件的到来, 事件有以下类型:
- EventResize: 用户的终端改变大小
- EventKey: 用户敲击键盘
- EventInterrupt: 可以用来打断PollEvent的阻塞, 里面可以放置自己的数据类型, 用来实现自定义事件
- EventMouse: 用户鼠标事件
有的人很好奇, 为毛一个终端, 你还能有EventMouse? 明明我的linux命令行模式连鼠标都没有啊?
其实这是因为终端模拟器的支持, 我们现在的桌面系统, 是linux的桌面模式, 为了在桌面模式上使用到终端, 有人发明了终端模拟器, 它支持了更多的”能力”, 其中鼠标点击就是其中之一。可以调用screen.HasMouse()
检查是否支持鼠标, screen.EnableMouse/DisableMouse
开启关闭, 默认是关闭的。
此外, 用户能够自己发送事件, 它会被PollEvent
捕获到。其中PostEvent
在事件队列已满的情况下返回ErrEventQFull
, PostEventWait
在时间队列已满的情况下等待
优秀的设计模式
让一个单独的协程负责Poll事件并进行绘图(事件循环), 事件循环里面永远不要包含耗时的操作(否则绘图就阻塞住了)。如果要在事件循环中启动耗时任务, 请开一个新的协程, 执行完毕后发送事件, 永远用事件通信。
我做的二次开发库推荐
开过mc服务器的人可能知道, mc服务器的后台控制器很炫酷, 在终端的最后一行专门用来输入指令, 不会被后台满天的刷屏给影响。于是我就设计了这么一个库, 它基于tcell(因此跨平台), 能很容易地实现这样的效果。此库支持了左右滑动(当一行输出太长的时候, 可以按左键右键进行水平滑动), 且在终端屏幕大小改变的时候重绘, 表现依然正常。点击此处查看。