How HallelujahIM Integrates with the macOS InputMethodKit Framework
HallelujahIM implements a Cocoa input method plug-in by subclassing IMKInputController to receive system keyboard events, using IMKCandidates to display Chinese character suggestions, and calling setMarkedText: and insertText: to manage composition and text commitment.
HallelujahIM is an open-source Chinese input method for macOS hosted at dongyuwei/hallelujahim. The project demonstrates how to build a fully functional IME by leveraging the macOS InputMethodKit framework, Apple's official API for creating custom input methods. By implementing the required IMK protocols and lifecycle methods, HallelujahIM intercepts keystrokes, generates candidates, and commits text to host applications.
Core Architecture: Subclassing IMKInputController
The foundation of any InputMethodKit plug-in is a controller class that inherits from IMKInputController. In src/InputController.h, HallelujahIM declares the InputController interface:
#import <InputMethodKit/InputMethodKit.h>
@interface InputController : IMKInputController { … }
@end
By subclassing IMKInputController, HallelujahIM gains access to the IMK lifecycle and event-handling hooks. The controller maintains references to the current client, a conversion engine for candidate generation, and the shared candidate window.
Handling Keyboard Events with InputMethodKit
Registering for Event Types
InputMethodKit requires the controller to declare which events it wants to receive. In src/InputController.mm, the recognizedEvents: method returns a bitmask specifying key-down and modifier-change events:
- (NSUInteger)recognizedEvents:(id)sender {
return NSEventMaskKeyDown | NSEventMaskFlagsChanged;
}
This registration ensures macOS forwards keystrokes to HallelujahIM while ignoring events the IME does not handle.
Processing Keystrokes in handleEvent:client:
The handleEvent:client: method serves as the main entry point for input processing. When the user types alphabetic characters, the controller appends them to the composition buffer and triggers candidate updates:
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
[self originalBufferAppend:characters client:sender];
[sharedCandidates updateCandidates];
[sharedCandidates show:kIMKLocateCandidatesBelowHint];
return YES;
}
This implementation demonstrates how HallelujahIM uses InputMethodKit to intercept raw keystrokes, buffer them, and request candidate display.
Managing Candidates with IMKCandidates
The Shared Candidates Window
HallelujahIM leverages the IMKCandidates class to present the Chinese character selection window. A global sharedCandidates object (declared in InputController.mm) manages the candidate UI throughout the application lifecycle.
Updating and Displaying Suggestions
When the conversion engine generates candidates, the controller updates the shared instance and controls visibility:
[sharedCandidates updateCandidates];
[sharedCandidates show:kIMKLocateCandidatesBelowHint];
Navigation methods such as moveUp:, moveDown:, and clearSelection allow the controller to respond to arrow keys and digit selections, updating the composition buffer before final commitment.
Composition and Text Commitment
Rendering Pre-edit Text with setMarkedText:
While the user types, HallelujahIM displays the raw input as "marked" text—the standard IME visual feedback showing text under composition. The showPreeditString: method constructs an attributed string and calls the client's setMarkedText:selectionRange:replacementRange::
- (void)showPreeditString:(NSString *)input {
NSDictionary *attrs = [self markForStyle:kTSMHiliteSelectedRawText
atRange:NSMakeRange(0, input.length)];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:input
attributes:attrs];
[_currentClient setMarkedText:attrString
selectionRange:NSMakeRange(input.length, 0)
replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
}
This integration with InputMethodKit ensures the pre-edit text appears correctly in any Cocoa or Carbon application.
Committing Final Text with insertText:
When the user selects a candidate or presses space to commit, the controller finalizes the composition by calling insertText:replacementRange: on the client:
- (void)commitComposition:(id)sender {
NSString *text = [self composedBuffer] ?: [self originalBuffer];
[sender insertText:text replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
[self reset];
}
This method completes the InputMethodKit text input cycle, delivering the final Chinese characters to the target application.
Lifecycle and Keyboard Layout Management
Activation and Keyboard Override
When the user activates HallelujahIM, macOS calls activateServer: on the controller. The implementation in src/InputController.mm performs critical setup, including forcing the system keyboard layout to US to ensure raw key codes match the IME's expectations:
- (void)activateServer:(id)sender {
// Force US keyboard layout for consistent key code mapping
[self overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"];
// Initialize annotation window and reset state
[self reset];
}
This InputMethodKit API ensures the IME receives predictable key codes regardless of the user's physical keyboard layout.
Deactivation and Cleanup
When the user switches to another input method, deactivateServer: clears the composition buffers and hides the candidate window, ensuring no text remains in an incomplete state.
Menu Integration and User Interface
Connecting to the macOS Input Method Menu
HallelujahIM provides menu items such as "Preferences" and "About" through the InputApplicationDelegate class defined in src/InputApplicationDelegate.h and src/InputApplicationDelegate.m. This delegate wires IMK selectors like showIMEPreferences: and clickAbout: to the controller's actions, allowing macOS to invoke the plug-in's UI from the system input method menu.
The delegate also uses NSWorkspace to open external URLs, such as the GitHub repository or local preference panes, providing a complete user experience without leaving the InputMethodKit ecosystem.
Summary
- HallelujahIM integrates with the macOS InputMethodKit framework by subclassing
IMKInputControllerinsrc/InputController.h, establishing the required plug-in lifecycle. - The controller implements
recognizedEvents:andhandleEvent:client:to intercept keyboard input, filtering for alphabetic characters and special keys. - Candidate management relies on the global
IMKCandidatesinstance (sharedCandidates), updated viaupdateCandidatesand displayed withshow:. - Pre-edit composition uses
setMarkedText:selectionRange:replacementRange:to display marked text, whileinsertText:replacementRange:commits the final characters. - The
activateServer:method callsoverrideKeyboardWithKeyboardNamed:to enforce US keyboard layout consistency. - Menu integration occurs through
InputApplicationDelegate, connecting standard IMK menu actions to the controller's preference and about panels.
Frequently Asked Questions
What is the macOS InputMethodKit framework?
The macOS InputMethodKit framework (IMK) is Apple's official API for developing custom input methods on macOS. It provides abstract base classes such as IMKInputController and IMKCandidates that handle the complex machinery of intercepting keystrokes, displaying candidate windows, and communicating with client applications. HallelujahIM leverages this framework to avoid low-level event tapping while maintaining native integration with Cocoa and Carbon apps.
How does HallelujahIM receive keyboard events?
HallelujahIM receives events by subclassing IMKInputController and overriding two critical methods defined in src/InputController.mm. First, recognizedEvents: returns NSEventMaskKeyDown | NSEventMaskFlagsChanged to register interest in keystrokes and modifier changes. Then, handleEvent:client: receives the actual NSEvent objects, allowing the controller to inspect character codes and either buffer alphabetic input or handle special keys like Space and Return.
What role does IMKCandidates play in this input method?
IMKCandidates provides the floating selection window that displays Chinese character suggestions. HallelujahIM maintains a global sharedCandidates object (defined in InputController.mm) that persists across input sessions. When the ConversionEngine generates candidates, the controller calls updateCandidates to refresh the list and show: to position the window below the cursor. Navigation methods like moveUp: and moveDown: handle arrow key input, while clearSelection resets the highlight when composition ends.
How is the pre-edit composition text displayed?
Pre-edit text—the raw alphabetic input shown while typing—is rendered using the setMarkedText:selectionRange:replacementRange: API from IMKInputController. In src/InputController.mm, the showPreeditString: method constructs an NSAttributedString with styling attributes obtained from markForStyle:atRange:, then passes this marked text to the current client. This produces the standard underlined composition appearance that indicates pending input before the user selects a final candidate.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →