Skip to content

Driverless media keys, part 2: the session reads them as inputs (phase 6) - #1332

Merged
malpern merged 1 commit into
masterfrom
feat/driverless-media-input
Oct 20, 2026
Merged

malpern merged 1 commit into
masterfrom
feat/driverless-media-input

Conversation

@malpern

@malpern malpern commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Completes phase 6 (review §4.5): media keys on Apple keyboards work as rule inputs, not only outputs.

What

  • Bridge. physical_input_supported admits the ten consumer-page codes the worker handles (same table as outputs), in plain and managed-Caps validation and for chord participants.
  • Worker. Tap mask gains NX_SYSDEFINED (14). SessionKeyMap.mediaInput(subtype:data1:) decodes a subtype-8 event to Kanata's (usage, value); a key the profile maps is sent as a consumer-page input and consumed, anything else passes through untouched (so unmapped volume/brightness behave exactly as before). Held media inputs have their own ledger (mediaInputs — 0xE2 is Mute on page 12 but Left Alt on page 7); a release whose press predates the tap passes through; hardware repeats of a held mapped key are swallowed (Kanata repeats itself).
  • Per-device media rules remain impossible (system-defined events carry no source device).

Tests

Rust 24 (media keys are inputs; calc isn't; managed-Caps case). Swift: decode table incl. repeat/up/unknown/subtype, input↔output table agreement, and testCapturedMediaKeyIsAnAdmittedInput (captured Mute as input → saved → defsrc contains mute → real bridge admits it). KEYPATH_TEST_ENFORCE_CLEAN_SUMMARY=1 Scripts/run-tests-safe.sh: 5,805 passed, 0 app warnings/errors, 0 build warnings.

Hardware (Air) — pending

Rule with physical F10 (Mute) as input → fires; unmapped F11/F12 still change volume; held mapped key doesn't double-fire.

🤖 Generated with Claude Code

…e 6)

Apple keyboards deliver their media keys as macOS system-defined events,
which the worker's tap never asked for, so a rule with a media key as its
input could not fire. The bridge's physical-input rule now admits the same
ten consumer-page keys it posts; the worker adds NX_SYSDEFINED to its tap
mask, decodes subtype-8 events (SessionKeyMap.mediaInput) and hands a
mapped key to Kanata as a consumer-page input, consuming it. Unmapped media
keys pass through untouched. Held media inputs have their own ledger (usage
numbers overlap across pages), a release whose press predates the tap
passes through, and hardware repeats of a held mapped key are swallowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T17:56:12.596059Z 32e2338 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Oct 20, 2026

Copy link
Copy Markdown

Review of PR 1332 (media keys as session inputs): no blocking issues. Two things worth a look.

  1. Possible stuck key in receiveMedia (SessionRuntimeWorker.swift). The isInputMapped guard runs before the mediaInputs ledger check. If a mapped media key is held and a config reload makes it unmapped before release, the release (value 0) takes the return original path. mediaInputs keeps the usage and Kanata never sees the key-up, so the key stays down in Kanata. Checking mediaInputs.contains(usage) first would route releases of held keys to Kanata whether or not the key is still mapped. The same applies to a tap disable or re-enable mid-press, so consider clearing mediaInputs there.

  2. Test coverage. SessionKeyMap.mediaInput decoding and the bridge admission are covered. The worker state machine is not: swallowed repeats, a release whose press predates the tap, and pass-through of unmapped keys. If receiveMedia can't be unit-tested as it stands, pulling the ledger logic into a small pure type (like SessionOutputState) would let these cases be tested.

Minor: a duplicate down (value 1) for a key already in mediaInputs is sent to Kanata again. A guard like the one for repeats would avoid that.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 32e2338615

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

private static let mediaKeyTypeToConsumerUsage = Dictionary(
uniqueKeysWithValues: consumerUsageToMediaKeyType.map { ($0.value, $0.key) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude media inputs that cannot be named in Kanata

When hardware sends NX_KEYTYPE_FAST or NX_KEYTYPE_REWIND (19/20), reversing the entire output table decodes them as newly supported inputs, but KeyboardCapture.mediaKeyCodeToString produces fastforward/rewind and KeyMappingFormatter explicitly leaves those unchanged because they have no Kanata name. The Mapper therefore cannot generate a valid defsrc token for these two inputs, despite the bridge admitting their usages; either add a valid lowering or omit them from the input table.

Useful? React with 👍 / 👎.

Comment on lines +558 to +560
**Part 2 (inputs, 2026-10-20).** The bridge's physical-input rule
(`physical_input_supported`) admits the same ten consumer codes. The worker adds
NX_SYSDEFINED (14) to its tap mask; `SessionKeyMap.mediaInput(subtype:data1:)` decodes a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the published guides before declaring media inputs shipped

This is a user-visible feature, but the commit updates only developer architecture documents. The published guides still tell users that media and brightness remapping is unavailable in driverless mode (guides/fun-layer.md and guides/driverless-caps-lock.md) and provide no instructions or limitations for using media keys as inputs, so users receive directly contradictory guidance. Update the relevant guides/ content along with this shipped status.

AGENTS.md reference: AGENTS.md:L382-L383

Useful? React with 👍 / 👎.

@malpern
malpern merged commit 4b4417e into master Oct 20, 2026
9 checks passed
@malpern
malpern deleted the feat/driverless-media-input branch October 20, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant