Update (2026-05-29): Re-scoped after shipping the KeyPath consumer
(KeyPath#635). The query path
(RequestInputGrab) is promoted from a follow-up to the high-priority item —
it's what makes InputGrab actually deliver its value. Cross-platform + plumbing
remain genuine upstream-readiness work but are lower priority. See the priority
section below.
Goal
Make the ServerMessage::InputGrab TCP status (branch feat/input-grab-status, now merged to keypath/bundled at cd2a1e5) ready to submit upstream to jtroo/kanata. The feature is generic and vendor-neutral — "is kanata actually grabbing the keyboard?" is useful to any kanata TCP frontend — so it's a strong upstream candidate. Upstreaming it also retires fork rebase debt.
⚠️ Priority: add a query path (RequestInputGrab)
This is the most important item — without it, InputGrab misses its primary use case.
The no-replay gap. InputGrab is emitted only on grab-state transitions, and kanata does NOT replay current state when a client connects. Walk the timeline:
- kanata starts (root LaunchDaemon, often before/independent of the app), grabs, emits
InputGrab once.
- The TCP client (KeyPath) connects + does its
Hello handshake — after that.
- No replay → the startup emit was already dropped.
So the signal only ever reaches a client for transitions that happen while it is already connected. The case it misses is "did kanata succeed at grabbing at startup?" — which is both the most common health question and the exact original incident this whole effort came from (the karabiner-driverkit CString panic was a startup grab failure). For that case the consumer still falls back to inferential stderr log-scraping — the very thing InputGrab was meant to supersede.
The fix is small. Add a request/response so a client can query current grab state on connect:
This closes the gap completely and is what makes InputGrab the primary signal (today it's a mid-session-only safety net layered over jtroo#632). Should land before KeyPath leans on InputGrab for jtroo#624 (honest VNC-aware status).
Current state (shipped on keypath/bundled)
InputGrab {
active: bool, // ≥1 physical device seized
devices: Vec<String>, // seized device names; [] when inactive
reason: Option<String>, // omitted when None; typically a failure cause
}
- Emitted on macOS at every grab-state transition: startup success/deferred/failure, release (output-backend loss / screen lock / fast-user-switch), re-seize success/failure.
- Plumbing: a clone of the notification
Sender<ServerMessage> is stored as pub tcp_notify_tx on the Kanata struct (wired in main.rs), emitted from the macOS event loop via kanata.lock().emit_input_grab(...). All feature-gated behind tcp_server.
- KeyPath consumer merged in KeyPath#635: strictly-additive layering over the save multiple arguments in defvar to use with cmd jtroo/kanata#632 stderr detector (only
active:false overrides), wired into both health pipelines.
Remaining upstream-readiness work (lower priority)
Cross-platform emit sites
Upstream supports Linux and Windows; a macOS-only TCP message is a partial feature.
Plumbing cleanup for upstream taste
Submission hygiene
Notes
- Do the upstream work after the feature is proven in KeyPath production — don't block KeyPath on it.
- Keep the JSON schema stable (
active/devices/reason); KeyPath consumes it.
Goal
Make the
ServerMessage::InputGrabTCP status (branchfeat/input-grab-status, now merged tokeypath/bundledatcd2a1e5) ready to submit upstream to jtroo/kanata. The feature is generic and vendor-neutral — "is kanata actually grabbing the keyboard?" is useful to any kanata TCP frontend — so it's a strong upstream candidate. Upstreaming it also retires fork rebase debt.RequestInputGrab)This is the most important item — without it,
InputGrabmisses its primary use case.The no-replay gap.
InputGrabis emitted only on grab-state transitions, and kanata does NOT replay current state when a client connects. Walk the timeline:InputGrabonce.Hellohandshake — after that.So the signal only ever reaches a client for transitions that happen while it is already connected. The case it misses is "did kanata succeed at grabbing at startup?" — which is both the most common health question and the exact original incident this whole effort came from (the karabiner-driverkit
CStringpanic was a startup grab failure). For that case the consumer still falls back to inferential stderr log-scraping — the very thingInputGrabwas meant to supersede.The fix is small. Add a request/response so a client can query current grab state on connect:
ClientMessage::RequestInputGrab(or fold into a generic status request).InputGrabsnapshot (active + devices + reason).Hello(in KeyPath, next to the existingRequestCurrentLayerName), so the authoritative startup state arrives immediately on every connect.This closes the gap completely and is what makes
InputGrabthe primary signal (today it's a mid-session-only safety net layered over jtroo#632). Should land before KeyPath leans onInputGrabfor jtroo#624 (honest VNC-aware status).Current state (shipped on
keypath/bundled)Sender<ServerMessage>is stored aspub tcp_notify_txon theKanatastruct (wired inmain.rs), emitted from the macOS event loop viakanata.lock().emit_input_grab(...). All feature-gated behindtcp_server.active:falseoverrides), wired into both health pipelines.Remaining upstream-readiness work (lower priority)
Cross-platform emit sites
Upstream supports Linux and Windows; a macOS-only TCP message is a partial feature.
InputGrabaroundEVIOCGRABsuccess/failure and on release/regrab.devicessemantics coherent per-platform (device naming differs).Plumbing cleanup for upstream taste
pub tcp_notify_txfield reached via the lock from the event loop is fine for a fork; a maintainer may want a dedicated status broadcaster / non-pubaccessor.Submission hygiene
main(keypath/bundleddiverges).jtroo/kanataand link this issue.Notes
active/devices/reason); KeyPath consumes it.