> 五笔的"死键透传"和 wbpy 双 buffer 同步的设计哲学。

示意图
示意图

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

第 5 篇 · 五笔与混打:Wubi + Wbpy 两种哲学

拼音是"流"——一串字母对应一串候选。五笔是"码"——一组字母直接对应一个汉字。两种输入法的数据流方向完全不同。这篇讲 TUX IM 怎么用同一套 InputMode 协议容纳这两种哲学,以及混打(Wbpy)模式的 tricky 之处。

五笔:每个按键都是承诺

五笔 86 是这样工作的:

  • 用户按 1-4 个字母,构成一个编码(如 ycfg 对应"主")。
  • 候选框弹出候选字。
  • 按数字键选候选。

核心难点:如何判断用户输入完了?

  • 拼音:buffer 越长越好,可以连打。
  • 五笔:buffer 到达当前 trie 的最深层就强制 commit。

五笔分段(简化)

Python
def candidates(self, limit=9):
    # 五笔:buffer 到达 trie 末端,强制 commit
    if not self._trie.has_more(self.buffer):
        return [Candidate(text=hit.word, freq=hit.freq)]
    # 否则返回所有 prefix 匹配
    return [Candidate(text=e.word, freq=e.freq)
            for e in self._trie.prefix(self.buffer)[:limit]]

死键透传:J 也能输出了

用户反馈:按 j 单独一个字母时,没反应——既没出候选,也没把字母送到应用。

原因:五笔模式把任何按键都吃掉了(handled=True),所以单字母 j 在五笔模式下找不到候选,字母也没透传到应用。

修复(1dbecf0):

五笔死键透传

Python
def feed_key(self, keyval, state):
    char = chr(keyval)
    new_buffer = self.buffer + char
    # 检查是否是任何 wubi 编码的前缀
    if self._trie.has_prefix(new_buffer):
        self.buffer = new_buffer
        return KeyResult(handled=True)
    # 否则:当作拉丁字母透传
    return KeyResult(handled=False, commit=char)

> 这个修复背后的哲学:五笔模式不应该"绑架"用户的键盘。如果输入的字母不是任何编码的前缀,说明用户想输英文字母,那就老老实实把字母交还给应用。

混打 Wbpy:两套引擎并行

Wbpy = Wubi + Pinyin。用户可以一段输入里既有五笔码又有拼音字母——引擎自动判断每段属于哪个。

设计思路:让两个引擎并行运行,按键喂给两个,看哪个出候选

Wbpy 伪代码

Python
class WbpyMode:
    def __init__(self):
        self._wubi = WubiMode()
        self._pinyin = PinyinMode()

    def feed_key(self, keyval, state):
        # 让两个引擎都尝试处理
        wb_result = self._wubi.feed_key(keyval, state)
        py_result = self._pinyin.feed_key(keyval, state)

        # 启发式:哪个更可能是 wubi?
        if self._looks_like_wubi(self.buffer):
            return wb_result  # 五笔优先
        return py_result  # 拼音兜底

    def _looks_like_wubi(self, buf):
        # 看 buffer 是否是任何 wubi 编码的前缀
        if self._wubi._trie.has_prefix(buf):
            return True
        # 兜底启发式:1-4 字母 + 第一个字母是 wubi 起手字母
        return 1 <= len(buf) <= 4 and buf[0] in self._WUBI_STARTS

双 buffer 同步问题

wbpy 维护两个内部引擎(wubi 和 pinyin),每个都有自己的 buffer。问题:

  • 用户按 BackSpace,buffer 从 ni3y 变成 ni3
  • 拼音引擎需要从自己的 ni3y buffer 里删一个字符(变成 ni3)。
  • 五笔引擎需要从自己的 ni3y buffer 里删一个字符(变成 ni3)。

如果两个引擎的 buffer 状态不一致,候选就会混乱。

修复(aed57fd fix(wbpy): backspace actually deletes characters):

双 buffer 同步

Python
def backspace(self) -> bool:
    if not self.buffer:
        return False
    self.buffer = self.buffer[:-1]
    # 两个子引擎也要同步
    self._wubi.backspace()
    self._pinyin.backspace()
    return True

这条修复来自用户反馈:"我打了几个字母又删,候选框乱了"。

> :如果 wbpy 的 buffer 比子 engine 长(因为某种不一致),删一个字符可能导致拼音的 ni3y 删成 ni3,但 wbpy 的主 buffer 是 ni3——再次输入 y,主 buffer 变 ni3y,拼音 buffer 变 ni3y,五笔 buffer 变 yi——彻底错乱。aed57fd 之后这种 bug 再没出现过。

启发式算法的另一面

_looks_like_wubi 启发式有个早期版本(4bee4ca 修复前):

旧启发式(有 bug)

Python
def _looks_like_wubi(self, buf):
    # 任何 1-4 字母都算 wubi ❌
    return 1 <= len(buf) <= 4

问题:用户输入 nihao(拼音"你好")时,buffer 长度 5 不算 wubi;但输入 nih(任何拼音前缀)长度 3,会被误判为 wubi,触发 wubi 引擎的奇怪行为。

修复:先查 trie.has_prefix(),只有真的可能是 wubi 编码时才走 wubi 路径。trie 没查到的 fallback 才用首字母启发式。

两种哲学的本质

> 五笔哲学:每个按键都是承诺——按了就是"我要构成编码"。死键必须透传,否则用户键盘被绑架。

拼音哲学:每个按键都是 hint——按了只是"可能的开头",越长越好。缓冲区永远清不掉用户输入。

混打哲学:让两个引擎并行,根据 buffer 内容动态决定哪个更可能正确。代价是双 buffer 同步的复杂度。

本篇小结

  • 五笔:死键必须透传,否则用户键盘被绑架。
  • wbpy:双 engine 并行,buffer 必须同步(包括 backspace 路径)。
  • 启发式判断:先查 trie,再 fallback 首字母。
  • InputMode 协议的胜利:wubi/wbpy/pinyin/latin 共享 feed_key/candidates/select/reset 接口。

> 下一篇让中文输入"会说话"——ASR 语音输入子系统。讲讲 AudioCapture / ASRClient / OverlayWindow 三个模块如何协同,按一下 Ctrl+` 就能用说话打字。

最后修改: 2026年7月1日

作者

评论

发表评论

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