aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/specs')
-rw-r--r--docs/superpowers/specs/2026-10-13-controls-panel-design.md147
1 files changed, 147 insertions, 0 deletions
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.