缓存时间:
2026/06/01 13:42
# movwin:我的(未发布)TUI框架
来源:https://movq.de/blog/postings/2026-05-29/0/POSTING-en.html
博客 (https://movq.de/blog/)\-git (https://movq.de/git/)\-桌面 (https://movq.de/desktop/)\-联系 (https://movq.de/contact.html)
---
2026-05-29
很长一段时间以来,编写带某种 GUI(或 TUI)的程序对我来说一直不太满意。库来了又走,趋势来了又走。你不得不不断跟随上游的新决策并调整代码。有时你完全不认同上游的决定,然后就得寻找新的框架。这有点令人疲惫。我的项目经常维持 5 年、10 年甚至更久,而在这段时间里**会有很多**变化发生。
于是,在 2025 年 12 月底的上一届 Advent of Code 之后,我决定开始自己做一个 TUI 框架。这不是一个容易的决定,因为我知道这将耗费大量工作。我寻找过替代方案,但找不到任何我喜欢的——或者足够快的。最近性能似乎真的变差了,有些框架光是初始化就需要两秒钟。
这篇博文只是对 movwin 当前状态的一次简单巡览,因为我决定暂时不发布这些代码。目前情况不太好:我发布的任何东西都会被"AI"公司抓取,然后他们拿去卖,完全无视代码附带的许可协议。我对此无法接受。
## 基础
它是一个 Python 库。我不再是 Python 的最大粉丝,但它确实有它的优势,主要是**庞大**的标准库,让我能轻松做很多事情。
movwin——代表"movq's windows and widgets"——构建在 ncurses 之上。ncurses 在终端兼容性方面承担了繁重的工作。movwin 没有使用的是 ncurses 的子窗口或 pads。相反,ncurses 充当一种(智能的)帧缓冲,我可以向其绘制内容,同时它也是键盘和鼠标输入的来源。
一个主要目标是"可接受"的 Unicode 支持。movwin 不太可能支持从右到左之类的功能,但我**绝对不**想在某处放一个 emoji 就导致整个布局爆炸。换句话说,movwin 必须知道一个 Unicode 序列在终端中可能占用多少个单元格。这里的大问题是这取决于终端,所以永远无法完美。
只有一个依赖(除了 Python 3.14 本身):wcwidth (https://pypi.org/project/wcwidth/) 及其 `wcswidth()` 函数。这用于测量文本的"外观尺寸":例如,"♀️" 占两个单元格宽度。
从很早开始,movwin 就有了"Window"和"Window Manager"的概念。我在制作整个东西时脑海中浮现的是古老的 DOS TUI:
tc.png (https://movq.de/blog/postings/2026-05-29/0/tc.png)
pcdoshelp.png (https://movq.de/blog/postings/2026-05-29/0/pcdoshelp.png)
pcdosshell.png (https://movq.de/blog/postings/2026-05-29/0/pcdosshell.png)
要是能真正重现这种体验就好了,在某种程度上我也做到了,但 UNIX 终端中的鼠标支持并不太好。在我的测试中,没有终端默认报告鼠标**移动**事件(只有"按下/释放"),我不得不调整 terminfo 文件。更糟糕的是,在大的终端窗口中,有些鼠标事件根本不会触发。因此,movwin 中的鼠标支持目前相当有限。也许我甚至会完全移除它,因为,嗯,用键盘驱动其实也是好事。不过对于移动窗口或控制滚动区域之类的事情,鼠标支持还是很不错的。
另一个主要目标是"可接受"的性能,意思是:"在我那台 10 年旧的、装有 Celeron CPU 的小 Intel NUC 上,启动时间大约 200-300 毫秒是可以接受的,但不应超过这个数。" Python 的启动时间确实是个问题,因为 `import` 超级慢。我不得不在一些地方做出牺牲,比如不使用 `dataclasses`。在这台 NUC 上,光是 import 的时间就足以致命:
```
$ time python -c 'exit(0)'
real 0m0.061s
user 0m0.048s
sys 0m0.011s
$ time python -c 'from dataclasses import dataclass'
real 0m0.151s
user 0m0.115s
sys 0m0.027s
```
## 应用程序
这一切的起因是一个叫做 `tracktivity` 的小程序:我用它来追踪活动和事件,比如咖啡因摄入或天气事件。它操作 CSV 文件:
```
rfc3339datetime,Food[coffee;blacktea;greentea],Comment
2026-05-29T14:04:00+00:00,coffee,at home
2026-05-29T14:04:43+00:00,blacktea,just some dummy entry :-)
2026-05-29T14:04:51+00:00,blacktea,more tea
```
`Food` 列被配置为有几个选项可以选,`Comment` 列是自由文本。`tracktivity` 基于该文件构建一个 UI 表单,看起来像这样:
tracktivity.png (https://movq.de/blog/postings/2026-05-29/0/tracktivity.png)
它没什么花哨,而且一些控件类型仍然缺失,例如还没有合适的表格或列表控件。
`tracktivity` 是一个简单的单窗口全屏程序,但它仍然使用 `Window` 类(像所有 movwin 程序应该做的那样),所以如果我愿意,我可以调整大小并移动这个窗口(点击观看视频):
tracktivity-moveresize.mp4 (https://movq.de/blog/postings/2026-05-29/0/tracktivity-moveresize.mp4)
"弹出窗口"也被实现为新的窗口。movwin 自带一些内置弹出窗口,比如"是/否框"、"输入框"或"消息框"。
`bine` 是另一个使用 movwin 的程序:它是一个基础的十六进制编辑器。我真正想要的是一个**简单**的十六进制编辑器,性能良好,并且底部有一个小信息面板(点击观看视频):
bine.mp4 (https://movq.de/blog/postings/2026-05-29/0/bine.mp4)
比如,"光标下的字节,当解释为有符号 8 位整数时,其值是多少"或同样的事情对于 2、4、8 字节,有符号和无符号。或者尝试将字节(从光标位置开始)解释为 UTF-8。
这是我滚动菜单以显示更多可用功能(点击观看视频):
bine-menus.mp4 (https://movq.de/blog/postings/2026-05-29/0/bine-menus.mp4)
还有两个演示,第一个展示"编辑二进制"功能,它使用了一个带有应用定义控件的自定义弹出窗口,第二个视频展示了撤销/重做(点击观看视频):
bine-binary.mp4 (https://movq.de/blog/postings/2026-05-29/0/bine-binary.mp4)
bine-undoredo.mp4 (https://movq.de/blog/postings/2026-05-29/0/bine-undoredo.mp4)
`bine` 大量使用 `mmap()`,用 Python 编写并不影响性能。正如你在第一个视频中看到的,在一个 2GB 的文件中搜索 ASCII 字符串也许只用了一秒。对我来说这已经足够好了,而且我没有花任何时间试图优化它。Python 的 `bytes.find()` (https://docs.python.org/3/library/stdtypes.html#bytes.find) 操作在 mmap 对象上,它完成了所有繁重的工作。
`bine` 也能很好地利用窗口系统:你可以在一个新窗口中打开另一个文件,而第一个窗口不受影响。当打开多个文件时,`WindowManager` 类会应用平铺布局(或者你可以切换到"将焦点窗口置于全屏模式"——或者如果你愿意,也可以自由移动它们):
bine-multiwin.png (https://movq.de/blog/postings/2026-05-29/0/bine-multiwin.png)
bine-multiwin-chaos.png (https://movq.de/blog/postings/2026-05-29/0/bine-multiwin-chaos.png)
最后,我最近制作了一个非常简单的计时程序:
watwar2.png (https://movq.de/blog/postings/2026-05-29/0/watwar2.png)
它有一个开始/停止按钮,并显示一个简单的"记录"列表:在那个截图中,我从 4:59 开始工作到 10:08,然后休息,之后在 12:21 又开始工作。到我截图时,总计为 9 小时 28 分钟。
截图中看不到的是,这个工具还会将 9:28 显示在连接到 Arduino 的一个小七段数码管上:
arduino.webp (https://movq.de/blog/postings/2026-05-29/0/arduino.webp)
我在工作中用这个东西来注意我已经工作了几个小时。:-) 当一天结束时,我可以将这些数据上传到我们基于云的时间追踪服务(这就是"传输"菜单的作用)。
顺便提一下:在 Linux 上,可以使用 timerfd (https://docs.python.org/3/library/os.html#os.timerfd_create) 来实现定时器事件。movwin 的主循环基于 `select()`,你可以注册任意文件描述符。timerfd 以固定间隔触发,所以这允许 UI 和 Arduino 显示定期更新。
## 更多好东西
### 主题
movwin 自带两个内置主题:
bine-bluegray.png (https://movq.de/blog/postings/2026-05-29/0/bine-bluegray.png)
bine-amber.png (https://movq.de/blog/postings/2026-05-29/0/bine-amber.png)
"琥珀色"主题让我充满怀旧感,因为它让我想起我第一台琥珀色 CRT 显示器上的 DOS 程序。
如果你愿意,可以把颜色定义放在 `~/.config/movwin/colors.json` 中,然后创建自己的主题,但这显然会有些棘手,因为应用程序可能也定义了自己的颜色(你**可以**在该文件中覆盖它们的颜色)。
默认情况下,movwin 根据月份选择当前主题。如果你在北半球,在十二月左右的月份会看到"琥珀色"主题,其他月份是更亮的"蓝灰色"主题。自动选择可以切换到"南半球",或者你也可以直接设置 `$MOVWIN_THEME` 来选择某个主题。
我最近在别处写道:
> 在过去的几周里,我越来越欣赏**菜单**的概念。它们提供了功能易于发现的能力。无需阅读手册。它们按类别组织。例如,我在查找程序的全局设置时不会去打开"格式"菜单。由于"加速键",它们可以用键盘驱动。而且当一个菜单项有直接快捷键(如 `Ctrl+U`)时,它会直接在菜单中显示出来。(当然,并非所有菜单系统都实现了所有这些。)我们曾经认为菜单是理所当然的,因为那是 GUI 程序的常规操作。但感觉它们在某处丢失了……尤其是在(UNIX)终端程序和网站上。还有智能手机上。
我特别喜欢 movwin 中热键的工作方式:
```python
menu_tree = MenuRoot(
[
MenuSub(
'&File',
children=[
MenuItem(
'&Quit',
cb_quit,
hotkey='KEY_F3',
),
],
),
MenuSub(
'&Edit',
children=[
MenuItem(
'Edit &raw file',
cb_edit_raw,
hotkey='KEY_F5',
arg=state,
),
],
),
...
],
)
```
所以当你定义菜单结构时,你可以直接在那里指定菜单项的热键。我喜欢这一点,因为它是自文档的。不需要专门的 F3 处理函数,也不需要将 F3 放到帮助页面中,因为菜单已经告诉你这个键是干什么的。(我可以在 `bine` 中更多地利用这一点。)
至于加速键:在上面的例子中,`Alt+f, q` 会调用 `cb_quit()`,`Alt+e, r` 会调用 `cb_edit_raw()`。
菜单可以任意嵌套,尽管我还没有实际需要用到这一点:
menu-nesting.png (https://movq.de/blog/postings/2026-05-29/0/menu-nesting.png)
不过菜单缺少鼠标支持。目前,我懒得实现它。而且正如我说的,也许我无论如何都会移除鼠标支持。
而且菜单不会在下面的项目上投下阴影。同样,我现在也懒得做。:-)
### 编辑框和 Unicode
编辑框对 Unicode 的理解足够到位,使得水平滚动不会出现故障(点击观看视频):
edit-unicode.mp4 (https://movq.de/blog/postings/2026-05-29/0/edit-unicode.mp4)
当两只单元格宽的企鹅从屏幕左侧离开时,滚动并不完美,但这里主要的是控件不会将企鹅绘制到分配的区域之外。
(这种裁剪适用于所有文本,不仅仅是编辑框。不过编辑框有点特殊,因为它们实现了自己的水平滚动。)
## 接下来往哪里去?
首先,我对目前的状态非常满意。代码中还有一些待办事项,但它已经相当可用了。拥有一个能做我所需所想、并且不会在下一个版本中意外改变的框架,感觉真好。ncurses 超级稳定,它不会做奇怪的事情。wcwidth 如果必要的话我可以 fork。Python 本身也相对稳定,而且我**认为**他们从艰难的 2 到 3 过渡中学到了教训,短期内不会再这样做?不确定。不管怎样,movwin 感觉像是一个在 5 年、10 年、15 年后仍然能工作——而不会太折腾的东西。
还有一些"更大"的事情我想实现,比如合适的列表视图,也许还有树形视图,以及最重要的文件选择对话框。
然后……我们拭目以待。