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


本篇是 《TUX IM 开发日志:从零到 0.1》 系列第 5/8 篇。
第 5 篇 · 五笔与混打:Wubi + Wbpy 两种哲学
拼音是"流"——一串字母对应一串候选。五笔是"码"——一组字母直接对应一个汉字。两种输入法的数据流方向完全不同。这篇讲 TUX IM 怎么用同一套 InputMode 协议容纳这两种哲学,以及混打(Wbpy)模式的 tricky 之处。
五笔:每个按键都是承诺
五笔 86 是这样工作的:
- 用户按 1-4 个字母,构成一个编码(如
ycfg对应"主")。 - 候选框弹出候选字。
- 按数字键选候选。
核心难点:如何判断用户输入完了?
- 拼音:buffer 越长越好,可以连打。
- 五笔:buffer 到达当前 trie 的最深层就强制 commit。
五笔分段(简化)
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):
五笔死键透传
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 伪代码
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。 - 拼音引擎需要从自己的
ni3ybuffer 里删一个字符(变成ni3)。 - 五笔引擎需要从自己的
ni3ybuffer 里删一个字符(变成ni3)。
如果两个引擎的 buffer 状态不一致,候选就会混乱。
修复(aed57fd fix(wbpy): backspace actually deletes characters):
双 buffer 同步
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)
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+` 就能用说话打字。
评论