The InputMode protocol wasn’t designed — it was carved out by bugs, one by one.
This is part 3/8 of the TUX IM Dev Log: From Zero to 0.1 series.
Part 3 · The Core Architecture Carved Out by 6 Bug Fixes
The previous post covered crashes — but crashes are just the surface. Beneath the surface lies something deeper: these 6 commits actually defined the entire project’s architecture. The boundaries of modules like engine/shortcut/pinyin/lexicon/user-word weren’t carefully designed one day — they were carved out by bugs, one by one. This post covers that process.
Architecture Isn’t Designed, It’s Carved by Bugs
When I started writing, my “architecture” looked like this:
- engine.py: one file, 800 lines.
- pinyin.py: directly called trie.lookup().
- User dictionary, word frequency, Rime dictionary loading: all piled into lexicon.py.
- Shortcut: a direct if/elif chain.
No InputMode protocol, no KeyResult contract, no “pluggable modes” abstraction. Then the bugs came — each one forced me to extract an abstraction layer.
First Bug Forces InputMode Protocol: ae1084b
Bug: User presses Shift+Space (commit_first), but the Pinyin buffer doesn’t commit. Instead, the space is sent to the application.
Cause: The shortcut path runs after the mode path. When Pinyin mode sees the space, it consumes it (handled=True), so commit_first shortcut never intercepts it.
Fix: Move shortcut before mode. This one change made me realize — the engine must be a container capable of hosting multiple input modes. So I started thinking: what interface must all modes obey?
The InputMode protocol grew out of this single reordering bug. It wasn’t a design-phase artifact; it was forced into existence during bug fixing.
KeyResult Contract
All modes must use the same way to tell the engine “how this key was handled”:
@dataclass
class KeyResult:
handled: bool = False # True = engine consumes this key
commit: str | None = None # text to commit to screen
clear: bool = False # True = engine calls mode.reset() after commitThree fields define: whether to pass to the application, whether to commit text, whether to clear the buffer. This contract later became the foundation of the entire project’s extensibility — when adding emoji mode or Google Pinyin mode, engine.py wasn’t changed at all.
Second Bug Forces Lexicon Module: 215a797
Bug: First boot used one dictionary; after restart, to switch dictionaries, you had to modify the code and restart the engine.
Fix: Add Gio.FileMonitor in main.py to watch config.toml. When the file changes, reload Config, rebuild Lexicon, rebuild ShortcutManager.
This made me realize — Lexicon must be an independent, replaceable object, not part of engine. So Lexicon was extracted.
Comments