aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-14 10:21:36 +0200
committerDanilo M. <danix@danix.xyz>2026-09-14 10:21:36 +0200
commite91a3a17b625724e934b4985588f37b6904829c0 (patch)
tree625135b57d39a98da87cf14c2683e49f5b2f3121 /docs/superpowers
parente866ecf1680cb5288ba7efa34ef8c618d829bf92 (diff)
downloadquickshell-e91a3a17b625724e934b4985588f37b6904829c0.tar.gz
quickshell-e91a3a17b625724e934b4985588f37b6904829c0.zip
docs: design for a unified desktop shell with a drawer and modules
Five components mean five processes, five autostart lines, five blur rules, and functionality scattered across surfaces with nothing in common. Nothing is discoverable without remembering it exists. The design is one left-side drawer on DP-1, reached from a launcher at the left end of waybar, hosting modules as tiles that may open full-height pages. Modules declare a tile, a page, both or neither, plus whether their background service runs while the drawer is closed: sound and mail must (the OSD reacts to keypresses, the unread count outlives the drawer), vm must not, since polling virsh only paints a page nobody is looking at. Three components merge in (volume-osd, mail-overview, vm-manager), two stay separate because a 600px drawer is the wrong shape for them: window-switcher is a fullscreen ALT+TAB grid, appearance needs room for a wallpaper picker. Appearance still earns a tile that fires its existing IPC, which is the case that proves a tile needs no page of its own. The top of the drawer is reserved and empty. The notification engine is a later project, but the space has to be claimed now: build the grid anywhere else and it moves the day notifications land, having been tuned against a position that does not survive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
Diffstat (limited to 'docs/superpowers')
-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.