The InputMode protocol allowed emoji mode and Google Pinyin ctypes binding to be integrated with nearly zero cost.

This is part 7/8 of the TUX IM Dev Log: From Zero to 0.1 series.
Part 7 · From Emoji to Google Pinyin: The Triumph of the InputMode Protocol
The previous post covered ASR, which was a relatively independent subsystem. This post tells a story that better demonstrates “extensibility” — how two completely different input modes were added with zero modifications to engine.py.
Emoji Mode: :cat → 🐱
Commit cf03200 added EmojiMode. Rule: when a user types a keyword starting with a colon (e.g., :cat, :smile), the candidate list pops up the corresponding emoji, and selecting 1 commits 🐱.
Looks simple, but there were several tricky design decisions.
Decision 1: Is emoji a “mode” or a “trigger”?
The user is already in pinyin mode. Pressing : triggers emoji — does this count as switching modes? Or are they still in pinyin mode with a special rule?
My choice: emoji is an independent input mode, activated automatically via buffer pattern (when buffer starts with :, enter emoji logic).
Decision 2: How to determine emoji state
The first version used if not self.buffer to determine if emoji was active — big mistake.
Old code (with bug):
def feed_key(self, keyval, state):
if not self.buffer:
# buffer empty ('' is falsy) → start emoji mode
if char == ':':
self.buffer = ':'
return KeyResult(handled=True)Problem: self.buffer = '' (empty string) is falsy, causing every keypress to be treated as “start emoji”. Worse, when the user enters a second :, buffer becomes :: — which should be the semantics for exiting emoji mode, but the code enters the “buffer non-empty” branch, logic breaks.
Fix (cf03200): Add explicit flag _emoji_active.
New code:
def feed_key(self, keyval, state):
if not self._emoji_active and char == ':':
self._emoji_active = True
self.buffer = ':'
return KeyResult(handled=True)
if self._emoji_active:
# emoji mode processing
...This is the subtlest pitfall of the InputMode protocol: don’t use a falsy empty buffer to determine state. An empty buffer is a valid state (user hasn’t typed yet), not “inactive”. Always use explicit flags.
Decision 3: Should buffer be cleared after select?
When a user selects an emoji, should the buffer be cleared? Yes. But how to handle the case where the user wants to type multiple emojis? Each emoji selection should commit and reset, allowing the next : to trigger emoji mode again.
Google Pinyin Mode: ctypes binding
The Google Pinyin input engine is a C++ library. TUX IM integrates it via Python’s ctypes module, loading the shared library and calling its API functions. The beauty of the InputMode protocol is that GooglePinyinMode simply implements the same feed_key(), candidates(), and commit() interface — engine.py doesn’t need to know anything about it.
Summary
The InputMode protocol proved its worth:
- Emoji mode: ~100 lines of code, zero changes to engine.py
- Google Pinyin mode: ~200 lines of ctypes binding code, zero changes to engine.py
- Both modes coexist peacefully, selected by the user via a simple hotkey
“Good architecture is not designed upfront — it emerges from the need to accommodate diversity.”
Comments