feat(ime): generalize IME key-mapping to any installed input source (multilingual) - #133
Merged
Conversation
Member
Author
Confirms TIS-based switching works for Pinyin/Korean once the parent input method (e.g. com.apple.inputmethod.SCIM) is enabled alongside the child mode — unlike the known-broken Japanese Eisu/Kana internal mode toggle, this is a real cross-source switch and TIS handles it.
…ource Adds InputSourceCatalog, which enumerates selectable keyboard input sources (TIS category=Keyboard + selectCapable, so palette sources like Character Viewer/emoji are excluded) and selects one via TISSelectInputSource, auto-enabling its parent input method (e.g. com.apple.inputmethod.SCIM) and itself if needed — confirmed necessary and sufficient by InputSourceSwitchSpikeTests. KeyMapping gains outputInputSourceID, an optional field mutually exclusive with the existing key-post output. KeyEvent.modifierKeyUp dispatches on it: input-source mappings call TISSelectInputSource through InputSourceCatalog.select(id:), everything else (Eisu/Kana/ Disable/plain remaps) keeps posting a key exactly as before. Default mappings (left CMD -> Eisu key post, right CMD -> Kana key post) are unchanged. Settings > Shortcuts' action picker gained a "Switch to Input Source" submenu listing every selectable source on the machine. New tests: InputSourceCatalogTests (parentCandidateID pure logic, live enumeration filters out palette sources and dedupes), KeyMapping outputInputSourceID persistence round-trip (incl. legacy dictionaries without the field), AppSettings.updateKeyMappingOutputSource behavior, KeyEvent modifier-tap mechanism dispatch.
kazukinakai
force-pushed
the
feat/multilang-input-source
branch
from
August 9, 2026 05:47
51edddd to
0e1bf5c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Generalizes the ⌘IME key-mapping action from the fixed Eisu(102)/Kana(104) key-post pair to any installed keyboard input source (Pinyin, 2-Set Korean, etc.), while keeping the existing Japanese switching mechanism byte-for-byte unchanged.
Spike (Stage 0) — corrected claim, see below
InputSourceSwitchSpikeTests.swift(env-gated,CMDIME_SPIKE_INPUT_SOURCE_SWITCH=1) confirmed live on macOS 26.6 (Tahoe), at the TIS API layer only:TISEnableInputSource/TISSelectInputSource/TISCopyCurrentKeyboardInputSourceread-back all succeed and agree for switching between separate input sources (ABC ↔ Pinyin, ABC ↔ 2-Set Korean).This does not by itself prove real typing is routed through the selected IME. The historical Japanese Eisu/Kana bug's failure mode was exactly "the menu-bar indicator / TIS state switches correctly, but real typing doesn't" — read-back equality doesn't rule out an analogous failure for Pinyin. I attempted two automated typing-level checks (in-process synthetic
NSEventdelivery viainterpretKeyEvents, then realCGEvent.post(tap: .cghidEventTap)HID posting into a liveNSTextView) to close this gap. Both failed for environmental reasons: thexctesthost process running underswift testhere has no real window-server session, andNSWindowconstruction crashes outright (Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional valueinside AppKit's own window init) rather than degrading gracefully. This is a hard environment limitation, not evidence about Pinyin either way, so I removed that test rather than ship something that crashes the suite for anyone who sets the env var.Corrected status: API layer (enable/select/read-back) is confirmed green. Typing-level effectiveness is unproven by automation and needs a manual check — see "Real-machine confirmation" below for the exact steps.
Key finding (still valid, API layer): a mode source (e.g.
com.apple.inputmethod.SCIM.ITABC) belongs to a parent input method (com.apple.inputmethod.SCIM) that must itself be enabled first — enabling only the child mode leaves itenabled=falseandTISSelectInputSourcefails withOSStatus -50. Once the parent is enabled too, both directions (ABC→source, source→ABC) succeed with correct read-back.Stage 1 — implementation
InputSourceCatalog.swift(new): enumerates installed selectable keyboard input sources (TIS category=Keyboard + select-capable — this excludes palette sources like Character Viewer/emoji, a known contamination risk) and selects one, auto-enabling its parent input method and itself if needed.parentCandidateID(for:)derives the parent id by dropping the id's last dot-component — verified against real TIS dumps for both an Apple built-in IME (SCIM.ITABC→SCIM) and a third-party one (com.google.inputmethod.Japanese.base→com.google.inputmethod.Japanese).KeyMappinggainsoutputInputSourceID: String?, mutually exclusive with the existing key-postoutput. Persists/round-trips through the existing dictionary-based UserDefaults storage; absent on legacy stored mappings (nil-safe).KeyEvent.modifierKeyUpdispatches onoutputInputSourceID: set →InputSourceCatalog.select(id:)via a new test seam (selectInputSourceAction); unset → the existing key-post path, byte-for-byte unchanged (Eisu/Kana/Disable/plain remaps all unaffected).Tests
InputSourceCatalogTests:parentCandidateIDpure-logic cases (Apple + third-party vendor ids, no-dot id → nil), live read-only enumeration includes ABC, excludes palette sources, no duplicate ids.KeyMappingTests:outputInputSourceIDround-trips throughtoDictionary()/init(dictionary:), absent by default and on legacy (pre-field) dictionaries.AppSettingsTests:updateKeyMappingOutputSourcepersists and enables the row; choosing a key-post output clears a previously-set input-source output (mutual exclusivity).KeyEventTests: modifier-tap mechanism dispatch — input-source mapping callsselectInputSourceActionand does not post a key; key-post mapping posts a key and does not callselectInputSourceAction.Acceptance
Real-machine confirmation needed (upgraded from "nice to have" to required before relying on this)
nihao.nihao) — this is the step automation could not verify here.If step 3 fails (menu/TIS state looks switched but typing stays ASCII), that reproduces the Eisu/Kana failure mode for a different IME family and Stage 1's mechanism-selection design (TIS for non-Japanese, key-post for Japanese) would need to fall back to a key-post-based approach for that IME too — flagging this explicitly so it isn't missed in review.
Scope note for the companion PR
feat/i18n-string-catalog(same base) also touchesSettings/ShortcutsSettingsView.swift(localizes all its strings). Merging both will need a small textual conflict resolved there.