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

Diagram

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):

Python
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:

Python
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.”

Last modified: 2026年6月25日

Author

Comments

Write a Reply or Comment

Your email address will not be published.