> TUX IM 是怎么从 5 个文件、132 行 Python 起步的。

本篇是 《TUX IM 开发日志:从零到 0.1》 系列第 1/8 篇。

第 1 篇 · 从一片空仓库到第一个"滴"字——为什么我要重写一个输入法

引子:为什么是输入法

2026 年春天,我在 Linux 桌面上打字时又一次被输入法卡住了。

事情很小:我按 Shift 想切英文,结果候选框不动,焦点丢了,拼音字母跑进了编辑器。我想骂人,但骂完还得继续用——因为没有别的选择。

fcitx5 是 Linux 桌面输入法的事实标准。它能用,但"能用"和"顺心"之间隔着一道很深的天堑。词库大、配置多、社区分裂——我每次想调点东西,都要在五六个 GUI 面板和七八个配置文件之间反复横跳,最后说服自己"凑合用吧"。

凑合用了五年。这一次我决定:不凑合了,自己写一个。

> 这不是"又一个输入法"的故事。这是一个程序员如何用最朴素的工具——一个 IBus 引擎、一份 RIME 词典、一堆 Python 代码——从零搭出一个能跑、能用、能发布的输入法引擎的故事。

整个过程 46 个 commit,从 aa4b0941dfb63a,跨度约三个月,最终产出 v0.1.0-12 正式 deb 包。

输入法在 Linux 桌面是什么

在 Linux 上,输入法不是应用程序,而是一个独立的进程,通过 DBus 跟桌面环境通信。这套机制叫做 IBus(Intelligent Input Bus),是 GNOME 默认采用的输入法框架。KDE 也能用,只是 KDE 自己的 Plasma 桌面默认推 fcitx5。

IBus 架构里有两个角色:

  • ibus-daemon:常驻进程,负责把按键事件分发给当前激活的输入法引擎。
  • ibus-engine-xxx:具体的输入法引擎,接收按键、产生候选词、把候选词"上屏"(commit)到焦点应用。

听起来很轻量对吧?实际上 IBus 引擎崩溃 = 整个 daemon 死掉——你打的字会直接卡在键盘缓冲区,桌面所有需要输入法的窗口全部失声,必须重启 ibus-daemon 才能恢复。这条规则我们后面会反复遇到,它是整个项目里最重要的约束。

示意图

为什么选 IBus,不选 fcitx5

这是一个一开始就决定好的事。fcitx5 在国内社区很流行,但它的 C++ 代码库和插件机制对我来说太重了。我想要的只是一个干净的、能让我用 Python 写业务逻辑的引擎。

IBus 提供了官方的 Python 绑定(ibus-1.0),通过 GObject Introspection 暴露 API。这意味着:

  • 引擎主循环用 GLib 跑,Python 写业务逻辑,胶水代码极少。
  • 不需要管线程、异步、事件循环——GLib 帮你做了。
  • 桌面环境兼容性好(GNOME、KDE、Sway 都能用)。

代价是 IBus 文档稀少,Python 绑定有几个 API 跟 C 不一致(IBus.Text.append 不存在这种事情后续会单独写一篇),但这些坑都能踩过去。

第一个 commit:能跑就行

项目起点是 aa4b094 Initial commit: TUX IM IBus engine。我打开 git log 把这个 commit 的内容拉出来看了一眼——132 行 Python,5 个文件

  • tux_im/__init__.py
  • tux_im/__main__.py
  • tux_im/main.py
  • tux_im/engine.py
  • tux_im/shortcut.py

引擎、shortcut 解析器、主入口、模块引导——四件套。当时只能切中英文、显示 preedit,没有任何候选词功能。换句话说,它什么都不能干。但它能跑——这是我给自己定的第一个里程碑:先有一个能跑起来的骨架。

示意图

> 第一个里程碑:在 IBus 里注册一个引擎、能切换中英文、能显示 preedit 框——仅此而已。版本号都没有。

工具链:开发环境是怎么搭的

项目根目录的 AGENTS.md(给后续所有 AI 助手看的开发指南)里记录了完整的依赖:

系统依赖

Bash
sudo apt install ibus ibus-dev libibus-1.0-dev 
                 python3-gi python3-ibus-1.0 
                 python3-pip python3-venv 
                 portaudio19-dev

关键点:必须用系统 Python,不能用 venv 里的 Python。原因是 python3-gi(GObject Introspection 的 Python 绑定)只能装在系统 Python 上,venv 里装不上 gi。这个坑我们后面会单独拎出来讲——它在项目中期差点让我重装系统。

开发流程简单粗暴:

日常开发循环

