How InputController Handles Key Events and Modifiers in the Hallelujah IME

The InputController class in the hallelujahim repository filters system shortcuts via bitmask checks, toggles English input mode on right-shift release, maintains a modifier history buffer to suppress duplicate flag-changed events, and delegates printable character processing to the conversion engine.

The InputController sits at the core of the dongyuwei/hallelujahim macOS input method, bridging raw hardware key codes and Chinese text conversion. Located in src/InputController.mm, this Objective-C class implements the essential event-handling protocol that determines whether to consume keystrokes for pinyin composition or pass them through to the underlying application.

Declaring Recognized Events

The controller first advertises its interest in specific event types through the recognizedEvents: method. According to the source code at InputController.mm lines 24-26, it returns a bitmask combining only key-down and modifier-change events:

- (NSUInteger)recognizedEvents:(id)sender {
    return NSEventMaskKeyDown | NSEventMaskFlagsChanged;
}

This declaration tells macOS to route all standard keystrokes and modifier key state changes (Shift, Command, Option, Control) to this instance while ignoring other event types like mouse movements or scroll wheel actions.

Central Event Dispatch Pipeline

Every intercepted event flows through ‑handleEvent:client:, the primary entry point at InputController.mm. This method extracts the current modifier flags, updates an internal history buffer, and branches based on the event type:

switch (event.type) {
    case NSEventTypeFlagsChanged: …   // modifier keys only
    case NSEventTypeKeyDown: …        // actual character keys
    default: break;
}

At the end of the method (lines 75-79), it stores the current state into two C-style arrays—_lastModifiers[2] and _lastEventTypes[2]—to enable debouncing logic for rapid modifier presses.

Modifier-Change Handling

When a NSEventTypeFlagsChanged event arrives, the controller first checks for duplicate events using the history buffer. If the current modifier mask matches the previous entry (_lastModifiers[1]), it returns YES immediately to avoid double-processing (lines 35-38).

The only modifier that triggers an IME state change is Shift. When the right-shift key (keyCode KEY_RIGHT_SHIFT) is released without any other shift key being held, the controller toggles the _defaultEnglishMode flag. If the mode switches on while Chinese text remains in the buffer, the composition is cancelled and committed immediately to prevent mixed-language output (lines 39-49):

_defaultEnglishMode = !_defaultEnglishMode;
if (_defaultEnglishMode) {
    NSString *bufferedText = [self originalBuffer];
    if (bufferedText.length > 0) {
        [self cancelComposition];
        [self commitComposition:sender];
    }
}

Key-Down Event Filtering

Key-down events respect the _defaultEnglishMode switch first—when enabled, the controller bypasses all IME logic and returns NO to let the system handle the keystroke normally (lines 53-55).

Next, the method filters out system shortcut modifiers. If Command, Option, or Control bits are set in the modifier flags, the controller exits early to preserve standard macOS shortcuts like Cmd+C or Ctrl+A (lines 57-68):

if (modifiers & NSEventModifierFlagCommand) break;
if (modifiers & NSEventModifierFlagOption)  return false;
if (modifiers & NSEventModifierFlagControl) return false;

If none of these modifiers are present, the event proceeds to ‑onKeyEvent:client: for character-level processing (lines 69-71).

Detailed Key Processing Logic

The ‑onKeyEvent:client: method contains the granular keymap implementation. It extracts the keyCode and characters from the NSEvent, then executes a cascade of conditional checks:

  • Delete (KEY_DELETE): Calls ‑deleteBackward: if buffered text exists (lines 90-96).
  • Space (KEY_SPACE): Commits the current composition with a trailing space (lines 98-103).
  • Return (KEY_RETURN): Commits the composition without appending a space (lines 106-111).
  • Escape (KEY_ESC): Cancels the composition, clears the buffer, and resets internal state (lines 114-119).
  • Alphabetic characters (a-z/A-Z): Appends to the original buffer, refreshes the candidate list via the ConversionEngine, and displays the candidate window (lines 121-128).
  • Arrow keys (KEY_ARROW_DOWN/KEY_ARROW_UP): Navigate the candidate list when the window is visible; guarded by isMojaveAndLaterSystem checks for macOS 10.14+ compatibility (lines 130-144).
  • Digits 1-9: When running on Mojave or later with the candidate window visible, these keys select candidates directly using single-page or paged indexing logic (lines 146-168):
