Keybinds: capture the typed key (layout-aware) and warn on conflicts #43

Closed
Breadway wants to merge 0 commits from fix/shortcut-capture into main
Owner

First half of BSET-32 (the correctness part). The rest of the issue (human action names, grouping, search filter, Advanced disclosure for the raw command) is UI redesign and lands in the feature/settings-v2 PR, which closes BSET-32; this PR deliberately does not use a closing keyword.

Capture. Keybinds.svelte derived letters/digits from KeyboardEvent.code (the physical QWERTY position) while Hyprland binds by keysym, so on AZERTY the key labelled A (code: KeyQ) was saved as Q. New pure module frontend/src/lib/keycapture.ts:

  • printable keys come from event.key: letters upper-cased, digits as is, punctuation as xkb names ([ to bracketleft, / to slash, space to SPACE, ...), other characters as Uxxxx;
  • with Shift held, a shifted symbol (! on a US layout) is mapped back to its base key (1) because SHIFT is a modifier of the bind (limit: the reverse table is US pairs; layouts that produce a digit or letter under Shift are handled as that digit or letter);
  • named keys (Enter, arrows, F1..F24, media) use event.key; event.code is only the fallback for dead keys and generically named media keys.

Conflicts. frontend/src/lib/bindConflicts.ts (findConflict, modifier aliases normalized, the file's default_mods applied when a bind has no mods, an explicitly empty mods list kept distinct). The panel holds a captured combo that another live bind already uses (flat: all binds; layouts: globals + common + the bind's layout) and shows "X is already used by Y. Replacing it removes that shortcut." with Replace / Cancel. Replace removes the other bind (nothing is written until Save, as today).

Tests. frontend/src/lib/keycapture.test.mjs (node --experimental-strip-types): AZERTY, Dvorak, Colemak and QWERTY letters, Caps Lock, shifted symbols, punctuation, named keys, dead keys, Uxxxx, bare modifiers, and the conflict rules.

Manual test checklist (UI not run, by instruction)

  1. Keyboard & mouse > Shortcuts: click a shortcut, press Super+B: recorded as SUPER + B. On a non-QWERTY layout (e.g. switch to fr), press the key labelled A: recorded as A, not Q.
  2. Press Super+Shift+1: recorded as SUPER SHIFT 1 (not !).
  3. Record a combo already used by another bind: the yellow bar names the other bind; Cancel keeps both unchanged, Replace removes the other.

Verification: npm run check (0 errors, 0 warnings), npm run build, node --experimental-strip-types frontend/src/lib/keycapture.test.mjs, cargo fmt --all -- --check, cargo clippy --all-targets --locked -- -D warnings, cargo test --locked (75 passed, unchanged).

Refs BSET-32

First half of BSET-32 (the correctness part). The rest of the issue (human action names, grouping, search filter, Advanced disclosure for the raw command) is UI redesign and lands in the `feature/settings-v2` PR, which closes BSET-32; this PR deliberately does not use a closing keyword. **Capture.** `Keybinds.svelte` derived letters/digits from `KeyboardEvent.code` (the physical QWERTY position) while Hyprland binds by keysym, so on AZERTY the key labelled A (`code: KeyQ`) was saved as Q. New pure module `frontend/src/lib/keycapture.ts`: - printable keys come from `event.key`: letters upper-cased, digits as is, punctuation as xkb names (`[` to `bracketleft`, `/` to `slash`, space to `SPACE`, ...), other characters as `Uxxxx`; - with Shift held, a shifted symbol (`!` on a US layout) is mapped back to its base key (`1`) because SHIFT is a modifier of the bind (limit: the reverse table is US pairs; layouts that produce a digit or letter under Shift are handled as that digit or letter); - named keys (Enter, arrows, F1..F24, media) use `event.key`; `event.code` is only the fallback for dead keys and generically named media keys. **Conflicts.** `frontend/src/lib/bindConflicts.ts` (`findConflict`, modifier aliases normalized, the file's `default_mods` applied when a bind has no `mods`, an explicitly empty `mods` list kept distinct). The panel holds a captured combo that another live bind already uses (flat: all binds; layouts: globals + common + the bind's layout) and shows "X is already used by Y. Replacing it removes that shortcut." with Replace / Cancel. Replace removes the other bind (nothing is written until Save, as today). **Tests.** `frontend/src/lib/keycapture.test.mjs` (`node --experimental-strip-types`): AZERTY, Dvorak, Colemak and QWERTY letters, Caps Lock, shifted symbols, punctuation, named keys, dead keys, `Uxxxx`, bare modifiers, and the conflict rules. **Manual test checklist (UI not run, by instruction)** 1. Keyboard & mouse > Shortcuts: click a shortcut, press Super+B: recorded as SUPER + B. On a non-QWERTY layout (e.g. switch to `fr`), press the key labelled A: recorded as A, not Q. 2. Press Super+Shift+1: recorded as SUPER SHIFT 1 (not `!`). 3. Record a combo already used by another bind: the yellow bar names the other bind; Cancel keeps both unchanged, Replace removes the other. Verification: `npm run check` (0 errors, 0 warnings), `npm run build`, `node --experimental-strip-types frontend/src/lib/keycapture.test.mjs`, `cargo fmt --all -- --check`, `cargo clippy --all-targets --locked -- -D warnings`, `cargo test --locked` (75 passed, unchanged). Refs BSET-32
fix(keybinds): capture the key that was typed, and warn on shortcut conflicts
All checks were successful
check / check (push) Successful in 2m10s
4b2f826a52
Shortcut capture derived letters and digits from KeyboardEvent.code, the
physical QWERTY position, but Hyprland binds by keysym, so on AZERTY, Dvorak or
Colemak the saved bind fired on a different key than the one pressed.

- keycapture.ts: read printable keys from event.key (letters upper-cased,
  shifted symbols mapped back to their base key since SHIFT is a modifier,
  punctuation as xkb names, other characters as Uxxxx); code is only a fallback
  for dead keys and generically named media keys.
- bindConflicts.ts: normalized combo identity (modifier aliases, default mods
  when a bind has none) and findConflict.
- Keybinds panel: a combo that another live bind already uses is held until
  the user picks Replace (the other bind is removed) or Cancel.
- Unit tests for layouts, shifted symbols, named keys, dead keys and conflicts.
Breadway closed this pull request 2026-10-13 19:05:28 +08:00
Author
Owner

Closing: these commits reached main through #46 (feature/settings-v2 was merged into the v2 branch).

Closing: these commits reached main through #46 (feature/settings-v2 was merged into the v2 branch).
All checks were successful
check / check (push) Successful in 2m10s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Breadway/bos-settings!43
No description provided.