aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-09-14-desktop-shell-design.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/specs/2026-09-14-desktop-shell-design.md')
-rw-r--r--docs/superpowers/specs/2026-09-14-desktop-shell-design.md271
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.