| name | squirrel-input-method-architecture |
|---|---|
| description | Understand and modify the Squirrel macOS input method frontend. Use this when working on input handling, librime sessions, candidate UI, configuration, lifecycle, installer commands, or backend/frontend coordination in this repository. |
Use this skill when making changes to Squirrel, a macOS InputMethodKit frontend for librime. Squirrel is an input method, so correctness depends on event ordering, session lifetime, marked text behavior, candidate window geometry, and clean handoff between the macOS text client and librime.
The Xcode project is organized around one app target, Squirrel.app, plus bundled resources and librime plugins.
Squirrel/Sources/Main.swift: process entry point, command-line maintenance commands, IMK server creation, app setup, and global librime startup.Squirrel/Sources/SquirrelApplicationDelegate.swift: app-wide state. Owns the candidate panel, globalSquirrelConfig, status item, Sparkle update integration, distributed notifications, and librime setup/finalization.Squirrel/Sources/SquirrelInputController.swift: the main InputMethodKit controller. Owns one active librime session per controller instance, receives key events, translates macOS events to Rime key events, commits text, updates marked text, and drives the candidate panel.Squirrel/Sources/MacOSKeyCodes.swift: maps AppKit/Carbon key codes and modifier flags to librime/X11 key symbols and masks.Squirrel/Sources/SquirrelConfig.swift: thin typed wrapper overRimeConfig, with base config/schema fallback and cached option reads.Squirrel/Sources/SquirrelTheme.swift: converts Rime/Squirrel style configuration into fonts, colors, layout flags, candidate formatting, and drawing attributes.Squirrel/Sources/SquirrelPanel.swift: nonactivating candidate/status panel. Builds attributed candidate text, positions the panel near the text cursor, handles paging/candidate mouse events, and delegates selection actions back to the input controller.Squirrel/Sources/SquirrelView.swift: custom AppKit drawing surface for candidate/preedit backgrounds, highlights, paging affordances, vertical text, and hit testing.Squirrel/Sources/ReservedProperty.swift: reserved librime plugin property protocol for frontend UI hints such as comment highlighting and UI refresh.Squirrel/Sources/BridgingFunctions.swift: Swift helpers for C bridge structs, persistent C strings, optional assignment, and geometry utilities.Squirrel/Sources/InputSource.swift: Text Input Source registration, enable/disable/select helpers, and current input source lookup.Squirrel/Resources/Info.plist: InputMethodKit registration metadata, input modes (Hans,Hant), IMK controller class names, connection name, Sparkle metadata, and input-source properties.Squirrel/Resources/Squirrel.entitlements: disables App Sandbox, enables network client access, and disables library validation for bundled dylibs/frameworks.Squirrel/SharedSupport: bundled Rime data, default schemas, OpenCC data, andsquirrel.yaml.Squirrel/librime-*.dylib,Squirrel/Frameworks/Linked Frameworks/librime.1.dylib: backend libraries and plugins used by the frontend.
SquirrelApp.main() is the only entry point.
- It first checks command-line arguments and exits early for maintenance commands:
--quit,--reload,--sync--install/--register-input-source--enable-input-source,--disable-input-source,--select-input-source--build--ascii,--nascii,--getascii
- If no maintenance command is handled, it creates an
IMKServerusingInputMethodConnectionNamefromInfo.plist. - It creates
NSApplication.shared, assignsSquirrelApplicationDelegate, sets accessory activation policy, and changes the current directory toBundle.main.sharedSupportPath. This is important because OpenCC/librime configuration may use relative dictionary paths. - It runs a quick problematic-launch detector to avoid repeated crash/freeze loops from bad configuration.
- Normal startup calls:
setupRime()startRime(fullCheck: false)loadSettings()app.run()
- On app-run return, it calls
rimeAPI.finalize().
SquirrelApplicationDelegate owns global librime setup.
setupRime()creates the user data directory (~/Library/Rime) and temporary log directory, setsRIME_LOG_DIR, installs librime's notification handler, fillsRimeTraits, and callsrimeAPI.setup(&traits).- Important trait paths and identity fields:
shared_data_dir: app bundle shared support path.user_data_dir:~/Library/Rime.log_dir: temporaryrime.squirreldirectory.- distribution code/name/version and
app_name = rime.squirrel.
startRime(fullCheck:)callsrimeAPI.initialize(nil), thenstart_maintenance(fullCheck). On successful maintenance it deployssquirrel.yamlwith theconfig_versionmarker.loadSettings()opens basesquirrelconfig, refreshes notification/status-icon settings, and loads light/dark panel themes.loadSettings(for schemaID:)opens the active schema config and, when it has astylesection, overlays schema-specific panel style. Otherwise it falls back to base config.shutdownRime()closes config and callsrimeAPI.finalize().applicationShouldTerminate(_:)callscleanup_all_sessions()before termination.
Do not initialize/finalize librime from individual input controllers. Controllers own sessions; the application delegate owns the backend lifetime.
SquirrelInputController subclasses IMKInputController and is the core input-method object.
init(server:delegate:client:)stores the initialIMKTextInputclient, callscreateSession(), and registers local notification observers for ASCII-mode set/report requests.createSession()chooses a client bundle identifier, creates a librime session withrimeAPI.create_session(), clearsschemaId, and applies app-specific options.destroySession()callsrimeAPI.destroy_session(session)and clears chord typing state.deinitdestroys the session.activateServer(_:)refreshes the current client, optionally overrides the keyboard layout fromkeyboard_layout, clears local preedit cache, and updates the menu-bar status label fromascii_modeif a session already exists.deactivateServer(_:)hides palettes, commits the current composition to the client, and releases the client reference.commitComposition(_:)commits raw pending librime input viaclient.insertText, then clears the librime composition.
The controller keeps client weak. Always guard client access. An input method may be activated, deactivated, or retargeted by macOS at awkward times.
The critical loop is handle(_:client:) -> Bool in SquirrelInputController.
- Ensure there is a valid librime session. If
session == 0orfind_session(session)fails, callcreateSession(). - Update the weak
IMKTextInputclient fromsenderwhen possible. - Detect client app bundle ID changes and apply
app_options/<bundle-id>fromsquirrel.yaml. - For
.flagsChanged:- Compute changed modifier flags by comparing with
lastModifiers. - Convert modifiers with
SquirrelKeycode.osxModifiersToRime. - Validate or infer modifier keycode. This protects against remote desktop tools sending bogus keycode 0 for modifier events.
- Handle caps lock specially because librime expects
XK_Caps_Lockbefore lock-mask state changes. - Process modifier releases before presses to handle delayed release events.
- Update
lastModifiersand callrimeUpdate().
- Compute changed modifier flags by comparing with
- For
.keyDown:- Ignore Command-modified shortcuts so the client application receives them.
- Choose
charactersIgnoringModifiersorcharactersdepending on modifiers and ASCII/non-ASCII behavior. - Convert keycode/character/modifiers to librime keycode and masks.
- Call
processKey(...). - Call
rimeUpdate()when a valid rime keycode was processed.
- Return
trueonly when the event was handled and should not continue to the client application.
recognizedEvents(_:) returns key-down and flags-changed masks only.
processKey(_:, modifiers:) is the narrow frontend/backend key boundary.
- Before calling librime, it synchronizes
_linearand_verticaloptions from the current panel theme. Arrow-key behavior can depend on candidate layout and text orientation. - It calls
rimeAPI.process_key(session, keycode, modifiers). - If librime does not handle a Vim-like command-mode escape (
Esc,Ctrl-C,Ctrl-[) andvim_modeis set, it forcesascii_modeon unless already in ASCII mode. - If librime handles a key while
_chord_typingis active, printable keys and modifiers are recorded and later released by a timer. Non-chording keys clear the chord buffer.
MacOSKeyCodes.swift is intentionally centralized. Add key translations there rather than scattering keycode conditionals through the controller.
rimeUpdate(clearReservedComments:) consumes all frontend-visible librime state after key processing, paging, selection, caret movement, or plugin UI refresh.
Main sequence:
- Clear reserved comment UI hints unless the caller explicitly preserves them.
rimeConsumeCommittedText()callsget_commit, inserts committed text into the client, frees the commit struct, resets local preedit, and hides the panel.get_statusdetects schema changes:- reloads schema-specific settings through the app delegate;
- calculates
inlinePreeditandinlineCandidateusing panel config plus librime options (no_inline,inline); - sets librime
soft_cursorto the inverse of inline preedit.
get_contextreads composition and menu state:- preedit string;
- selected segment byte offsets converted to Swift indices;
- cursor position;
- candidate texts, comments, labels, page number, last-page flag, highlighted index.
- It updates marked text through
show(preedit:selRange:caretPos:). - It updates the candidate panel through
showPanel(...)unless no context is available, in which case it hides palettes. - It frees the librime context.
The text path is:
NSEvent -> SquirrelInputController.handle -> processKey -> rimeAPI.process_key -> rimeUpdate -> get_commit/get_status/get_context -> client.insertText and/or client.setMarkedText plus SquirrelPanel.update.
- Committed text must go through
client.insertText(_, replacementRange: .empty). - Active composition should go through
client.setMarkedText(_, selectionRange:, replacementRange: .empty). show(preedit:selRange:caretPos:)caches the last marked preedit, caret, and selected range to avoid redundant marked-text calls.- When non-inline preedit is configured, the controller may set a full-width space (
U+3000) as marked text so clients such as iTerm2 do not echo every raw preedit character. commitComposition(_:)commits raw pending librime input during deactivation. This matters when macOS switches input sources or the focused text client changes.
Input methods must be conservative about when they consume events. Incorrect true returns drop app shortcuts or text; incorrect false returns can duplicate input.
The app delegate creates one shared SquirrelPanel during applicationWillFinishLaunching. The active input controller assigns itself to panel.inputController before updating the panel.
showPanel(...) gets cursor geometry from client.attributes(forCharacterIndex:lineHeightRectangle:), stores it in panel.position, and calls panel.update(...).
SquirrelPanel.update(...):
- stores the latest preedit/candidate state;
- builds a single attributed string containing preedit and candidate rows;
- applies theme attributes, candidate labels, comments, semantic comment colors, no-break hints, and paragraph styles;
- updates the
NSTextViewstorage and layout orientation; - forces TextKit layout before measuring geometry;
- calls
SquirrelView.drawView(...)for background/highlight paths; - calls
show()to position and display the panel.
SquirrelPanel.show():
- chooses screen based on cursor position;
- sets effective appearance;
- measures text with TextKit 2;
- constrains oversized panels to most of the screen and scales via content-view bounds;
- positions normal panels near the cursor, with special handling for vertical text;
- applies content-view rotation for vertical mode;
- configures translucency background (
NSGlassEffectViewon macOS 26+,NSVisualEffectViewotherwise); - orders the nonactivating panel front.
Mouse and scroll events on the panel are forwarded back to the input controller:
- click candidate ->
selectCandidate(_:)->rimeUpdate(); - click/scroll paging controls ->
page(up:)->rimeUpdate(); - click preedit position ->
moveCaret(forward:)->rimeUpdate().
SquirrelView owns the drawing and hit-testing model.
- It uses an
NSTextViewwith TextKit 2 layout to measure actual rendered text segments. contentRectandcontentRect(range:)enumerate text layout segments to compute bounds.draw(_:)builds Core Graphics paths for panel background, preedit background, candidate backgrounds, highlighted candidate, highlighted preedit range, border, shadow, and paging controls.shapeis also used as the panel background mask and hit-test region.click(at:)maps mouse points back into TextKit offsets and candidate/preedit ranges.
When changing panel layout, preserve the order: set attributed text, set layout orientation, force layout, measure, draw paths, then show/reposition.
SquirrelConfig is a typed facade over RimeConfig.
openBaseConfig()openssquirrelconfig.open(schemaID:baseConfig:)opens schema config and falls back to base config for missing values.getBool,getDouble,getString, andgetColorcache successful reads.getAppOptions(_:)reads boolean options underapp_options/<bundle-id>.
SquirrelTheme.load(config:dark:) reads global style/*, then optional preset color scheme settings. Per-color-scheme values can override style values for layout, color, fonts, alpha, spacing, and candidate formatting.
Important theme flags:
candidate_list_layout: linear vs stacked candidate list.text_orientation: horizontal vs vertical.inline_preedit,inline_candidate: marked text vs panel display strategy.translucency,mutual_exclusive,memorize_size,show_paging.candidate_format: template using[label],[candidate],[comment]; legacy%cand%@are normalized.
The app uses distributed notifications for process-to-running-instance commands.
SquirrelReloadNotification-> deploy: shutdown Rime, reinitialize, reload settings.SquirrelSyncNotification->sync_user_data().SquirrelToggleASCIIModeNotification-> posts localSquirrelSetASCIIModeNotificationwithBool.SquirrelGetASCIIModeNotification-> posts local report request; active controller responds withSquirrelASCIIModeResponse(asciiornascii).kTISNotifySelectedKeyboardInputSourceChanged-> updates status item visibility and finalizes stranded compositions.
The finalization fallback is important: some macOS/input-source switch paths may not call deactivateServer. When the selected input source no longer starts with im.rime.inputmethod.Squirrel, the app delegate calls deactivateServer on the panel's current input controller to avoid orphaned composition/panel state.
notificationHandler(...) is installed by setupRime() and receives backend notifications.
deploy/start,deploy/success,deploy/failure: show user notifications.option: parses enabled/disabled option names, gets abbreviated and long state labels from librime, updates status icon forascii_mode, and optionally shows a status message on the panel.propertywhere the value starts with_and contains=: treats it as a reserved frontend property and callshandleReservedProperty(...)on the current panel input controller on the main actor.schema: when notifications are enabled, extracts and shows schema name.
Reserved properties currently include:
_comment_highlight: comma-separated candidate indices to draw withaccent_text_color._comment_warning: comma-separated candidate indices to draw withwarning_text_color._refresh_ui: requestsrimeUpdate(clearReservedComments: false).
Reserved-property values are query-string compatible; bare comma lists are parsed under the value field.
SquirrelInstaller wraps Text Input Source Services.
- Input modes are
im.rime.inputmethod.Squirrel.Hansandim.rime.inputmethod.Squirrel.Hant;Hansis the primary default. register()callsTISRegisterInputSourcefor/Library/Input Library/Squirrel.appwhen no Squirrel modes are already enabled.enable,disable, andselectoperate on TIS input sources.currentInputSourceID()readsTISCopyCurrentKeyboardInputSource()and is used to control status item visibility and stranded-composition cleanup.
Info.plist must stay consistent with InputSource.swift: input mode identifiers, InputMethodConnectionName, and controller class names are part of macOS input method registration.
The Swift/C boundary uses generated librime types plus helpers in BridgingFunctions.swift.
- Initialize librime structs with
.rimeStructInit()so memory is zeroed anddata_sizeis set correctly. - Free librime-owned structs after successful reads: commits with
free_commit, statuses withfree_status, contexts withfree_context. setCString(_:to:)duplicates Swift strings for C fields. Be mindful that duplicated C strings are manually allocated.RimeStringSlice.asStringmust respect.length; do not replace it withString(cString:)for abbreviated labels.
Follow the existing Swift/AppKit style unless there is a strong local reason to do otherwise.
- Types use PascalCase:
SquirrelInputController,ReservedPropertyValue,SquirrelTheme. - Methods, properties, local variables, and enum cases use camelCase.
- Keep local acronym style consistent with nearby code:
rimeAPI,schemaId,currentApp,asciiMode. - Boolean names should read naturally with
is,has,can,should, or a clear state noun when the existing API already uses one. - Generated C bridge fields may keep snake_case names such as
data_size; use narrow SwiftLint suppressions rather than renaming generated API concepts. - Prefer
letfor values that do not change,privatefor implementation details, andprivate(set)when other types need read-only state. - Keep IMK lifecycle and event handling in
SquirrelInputController; keep global Rime/app lifetime inSquirrelApplicationDelegate. - Keep config access in
SquirrelConfigand configurable visual state inSquirrelTheme. - Keep key translation in
MacOSKeyCodes; do not scatter raw Carbon/Rime key mappings through input handling code. - Keep candidate panel state and positioning in
SquirrelPanel; keep drawing, geometry, and hit testing inSquirrelView.
Reuse existing helpers before adding new abstractions.
- Use
.rimeStructInit()for librime structs that need zeroed memory anddata_size. - Use
setCString(_:to:)when assigning Swift strings into Rime trait/config structs. - Use the
?=operator for optional config overrides in theme/config-loading code. - Use
NSRange.emptyfor the project's sentinel empty range. - Use
RimeStringSlice.asStringfor Rime slices because it respects the slice length. - Use
SquirrelKeycodefor macOS-to-Rime key conversion. - Extend
ReservedPropertyValuefor reserved-property parsing instead of adding one-off string parsing. - Add a shared helper only when multiple call sites need the same non-trivial behavior. Avoid wrapping a single straightforward expression.
Comment style is intentionally sparse.
- Keep the simple file headers already used by the project.
- Keep SwiftLint directive comments exactly where they are needed.
- Use English for retained comments.
- Comments should explain why, ordering constraints, ownership, or platform/backend quirks. Do not restate what the next line does.
- Remove commented-out debug prints and temporary tracing instead of preserving them in source.
- Keep comments near IMK/librime event ordering, TextKit measurement constraints, vertical-mode coordinate transforms, C memory ownership, and plugin/frontend contracts.
- Prefer one compact explanatory comment over long branch-by-branch examples unless the example prevents a likely regression.
Keep these invariants in mind for any change:
- Global librime lifetime belongs to the app delegate; session lifetime belongs to input controllers.
- Every key event path that changes librime state should call
rimeUpdate()exactly when frontend state needs to be consumed. - Do not consume Command shortcuts in normal text input; let client applications handle them.
- Deactivation must hide the panel and commit or clear active composition so no marked text or panel is stranded.
- Always guard against nil or stale
IMKTextInputclients. - Convert librime byte offsets into Swift string indices before building
NSRangevalues. - Keep
get_context,get_status, andget_commitfree calls paired with successful reads. - Preserve app-specific options on session creation and when the focused client bundle changes.
- Candidate panel geometry depends on TextKit layout results. Avoid measuring before layout is forced.
- Vertical text affects key behavior, layout orientation, content rotation, panel positioning, and scroll paging direction.
inlinePreeditandinlineCandidateare determined jointly by theme config and librime options.- Shared panel state should always point to the active input controller before candidate updates or mouse actions.
For key handling changes:
- Start in
SquirrelInputController.handleandprocessKey. - Put reusable key mappings in
MacOSKeyCodes.swift. - Preserve modifier ordering and caps-lock behavior.
- Verify event consumption semantics.
For candidate UI changes:
- Start in
SquirrelPanel.updatefor text/attributes/data shaping. - Use
SquirrelThemefor configurable style values. - Use
SquirrelViewfor geometry, drawing, and hit testing. - Test horizontal, linear, vertical, paging, inline preedit, and no-candidate states.
For config changes:
- Add reads in
SquirrelThemeorSquirrelConfigonly where the value belongs. - Keep base config and schema-specific fallback behavior intact.
- Consider dark/light theme loading separately.
For lifecycle or command changes:
- Start in
Main.swiftfor command-line behavior. - Start in
SquirrelApplicationDelegatefor app-global observers, Rime setup, status item behavior, and termination. - Keep distributed notification names stable unless all callers are updated.
For librime plugin/frontend coordination:
- Add reserved keys to
ReservedPropertyKey. - Parse values in
ReservedPropertyValueor inhandleReservedProperty. - Apply UI effects in
SquirrelInputControllerorSquirrelPanel, depending on whether the state belongs to the session or rendering. - Preserve
_refresh_uibehavior for plugin-driven redraws.
When possible, validate with Xcode build diagnostics or a full Xcode build. For behavior changes, manually exercise:
- input activation/deactivation in multiple apps;
- typing, committing, cancelling, and switching input sources mid-composition;
- ASCII mode toggle and status reporting;
- schema switching and schema-specific style reload;
- candidate selection by number key and mouse;
- paging by key, mouse, and scroll;
- inline and non-inline preedit;
- vertical and linear candidate layouts;
- deployment/reload and sync commands;
- app quit/log out cleanup.
Input-method bugs often appear as duplicated text, dropped shortcuts, orphaned candidate panels, stale marked text, or session-specific state leaking between client apps. Test around those failure modes first.