kagikey 0.4.1

Cross-platform key mapper with first-class IME control
kagikey-0.4.1 is not a library.

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:

[[rule]]
from = "ctrl-["
to = ["esc", "ime:off"]

kagi also needs no kernel extension and no DriverKit driver on macOS — an event tap plus Accessibility permission is enough.

Install

cargo install kagikey    # the crate is `kagikey`; the binary is `kagi`
cargo install --path .   # or from a checkout

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:

cargo install kagikey --no-default-features

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.

[[rule]]
desc = "Ctrl+[ -> Esc, and close the IME"
from = "ctrl-["
to = ["esc", "ime:off"]

[[rule]]
desc = "Esc also closes the IME"
from = "~esc"
to = ["ime:off"]

[[rule]]
from = "~henkan"
to = ["ime:on"]
os = ["windows", "linux"]

[[rule]]
from = "~muhenkan"
to = ["ime:off"]
os = ["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 run         # capture and remap (default)
kagi check       # parse the config and print the rules that apply here
kagi watch       # print key events as they arrive, to discover key names
kagi service     # install | uninstall | status | start | stop
kagi permissions # request the OS grants kagi needs, and report what is missing
kagi update      # install the latest release (--check to only look)

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 service install

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.

macOS permissions

An event tap needs Accessibility and Input Monitoring, and macOS grants both per binary — the agent is not the terminal you installed from, so its first run fails regardless of what your terminal is allowed to do.

A binary that has never asked does not even appear in those lists; the only way in would be the + button and a file picker aimed at ~/.cargo/bin. So kagi asks, through IOHIDRequestAccess and AXIsProcessTrustedWithOptions, which is what registers the entry. kagi service install does it for you, kagi permissions repeats it on demand, and a failing start does it once.

Tick both entries, then kagi service start. Logs go to ~/Library/Logs/kagi.log.

The grant is keyed to the binary's contents, so replacing the binary invalidates it — after a cargo install you have to toggle each entry off and on again. That is why the generated agent sets KAGI_NO_AUTOUPDATE=1: a silent self-update would swap the binary out and leave the agent running blind, with nothing in the foreground to prompt you. 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

[macos]
ime = "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's ImmSetOpenStatus.
  • source switches the input source outright via Text Input Services, using ascii_source / japanese_source.

Development

cargo test
cargo check --target x86_64-pc-windows-msvc
cargo check --target x86_64-unknown-linux-gnu

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