aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-10-07-island-core-design.md
blob: 93af74304db278c6767cffd7a265091e6d8b64d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# 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 and which broke when the same keyboard moved from its 2.4GHz
dongle to USB. The main keyboard is resolved again on each switch and on
`activelayout`, never cached, because plugging the other connection changes
which device is main.

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