From ea0df01c3bcc8288e76b87eef419dbe5dfc51651 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 13 Oct 2026 09:39:56 +0200 Subject: docs(spec): controls panel and preferences store Sub-project 2. A grid of seven toggles opened from the status pill and by a bare SUPER release, with an edit mode to reorder, hide and show tiles. The visible list lives in a JSON file under the state home, so a tile added to the code later starts hidden rather than appearing unasked. The drawer keeps the detail: right-click on a tile opens its page. Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-10-13-controls-panel-design.md | 147 +++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-13-controls-panel-design.md (limited to 'docs/superpowers/specs') diff --git a/docs/superpowers/specs/2026-10-13-controls-panel-design.md b/docs/superpowers/specs/2026-10-13-controls-panel-design.md new file mode 100644 index 0000000..57ecd68 --- /dev/null +++ b/docs/superpowers/specs/2026-10-13-controls-panel-design.md @@ -0,0 +1,147 @@ +# Controls panel design + +A small panel of one-click toggles that opens from the island's status pill, +backed by the first piece of a preferences store. This is sub-project 2 of the +roadmap in `2026-10-07-island-core-design.md`. The drawer stays the place for +detail; the panel is for the toggles reached for several times a day, and each +tile links to the drawer page that holds the rest. + +The model is Modesty's control center, read for ideas only (GPLv3, this repo +is GPLv2-only). + +## Scope + +In v1: + +- Seven tiles: wifi, bluetooth, do not disturb, presentation, auto-lock, + output mute, mic mute. +- An edit mode: drag to reorder, hide a tile, show a hidden one. +- A preferences file holding the visible tiles and their order. +- Opening on a bare SUPER press and release. + +Not in v1: a settings window (other tunables move into the store when there +is a UI for them), night light (hyprsunset publishes no state the shell can +read), breaktimer and KDE Connect tiles (status displays, the drawer has them). + +## Where it lives + + desktop/ + Prefs.qml singleton: the preferences file + island/ + Controls.js pure: tile table, order helpers, fill + test_controls.js node test for Controls.js + ControlTile.qml one tile: live state, click, right-click, scroll + ControlsPanel.qml the grid, edit mode, the hidden strip + +`Prefs.qml` sits in `desktop/`, not `shared/`: no other shell reads it yet. + +## Preferences store + +`Prefs.qml` is a `FileView` with a `JsonAdapter` on +`$XDG_STATE_HOME/quickshell/desktop.json`, falling back to +`~/.local/state/quickshell/desktop.json` when the variable is unset. State +home rather than the runtime directory, because preferences must survive a +reboot, and rather than `~/.config`, because the shell writes the file and a +hand-kept dotfile would be overwritten. + +One key in v1: + + { "controls": ["wifi", "bluetooth", "dnd", "presentation", "nolock", + "output", "mic"] } + +`controls` lists the visible tiles in display order. A tile is hidden when it +is known to `Controls.js` and absent from the list. A tile added to the code +later therefore starts hidden, and the user adds it in edit mode. Unknown ids +in the file (a tile since removed from the code) are dropped on read. + +A missing file means the default: all seven, in the order above. A file that +fails to parse also reads as the default, and is not rewritten until the user +changes something, so a hand edit with a typo is not silently replaced by a +panel that was merely opened. Every edit writes the file immediately; the +directory is created on first write. + +## Tiles + +`Controls.js` holds the table, one entry per id: glyph, label, and the drawer +page right-click opens. + +| id | state | left-click | right-click | +| --- | --- | --- | --- | +| wifi | `Networking.wifiEnabled` | flip it | `network` | +| bluetooth | `Bluetooth.defaultAdapter.enabled` | flip it | `bluetooth` | +| dnd | `Status.dnd` | `Status.toggleMode("dnd")` | `status` | +| presentation | `Status.presentation` | `Status.toggleMode("presentation")` | `status` | +| nolock | `!Status.nolock`, labelled "auto-lock" | `Status.toggleMode("nolock")` | `status` | +| output | `sound.sink.audio` | flip `muted` | `sound` | +| mic | `sound.source.audio` | flip `muted` | `sound` | + +A tile is an icon over a short label. Toggle tiles fill their background when +on. The two audio tiles fill a bar from the left whose width is the volume +clamped to 1 (output goes to 1.5); muted keeps the width and draws it in +`Theme.overlay` with the muted glyph, so the level stays readable. The accent +is not used as a neutral anywhere, per the AGENTS.md note on udt snapping the +accent onto green. + +Scrolling an audio tile steps its volume by 5%, through `MouseArea.onWheel` +(a `WheelHandler` never fired in the island). Output is capped at 1.5 like the +volume key, the mic at 1.0. Scroll on other tiles does nothing. + +A tile whose backing object is not there yet shows dimmed and ignores input: +no bluetooth adapter, a null sink or source, and the roughly two seconds after +launch before `Quickshell.Networking` and `Quickshell.Bluetooth` populate. +Bindings repaint when they arrive. + +Presentation reads as on while a game holds it (`Status.gaming`); clicking +then clears only the manual half, so the tile stays lit until the game ends. +That is the registry's existing rule and the tile does not hide it. + +Glyphs must exist in Inconsolata Nerd Font and be checked in its cmap. + +## Panel + +The status pill gains one glyph that opens the panel in its capsule, the same +way the volume glyph opens the sound panel: `panel === "controls"` in +`Island.qml`. Focus grab and Escape close it as they close the sound panel. + +The grid is four columns, so seven tiles make two rows. A pencil glyph in a +corner enters edit mode, where: + +- each visible tile carries a small × that hides it; +- a tile dragged onto another moves to that position (`DragHandler` on the + tile, `DropArea` on each slot); +- hidden tiles appear in a strip under the grid, each with a + that appends it + to the visible list; +- clicks do not toggle and right-click does not open the drawer. + +Edit mode ends on the pencil, Escape, or the panel closing. Hiding every tile +is allowed; the panel then shows only the strip. + +The order helpers in `Controls.js` are pure: `known()`, `normalize(list)` (drop +unknown ids and duplicates), `hidden(list)`, `move(list, from, to)`, +`hide(list, id)`, `show(list, id)`, `fill(volume)`. `test_controls.js` runs +them under `node`, the same way `test_logic.js` does. + +## Opening + +`desktop/shell.qml` gains `IpcHandler { target: "island" }` with +`controls()`, which reveals the island and toggles the panel. + +Hyprland binds it on a bare SUPER press and release (the keyboard has no +right SUPER, so only `Super_L` is bound): + + hl.bind("SUPER + Super_L", ..., { release = true, ... }) + +calling `qs -p ~/Programming/GIT/quickshell/desktop ipc call island controls`. +A release bind is meant to be cancelled by any key pressed in between, so +`SUPER+c` does not also open the panel. Mouse binds are the risk: older +Hyprland fired a modifier release bind after `SUPER+drag` and `SUPER+scroll`, +both of which are bound here. The build checks both, and SUPER+c, by hand. If +either mouse case opens the panel, the bind falls back to `SUPER+q`. + +## Testing + +- `node desktop/island/test_controls.js` for the pure helpers. +- A smoke run of `desktop/` confirming a clean load and no new warnings. +- The user checks on screen: the grid, toggles and their fill, audio fill and + scroll, right-click to the drawer, edit mode (drag, hide, show), the file + surviving a restart, and the keybind against the mouse cases above. -- cgit v1.2.3