Bash
ruff check .          # lint
mypy --strict .       # 类型检查
pytest tests/         # 测试
TUX_IM_DEBUG=1 ibus-engine-tux-im --ibus  # 跑起来看效果

三个静态检查 + 一个手动验证。顺序很重要:ruff → mypy → pytest,前一步没过就不要进下一步。这条规则到项目后期救了无数次命。

第一个候选词:commit 1475a52

从"什么都不能干"到"能拼音输入",中间隔了 9 个 commit。最关键的是 1475a52 Add pinyin 连打 and hide lookup table on commit

连打是拼音输入的核心体验:用户输入 nihao,引擎要切成 ni hao,然后给出"你好"、"泥猴"等候选词;用户按数字键选第一个,引擎再把 hao 的候选词展开给用户选。

这个机制说起来简单,做起来是一整套流水线:

  • 接收按键,追加到 buffer(ni3hao3)。
  • 拼音切分(ni3 + hao3)。
  • 从词典里查每个音节的所有候选字。
  • 组合成候选词列表,按词频排序。
  • 送进 IBus lookup table 显示。

示意图

第一步的代码在 tux_im/engine.pydo_process_key_event,第二步在 tux_im/input/pinyin.py。它们之间的契约是一个简单的 KeyResult 数据类:

KeyResult 定义

Python
@dataclass
class KeyResult:
    handled: bool = False   # 是否被引擎消费
    commit: str | None = None  # 上屏文本
    clear: bool = False     # 是否清空 buffer

示意图

这套契约贯穿整个项目——所有 InputMode 实现(拼音、五笔、混打、英文、emoji、谷歌拼音)都遵循同一套接口。这是我当时没有意识到、但事后证明最重要的一个设计决定:引擎层和输入层完全解耦。这套设计让我们后面加 ASR、加 emoji、加 Google 拼音时,几乎不需要动 engine.py。

示意图

第一道墙:IBus 崩溃 = 桌面崩溃

故事不能光讲顺利的。第二次让我想骂人的事情发生在 1475a52 之后:有一次我在测试连打,输入 nihao,候选词弹出来的那一刻,整个 IBus daemon 死了。

所有打开的终端、浏览器、IDE 全部失声。我 ibus restart 之后恢复。

日志里报的是 AttributeError但 AttributeError 不应该让 daemon 死掉。后来翻 IBus 文档才发现:引擎进程异常退出时,ibus-daemon 默认会自杀以避免脏状态。换成代码描述:

错误做法(早期代码)

Python
def do_process_key_event(self, keyval, state):
    result = self.mode.feed_key(keyval, state)  # 假设这里抛异常
    self._update_lookup_table(...)              # 这行永远到不了
    return result.handled

正确做法是把整个函数体包进 try/except——任何异常都不能逃出引擎进程边界。这条规则后来写进了 AGENTS.md,红色标注:

> NEVER let do_process_key_event raise — all exceptions must be caught at the boundary.

示意图

cddcb40 feat: crash isolation for all IBus lifecycle hooks 这个 commit,我花了 2 周时间把所有生命周期 hook(do_focus_indo_focus_outdo_property_activatedo_process_key_eventdo_reset)全部包了 try/except。这是一篇单独的故事——下一篇会展开讲。

第一个里程碑完成

1475a52 之后,我可以:

在 IBus 里注册并激活 TUX IM用拼音连打输入中文用数字键选候选词用 Shift 切英文焦点切换时自动提交残留 buffer崩溃不杀死 daemon(这是后面才补的)看起来很简陋对吧?但这就是 0.0.1。一个能跑、能拼音输入的输入法引擎。

本篇小结

  • 动机:Linux 桌面输入法体验不满意,又不打算用 fcitx5。
  • 技术选型:IBus + Python(ibus-1.0 绑定) + GLib 主循环。
  • 第一个里程碑:能注册的引擎 + 拼音连打(commit 1475a52)。
  • 最重要的设计决定InputMode 协议 + KeyResult 契约,让引擎层和输入层解耦。
  • 第一个大教训:IBus 引擎崩溃 = 桌面崩溃,任何异常都不能逃出引擎进程

> 下一篇IBus 引擎的第一声惨叫——我是怎么把 daemon 写崩的。讲讲 cddcb40 之前那些差点让我放弃的崩溃、daemon 死循环、和按下 Shift 突然没反应的诡异 bug。

Code: https://github.com/tux-dot-fan/tux-im
Debian Package: https://github.com/tux-dot-fan/tux-im/releases/tag/v0.1.0-12

最后修改: 2026年7月1日

作者

评论

发表评论

您的邮箱地址不会被公开。