Shortcuts you invent, rules you write, gestures you own - all still just data

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 01:51:59 -04:00
parent f9e5d3f470
commit 06c53d6c21
48 changed files with 4749 additions and 140 deletions
+2 -1
View File
@@ -4,7 +4,7 @@
Do not edit this file. Run `quickshell/scripts/panama-settings-docs`
after changing the schema; a contract fails when this copy is stale.
174 settings across 36 groups. 77 of them are applied to the compositor and confirmed by reading the value back.
175 settings across 36 groups. 77 of them are applied to the compositor and confirmed by reading the value back.
## accessibility
@@ -17,6 +17,7 @@ Found on **Accessibility**.
| **High contrast**<br>`highContrast` | false | Increases contrast in applications that support it. Modern GTK applications read this from the desktop portal and restyle themselves; older ones need a high-contrast theme, which is not installed here. |
| **Dim inactive windows**<br>`dimInactive` `decoration:dim_inactive` | false | Darkens every window except the focused one, so the active window is unmistakable |
| **Dim amount**<br>`dimStrength` `decoration:dim_strength` | 0.5 | How much darker unfocused windows are. Range 0.050.9. |
| **Color filter**<br>`colorFilter` | none | A whole-screen filter rendered by the compositor — grayscale, or a correction for one kind of color blindness. Costs nothing when off. Choices: None, Grayscale, Protanopia, Deuteranopia, Tritanopia. |
| **Flash the screen for notifications**<br>`visualAlerts` | false | A single flash at the edges of every screen when a notification arrives that would ring the bell |
| **Pointer size**<br>`cursorSize` | 24 px | Applies to the compositor and to applications. Range 1664. |
| **Text size**<br>`textScale` | 1.0 | Scales interface text everywhere; 1.00 is the design size. Range 0.752.0. |
@@ -0,0 +1,201 @@
# Tier 2 — the identity block
Approved from mock `tier2-identity.html` (2026-08-25). Custom shortcuts, per-app window rules,
assignable four-finger gestures — all fed by one named-action system — plus network truths
(static IP both stacks, metered, saved networks, hidden SSID), color filters, the DND tile,
tokenized search, the known-hosts cap, and the GNOME umbrella button's removal.
## The one safety property
`settings.json` stays non-executable. No stored value is ever a command. A custom shortcut, a
gesture assignment, and a window rule are all **data**: an enum kind, a validated target id, and
booleans. The Lua resolves data → action at config time through whitelist tables only; an unknown
kind or an invalid target means the bind/gesture/rule is silently not emitted (the `prefs.get`
philosophy: never raise, never guess).
## Named actions (shared vocabulary)
Stored shape, used by `customBinds` and the four gesture keys:
```json
{ "kind": "app" | "shell" | "window", "target": "<id>", "label": "<display text>" }
```
- `app` — target is an application/desktop id matching `^[A-Za-z0-9@._-]{1,128}$`, launched via
the existing launch-or-focus path. The id is an *argument*, never interpolated into a shell string.
- `shell` — target is a key of a fixed Lua table mapping to existing shell IPC verbs
(`dnd-toggle`, `screenshot-area`, `color-picker`, `clipboard`, `overview`, `lock`, … — the
final list is exactly the verbs `shell.qml` already exposes; verify there, don't invent).
- `window` — target is a key of a fixed dispatcher table (`float-toggle`, `fullscreen`, `pin`,
`workspace:1`..`workspace:10`; `workspace:N` validated 110).
Resolution lives in a new `config/dot/hypr/actions.lua` required by both `keybinds.lua` and
`input.lua`. QML renders labels from the stored `label` and re-derives validity with the same
rules (a service-side `describeAction()` in Keybinds.qml is the single QML authority).
## Schema keys (already added by the orchestrator — do not re-add)
| key | type | def | group | internal | notes |
|---|---|---|---|---|---|
| `customBinds` | json | `[]` | input | yes | array of `{chord, kind, target, label}` |
| `windowRules` | json | `[]` | multitasking | yes | array, shape below |
| `gestureFourUp/Down/Left/Right` | json | `({})` | touchpad | yes | `{}` = unassigned, else a named action |
| `colorFilter` | string enum `none/grayscale/protanopia/deuteranopia/tritanopia` | `"none"` | accessibility | no | **no `hypr:` block** — stored value is the enum, not the shader path |
## 1. Custom shortcuts
- `keybinds.lua`: after the shipped binds, a `category("Custom")` loop over
`prefs.get("customBinds", {})`. Each entry: `valid_chord(chord)` must pass, `label` non-empty
(it becomes the bind description — `keybinds-contract` fails builds with description-less
binds), action resolved via `actions.lua` or the entry is skipped. Custom chords must also not
collide with an already-emitted chord (skip + keep the shipped one; QML prevents this upstream).
- `keybindOverrides` stays chord-only and keys by *shipped* chord; custom binds are edited in
place in `customBinds` (rebind rewrites the entry's `chord`), so the two mechanisms never meet
in `shippedChordFor`/`overrideOccupantFor`. Keybinds.qml must exclude custom chords from the
override map's domain.
- Keybinds.qml grows: `customBinds` reader, `addCustomBind(chord, kind, target, label)`,
`rebindCustomBind`, `removeCustomBind` (all conflict-checked via `boundTo`, all ending in
`applyReload()`), `describeAction(entry)`. `groupOrder` gains `Custom` **first**.
- ShortcutsPage: " Add shortcut" in the card header → the two-step flow from the mock
(ShortcutCapture for the chord, then kind picker + target picker; apps from the applications
catalog, shell verbs and window verbs from fixed lists mirroring `actions.lua`). Custom rows
render in the pinned-first "Custom" group with rebind + remove (remove via ConfirmAction,
`actionId: "custom-bind-remove:" + chord`).
- Contracts: `tests/hypr/keybind-categories-contract` — add `Custom` to `known` (keep the
Other-must-be-empty rule; Custom may be non-empty). `tests/quickshell/keybind-rebind-contract`
— keep the overrides pin verbatim; add a customBinds pin: every entry's `kind` ∈ the enum,
`target` matches the safe regex, `chord` < 64 chars, and a static check that `actions.lua`
builds exec strings only from its own whitelist tables (no `entry.target` concatenated into a
command except as a quoted argv element).
## 2. Window rules
Stored shape:
```json
{ "class": "<window class, literal>", "label": "<app display name>",
"float": bool, "center": bool, "size": [w, h] | null, "workspace": int | null,
"noAnim": bool, "game": bool, "noDim": bool, "pin": bool }
```
- `rules.lua`: after the shipped rules, a loop over `prefs.get("windowRules", {})` emitting one
`hl.window_rule` per entry, **class regex-escaped** (literal match). Validation: class
non-empty ≤ 128 chars, size within 50..10000, workspace 1..10; invalid entry skipped whole.
Shipped rules always emit first (user rules are anonymous → evaluated after named ones anyway;
do not name user rules).
- New `services/WindowRules.qml` singleton modeled on `Workspaces.qml`: owns the pref array,
`addRule/updateRule/removeRule`, the 120 ms write-settle + `hyprctl reload` + 350 ms refresh,
and an honest `applied` readback (`hyprctl -j` — use whatever rules listing the running
Hyprland exposes; if none exists, readback = "reload exit status + re-parse of prefs" and the
card says applied-on-reload rather than pretending).
- TilingPage: "App rules" card above the shipped Tiling card, per the mock — list rows (label,
plain-language behavior summary, mono rule line), edit + remove (ConfirmAction,
`actionId: "window-rule-remove:" + class`), add flow: app picker (running toplevels via
`Hyprland.toplevels` first, then the applications catalog) + behavior ticks.
- The shell's own surfaces cannot be matched: refuse classes matching `^(quickshell|qs-)`.
- New contract `tests/hypr/window-rules-contract` cloned from `workspace-rules-contract`'s
stub-lua harness: synthetic `settings.json` with N rules (one invalid, one `qs-` class) →
assert shipped count + N2 emitted, escaping applied, invalid skipped.
## 3. Gestures
- `input.lua`: the three shipped 3-finger gestures stay **verbatim**. After them, for each of
the four `gestureFour*` prefs that resolves to a valid named action, emit
`hl.gesture({ fingers = 4, direction = <up/down/left/right>, action = <resolved> })`.
- MousePage: new "Gestures" card above Touchpad per the mock — three read-only shipped rows,
four OptionPickerRows (options: Nothing + the named-action vocabulary; assigning writes the
pref and triggers `Keybinds.applyReload()` — reuse that seam, do not build a second reload),
then `swipeDistance` + `swipeInvert` moved up from the Touchpad card (schema keys unchanged —
`search-routing-contract` keeps passing because the group's keys stay on the same page).
- `tests/hypr/gestures-contract` rewrite: the three 3-finger pins stay verbatim (including the
no-`overview("toggle")` rule); the `== 3` count becomes "exactly 3 three-finger literals, plus
four-finger emission only inside the customGestures loop"; add a stub-lua run with a synthetic
settings.json asserting 0 four-finger gestures when unassigned and N when assigned, and that
`swipeDistance`/`swipeInvert` remain schema keys.
## 4. Network
New `panama-network` verbs (all: `shape_for` + `FALLBACKS` entries, `connection_state()` reply
shape for per-connection mutations, no absolute binary paths, secrets never in argv):
- `set-ip CONNECTION 4|6 auto` / `set-ip CONNECTION 4|6 manual ADDR/PREFIX GATEWAY DNS[,DNS…]`
`ipv4/ipv6.method`, `.addresses`, `.gateway`, `.dns`, `.ignore-auto-dns yes` on manual;
clears them on auto. New validation regexes (IPv4, IPv6, CIDR prefix ranges) in house style.
Reactivates the connection after the change (`nmcli connection up`) only if it was active.
- `details` additionally reports the *profile's* configured method + addresses/gateway/dns for
both stacks (today it only reads active state) and `metered` (`connection.metered`).
- `set-metered CONNECTION yes|no|auto`.
- `saved``nmcli connection show` listing (name, uuid, type, autoconnect, last-used,
in-range flag by cross-referencing active scan), secrets filtered.
- `join-hidden SSID PROFILE SECURITY` — password on stdin, `wifi.hidden yes`; security ∈
`wpa-psk|sae|none`.
- NetworkTools.qml: plumbing for each verb (same queued-Process pattern), drafts live in the
page.
- ConnectionDetails.qml: metered toggle + the IPv4/IPv6 editors from the mock (Automatic/Manual
choice; manual reveals address/gateway/DNS drafts; nothing applies until Apply; commit only
when the address validates — the proxy-drafts pattern from ConnectivityPage).
- ConnectivityPage: "Saved networks" card (house cap 6 + fold, in-range/connected/autoconnect
detail lines, Forget via ConfirmAction naming the password loss). WifiPanel: "Join a hidden
network…" row → SSID + security + password flow (reuse the join-path components).
- `network-tools-contract`: static half — new regexes exist, `shape_for` knows the new verbs,
AST secret check still passes; dynamic half — stubbed nmcli sees the right argv for set-ip
manual/auto, set-metered, join-hidden (password via stdin only, sentinel never in argv/JSON).
## 5. Color filters
- Ship 4 shaders under `config/dot/hypr/shaders/` (grayscale, protanopia, deuteranopia,
tritanopia — the mock's feColorMatrix values, as Hyprland screen shaders; end-of-pipe
`vec4 → vec4` GLSL). `declared-assets-contract`: they're installed by the existing hypr dir
symlink — verify, and add whatever declaration that contract wants.
- `looks.lua`: `local filter = prefs.get("colorFilter", "none")` → table lookup enum→absolute
shader path → `decoration.screen_shader` (empty string when none/unknown).
- Live apply: `SystemSettings` gains `applyColorFilter(name)` doing the same lookup +
`hyprctl eval decoration:screen_shader <path>` with read-back; the Accessibility page's
ChoiceRow (Seeing card, per mock) writes the pref and calls it. No `hypr:` block on the schema
entry (stored enum ≠ hyprctl's path value, which would break the shape/sweep contracts).
- `hypr-prefs-contract` requires the `prefs.get` default to match the schema default (`"none"`).
## 6. Smaller items
- **DND tile** (QuickSettingsPanel): beside Presentation, bound to `Notifs.doNotDisturb`,
subtitle On/Off; Presentation keeps its combined role. Check `control-center-contract` +
`control-center-services-contract` pins before and after.
- **Search tokenization** (SettingsSearch.qml): the needle splits on whitespace; every token
must match the haystack (AND). Ranking gains "all tokens in label" above the existing rules.
Keep the empty-query → `[]` behavior and all 21 pinned cases in `settings-search-contract`
(they are single-token and must not regress). Add `extraEntries` for every new Tier-2 surface
(custom shortcuts, app rules, gestures, saved networks, hidden network, metered, static IP,
color filter, DND tile) and synonyms for the known holes (`log out`, `wired`, `ethernet`,
`gestures`, `metered`). Results may carry an optional `section`; SettingsSidebar uses
`ShellState.openSettingsSection(page, section)` when present, else `pageRequested` as today.
- **Known hosts cap** (SshKeysPage): house cap 6 + "N more ▾" fold on the hosts Repeater.
- **GNOME umbrella button** (HealthPage): remove the "Fedora system settings" card
(`openGnomePanel("system")`). Wellbeing card stays. Add `[system]=about` to
`gnome-handoff-contract`'s OWNED map so the door stays shut.
## Ownership (disjoint)
- **Agent A — the Lua plane**: `config/dot/hypr/actions.lua` (new), `keybinds.lua`, `input.lua`,
`rules.lua`, `looks.lua`, `shaders/` (new); `tests/hypr/gestures-contract`,
`keybind-categories-contract`, `window-rules-contract` (new), `tests/quickshell/keybind-rebind-contract`.
- **Agent B — input-block services & UI**: `services/Keybinds.qml`, `services/WindowRules.qml`
(new), `services/SystemSettings.qml` (applyColorFilter only), `modules/settings/ShortcutsPage.qml`,
`TilingPage.qml`, `MousePage.qml`, the Accessibility Seeing page (color filter row),
`modules/settings/qmldir` + any new components; `tests/quickshell/keybinds-contract` needles if
needed.
- **Agent C — network & the rest**: `scripts/panama-network`, `services/NetworkTools.qml`,
`services/SettingsSearch.qml`, `modules/settings/ConnectivityPage.qml`, `ConnectionDetails.qml`,
`WifiPanel.qml`, `SettingsSidebar.qml`, `HealthPage.qml`, `SshKeysPage.qml`,
`modules/quicksettings/QuickSettingsPanel.qml`; `tests/quickshell/network-tools-contract`,
`settings-search-contract` (additions only), `gnome-handoff-contract` (OWNED addition),
`ssh-keys-contract` (cap needle if needed).
- **Orchestrator**: `PreferenceSchema.qml` (done first), `SettingsRoutes`/docs/commands regen,
seam audit, `settings-ownership-contract` if mirrors appear.
## Standing rules
Live desktop: every save must leave valid QML; check the journal (errors AND "Unable to
assign" warnings) after each. No live mutations: no real nmcli writes, no hyprctl reload storms
(batch: reload once per verified change-set, announce). Contracts run as you go (grant active);
never `settings-system-contract`. Theme tokens only; no continuously repainting animations;
destructive actions via ConfirmAction; errors via ErrorRow; unmeasured via NotMeasuredRow.