Skip to content

InputGrab: add RequestInputGrab query path (priority) + upstream-readiness #20

Description

@malpern

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:

  1. kanata starts (root LaunchDaemon, often before/independent of the app), grabs, emits InputGrab once.
  2. The TCP client (KeyPath) connects + does its Hello handshake — after that.
  3. 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:

  • ClientMessage::RequestInputGrab (or fold into a generic status request).
  • Server replies with the current InputGrab snapshot (active + devices + reason).
  • Client sends it right after Hello (in KeyPath, next to the existing RequestCurrentLayerName), so the authoritative startup state arrives immediately on every 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.

  • Linux (evdev): emit InputGrab around EVIOCGRAB success/failure and on release/regrab.
  • Windows (interception / low-level hook): emit equivalent grab-state transitions, or document why the concept differs.
  • Make devices semantics coherent per-platform (device naming differs).

Plumbing cleanup for upstream taste

  • The pub tcp_notify_tx field reached via the lock from the event loop is fine for a fork; a maintainer may want a dedicated status broadcaster / non-pub accessor.
  • Confirm the lock-from-event-loop pattern doesn't risk contention/ordering surprises on the hot path.

Submission hygiene

  • Rebase onto current upstream main (keypath/bundled diverges).
  • Add docs to the upstream TCP protocol reference.
  • Open the PR against jtroo/kanata and link this issue.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions