{"id":50311100,"url":"https://github.com/widgetii/karabiner-cjk-helper","last_synced_at":"2026-05-28T21:01:10.442Z","repository":{"id":354595030,"uuid":"1224319483","full_name":"widgetii/karabiner-cjk-helper","owner":"widgetii","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-29T07:09:46.000Z","size":16,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-29T09:10:52.399Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/widgetii.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-29T07:01:05.000Z","updated_at":"2026-04-29T07:09:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/widgetii/karabiner-cjk-helper","commit_stats":null,"previous_names":["widgetii/karabiner-cjk-helper"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/widgetii/karabiner-cjk-helper","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/widgetii%2Fkarabiner-cjk-helper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/widgetii%2Fkarabiner-cjk-helper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/widgetii%2Fkarabiner-cjk-helper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/widgetii%2Fkarabiner-cjk-helper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/widgetii","download_url":"https://codeload.github.com/widgetii/karabiner-cjk-helper/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/widgetii%2Fkarabiner-cjk-helper/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33626142,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-28T21:01:08.080Z","updated_at":"2026-05-28T21:01:10.425Z","avatar_url":"https://github.com/widgetii.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# karabiner-cjk-helper\n\nA tiny background helper that fixes macOS' broken programmatic input-source\nswitching, so that Karabiner-Elements key taps for Pinyin / Russian / Latin\nlayouts actually engage the right input method every time.\n\n## The problem\n\nYou have Karabiner-Elements bound to switch input sources on key tap\n(e.g. `left_command` → English, `right_command` → Russian, `left_control`\n→ Pinyin). It works for plain keyboard layouts, but for Apple Pinyin\n(`com.apple.inputmethod.SCIM.ITABC`) — and for any switch that crosses\nbetween a non-ASCII layout (Russian, Arabic, Hebrew, Greek) and a CJK IME\n— you observe one or more of:\n\n- The macOS menu-bar input-source icon updates, but typing keeps\n  going through the previous layout. You have to click the menu bar\n  manually to make the switch \"take\".\n- After typing in Pinyin, switching back to Russian leaves you typing\n  Latin (`dhbsgk`) instead of Cyrillic (`привет`).\n- The above behaves randomly: works most of the time, fails seemingly\n  at random in heavy use.\n\nThis is **not a Karabiner bug** — Karabiner's `select_input_source` action\njust calls Carbon's `TISSelectInputSource`, which is what every other tool\nuses. The Karabiner maintainer has explicitly declared this out of scope\n(see [pqrs-org/Karabiner-Elements#4266](https://github.com/pqrs-org/Karabiner-Elements/issues/4266)).\n\nThe actual bugs are in macOS:\n\n1. **Faceless launchd-managed daemons** (which is what\n   `karabiner_console_user_server` is) aren't treated as \"valid TSM clients\"\n   for CJK IME activation. `TISSelectInputSource` returns success but the\n   IME server never binds the new source to the focused text input client.\n2. **Switching between a non-ASCII layout and a CJK IME** leaves the\n   underlying Latin layout half-resolved. Apple's documented workaround\n   is to interpose an ASCII layout (e.g. ABC). See\n   [Apple Discussions #8197631](https://discussions.apple.com/thread/8197631).\n\n## What this helper does\n\nThree components, all outside the Karabiner-Elements source tree:\n\n- **`karabiner-cjk-helper`** — a long-lived launchd agent. Listens on\n  `/tmp/karabiner-cjk-helper.sock`. On each request it spawns the kicker\n  with `posix_spawn(POSIX_SPAWN_SETSID)` so the kicker gets its own\n  session and is treated as a foreground-eligible TSM client. The daemon\n  itself never touches Carbon/TIS — that's deliberate, because long-lived\n  TIS-touching processes appear to leak IME state.\n- **`karabiner-cjk-kicker`** — a short-lived AppKit one-shot that does\n  the actual switching:\n  - For non-CJK targets: plain `TISSelectInputSource`.\n  - For CJK targets: optional pre-switch through Colemak (when current\n    is non-ASCII, e.g. Russian), then `TISSelectInputSource`, then\n    `TISSetInputMethodKeyboardLayoutOverride` to pin Pinyin's underlying\n    Latin layout to Colemak (so your Colemak fingers produce the right\n    pinyin syllables, not QWERTY transliterations of them), then the\n    macism-style window-flash to engage the IME server.\n  - For CJK → non-ASCII transitions: two-step via Colemak with a 40 ms\n    gap so the second TIS call isn't coalesced.\n- **`karabiner-cjk-trigger`** — a 30-line C client that opens the socket\n  and writes the target input-source id. Fast (~2 ms cold), so Karabiner's\n  `shell_command` invocation of it doesn't add perceptible latency.\n\nThe Latin layout used as the override / interpose is hard-coded to prefer\nColemak first, then ABC, then U.S. — adjust `pickAsciiLatin()` in\n`src/karabiner-cjk-kicker.swift` if your preferred Latin layout is\ndifferent.\n\n## Installation\n\nRequires macOS 13+, Xcode command line tools, and Homebrew (only used\nfor `make`).\n\n```sh\ngit clone https://github.com/widgetii/karabiner-cjk-helper.git\ncd karabiner-cjk-helper\nmake build\nmake install        # sudo for /usr/local/bin install + launchctl bootstrap\n```\n\nVerify the daemon is running:\n\n```sh\nlaunchctl print gui/$(id -u)/org.dilyin.karabiner-cjk-helper | grep state\n# → state = running\n```\n\n## Karabiner-Elements config\n\nReplace your existing `select_input_source` actions with `shell_command`\ncalls to the trigger. Example (from `~/.config/karabiner/karabiner.json`):\n\n```json\n{\n  \"to_if_alone\": [\n    { \"shell_command\": \"/usr/local/bin/karabiner-cjk-trigger com.apple.keylayout.Colemak\" }\n  ]\n}\n```\n\n```json\n{\n  \"to_if_alone\": [\n    { \"shell_command\": \"/usr/local/bin/karabiner-cjk-trigger com.apple.keylayout.Russian\" }\n  ]\n}\n```\n\n```json\n{\n  \"to_if_alone\": [\n    { \"shell_command\": \"/usr/local/bin/karabiner-cjk-trigger com.apple.inputmethod.SCIM.ITABC\" }\n  ]\n}\n```\n\nKarabiner reloads its config automatically when you save the file.\n\n## Uninstall\n\n```sh\nmake uninstall\n```\n\n## Tests\n\n`tests/` contains diagnostic scripts (a live input-source poller, a\nTextEdit-driven multi-mechanism stress test, and a focused Russian↔Pinyin\nalternation test). See [tests/README.md](tests/README.md). They use\nsynthetic events so they're not perfect simulations of real hardware\ntyping, but they reproduce the same TIS/IME state machinery and catch\nmost regressions.\n\n## Reliability\n\nIn a TextEdit-driven end-to-end test (synthetic keystrokes via\nAppleScript `keystroke`, 30 cycles alternating Russian↔Pinyin):\n\n| Transition       | Reliability |\n|------------------|-------------|\n| Colemak ↔ Russian | 100%        |\n| Colemak ↔ Pinyin  | ~95%        |\n| Russian ↔ Pinyin  | ~85% Pinyin engage, 100% Russian engage |\n\nThe remaining Pinyin failures appear to be the underlying macOS-level\nrandomness — they happen at the same rate with `macism` directly.\n\n## Credits\n\n- The \"tiny key window\" IME-engagement trick is from\n  [`laishulu/macism`](https://github.com/laishulu/macism), which itself\n  credits a [Squirrel issue\n  comment](https://github.com/rime/squirrel/issues/866#issuecomment-2800561092)\n  for the empirical insight.\n- The \"interpose ASCII layout to fix Cyrillic↔CJK\" workaround is documented\n  in [Apple Discussions](https://discussions.apple.com/thread/8197631).\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwidgetii%2Fkarabiner-cjk-helper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwidgetii%2Fkarabiner-cjk-helper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwidgetii%2Fkarabiner-cjk-helper/lists"}