# 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.