https://github.com/widgetii/karabiner-cjk-helper
https://github.com/widgetii/karabiner-cjk-helper
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/widgetii/karabiner-cjk-helper
- Owner: widgetii
- License: mit
- Created: 2026-04-29T07:01:05.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-29T07:09:46.000Z (2 months ago)
- Last Synced: 2026-04-29T09:10:52.399Z (2 months ago)
- Language: Swift
- Size: 15.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# karabiner-cjk-helper
A tiny background helper that fixes macOS' broken programmatic input-source
switching, so that Karabiner-Elements key taps for Pinyin / Russian / Latin
layouts actually engage the right input method every time.
## The problem
You have Karabiner-Elements bound to switch input sources on key tap
(e.g. `left_command` → English, `right_command` → Russian, `left_control`
→ Pinyin). It works for plain keyboard layouts, but for Apple Pinyin
(`com.apple.inputmethod.SCIM.ITABC`) — and for any switch that crosses
between a non-ASCII layout (Russian, Arabic, Hebrew, Greek) and a CJK IME
— you observe one or more of:
- The macOS menu-bar input-source icon updates, but typing keeps
going through the previous layout. You have to click the menu bar
manually to make the switch "take".
- After typing in Pinyin, switching back to Russian leaves you typing
Latin (`dhbsgk`) instead of Cyrillic (`привет`).
- The above behaves randomly: works most of the time, fails seemingly
at random in heavy use.
This is **not a Karabiner bug** — Karabiner's `select_input_source` action
just calls Carbon's `TISSelectInputSource`, which is what every other tool
uses. The Karabiner maintainer has explicitly declared this out of scope
(see [pqrs-org/Karabiner-Elements#4266](https://github.com/pqrs-org/Karabiner-Elements/issues/4266)).
The actual bugs are in macOS:
1. **Faceless launchd-managed daemons** (which is what
`karabiner_console_user_server` is) aren't treated as "valid TSM clients"
for CJK IME activation. `TISSelectInputSource` returns success but the
IME server never binds the new source to the focused text input client.
2. **Switching between a non-ASCII layout and a CJK IME** leaves the
underlying Latin layout half-resolved. Apple's documented workaround
is to interpose an ASCII layout (e.g. ABC). See
[Apple Discussions #8197631](https://discussions.apple.com/thread/8197631).
## What this helper does
Three components, all outside the Karabiner-Elements source tree:
- **`karabiner-cjk-helper`** — a long-lived launchd agent. Listens on
`/tmp/karabiner-cjk-helper.sock`. On each request it spawns the kicker
with `posix_spawn(POSIX_SPAWN_SETSID)` so the kicker gets its own
session and is treated as a foreground-eligible TSM client. The daemon
itself never touches Carbon/TIS — that's deliberate, because long-lived
TIS-touching processes appear to leak IME state.
- **`karabiner-cjk-kicker`** — a short-lived AppKit one-shot that does
the actual switching:
- For non-CJK targets: plain `TISSelectInputSource`.
- For CJK targets: optional pre-switch through Colemak (when current
is non-ASCII, e.g. Russian), then `TISSelectInputSource`, then
`TISSetInputMethodKeyboardLayoutOverride` to pin Pinyin's underlying
Latin layout to Colemak (so your Colemak fingers produce the right
pinyin syllables, not QWERTY transliterations of them), then the
macism-style window-flash to engage the IME server.
- For CJK → non-ASCII transitions: two-step via Colemak with a 40 ms
gap so the second TIS call isn't coalesced.
- **`karabiner-cjk-trigger`** — a 30-line C client that opens the socket
and writes the target input-source id. Fast (~2 ms cold), so Karabiner's
`shell_command` invocation of it doesn't add perceptible latency.
The Latin layout used as the override / interpose is hard-coded to prefer
Colemak first, then ABC, then U.S. — adjust `pickAsciiLatin()` in
`src/karabiner-cjk-kicker.swift` if your preferred Latin layout is
different.
## Installation
Requires macOS 13+, Xcode command line tools, and Homebrew (only used
for `make`).
```sh
git clone https://github.com/widgetii/karabiner-cjk-helper.git
cd karabiner-cjk-helper
make build
make install # sudo for /usr/local/bin install + launchctl bootstrap
```
Verify the daemon is running:
```sh
launchctl print gui/$(id -u)/org.dilyin.karabiner-cjk-helper | grep state
# → state = running
```
## Karabiner-Elements config
Replace your existing `select_input_source` actions with `shell_command`
calls to the trigger. Example (from `~/.config/karabiner/karabiner.json`):
```json
{
"to_if_alone": [
{ "shell_command": "/usr/local/bin/karabiner-cjk-trigger com.apple.keylayout.Colemak" }
]
}
```
```json
{
"to_if_alone": [
{ "shell_command": "/usr/local/bin/karabiner-cjk-trigger com.apple.keylayout.Russian" }
]
}
```
```json
{
"to_if_alone": [
{ "shell_command": "/usr/local/bin/karabiner-cjk-trigger com.apple.inputmethod.SCIM.ITABC" }
]
}
```
Karabiner reloads its config automatically when you save the file.
## Uninstall
```sh
make uninstall
```
## Tests
`tests/` contains diagnostic scripts (a live input-source poller, a
TextEdit-driven multi-mechanism stress test, and a focused Russian↔Pinyin
alternation test). See [tests/README.md](tests/README.md). They use
synthetic events so they're not perfect simulations of real hardware
typing, but they reproduce the same TIS/IME state machinery and catch
most regressions.
## Reliability
In a TextEdit-driven end-to-end test (synthetic keystrokes via
AppleScript `keystroke`, 30 cycles alternating Russian↔Pinyin):
| Transition | Reliability |
|------------------|-------------|
| Colemak ↔ Russian | 100% |
| Colemak ↔ Pinyin | ~95% |
| Russian ↔ Pinyin | ~85% Pinyin engage, 100% Russian engage |
The remaining Pinyin failures appear to be the underlying macOS-level
randomness — they happen at the same rate with `macism` directly.
## Credits
- The "tiny key window" IME-engagement trick is from
[`laishulu/macism`](https://github.com/laishulu/macism), which itself
credits a [Squirrel issue
comment](https://github.com/rime/squirrel/issues/866#issuecomment-2800561092)
for the empirical insight.
- The "interpose ASCII layout to fix Cyrillic↔CJK" workaround is documented
in [Apple Discussions](https://discussions.apple.com/thread/8197631).
## License
MIT. See [LICENSE](LICENSE).