Skip to content

feat(ime): generalize IME key-mapping to any installed input source (multilingual) - #133

Merged
kazukinakai merged 2 commits into
mainfrom
feat/multilang-input-source
Aug 9, 2026
Merged

feat(ime): generalize IME key-mapping to any installed input source (multilingual)#133
kazukinakai merged 2 commits into
mainfrom
feat/multilang-input-source

Conversation

@kazukinakai

@kazukinakai kazukinakai commented Aug 6, 2026

Copy link
Copy Markdown
Member

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/TISCopyCurrentKeyboardInputSource read-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 NSEvent delivery via interpretKeyEvents, then real CGEvent.post(tap: .cghidEventTap) HID posting into a live NSTextView) to close this gap. Both failed for environmental reasons: the xctest host process running under swift test here has no real window-server session, and NSWindow construction crashes outright (Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value inside 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 it enabled=false and TISSelectInputSource fails with OSStatus -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.ITABCSCIM) and a third-party one (com.google.inputmethod.Japanese.basecom.google.inputmethod.Japanese).
  • KeyMapping gains outputInputSourceID: String?, mutually exclusive with the existing key-post output. Persists/round-trips through the existing dictionary-based UserDefaults storage; absent on legacy stored mappings (nil-safe).
  • KeyEvent.modifierKeyUp dispatches on outputInputSourceID: 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).
  • Settings › Shortcuts action picker gained a "Switch to Input Source" submenu listing every selectable source on the machine.
  • Default mappings unchanged: left ⌘ → Eisu key-post, right ⌘ → Kana key-post (Japanese mode).

Tests

  • InputSourceCatalogTests: parentCandidateID pure-logic cases (Apple + third-party vendor ids, no-dot id → nil), live read-only enumeration includes ABC, excludes palette sources, no duplicate ids.
  • KeyMappingTests: outputInputSourceID round-trips through toDictionary()/init(dictionary:), absent by default and on legacy (pre-field) dictionaries.
  • AppSettingsTests: updateKeyMappingOutputSource persists 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 calls selectInputSourceAction and does not post a key; key-post mapping posts a key and does not call selectInputSourceAction.

Acceptance

$ swift build   # Build complete
$ swift test    # 64 tests, 2 skips (diagnostic dump + gated spike), 0 failures

Real-machine confirmation needed (upgraded from "nice to have" to required before relying on this)

  1. Assign an installed input source (e.g. Pinyin, if added via System Settings → Keyboard → Input Sources) to right ⌘ through Settings → Shortcuts → "Switch to Input Source".
  2. In any text field, tap right ⌘, then type nihao.
  3. Confirm a real Pinyin composition/candidate window appears (not literal ASCII nihao) — this is the step automation could not verify here.
  4. Repeat for 2-Set Korean if available, typing a few jamo and confirming Hangul composition.

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 touches Settings/ShortcutsSettingsView.swift (localizes all its strings). Merging both will need a small textual conflict resolved there.

@kazukinakai

Copy link
Copy Markdown
Member Author

追加commit(51edddd)について: このbranchが元々main由来で持っていたテストの既存バグ(実SMAppService.register()呼び出しがxctestプロセス自体をログイン項目として登録してしまう)を修正するため、別PR #134 で作った修正を安全のためこのbranchにもportしました。PR #134の内容と同一・本PRの本題(多言語対応)とは無関係です。詳細はPR #134参照。

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
kazukinakai force-pushed the feat/multilang-input-source branch from 51edddd to 0e1bf5c Compare August 9, 2026 05:47
@kazukinakai
kazukinakai merged commit c014be3 into main Aug 9, 2026
11 checks passed
@kazukinakai
kazukinakai deleted the feat/multilang-input-source branch August 9, 2026 05:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant