Skip to content

[pull] main from jtroo:main#31

Open
pull[bot] wants to merge 820 commits into
f8ith:mainfrom
jtroo:main
Open

[pull] main from jtroo:main#31
pull[bot] wants to merge 820 commits into
f8ith:mainfrom
jtroo:main

Conversation

@pull

@pull pull Bot commented Jun 28, 2023

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot]

Can you help keep this open source service alive? 💖 Please sponsor : )

FlameFlag and others added 30 commits April 19, 2026 13:32
…#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.