# How InputController Handles Key Events and Modifiers in the Hallelujah IME

> Learn how the InputController in hallelujahim handles key events and modifiers by filtering shortcuts, toggling input modes, managing modifier history, and processing characters.

- Repository: [dongyuwei/hallelujahim](https://github.com/dongyuwei/hallelujahim)
- Tags: internals
- Published: 2026-02-28

---

**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](https://github.com/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](https://github.com/dongyuwei/hallelujahim/blob/master/src/InputController.mm#L24), it returns a bitmask combining only **key-down** and **modifier-change** events:

```objc
- (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](https://github.com/dongyuwei/hallelujahim/blob/master/src/InputController.mm). This method extracts the current modifier flags, updates an internal history buffer, and branches based on the event type:

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

```objc
_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):

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

```objc
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:

```objc
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).