diff options
Diffstat (limited to 'docs/superpowers/specs')
| -rw-r--r-- | docs/superpowers/specs/2026-09-14-desktop-shell-design.md | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-09-14-desktop-shell-design.md b/docs/superpowers/specs/2026-09-14-desktop-shell-design.md new file mode 100644 index 0000000..d1a8b47 --- /dev/null +++ b/docs/superpowers/specs/2026-09-14-desktop-shell-design.md @@ -0,0 +1,271 @@ +# Desktop shell: drawer and module contract + +A single quickshell component, `desktop/`, presenting one left-side drawer that +hosts several modules. It absorbs three of the five existing components and +leaves two alone. + +This is the first of several projects. Notifications, do not disturb, +breaktimer, wifi, bluetooth and kdeconnect are named here only where they +constrain the design; none of them is built now. + +## Why + +Five components, five processes, five autostart lines, five blur rules, and +functionality scattered across surfaces that have nothing in common. Mail lives +behind a waybar click, VMs behind `SUPER+v`, appearance behind `SUPER+Return`, +and nothing is discoverable without remembering it exists. + +The goal is one place, reached one way, where the things a desktop lets you +adjust are visible together. The model is the Windows 11 quick settings panel: +click the bar, a panel appears, everything is in it. + +## Scope + +Merged into the new shell: + +- `volume-osd` becomes the `sound` module; its OSD surface survives unchanged. +- `mail-overview` becomes the `mail` module; its drawer becomes a page. +- `vm-manager` becomes the `vm` module; its drawer becomes a page. + +Left alone, still separate processes: + +- `window-switcher`, whose ALT+TAB fullscreen grid has no place in a drawer. +- `appearance`, whose wallpaper picker needs more room than a 600px panel; it + gains a tile in the drawer that fires its existing IPC. + +Process count goes from five to three. + +Not in this project: the notification engine, do not disturb, breaktimer, wifi, +bluetooth, kdeconnect, and any absorption of waybar's own duties. + +## Layout + +One `PanelWindow` on `DP-1`, left-anchored, `ExclusionMode.Normal` so it starts +below waybar and runs to the bottom of the screen. 600px wide. Namespace +`quickshell-desktop`, with a matching `blur-desktop` layer rule. + +Left rather than right because conky occupies the right side of `DP-1`. Below +the bar rather than over it because the drawer is reached from the bar, which +should stay visible and clickable while it is open. + +Top to bottom: + +**Notification area.** Takes all remaining vertical space. Empty in this +project, reserved for the notification engine. It is an empty `Item` that +claims the space, not a placeholder graphic: the grid must sit at the bottom +exactly where it will sit once notifications arrive, or the layout is tuned +against a position that does not survive. Scrollable once it has content, since +a notification list is the one part of this design with unbounded content. + +**Tile grid.** Fixed, never scrolled. A `Flow` with a minimum tile width of +about 180px, which yields three columns at 600px. Tiles wrap and add rows. The +ceiling is 3x3, nine tiles, which is the module count currently foreseen. + +The notification area is intended to keep at least 70% of the drawer height. +This holds until the grid exceeds two rows; a full 3x3 grid takes roughly 46% +of a 1038px drawer and the notification area keeps the rest. The 70% figure is +a target that yields to a full grid, not a hard constraint, and tiles can be +reshaped later if more modules arrive. + +**Pages.** Opening a page replaces the whole drawer content, grid included, so +a page gets full height. Mail and VM both need it. A header row carries a back +arrow and the page title. One page at a time, no nesting. The page enters from +the right on a slide; this is the only motion in the design, and it is what +makes the drawer read as one surface rather than a window swapping contents. + +The drawer resets to the grid when closed. Reopening lands on the grid unless +the trigger deep-links to a page. + +Dismissal is Escape or a click outside. Escape requires +`WlrLayershell.keyboardFocus` and a focused item to receive the key, since +`Keys.onEscapePressed` on a `PanelWindow` never fires. Click-outside needs a +click-catching region outside the drawer, which at `ExclusionMode.Normal` +covers only part of the screen; whether that is a second transparent window or +an enlarged mask is an implementation detail. + +The 1x1 transparent keepalive `PanelWindow` with `mask: Region {}` is required, +as in every component here. The OSD does not substitute for it, being itself +shown lazily. + +## Module contract + +```qml +// modules/<name>/Module.qml +Module { + name: "vm" + icon: "..." + alwaysActive: false + tile: Component { ... } + page: Component { ... } + function activate() { ... } +} +``` + +A module provides a tile, a page, both, or neither. A tile with no page calls +`activate()` when clicked. A module with neither is a pure background service. +A module may also own windows outside the drawer entirely, as `sound` does with +its OSD. + +The registry is a list in `shell.qml`. `Module.qml` is a thin base type whose +value is being the one file to read to learn what a module is; it does not +enforce, and nothing stops a module doing something unusual. + +### alwaysActive + +Whether a module's background service runs while the drawer is closed. The +drawer is closed most of the time, and three modules have background work, so +this is the property that decides whether the shell is cheap to run all +session. + +- `sound`: true. The OSD must react to a volume keypress with no drawer open. +- `mail`: true. Account state and the unread count outlive the drawer. +- `vm`: false. Polling virsh exists only to paint a page nobody is looking at. + +Pages are lazily loaded regardless. `alwaysActive` governs the service only. + +## Modules + +### sound (alwaysActive: true) + +`VolumeOsd.qml` currently does three jobs in 302 lines, and splits into: + +- `Service.qml`, the PipeWire bindings and `PwObjectTracker`. Nodes report + their initial volume before `ready` goes true, so the `ready` check stands + alone with no extra guard on top of it. `PwObjectTracker` is mandatory, or + node properties go stale. +- `Osd.qml`, today's transient OSD, unchanged, keeping its own window, + its `quickshell-volume-osd` namespace and its existing blur rule. +- `Tile.qml`, output volume, mute state, current player at a glance. +- `Page.qml`, output and input device lists with per-device volume, plus the + MPRIS player and transport. playerctld publishes a duplicate of every player + under its own bus name, so filtering stays by `dbusName`. + +### mail (alwaysActive: true) + +`MailPanel.qml`'s body becomes `Page.qml`; `Accounts.qml` becomes the service. +The heartbeat and watcher-dot logic move intact, including the deliberate +absence of a `watch` on the heartbeat file, which is written by atomic replace +and would kill a watch held on the old inode. + +`mail-notify.sh`, `waybar-mail.sh` and `test-mail-notify.sh` move to +`modules/mail/`. The waybar mail module stays on the bar for the unread count +it displays, which nothing else shows; its click target becomes the drawer's +mail page. + +### vm (alwaysActive: false) + +`Virsh.qml` becomes the service, `VmPanel.qml` the page. Managed-save detection +stays `virsh dominfo` grepped for `Managed save: yes`, polled per VM, since +`virsh list --name` does not report it. Memory and disk figures stay +guest-agent-only, showing a dash rather than substituting libvirt's +host-side numbers, which measure something else. + +The tile shows a status dot. Since the service sleeps while the page is closed, +the tile needs some state to show: either a neutral dot until the page has been +opened, or one cheap `virsh list` when the drawer opens rather than when the +page does. The latter is preferred and will be settled during implementation. + +`vms_dots.sh` stays on waybar, unchanged and outside the shell, so nothing +loses glanceable VM state when the in-shell service sleeps. + +The existing `notify-send` on action failure stays as it is. + +### appearance (tile only) + +`activate()` calls the external appearance shell's existing IPC. No page, no +service. The module that demonstrates a tile needs neither. + +## Triggers + +The drawer opens from a new waybar custom module at the left end of the bar, +shaped like a Slackware icon: a static button with `on-click` and no `exec`. It +cannot show open/closed state, which would require the shell to feed waybar, +and that is deliberately not built. + +One IPC function, `open(page)`, with the page name optional: + +| trigger | destination | +|---|---| +| waybar launcher | grid | +| waybar mail click | mail page | +| `SUPER+v` | vm page | +| `SUPER+Return` | appearance, unchanged, external | +| ALT+TAB | window switcher, unchanged, external | + +Deep-linking preserves the one-keypress paths that exist today rather than +taxing them with an extra click. The back arrow then has an obvious meaning: +back to the grid, from wherever you landed. + +The drawer may gain a keyboard shortcut of its own later. + +## Files + +``` +desktop/ + shell.qml ShellRoot: keepalive window, IPC, module registry + Drawer.qml panel window, notification area, grid, page stack + Tile.qml + Page.qml header, back arrow, content slot + Module.qml the contract + Theme.qml symlink to ../shared/Theme.qml + Button.qml the file duplicated in mail-overview and vm-manager + modules/ + sound/ Module.qml Service.qml Osd.qml Tile.qml Page.qml + mail/ Module.qml Service.qml Tile.qml Page.qml + the three scripts + vm/ Module.qml Service.qml Tile.qml Page.qml + appearance/ Module.qml +``` + +`Button.qml` is currently byte-identical in `mail-overview` and `vm-manager`; +the merge collapses it to one file. `Theme.qml` remains a symlink to +`shared/Theme.qml`, per the existing convention, and the count of links drops +from five to three. + +The three absorbed directories are deleted in the same commit that adds them +under `desktop/`, so history follows the move and nothing is duplicated on +disk. + +## Changes outside this repo + +Six edits to the live Hyprland and waybar configuration, none of them in this +repository, all applied by the user: + +| file | change | +|---|---| +| `autostart.lua` | five `qs` lines to three; `mail-notify.sh` path | +| `keybindings.lua` | `SUPER+v` to the drawer deep-link | +| `decorations.lua` | add `blur-desktop`; drop `blur-mail` and `blur-vm-manager`; keep `blur-volume-osd` | +| `waybar/config.jsonc` | add the launcher at the left end | +| `modules/custom/mail.jsonc` | `exec` path and `on-click` destination | +| `modules/custom/launcher.jsonc` | new static button | + +`hyprctl reload` picks up binds and layer rules. `hl.exec_cmd` is exec-once, so +the process changes need a logout or a manual start. + +## Failure and risk + +Each module's failures stay its own: vm surfaces libvirt failures as a +notification, mail renders the watcher dot red and treats an unparseable +`notmuch` count as an error rather than an empty inbox. + +The new risk is the merge itself. One process means a QML error in any module +can take down all four. The drawer and each page are lazily loaded, so a broken +page fails when it is opened rather than at startup, which leaves the +`alwaysActive` services as the only code that must be sound at launch. + +## Verification + +Per AGENTS.md, and the traps there are about measurement rather than code: + +- A detached `qs` does not survive an agent's tool call. Start it so the + harness owns the process and confirm from the log, not a later `pgrep`. +- The process is `qs`, not `quickshell`. Use `pkill -x qs` and `pgrep -cx qs`, + and check the count afterwards. +- `pkill -f` matches the agent's own shell and kills the caller. +- A config with no visible window exits silently, reporting no error. + +`test-mail-notify.sh` moves with the mail module and must still pass; its +sixteen tests are the only automated oracle in this project. + +Anything visual is for the user to look at. A screenshot of a transient OSD is +a race. |
