aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/specs/2026-10-07-island-core-design.md213
1 files changed, 213 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-10-07-island-core-design.md b/docs/superpowers/specs/2026-10-07-island-core-design.md
new file mode 100644
index 0000000..79828ce
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-07-island-core-design.md
@@ -0,0 +1,213 @@
+# Island core design
+
+A top-centre island on DP-1, hidden behind a thin sliver, with two pills that
+slide out of it: a clock pill and a status pill. It replaces waybar, both
+bars, which is removed when this ships.
+
+This is sub-project 1 of a roadmap that moves the desktop toward the
+integrated model of [Modesty](https://github.com/sandeepbist/Modesty). Modesty
+is GPLv3 and this repo is GPLv2-only, so its ideas and layouts are borrowed
+and none of its code. Later sub-projects, each with its own spec: preferences
+store and small control panel (2), calendar with full CRUD (3), performance
+group from conky (4), expose on Alt+Tab (5), drawer as the large control
+center (6).
+
+## Where it lives
+
+The island is part of the `desktop/` process, next to the drawer. The repo is
+moving to a hybrid model: `desktop/` hosts the parts that need to talk to each
+other (island, control panel, drawer), while window-switcher, keybinds,
+notifications and assistant stay separate shells. `desktop/shell.qml`
+instantiates the island; it gets the drawer's IPC and keepalive window for
+free.
+
+ desktop/island/
+ Island.qml the window, the sliver, reveal and hide
+ Capsule.qml one animated pill: rest content or an open panel
+ ClockPill.qml date, time, VM dots, the flick
+ StatusPill.qml volume icon, tray, layout flag
+ SoundPanel.qml the volume panel opened from the status pill
+ Privacy.qml singleton: what is capturing right now
+ Keyboard.qml singleton: current layout, switching
+ assets/ flag-it.svg, burger.svg (copied from waybar-theme-udt)
+ shared/Workspaces.qml
+ the workspace icon map, moved out of
+ window-switcher/Windows.qml and linked from both
+
+`Workspaces.qml` is a symlinked shared singleton like `Theme.qml`, for the
+same reason: a singleton outside a config directory needs a `qmldir`, a
+symlink does not. Remember hot reload does not follow symlinks (AGENTS.md):
+after editing it, force a reload of both components.
+
+## Theming
+
+Colours come only from `Theme`, which follows the unified-desktop-theme
+palette live. Icons are freedesktop names resolved with
+`Quickshell.iconPath(name, true)` through the active icon theme, the theme the
+appearance Icons tab sets, so switching icon theme changes the island like it
+changed waybar. Nerd Font glyphs, in `Theme.iconFamily`, only where no named
+icon exists. The island defines no colour and no icon theme of its own.
+
+## The window
+
+One `PanelWindow` on DP-1, layer Overlay, namespace `quickshell-island`,
+`ExclusionMode.Ignore`: it floats over windows and reserves no space, so
+there is nothing to special-case for fullscreen. It is anchored top, full
+width, and tall enough for the largest panel. **Its size never changes.**
+Resizing a layer surface while animating causes configure churn; instead the
+pills animate inside the fixed surface and the input `mask` follows them.
+
+The mask is the union of the sliver's hover strip and the live rectangle of
+each visible capsule, bound to their animated geometry, so the clickable area
+tracks the pills frame by frame and everything else on the surface is
+click-through.
+
+Keyboard focus is `OnDemand` while a panel is open, `None` otherwise. Escape
+is handled on a focused item inside the panel, not on the window (AGENTS.md:
+key events reach a focused item).
+
+A blur rule for namespace `quickshell-island` goes in
+`~/.config/hypr/sections/decorations.lua`; without it the island renders flat
+translucent and still works.
+
+## States
+
+**Hidden.** Only the sliver shows: a short, thin bar at top centre in
+`Theme.accent`. While anything captures, the sliver takes the capture colours
+instead, split into equal segments, one per active kind:
+
+| Kind | Colour | Source |
+| --- | --- | --- |
+| mic | `Theme.green` | a running PipeWire `Audio/Source` node with a `Stream/Input/Audio` client |
+| camera | `Theme.yellow` | a process holding `/dev/video*` open |
+| location | `Theme.blue` | geoclue running |
+| screen | `Theme.red` | a PipeWire node whose `media.name` matches `xdph-streaming` or `gsr-default` |
+
+The detection rules are those of `waybar-theme-udt/bin/privacy_dots.sh`. Mic
+and screen use `Quickshell.Services.Pipewire` instead of `pw-dump`; camera and
+location have no binding and stay as a short poll.
+
+**Revealed.** The pointer enters the sliver's strip. The two pills slide down
+and split apart from it in one continuous move: they start overlapping at the
+sliver, travel down and outward, and settle side by side with a gap, clock on
+the left, status on the right. The motion is one animation driven by a single
+`reveal` amount from 0 to 1, so an interrupted reveal reverses smoothly from
+wherever it is.
+
+**Hide.** 400ms after the pointer leaves both pills, unless a panel is open.
+Re-entering cancels it.
+
+**Panel open.** A pill morphs into a larger capsule holding its panel, and
+the other pill slides aside. Closed by an outside click (`HyprlandFocusGrab`
+`onCleared`) or Escape. The island hides after the panel closes if the pointer
+is not over it.
+
+**Flick.** On a workspace change or an app opening, and not on a plain focus
+change, the clock pill shows `<desktop icon> <app icon> <app name>` for 1.5s
+and then returns to its rest content. If the island is hidden, the clock pill
+alone drops out of the sliver for the flick and retracts afterwards. A new
+event during a flick restarts its timer. Events in the first 1.5s after
+startup are ignored, because Hyprland reports the initial state as events.
+
+- Desktop icon: the workspace icon from `Workspaces.qml`, the same eight icons
+ waybar draws and window-switcher's cards use. A named workspace, or one past
+ eight, falls back to its number.
+- App icon and name: the window focused on that workspace after the switch,
+ or the window that just opened. An empty workspace shows the desktop icon
+ and number only. App icon by `appId` through `Quickshell.iconPath`.
+- Sources: `Hyprland.rawEvent`, `workspacev2` and `openwindow`.
+
+## Clock pill
+
+Rest: `Wed 07/10 15:07`, waybar's two formats (`%a %d/%m`, `%H:%M`) from
+`SystemClock`. When at least one VM is running, one dot per VM follows the
+time, coloured by state as waybar's `vms_dots.sh` did:
+
+| State | Colour |
+| --- | --- |
+| running | `Theme.green` |
+| paused, idle | `Theme.yellow` |
+| pmsuspended | `Theme.accent` |
+| saved (managed save) | `Theme.blue` |
+| crashed | `Theme.red` |
+
+Shut-off VMs get no dot, and with no VM running there are no dots at all.
+States come from `desktop/modules/vm/Virsh.qml`, whose lifecycle event stream
+already runs whether the drawer is open or not.
+
+Click: nothing in this sub-project. The calendar panel is sub-project 3.
+
+## Status pill
+
+Rest: volume icon, tray, layout flag, left to right.
+
+**Volume icon.** Shows the default sink's state: `audio-volume-muted`, `-low`,
+`-medium` or `-high` by thresholds of 33 and 66. Click opens the sound panel.
+No scroll, middle-click or right-click: the icon shows state and is the way
+into the sound panel, and from there to the drawer. The volume OSD is
+unchanged and separate.
+
+**Sound panel.** Output slider and mute, input slider and mute, and an arrow
+that opens the drawer's sound page (`drawer.show("sound")`) and closes the
+panel. Values come from `desktop/modules/sound/Service.qml`, which already
+binds the default sink and source, tracks them with `PwObjectTracker`, and
+handles PipeWire's pre-`ready` values. No device pickers and no per-app
+volume here; those are on the drawer page.
+
+**Tray.** `SystemTray` items with their own icons. Left click activates, right
+click opens the item's menu (`QsMenuAnchor`), as a normal desktop tray. No
+panel. Removing waybar would otherwise leave the session with no
+StatusNotifier host.
+
+**Layout flag.** The flag for the current layout: `flag-it.svg` for Italian,
+`burger.svg` for English, copied into `assets/`. Scroll switches layout with
+`hyprctl switchxkblayout <device> next|prev`; click does nothing. The current
+layout comes from Hyprland's `activelayout` event rather than a 2s poll. The
+keyboard is the one Hyprland marks `main`, not a hardcoded device name, which
+`wb-lang` used.
+
+## Removing waybar
+
+Done when the island is verified, in this order:
+
+1. Remove the waybar line from `~/.config/hypr/sections/autostart.lua`. It is
+ the live config, outside this repo: confirm with the user before editing.
+2. Drop `killall -SIGUSR2 waybar` from `appearance/Icons.qml` and from
+ `unified-desktop-theme/install.sh`. The icon switch still reruns
+ `udt-accent`; the waybar `install.sh` call goes too.
+3. Delete `desktop/modules/mail/waybar-mail.sh`. The drawer now carries the
+ only unread count, kept live by its own watcher (commit d0ebda2).
+4. The drawer uses `ExclusionMode.Normal` to sit below waybar's exclusive
+ zone. With no waybar the zone is gone: check where the drawer now sits and
+ adjust.
+5. AGENTS.md: rewrite "What this is" for the hybrid model, add the island to
+ the component list, add any new gotchas found while building it. Mail
+ README: drop the waybar wiring section.
+
+Waybar-theme-udt itself is left alone; whether to archive it is the user's
+call.
+
+## Verification
+
+Agent side, following AGENTS.md: `qmllint` on new files, then the user
+reloads (`./reload.sh desktop`) and the agent reads the log for errors and
+checks `pgrep -cx qs` did not grow.
+
+User side, visual, on DP-1:
+
+- sliver visible; pointer on it reveals both pills with the split motion;
+ leaving hides after the delay; brushing past does not flicker
+- switch workspace with the island hidden: clock pill drops out with desktop
+ icon, app icon, app name, then retracts
+- open an app: same flick
+- `pw-record /dev/null` running: sliver turns green; stops, back to accent
+- start a VM: dot appears, green; pause it: yellow
+- volume icon matches mute and level; click opens the sound panel; sliders
+ and mutes work; arrow opens the drawer sound page; Escape and outside click
+ close the panel
+- tray icons present, menus open
+- scroll on the flag switches layout and the flag follows; switching with the
+ keybind also updates the flag
+- palette change (`udt-accent <wallpaper>`) recolours the island live; icon
+ theme switch changes its icons
+- waybar not running, nothing missing from the list above