mirror of
https://github.com/qingjian-team/qingjian.git
synced 2026-11-06 16:13:55 +00:00
GNOME 默认的输入法框架是 IBus:加一个 IBus 前端(纯 Rust,与 Fcitx5 插件共用同一个 Server),安装脚本按机器上有哪个框架装对应的前端(--frontend auto/fcitx5/ibus/both),可选把 Server 装成随登录启动的 systemd 用户服务。 协议类型搬进 qingjian_platform::protocol::linux,两个前端共用。GNOME Wayland 上组句中按应用快捷键时先把拼音上屏到原来的框。 合并 main 时保留 #351 的 --fcitx4(单独一支,不与 --frontend 同用)。
198 lines
15 KiB
Markdown
198 lines
15 KiB
Markdown
# Linux 从零到能日用(2026-09-14)
|
||
|
||
> **这一页记的是走 IBus 那次,不是现行方案。** Linux 后来改成 Rust Server + Fcitx5 默认面板
|
||
> (见 [linux-fcitx5.md](linux-fcitx5.md)),IBus 壳与 deb / rpm / flatpak 打包都已从 main 移出,
|
||
> 完整代码在合并前的分支末端 `26ece65`(`git show 26ece65:apps/linux/ime/src/ibus/service.rs` 这样取);
|
||
> D-Bus 那层与按键翻译后来移植进了 `apps/linux/ibus`(改成连 Server 的前端,第二个框架)。留着这一页是因为**坑与验收数字仍然有效**:
|
||
> 下面五个坑(总线地址 / Shift+数字 / GVariant 嵌套 / 把 `/` 当仓库根 / 锁毒化)与 IBus 面板的限制,
|
||
> 换成瘦客户端接 Server 之后一样会碰到。当时的方案取舍文档 `plan/linux_plan.md` 已随路线一起删掉。
|
||
|
||
## 起因
|
||
|
||
Phase 5 只剩 Linux 没开工。这一天把它从「一行代码没有」推到「装在开发机上当会话输入法用」:
|
||
`apps/linux` 建起来、走 IBus 接上 Core、打成三个 `.deb` 装机、切成系统输入法日常打字。
|
||
各模块的实现要点在 [crate-notes.md](crate-notes.md),这一页只记**过程里踩的坑和验出来的数字**。
|
||
|
||
## 怎么分层
|
||
|
||
```text
|
||
ibus/ D-Bus 那层:GVariant 序列化、工厂、引擎对象、总线地址
|
||
↑
|
||
dispatch/ 按键 → 帧。不认 IBus 也不认 Wayland,所以能脱离输入法框架整段测
|
||
↑
|
||
assembly/ 装 Engine
|
||
↑
|
||
Core
|
||
```
|
||
|
||
`dispatch` 这一刀切得值:80 个单元测试里绝大多数在这一层,不用起 daemon、不用 D-Bus。
|
||
按键规则是照 macOS 的 `handle_text` / `handle_command` 与 Windows 的 `apply_key` 手工对齐的——
|
||
**这是第三份孪生逻辑**,`assembly` 也是第三份。(现行方案把这一份收进了 Linux Server,前端只转事件。)
|
||
|
||
比 Windows 少了会话分派:IBus 一个引擎实例服务当前焦点,没有「一个 Server 服务多个应用进程」那回事,
|
||
所以 Router 只有一份组句状态,形状更接近 macOS 的进程级单例。
|
||
|
||
## 五个只有真机才暴露的坑
|
||
|
||
### 1. ibus-daemon 不给引擎进程设 `IBUS_ADDRESS`
|
||
|
||
本来以为 daemon 拉起引擎时会把总线地址放进环境变量,写完发现引擎起来就退出:找不到地址。
|
||
要自己读 `~/.config/ibus/bus/` 下的地址文件。
|
||
|
||
**而且不能扫目录随便挑**:那个目录里常年躺着别的输入法或上次会话留下的陈旧文件——
|
||
开发机上就有 fcitx 留的 `<机器id>-unix-0`,按字典序还排在当前会话的 `<机器id>-unix-wayland-0` 前面,
|
||
挑中它连上去一个信号都收不到。改成按 ibus 自己的规矩算文件名(`<机器 id>-<主机>-<显示标识>`),
|
||
算出来的不在才退回扫目录,且只认 `IBUS_DAEMON_PID` 还活着的。
|
||
|
||
### 2. Shift + 数字的 keysym 是 `!` 不是 `1`
|
||
|
||
缺省的删候选快捷键正是 Shift + 数字。照 macOS 那样按字符认数字,这条快捷键在 Linux 上**整个失效**,
|
||
而且失效得很安静——键被当成普通标点吃掉了。得同时用硬件键码按**键位**认(evdev 的 `KEY_1` + X11 的 8 偏移 = 10)。
|
||
Windows 用虚拟键码干的是同一件事,只是那边字符与键位天然分开,不容易想到 Linux 也要。
|
||
|
||
### 3. IBus 的嵌套 GVariant 要包变体
|
||
|
||
`IBusText` 的签名是 `(sa{sv}sv)`,第四项那个 `v` 得把属性表**包成变体**再塞;
|
||
直接塞结构体会内联成 `(sa{sv}av)`。签名错了**不报错**,只是候选窗静默不出现。
|
||
`av` 的元素同理。现在三种对象的签名由单元测试钉死,改错了先红的是测试。
|
||
|
||
### 4. `bundled_root` 把 `/` 当成了仓库根
|
||
|
||
装完第一次跑 `--check` 就炸出来:随包数据根解析成了 `/`,回落到 148 条的样例词库。
|
||
原因是开发布局的回退写成「exe 往上数三层」(`exe → {debug,release} → target → 仓库根`),
|
||
装到 `/usr/bin` 之后往上三层正好是 `/`,而这台机器根目录下有个 `/data`,`has_resources` 就认了。
|
||
|
||
症状是**装好了、能启动、就是一个词都打不出**——最难查的那种。改成先确认 exe 真在
|
||
`target/{debug,release}/` 里才认开发布局。这个函数 **Windows 也在用**,所以算顺手修了 Windows。
|
||
|
||
### 5. `Mutex` 毒化 = 崩一次就再也打不了字
|
||
|
||
D-Bus 那层要求接口对象 `Send + Sync`,而 `Engine` 因为几个 `RefCell` 缓存不是 `Sync`。
|
||
一开始按「另开一条工人线程持有它」设计,写之前测了一下发现 **`Router` 是 `Send` 的**,
|
||
套个 `Mutex` 就够,工人线程省了(`dispatch::tests` 里有一条编译期断言钉着)。
|
||
|
||
但满地 `lock().expect(...)` 埋了个雷:`Mutex` 一旦被 panic 毒化,之后每次 `lock()` 都返回错误。
|
||
按「崩溃不丢」那条,崩一次之后输入法该照常服务,而不是永久瘫掉。现在统一走 `Shared::lock`,
|
||
毒化了就把状态取回来接着用;按键外面再套一层 `catch_unwind`,拦下后清组句、这个键让给应用。
|
||
|
||
## 「带内容的 preedit 收不到」——当天归因错了
|
||
|
||
写了个 D-Bus 客户端当「应用」跑端到端,候选表和上屏都收得到,**唯独带内容的 `UpdatePreeditText` 收不到**。
|
||
一路怀疑到序列化、面板、能力位,试了半天:起面板一样收不到,同一个 `IBusText` 在另外两条路上完全正常。
|
||
当天的结论是「真实应用的 preedit 由 GTK / Qt 的输入法模块自己渲染,合成客户端本来就验不到」,
|
||
测试于是退而断言 `ShowPreeditText`。
|
||
|
||
**这个归因是错的。** 接本地整句模型时顺手看了一眼 ibus-daemon 自己的日志,里面一直在刷:
|
||
|
||
```text
|
||
GLib-CRITICAL: the GVariant format string '(vubu)' has a type of '(vubu)' but the given value has a type of '(vub)'
|
||
IBUS-CRITICAL: bus_engine_proxy_g_signal: assertion 'arg0 != NULL' failed
|
||
```
|
||
|
||
`UpdatePreeditText` 的参数是 `(vubu)`——最后那个 `mode`(`IBUS_ENGINE_PREEDIT_CLEAR` / `COMMIT`)**不能省**。
|
||
我们只发了 `(vub)`,daemon 解不出来,`g_return_if_fail` 打一条 CRITICAL 就把整条信号丢掉了。
|
||
补上那个参数之后,合成客户端**立刻就收到了带内容的 preedit**,反倒是 `ShowPreeditText` 不再单独来——
|
||
`visible=true` 已经把它显示出来,daemon 判定「已经可见」就不再转发。端到端的断言跟着改了过来。
|
||
|
||
**两条经验**:
|
||
|
||
1. **错在「我们这边一点错都看不到」**:信号发出去了、zbus 没报错、引擎日志干干净净,
|
||
出错的是收信号那一头,而它的抱怨只写在 ibus-daemon 自己的 stderr 里。
|
||
验 IBus 这类「对面是个 C 程序」的协议,**得去看对面的日志**,这是当天漏掉的一步。
|
||
2. 合成客户端能验的东西比当天以为的多。它验不了的是排版与观感,协议对不对它验得了。
|
||
|
||
## 验收
|
||
|
||
装机后(GNOME / Wayland,真产品数据:22 万词库 + 42 MB 语言模型 + 24 万词释义表):
|
||
|
||
| 项 | 数字 |
|
||
|---|---|
|
||
| 每键耗时 | 最慢 **3.34 ms**、平均 **857 µs**(标准 10 ms 以内) |
|
||
| 整句转换 | `zhonghuarenmingongheguowansui` → 中华人民共和国万岁 |
|
||
| 引擎常驻 | 约 104 MB |
|
||
| 测试 | 84 个单元测试 + 1 个对着真 ibus-daemon 的端到端(没有 ibus 就跳过,CI 不受影响) |
|
||
|
||
日常打字验过的:词级、整句、简拼、拼写纠错(`zhonoghua` / `zonghua` 都救回了 中华)、
|
||
中英混输(`clone` 原样直通)、学习六张表落盘。
|
||
|
||
**X11 也能用**:方案 C 压根不碰显示服务器,候选窗的绘制与定位全是 IBus 面板的活。
|
||
`GDK_BACKEND=x11` 起的 XWayland 客户端里验过。计划里「只做 Wayland」说的是将来那一步自绘,不是整个 Linux 支持。
|
||
|
||
## 打包与安装
|
||
|
||
四个 `.deb`(当时的 `apps/linux/packaging/build-deb.sh`,已随路线删除):程序 3.5 MB、设置界面 1.4 MB、数据 27 MB、模型 49 MB。
|
||
数据与模型分开是因为发行版不收一百多兆的单包,而且它们的更新节奏跟代码不一样;
|
||
程序包不依赖这两个:缺语言模型退化成一元词频、缺模型不重排,Core 本来就支持。
|
||
**设置界面单独一个包**是后来拆的:输入法本体只链 libc / libm / libgcc(IBus 走纯 Rust 的 zbus),
|
||
设置界面是这一套里唯一要 GTK4 的东西,合在一起等于让 KDE / Qt 环境装个输入法就拖进整棵 GTK 树。
|
||
|
||
装到 `/usr/bin/qingjian-linux` + `/usr/share/qingjian/{data,assets}`,组件 XML 落 `/usr/share/ibus/component/`,
|
||
postinst / postrm 调 `ibus write-cache --system` 让 ibus 重扫。
|
||
|
||
**依赖没走 `dh_shlibdeps`,所以两头都自己算。** 漏了它们的后果不是装不上而是
|
||
**装得上、跑不起来**:dpkg 放行,启动才报错。
|
||
|
||
- `libc6`:取二进制里最高的强符号版本(`pidfd_spawnp` 这类 Rust 标准库的弱引用不算),当前是 2.34,
|
||
也就是 Ubuntu 22.04 / Debian 12 / RHEL 9 起。
|
||
- **动态库**:`readelf` 取 NEEDED、`ldd` 找路径、`dpkg -S` 问包名。
|
||
一定要现查而不能手写——接上云联想之后 `libssl` / `libcrypto` 就进了 NEEDED(reqwest 带的 OpenSSL),
|
||
手写的那份不会自己跟上。包名还有 Debian time_t 转换那一层(`libssl3t64` vs `libssl3`),
|
||
所以 `*t64` 的自动写成「或」依赖,两边发行版都装得上。
|
||
|
||
`qingjian-data` 进 Recommends 同理:缺了它只有几十条样例词库,症状又是「装好了打不出字」。
|
||
|
||
## 后来补上的
|
||
|
||
- **本地整句模型**(`dispatch/rescore/`):当天打了 `qingjian-model` 这个包,但壳根本没接
|
||
`qingjian-neural`,装了也不起作用——mac 的 `host/model.rs` 与 Windows 的 `dispatch/rescore/` 都有,
|
||
Linux 是第三份。接的时候顺带改了主循环:原先是固定一秒的 `tokio::time::interval`,
|
||
而重排要停键 80 ms 请求、20 ms 轮询,所以节拍改成由 `Router::next_tick()` 说了算,按键之后用
|
||
`Notify` 叫醒主循环重算。
|
||
- **按键之外怎么重画**:这是接模型时才发现的一个洞。zbus 把信号发出者(`SignalEmitter`)交在方法参数里,
|
||
主循环手上没有,所以原先 `tick()` 返回的那一帧**被丢掉了**——云联想的结果其实也从来没画出去过。
|
||
现在引擎对象拿到焦点时把自己的对象路径记进 `ibus/active.rs`,主循环照着它
|
||
`SignalEmitter::from_parts` 造一个再发。tick 也改成只有帧真变了才回,空转的一秒不往 D-Bus 上发信号。
|
||
|
||
- **云联想**:与本地模型同一个毛病——`assembly` 从没调过 `with_predictor`,`[predict]` 读进来只在 `--check` 里打印一行。
|
||
接法照 Windows 的 `attach_cloud`(第三份)。`CloudPredictor` 自己起工人线程、线程里自建 current-thread 运行时,
|
||
所以不占主循环那个多线程运行时,也不需要壳这边配合。
|
||
**Linux 特有的一处**:密钥不能指望环境变量——引擎进程是 ibus-daemon 拉起来的,用户在终端 export 的东西它看不到,
|
||
所以启动时用 dotenvy 读一遍 `~/.config/qingjian/.env`(设置界面写密钥也写在那里)。
|
||
|
||
- **Flatpak**(2026-09-16):给非 Debian 系发行版的一条路,但**它不能像别的应用那样一装就好**。
|
||
动手前先做了一个实验:把组件 XML 分别放进 `$XDG_DATA_HOME/ibus/component` 与 `XDG_DATA_DIRS` 里的
|
||
`ibus/component`,起一条私有 daemon,**两份都没被发现**,daemon 只认得 `/usr/share/ibus/component/` 里
|
||
系统装的那份。也就是说 **ibus 不按 XDG 数据目录找引擎**,而 Flatpak 暴露文件恰恰只靠 exports 进
|
||
`XDG_DATA_DIRS`——这条把「Flatpak 装完就能用」堵死了,只能由用户在宿主机上放一个
|
||
`<exec>flatpak run …</exec>` 的组件 XML 把两边接起来。
|
||
另外三处是沙箱的常规代价,都能用 `finish-args` 解决:总线 socket 与地址文件在宿主机上(给只读权限,
|
||
而且 `bus_dir` 要往 `$HOME/.config` 回退一次,因为沙箱改掉了 `XDG_CONFIG_HOME`)、
|
||
随包数据落在 `/app/share/qingjian`(`resources` 的系统布局里加了这条)、
|
||
用户数据进 `~/.var/app/<id>/`,与 `.deb` 装的那份不互通。
|
||
|
||
- **组句打到一半换输入框**(2026-10-15,评审时报的):原先在 `FocusOut` 里请 Server 把缓冲原样上屏、
|
||
自己发 `CommitText`。IBus 的引擎对象在各输入框之间共用,daemon 调完 `FocusOut` 不等回就把引擎挂到下一个框,
|
||
这条信号到的时候已经晚了:X11 上拼音打进了新框,GNOME Wayland 上两边都没有。改了两处:
|
||
- preedit 用 `IBUS_ENGINE_PREEDIT_COMMIT` 发、`FocusOut` 报 `client_preedit = true`(Fcitx5 插件那套),
|
||
失焦时由框架当场交给原来的框。**这只管用在 X11**(GTK 输入模块一个输入框一个上下文)。
|
||
- **GNOME Wayland 上失焦时交什么都晚了**:应用自己先挪焦点(Ctrl+F 打开搜索条),gnome-shell 之后才对上下文
|
||
`Reset` → `FocusOut` → `FocusIn`,它按 COMMIT 模式交出的 preedit 落进了新框(总线上一条 `CommitText` 都没有,
|
||
是 gnome-shell / mutter 自己交的;`gresource extract` 出 `inputMethod.js` 的 `vfunc_focus_out` 看得到)。
|
||
唯一来得及的时机是快捷键交给应用之前:组句中带 Ctrl / Alt / Super、Server 放行的键,前端先
|
||
`Deactivate` 原样上屏再放行,`CommitText` 在 `ProcessKeyEvent` 返回之前发出,比按键先到应用——与 macOS 在应用处理
|
||
快捷键前 `commitComposition` 的结果一样。鼠标点到别的框没有这样的时机,Wayland 上拼音会丢,写进了 `keys.md`。
|
||
- 排查办法:`dbus-monitor --address "$(ibus address)"` 能旁听 IBus 总线(不支持新式 monitor,会退回 eavesdrop),
|
||
事件先后一眼就看见。端到端测试 `pinyin_stays_in_the_old_box_when_focus_moves` 与
|
||
`shortcut_commits_pinyin_before_reaching_the_app` 对着私有 daemon 钉住这两处。
|
||
|
||
## 还没做的
|
||
|
||
- ~~**`.rpm`**~~:当天卡在「没有能验的环境」,2026-09-16 用 docker 解决了——构建与验证都在 fedora
|
||
容器里跑(`build-rpm.sh --verify` 会 `dnf install` 装上再 `--check`)。
|
||
rpm 的自动依赖比 deb 省事得多(自己扫 ELF),但它不分强弱符号,glibc 门槛因此比 deb 严一档。
|
||
- **L3 / L4**:自绘要先把 `renderer-spike` 分支合进 main(那份代码不在这台机器上),
|
||
而 L4 的 `input-method-v2` 只有 wlroots 系实现,GNOME / KDE 验不了。
|
||
- ~~**设置界面**~~:当天之后补上了,GTK4 七页(当时的 `apps/linux/settings`,已随路线删除);也仍可直接编辑
|
||
`~/.config/qingjian/config.toml`,热加载即时生效。
|
||
- **`[apps]` 按应用配置**:Wayland 拿不到前台应用标识,X11 可行但只覆盖一半平台,不做。
|