if (isCandidatesVisible) {
    int pressedNumber = characters.intValue;
    NSString *candidate = nil;
    int pageSize = 9;
    
    if (_currentCandidateIndex <= pageSize) {
        candidate = _candidates[pressedNumber - 1];
    } else {
        candidate = _candidates[pageSize * (_currentCandidateIndex / pageSize - 1)
                               + (_currentCandidateIndex % pageSize)
                               + (pressedNumber - 1)];
    }
    
    [self cancelComposition];
    [self setComposedBuffer:candidate];
    [self setOriginalBuffer:candidate];
    [self commitComposition:sender];
    return YES;
}
  • Punctuation and symbols: If text is buffered, the symbol acts as a commit trigger—the punctuation is appended and the composition finalized (lines 172-178).
  • All other keys: Returns NO to allow the system to process the input (lines 180-181).

Modifier History and Deduplication

To handle macOS quirks where holding a modifier key generates repeated NSEventTypeFlagsChanged events, the controller maintains two-element history arrays:

NSUInteger _lastModifiers[2];
NSEventType _lastEventTypes[2];

After processing each event in ‑handleEvent:client:, the controller shifts the current state into index [0] and the previous state into index [1] (lines 75-79). This allows the duplicate detection logic at lines 35-38 to compare the incoming event against the immediate predecessor, suppressing redundant processing cycles while preserving responsiveness for legitimate modifier transitions.

Summary

  • The InputController registers only for NSEventMaskKeyDown and NSEventMaskFlagsChanged to minimize system overhead.
  • It filters Command, Option, and Control modifiers at the entry point to prevent interference with global macOS shortcuts.
  • Right-Shift release toggles _defaultEnglishMode, immediately committing any pending Chinese composition to avoid language mixing.
  • A two-element history buffer (_lastModifiers and _lastEventTypes) eliminates duplicate flag-changed events caused by key hold behaviors.
  • Printable characters route through ‑onKeyEvent:client:, which supports candidate navigation via arrow keys (macOS 10.14+) and direct selection via number keys 1-9.

Frequently Asked Questions

How does InputController prevent system shortcuts from interfering with the IME?

In ‑handleEvent:client:, the method inspects the modifier flags bitmask immediately upon receiving a NSEventTypeKeyDown. If NSEventModifierFlagCommand, NSEventModifierFlagOption, or NSEventModifierFlagControl are detected (lines 57-68), the controller returns early or breaks out of the switch statement, allowing macOS to handle the shortcut while the IME remains inactive for that keystroke.

Why does the right Shift key toggle English mode instead of left Shift?

The implementation explicitly checks for KEY_RIGHT_SHIFT (keyCode 60) when the modifier flags transition to zero, verifying that the left Shift key (keyCode 56) is not currently held via the _lastModifiers history buffer. This design choice preserves the standard use of left Shift for uppercase input while dedicating the less frequently used right Shift as a dedicated mode toggle (lines 39-49).

How does the controller handle rapid modifier key presses without double-triggering?

It maintains two C-style arrays, _lastModifiers[2] and _lastEventTypes[2], storing the last two event states. When a NSEventTypeFlagsChanged event arrives, the code compares the current modifier mask against _lastModifiers[1]; if they match, indicating a duplicate event from a held key, the method returns YES immediately without processing the state change (lines 35-38 and 75-79).

Which macOS versions support numeric candidate selection in the InputController?

Candidate navigation via arrow keys and direct selection via number keys 1-9 are guarded by isMojaveAndLaterSystem checks, ensuring these features activate only on macOS 10.14 (Mojave) and newer. This compatibility layer prevents crashes on older systems while enabling rich candidate interaction on modern macOS versions (lines 130-170).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →