一个内核,两张脸:先把终端编辑器跑起来
文本能改、能撤销之后,接下来要把它变成一个能操作的程序。
砚最先接的是 tcell 终端前端。早期闭环很直接:打开文件、移动光标、输入、保存,再退出。后来 macOS 窗口接进来时,这些能力仍然来自同一个内核。
这件事成立,靠的是一开始把“谁能改状态”说清楚。
状态交给一个所有者
core/app.Editor 的可变状态归前端事件循环所在的 goroutine 所有。输入分发、命令执行、切换标签和构建画面都在这里完成。
搜索和目录遍历可以放到后台,但后台不随手去改当前标签、选区或侧边栏。它读快照,算完通过 Post 把结果交回来,由所有者处理。
键盘 / 鼠标
↓
前端翻译成语义输入
↓
Editor:修改状态、执行命令、构建 Frame
├─ 交出快照 → 后台计算 → Post → 回到 Editor
↓
前端绘制
这么做没有消灭并发问题。后面照样遇到了“旧工作区任务返回到新工作区”的错误。但至少排查时,不必先猜十几个 goroutine 谁正在修改同一份状态。
Frame 是两边的约定
内核给前端一份 Frame,描述要显示哪些内容。终端和窗口用自己的方式画出来,键盘鼠标再翻译回语义动作。
接入 GUI 后,Frame 变得更完整:侧边栏、多栏与标签、代码窗格、状态栏、浮层、查找框。这些信息仍然不要求内核知道某个具体 UI 组件。
这种分层也允许前端不同。窗口版能有富文本预览和内嵌终端;终端版可以只提示图片路径,把 shell 接管和恢复作为自己的工作流。共用内核,并不等于每个界面必须画出完全相同的东西。
命令是功能的共同入口
保存这件事,可以从快捷键、菜单或命令面板触发。三个入口最好调用同一条命令,否则很容易出现某个入口忘记处理冲突或状态提示。
砚用了命令注册表,命令 ID 尽量沿用熟悉的 VS Code 名称。键位解析支持组合键、连按和焦点上下文。资源管理器里的方向键与编辑器里的方向键,因此可以走不同处理。
终端还多了一层现实限制:Terminal.app 不能按窗口程序的想法接收所有 Cmd 组合。所以基础键位使用 Ctrl 和 Ctrl+K 连按回退;支持相应键盘协议的终端,再提供更多组合。
这件事让我意识到,快捷键表必须根据实际输入通道来设计。把熟悉的符号写在说明书里,并不能让终端把它送过来。
最早的终端功能:把 shell 还给用户
早期没有直接实现底部终端面板,而是提供 shell 往返。
按 Ctrl+K T,编辑器让出终端,在项目目录启动交互式 shell。运行命令后输入 exit 或按 Ctrl+D,再回到编辑器,文件、光标和撤销历史继续保留。返回时刷新工作区相关信息。
它很适合那个阶段:编辑器负责编辑,运行项目仍然用现成的 shell。后来窗口版有了内嵌终端,终端前端也没有因此失去这条路径。
架构边界要能被测试发现
“内核独立”如果只写在文档里,添加功能时很容易顺手导入一个 UI 包。
砚把几条规则做进了架构测试:core 不能依赖 tcell、MyGo、UI 或平台实现;两个前端互不导入;应用代码的依赖边界还检查 cgo 情况。平台剪贴板、shell、回收站等能力通过接口交给宿主实现。
这些测试没有证明整个架构永远正确,却能让最容易发生的依赖倒置及时报错。
终端骨架跑起来后,这个项目终于有了能反复使用的最小闭环。下一步要处理的,则是一个从第一天就不能拖到最后的问题:中文。