[pull] main from jtroo:main#31
Open
pull[bot] wants to merge 820 commits into
Open
Conversation
7d323fc to
c6e77c0
Compare
d10943b to
6fbffdd
Compare
005ce5b to
1df61b5
Compare
ac5c084 to
910f57e
Compare
9800562 to
17423fb
Compare
ba21f86 to
2a84fc3
Compare
…#2032) Users repeatedly hit `IOHIDDeviceOpen error: (iokit/common) not permitted` at startup after granting only Input Monitoring (issue #1211 and its many duplicates) The commonly-missed fix is a *second* TCC grant under Accessibility, or removing a stale entry pinned to a prior binary path. With only Input Monitoring pre-flighted, the user sees a noisy IOKit log followed by a terse `grab failed` Add an `AXIsProcessTrusted` pre-flight right after the Input Monitoring pre-flight in `KbdIn::new`, matching the style of `ensure_input_monitoring_permission`. Call the no-argument variant so root/LaunchDaemon contexts do not attempt a UI prompt they cannot display; surface a single actionable error pointing at System Settings -> Privacy & Security -> Accessibility, and explicitly mention the path-pinning gotcha after a move/rename/upgrade Also enrich the `grab failed` error at the karabiner grab call so that, in the rare case both gates report granted but the device open still fails (typical cause: stale TCC entry), the user gets concrete remediation steps instead of a two-word error Declare the `AXIsProcessTrusted` FFI locally under a `#[link(name = "ApplicationServices", kind = "framework")]` block, mirroring the existing IOKit binding. No new crate deps
Add a macOS-only `--macos-request-permissions` flag that requests/registers Accessibility permission without reading a config, opening keyboard devices, touching Karabiner, or starting the remap loop. Reuse the existing macOS Accessibility preflight code for both normal startup and the new installer-friendly CLI path. Avoid claiming that kanata definitely has its own Accessibility entry when macOS reports the current process as trusted, since CLI launches from Terminal may reflect Terminal/responsible-process trust instead. Print a clearer message telling users to add the kanata binary manually if it is not listed. Document that Input Monitoring is still required for reading keyboard devices, and that `--macos-request-permissions` only helps with the Accessibility prompt/registration flow. Manual installer testing was done with this macOS Kanata installer script: https://github.com/alenkimov/moonlander_msklc/blob/c767d241d443273b61462e59956dc6f1749b472c/MacOS/install-macos-kanata.sh#L40
…2041) Fix two issues that occur when the user is typing while kanata starts or restarts on macOS. 1. Channel overflow crash (keyboard freeze) `tx.try_send(key_event)?` in the event loop propagates `TrySendError::Full` as a fatal error, crashing kanata. The `sync_channel(100)` fills up during the 500ms init phase because the processing thread only drains releases at 1 event/ms while the event loop pushes all incoming key events. With the keyboard seized and no process reading input, the keyboard freezes. Fix: fall back to blocking `send()` when the channel is full. The pipe buffers HID input from the kernel during the brief block, so no events are lost. Only crash on genuine channel disconnection (processing thread panic), which is unrecoverable anyway. 2. Stuck keys after restart When kanata is killed (`launchctl kickstart -k`, `kill`, crash), the Karabiner virtual HID driver retains the keyboard report from the old process. Keys that were physically pressed when the old process died remain "held" on the virtual keyboard. The new kanata process starts with an empty `keyboard.keys` set in C++ but the driver still has the stale state. Nobody sends releases for those keys, so macOS autorepeat fires indefinitely. Observed: a single 'j' key remained stuck repeating for 8 seconds after a restart, producing `jujjjjmjpjjjjjejjjjdjj jjjjjover the lazy dog`. Fix: after `wait_until_ready()`, post a no-op release (`KEY_F24`) which calls `async_post_report()` with the new process's empty keyboard set. This overwrites the driver's stale report, clearing all stuck keys.
…CPU load (#2040) Set `QOS_CLASS_USER_INTERACTIVE` on the processing thread and event loop thread on macOS via `pthread_set_qos_class_self_np()`. This is the macOS equivalent of the existing Windows `REALTIME_PRIORITY_CLASS` elevation in `Kanata::new()`. Under heavy CPU load (e.g. compiling a large project), the processing thread can be starved for 100-275ms. During this window the key release is not posted to the Karabiner virtual HID, so macOS sees the key as still held and triggers OS-level autorepeat, producing duplicate characters the user did not type. This only manifests with `tap-hold` configurations because tap-hold intentionally extends the virtual key-down duration while decisions are pending (the mechanism documented in discussion #422 and issue #1441). Controlled A/B test on an M4 MacBook Air running macOS 26: - **No tap-hold config, under build load:** clean output - **tap-hold config, under build load, without patch:** duplicate characters (`thee`, `brrrrown`, `fiveeeeee`) - **tap-hold config, under build load, with patch:** clean output across two consecutive runs, zero `is_autorepeat` anomalies The `libc` crate (already a dependency) exposes `pthread_set_qos_class_self_np` and `qos_class_t::QOS_CLASS_USER_INTERACTIVE`. No new dependencies. Related: #422, #1441, #450
Map USB HID usage page 7, usage 0x89 (Keyboard International3, the JIS ¥
key)
to `OsCode::KEY_YEN` in both directions of parser/src/keys/macos.rs
The physical ¥ key on JIS keyboards sits above Enter, left of Backspace.
Previously kanata dropped it as unrecognized on macOS with a debug-log
entry like:
InputEvent { value: 137, page: 7, code: 4294967295 } is unrecognized!
This made the key invisible to defsrc and unusable for any remapping.
The
companion key KEY_RO (HID International1, 0x87) was already supported,
leaving
KEY_YEN as the last gap for JIS layouts on macOS.
No change is needed in parser/src/keys/mod.rs: KEY_YEN is already in the
OsCode enum and the Unicode "¥" already resolves to it in
DEFAULT_MAPPINGS.
Manually tested on macOS 15 with a JIS Apple Magic Keyboard. Before the
patch,
pressing ¥ produced "is unrecognized!" and bypassed the processing loop.
After the patch:
sending KeyEvent { code: KEY_YEN (124), value: Press } to processing
loop
process recv ev KeyEvent { code: KEY_YEN (124), value: Press }
Related: #1621
Fix #2052. The forward mapping (`OsCode::KEY_COMPOSE` → HID usage `0x07/0x65`) existed so kanata could *output* the key, but the reverse mapping (`PageCode` → `OsCode`) was missing — it jumped from `0x64` (KEY_102ND) to `0x66` (KEY_POWER). When a third-party keyboard sends the ContextMenu/Application key, the HID event arrived but was silently dropped because it couldn't be converted back to an OsCode. One-line fix: add the missing `0x65 → KEY_COMPOSE` entry in the reverse mapping table. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…2057) Fix #2050. - Updates `docs/setup-macos.md` Step 2 to distinguish between Karabiner-Elements (daemon auto-managed) and standalone DriverKit installs (daemon must be started manually) - Adds `cfg_samples/karabiner-vhid-daemon.plist` LaunchDaemon for persistent daemon startup without KE - Adds troubleshooting entry for the `connect_failed asio.system:2` error - Adds VHID daemon plist cleanup to the uninstall section Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#1989) ## Summary Add `definputdevices` top-level configuration block and `(device N)` switch condition for per-device key mappings. Implement end-to-end on macOS using driverkit 0.3.0's per-event `device_hash`. ```kbd (definputdevices 1 ((name "Apple Internal Keyboard")) 2 ((name "Go60") (vendor_id 0x1D50)) ) (defsrc a) (deflayer base (switch ((device 1)) x break ((device 2)) y break () a break)) ``` ## Design Per review feedback on #1974: - IDs are `NonZeroU8` (1-255); `Option<NonZeroU8>` fits in 1 byte - `definputdevices` required when `(device N)` is used; undefined IDs error at parse time - Matchers use ALL-of semantics; duplicate properties error; definition order preserved (first match wins) - Hash values validated as hex at parse time, matched case-insensitively - `vendor_id`/`product_id` validated as `u16` (USB range) - New `DEVICE_VAL` (855) sentinel opcode, consistent with `LAYER_VAL`/`BASE_LAYER_VAL` - `device_id` on `KeyEvent` conditionally compiled out for Windows LLHOOK (which cannot distinguish devices); accessed via `device_id()`/`set_device_id()` helpers ### device_id as direct parameter vs HistoricalEvent\<DeviceID\> The original review suggested using `HistoricalEvent<DeviceID>` for the switch integration. This implementation uses a direct `device_id: Option<NonZeroU8>` parameter on `evaluate_boolean()` instead: - Device identity is current-event context, not event history. The question is "which device sent THIS key?", not "which device sent the Nth most recent key?" - `HistoricalEvent<T>` carries `ticks_since_occurrence` which has no meaning for device identity - Semantically closer to `default_layer` (a simple value passed through) than to `historical_keys` (an iterator over past events) If historical device queries are desired in the future (e.g., "was the 2nd most recent keypress from device 1?"), a `History<NonZeroU8>` could be added then. If the extra parameter is a concern now, a `SwitchContext` struct grouping the evaluation state would be a cleaner path than inventing device history. ## macOS implementation - Bump `karabiner-driverkit` to 0.3.0 (adds `device_hash` to `DKEvent`) - At startup, match `fetch_devices()` results against matchers to build `hash → device_id` map (first match wins, warns on overlaps) - In event loop, look up each event's `device_hash` to set `device_id` on `KeyEvent` --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a note about the MacOS Accessibility permission for sending mouse events. I encountered this issue, and this felt to me like a good addition to the platform issues page.
Add bidirectional HID Usage ↔ `OsCode` mappings on macOS for seven JIS
keys that were previously unhandled:
| HID Usage (page 0x07) | OsCode |
|---|---|
| `0x88` International2 | `KEY_KATAKANAHIRAGANA` |
| `0x8A` International4 | `KEY_HENKAN` (変換) |
| `0x8B` International5 | `KEY_MUHENKAN` (無変換) |
| `0x8C` International6 | `KEY_KPJPCOMMA` |
| `0x92` LANG3 | `KEY_KATAKANA` |
| `0x93` LANG4 | `KEY_HIRAGANA` |
| `0x94` LANG5 | `KEY_ZENKAKUHANKAKU` |
PC-style Japanese keyboards send these HID usages for 無変換 / 変換 /
カタカナ-ひらがな / 半角-全角 etc. Before this change, `TryFrom<PageCode> for
OsCode` and `TryFrom<OsCode> for PageCode` in `parser/src/keys/macos.rs`
had no arms for them, so `KeyEvent::try_from(InputEvent)` returned `Err`
and `src/kanata/macos.rs` fell into the `"{event:?} is unrecognized!"`
branch, writing the raw event straight back to the output. As a result
the keys could neither be remapped on input nor produced on output —
`defsrc mhnk` etc. silently did nothing on macOS.
The fix is purely additive: only previously-`Err` arms gain `Ok` values,
and the HID usage IDs are reserved by the USB HID spec for these exact
JIS keys, so no existing mapping is affected. Keys not listed in the
user's `defsrc` still take the unchanged passthrough path via the
`MAPPED_KEYS` check.
The existing `0x90` / `0x91` (LANG1 / LANG2) entries that the JIS Eisu /
Kana keys on Apple keyboards send are intentionally left as-is (mapped
to `KEY_HANGEUL` / `KEY_HANJA` — the canonical Linux names for those HID
usages, which Korean Hangul / Hanja also use).
## Checklist
- Add documentation to docs/config.adoc
- [x] N/A — these key names (`mhnk`/`muhenkan`/`ncnv`, `hnk`/`henkan`,
etc.) are already accepted by the parser and documented implicitly via
the mapping tables; the change only makes the existing names actually
function on macOS.
- Add example and basic docs to cfg_samples/kanata.kbd
- [x] N/A — same reason; no new config surface.
- Update error messages
- [x] N/A — no user-facing error paths changed. The previous `"{event:?}
is unrecognized!"` debug log was an internal symptom of the missing
mappings and is no longer reached for these keys.
- Added tests, or did manual testing
- [x] Yes — manual testing on Apple Silicon (macOS 15, M-series) with a
PC-style Japanese keyboard: built `cargo build --release --target
aarch64-apple-darwin`, installed over the running binary, and confirmed
that `(defsrc mhnk)` / `(defsrc hnk)` mappings now fire and that the
keys can also be emitted as output. Also ran `cargo test -p
kanata-parser` — all 114 tests pass, including the existing
`roundtrip_oscode_keycode`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clarify that the `home-row-mod-advanced.kbd` sample config uses `tap-hold-release-keys`.
…utput (#2073) ## Summary - Fixes a regression from #2069 where caps lock remapping stopped working on macOS - The caps lock LED sync in `write()` was missing an early return, causing caps lock events to be sent through both `IOHIDSetModifierLockState` and VirtualHID — the double-send toggled state twice, effectively making caps lock a no-op - Now caps lock output events early-return through `IOHIDSetModifierLockState` only, which correctly drives both the system modifier state and the physical keyboard LED ## Why IOHIDSetModifierLockState instead of VirtualHID? macOS routes LED output reports to the originating HID device. Since the DriverKit virtual keyboard has no physical LED, the physical keyboard LED never updates via the VirtualHID path. `IOHIDSetModifierLockState` sets both the modifier state (so apps see correct case) and drives the LED on the physical keyboard. ## Test plan - [x] Verified `IOHIDSetModifierLockState` controls the physical LED with a standalone C test program - [x] `(defsrc caps) (deflayer base caps)` — caps lock toggles LED and produces uppercase - [x] `(defsrc caps) (deflayer base esc)` — produces escape, LED does not toggle - [x] `(defsrc a) (deflayer base b)` with `process-unmapped-keys yes` — basic sanity check --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#2064) ## Summary - Adds `(tap-repress-timeout <ms>)` as an optional parameter for `tap-hold-opposite-hand` and `tap-hold-opposite-hand-release` - When set, a quick re-press of the same key within the timeout window immediately fires the tap action without entering the hold decision - Brings parity with all other tap-hold variants which already support this via a positional parameter Closes #2063 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Adds `macos-continue-if-no-devs-found` defcfg option, the macOS equivalent of `linux-continue-if-no-devs-found` - When enabled and no matching devices are found at startup, kanata keeps running and captures devices when they connect - Essential for Bluetooth keyboards that aren't connected at boot time Relates to #1982 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ards (#2076) ## Summary - When no `macos-dev-names-include` or `macos-dev-names-exclude` filter is configured, skip the `fetch_devices()` enumeration and use `register_device("")` to grab all HID devices — restoring the v1.11 default behavior - The enumerate-and-register-individually path introduced in #2031 filters out problematic virtual devices (Sidecar, etc.) but loses some Bluetooth keyboards somewhere in the pipeline between enumeration and registration - This caused connected BT keyboards (e.g. Magic Keyboard) to silently stop being grabbed while the internal MacBook keyboard still worked - The enumeration + virtual-device filtering path is preserved for users who configure `macos-dev-names-include` or `macos-dev-names-exclude` ## Context Reported by @oschrenk in #1982 — on a MacBook, the internal keyboard works fine but an external Bluetooth Magic Keyboard is not grabbed on master (works on v1.11). The `register_device("")` catch-all was replaced with explicit `fetch_devices()` enumeration in #2031 (Apr 19). Both paths use the same IOKit iterator (`consume_devices` → `get_keyboards_iterator`), so the BT keyboard likely is enumerated but gets lost during kanata's filtering or individual name-based re-registration step. The fix restores the three-branch structure from before #2031: 1. **Include list set** → enumerate and register only matching devices 2. **Exclude list set** → enumerate all, filter out excluded + virtual devices, register the rest 3. **No filters** → `register_device("")` catch-all grabs everything (v1.11 behavior) ## Tradeoff Restoring `register_device("")` for the no-filter default means Sidecar's virtual keyboard can be grabbed again, which caused crashes for some users (#1342). However: - v1.11 shipped with this behavior and Sidecar crashes were not widespread - Silently dropping Bluetooth keyboards affects more users than the Sidecar edge case - Users who do use Sidecar can opt into protection by adding `macos-dev-names-exclude ("Sidecar")` to their config ## Test plan - [x] All existing tests pass - [x] `cargo fmt` and `cargo clippy` clean - [ ] Manual: verify Bluetooth keyboard is grabbed when no include/exclude filter is configured - [ ] Manual: verify internal MacBook keyboard still works alongside BT keyboard - [ ] Manual: verify `macos-dev-names-exclude` still filters out specified devices Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The project hasn't been updated in years and is no longer available on GitHub.
The current description of the `--nodelay` command line argument reads: > By default, Kanata adds a delay before reading keyboard inputs. This helps protect against stale states when started from a terminal. However, it doesn't explain what a "stale state" actually is. This PR clarifies what a stale state is by giving an example of a state the delay protects against.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot]
Can you help keep this open source service alive? 💖 Please sponsor : )