summaryrefslogtreecommitdiffhomepage
path: root/packages/ext-keybindings/ext-keybindings.md
blob: 7014b5882d75079ea8bc8f3675a44afa2eba7ff2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# ext-keybindings

Config-driven compositor keybindings — the first step to a usable DE. A **core**
extension (`id "keybindings"`, `depends_on {"xdg-shell"}`). It is a LEAF
consumer: its whole contract is the `create()` factory in
`include/unbox/ext-keybindings/ext_keybindings.hpp` — it exports no hooks or
services.

## Why it exists
Compositor shortcuts must live in ONE policy unit, not be hardcoded in the
shell. This unit owns them and reads them from `unbox.toml` (toml++). It also
SUBSUMES the two shortcuts ext-xdg-shell used to hardcode (`Alt+F1` focus-cycle,
`Ctrl+Alt+Backspace` quit), preserved as compiled defaults.

## Action vocabulary (`action = ...`)
- `spawn` — run `command` via `/bin/sh -c` (requires a non-empty `command`).
- `focus-next` / `focus-prev` — rotate keyboard focus across ALL mapped windows.
- `close-active` — close the focused toplevel (no-op if none).
- `quit` — `wl_display_terminate`.

Combos are `Mod+...+Key` (mods: `Super`/`Logo`, `Alt`, `Ctrl`/`Control`,
`Shift`, case-insensitive; final token an xkb keysym name). A BARE modifier
(`"Super"`) is a TAP binding. Unknown action / malformed combo = log + skip that
entry; never abort. No config / parse error / zero valid bindings → compiled
defaults (out-of-the-box == the repo-root sample `unbox.toml`).

## Focus ring: STABLE map order, not MRU
`focus-next`/`focus-prev` rotate a list kept in **map order** (append on
`on_toplevel_mapped`, drop on `on_toplevel_unmapped`, move-cursor on
`on_toplevel_focused`). Repeated Alt+Tab therefore walks all N windows and
wraps — it does NOT ping-pong the two most-recently-used (MRU was rejected as
wrong). The `Toplevel*` is stored only between its mapped and unmapped events
(the supported borrow window) and never dereferenced inside the pure ring core.

## The tap-Super gotcha
A bare-modifier binding fires on the modifier's RELEASE only if it was pressed
and released with NOTHING in between. The matcher arms on Super-down, marks
"used" on any other key press (or any Super-carrying chord), and fires on
Super-up only if still unused. The modifier press/release are NEVER consumed
(other combos need Super held); a fired chord IS consumed (`handled = true`), a
fired tap consumes nothing (the modifier already passed through). Pointer
Super+click cannot mark the tap used (the pointer is not in the key_filter) —
accepted for now.

## labwc-nested caveat
In nested dev under the live labwc session the parent compositor may swallow
Alt+Tab and the Super tap before they reach unbox, so live FEEL cannot be
verified here — that needs the orchestrator's hands-on on the real seat. No
Escape combo is bound (an established decision keeps all Escape chords passing
through to the parent session).

## Layout
- `include/unbox/ext-keybindings/ext_keybindings.hpp` — the factory (contract).
- `src/policy.hpp` — combo parser + matcher/tap state machine (pure;
  xkbcommon-only).
- `src/focus_ring.hpp` — stable-rotation ring over opaque tokens (pure).
- `src/config.{hpp,cpp}` — toml++ loader, string → bindings + warnings (pure).
- `src/extension.cpp` — glue: key_filter link, xdg-shell event subscriptions,
  fork/exec spawn, focus/close/terminate effects.
- `tests/test_policy.cpp` — the four cores, doctest-hard.
- `tests/test_glue.cpp` — headless install/activate/dispatch/shutdown smoke.