kagi
Cross-platform key mapper with IME on/off as a first-class action.
Ctrl+[ → Esc and close the IME, with one config, on macOS, Windows and
Linux.
Why another remapper
kanata, kmonad, Karabiner-Elements and xremap all remap keys well. None of them
treats "turn the IME off" as an action — you end up shelling out, or writing
per-OS glue like an AutoHotkey ImmSetOpenStatus helper next to a Karabiner
JSON rule next to an xremap YAML. kagi makes that one line:
[[]]
= "ctrl-["
= ["esc", "ime:off"]
kagi also needs no kernel extension and no DriverKit driver on macOS — an event tap plus Accessibility permission is enough.
Install
kagi keeps itself up to date in the background and exposes kagi update.
Packagers who own updates themselves can drop that (and with it the
reqwest/rustls tree) entirely:
Configure
kagi reads $KAGI_CONFIG, else ~/.config/kagi/kagi.toml, else
%APPDATA%\kagi\kagi.toml. See config/kagi.toml.
The file is rendered by teravars
before it is parsed, so one config can cover several machines: [vars] for
your own values, {{ system.os }} / {{ system.host }} / {{ system.user }}
for the machine, and include to pull in a shared fragment.
[[]]
= "Ctrl+[ -> Esc, and close the IME"
= "ctrl-["
= ["esc", "ime:off"]
[[]]
= "Esc also closes the IME"
= "~esc"
= ["ime:off"]
[[]]
= "~henkan"
= ["ime:on"]
= ["windows", "linux"]
[[]]
= "~muhenkan"
= ["ime:off"]
= ["windows", "linux"]
from
A chord such as ctrl-[, ctrl-shift-a, esc. - separates modifiers, so a
literal hyphen is minus (or the trailing - in ctrl--).
Modifiers: ctrl, shift, alt (Option on macOS), meta (Command / Win).
Modifiers must match exactly unless the rule is wildcarded.
Two prefixes, borrowed from AutoHotkey:
| prefix | meaning |
|---|---|
~ |
the original key still reaches the focused app |
* |
extra modifiers beyond the listed ones are tolerated |
to
| entry | effect |
|---|---|
esc, ctrl-a, … |
synthesize that key |
ime:on / ime:off / ime:toggle |
drive the platform IME |
source:<id> |
select an input source (macOS TIS id, Linux IME engine) |
cmd:<shell command> |
run a command, detached |
An empty to swallows the key.
os
os = ["windows", "linux"] restricts a rule to those platforms. Omit it for
every platform.
Commands
kagi watch is the way to find the name of a key your keyboard actually sends:
down ctrl-[ (keycode=0x21 flags=0x20040000)
Run it at login
kagi is a daemon, so this registers the mechanism each platform actually wants rather than one generic autostart entry. Everything is per-user; none of it needs root.
| mechanism | why not the obvious one | |
|---|---|---|
| macOS | launchd LaunchAgent | ProcessType = Interactive keeps launchd from throttling keyboard handling behind background QoS |
| Linux | systemd user unit, PartOf=graphical-session.target |
kagi grabs evdev devices, so it has to come up and go down with the session; a .desktop autostart entry gives no ordering and no restart-on-failure |
| Windows | logon scheduled task driving a wscript shim |
a console binary launched from the Startup folder leaves a window on screen for as long as the daemon runs |
install compiles your config first and refuses to register a service that
would die on startup. Pass --config to bake a non-default path into the
registration.
kagi service status reports registration and run state; uninstall,
start and stop do what they say.
On macOS the agent is a different binary from the terminal you installed
from, and permission is granted per binary, so the first run fails until
~/.cargo/bin/kagi is added under System Settings ▸ Privacy & Security ▸
Accessibility and Input Monitoring. Then
launchctl kickstart -k gui/$(id -u)/com.yukimemi.kagi. Logs go to
~/Library/Logs/kagi.log.
The generated agent sets KAGI_NO_AUTOUPDATE=1 deliberately: macOS keys
those grants to the binary, so a silent self-update would swap it out and
leave the agent running without being able to see any key — and with nothing
in the foreground, no prompt. Update deliberately with kagi update.
How each platform does it
| capture | IME | |
|---|---|---|
| macOS | CGEventTap at kCGHIDEventTap |
posts the JIS 英数/かな keycodes, or TISSelectInputSource |
| Windows | WH_KEYBOARD_LL hook + SendInput |
ImmGetDefaultIMEWnd + WM_IME_CONTROL/IMC_SETOPENSTATUS |
| Linux | evdev + uinput (works under X11 and Wayland) | fcitx5-remote, else ibus |
Permissions
- macOS — System Settings ▸ Privacy & Security ▸ Accessibility and Input Monitoring, for the kagi binary (or the terminal launching it). Granting them requires restarting the granted app. Event taps are bypassed while a secure input field has focus, and at the login window.
- Windows — an elevated foreground window only receives hooked input if kagi runs elevated too.
- Linux — read access to
/dev/input/event*(sudo usermod -aG input $USER) and write access to/dev/uinput(udev rule, or run as root).
macOS IME method
[]
= "eisu" # or "source"
eisu(default) posts the 英数 / かな keycodes a JIS keyboard sends. Every Japanese IME honours them and the selected input method is preserved — the closest analogue to AutoHotkey'sImmSetOpenStatus.sourceswitches the input source outright via Text Input Services, usingascii_source/japanese_source.
Development
cargo run --example synth posts a synthetic Ctrl+[ so the macOS tap can be
exercised without a human at the keyboard. Run kagi watch first, then
kagi run, then synth: the watcher should report esc and eisu rather
than [.
License
MIT