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
|
# island
A thin neutral bar at the top centre of `DP-1`. Point at it and two pills
slide out of it: a clock pill on the left and a status pill on the right. It
replaced waybar, both bars. It is not a separate shell: `desktop/shell.qml`
instantiates it next to the drawer, so it starts with `qs -p ./desktop` and
shares the drawer's IPC and keepalive window.
## Running it
qs -p ./desktop
The pure logic (volume icon thresholds, VM dot colours, flick text) lives in
`Logic.js` and has one check, which needs only node:
node desktop/island/test_logic.js
## States
**Hidden.** Only the sliver shows, in `Theme.overlay`. While anything is
capturing it takes the capture colours instead, split into equal segments, one
per active kind: mic green, camera yellow, location blue, screen red.
**Revealed.** The pointer enters the sliver and the pills slide down and apart
in one move, driven by a single `reveal` amount, so an interrupted reveal turns
around from wherever it is. They hide 400ms after the pointer leaves both,
unless a panel is open.
**Panel open.** Clicking the volume icon morphs the status pill into the sound
panel (output and input sliders with mute, and an arrow to the drawer's sound
page). An outside click or Escape closes 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.
If the island is hidden, the clock pill alone drops out for the flick and
retracts. Events in the first 1.5s after startup are ignored, because Hyprland
reports its initial state as events. The desktop icons come from
`shared/Workspaces.qml`, the same ones the window switcher uses.
## Clock pill
`Wed 07/10 15:07`, then one dot per VM that is running, paused, suspended,
saved or crashed (a plainly shut-off VM gets none). States come from the
drawer's `Virsh` service, whose lifecycle event stream runs whether the drawer
is open or not. Click opens the calendar panel.
## Status pill
Left to right: performance icon, controls icon, volume icon, tray, layout
flag.
- **Performance icon** opens the performance panel, see below. A dot on its
corner warns when something needs attention.
- **Controls icon** opens the controls panel, see below.
- **Volume icon** shows the default sink (muted, low, medium, high). Click
opens the sound panel.
- **Tray** shows `SystemTray` items. Left click activates, right click opens
the item's menu. The island is the session's StatusNotifier host. A tray
menu is a separate popup, so the island holds itself shown after `display()`
until the pointer returns.
- **Layout flag** shows the current keyboard layout, driven by Hyprland's
`activelayout` event. Scroll switches layout with
`hyprctl switchxkblayout current next|prev`.
## Controls panel
Opened by the status pill's controls icon, or by SUPER pressed and released
alone (`qs -p desktop ipc call island controls`, a Hyprland release bind;
any key or mouse bind pressed in between cancels it). Seven tiles: wifi,
bluetooth, DND, presentation, auto-lock, output, mic.
- Click toggles. Right-click closes the panel and opens the tile's drawer
page.
- Output and mic fill to their volume, clamped at 100%; muted greys the bar
instead of emptying it. Scroll steps 5%, output capped at 150%.
- A tile whose backing object has not arrived (the first two seconds, no
adapter, no sink) is dimmed and ignores input.
- The pencil enters edit mode: drag a tile to another slot, × hides it, the
strip below lists hidden tiles with a + each. Closing the panel ends it.
The visible tiles and their order live in
`$XDG_STATE_HOME/quickshell/desktop.json` (`../Prefs.qml`), written only when
they change and keeping keys it does not know. A tile added to the code
later starts hidden. Pure helpers in `Controls.js`, tested with
`node island/test_controls.js`.
## Performance panel
The system monitor icon, first in the status pill, opens a card grid of the
figures the conky dashboard used to show: CPU load with a bar per core, the
CPU model, RAM and five temperatures; the Arc B580's temperatures, VRAM and
fan (no utilisation, xe exposes none); download and upload over the last
minute, the LAN address and the public IP; and the five mounts with their
usage. `perf/Perf.qml` reads them; `perf/Perf.js` holds the parsing, tested by
`node desktop/island/perf/test_perf.js`.
A dot on the icon shows the worst of these, yellow or red:
| Input | Yellow | Red |
| --- | --- | --- |
| each temperature | its warn | its crit |
| RAM | 85% | 95% |
| each disk | 90% | 95% |
| public IP | | differs from the pinned one |
CPU load is left out, since spikes would flicker it. There is no hysteresis,
so a sensor sitting on its threshold can toggle the dot. The pin beside the
public IP stores the address shown as `perf.pinnedIp` in
`$XDG_STATE_HOME/quickshell/desktop.json`; the check exists because the VPN
into the LAN depends on the ISP's fixed address. The fetch is
`curl -4 https://ipinfo.io/ip` every 30 minutes from the last attempt, into
`~/.cache/udt/pubip.txt`, the file conky's sampler used. A failed fetch is
never a mismatch: the old value goes stale after 4 hours and shows as a dash.
CPU, RAM, temperatures and disks are sampled always, so the dot works with
the panel closed. Per-core load, the network rate, VRAM (a scan of every
fdinfo, ~14ms) and the fan run only while the panel is open, and the panel
itself is loaded only then. The mount list, `br0`, the GPU name and the
12 GiB VRAM size are constants in `Perf.qml`.
## Calendar panel
The clock pill opens a month grid beside the selected day's agenda. Dots
are one per calendar with something that day, in the khal colours; a bell on
an agenda row means the event carries its own alarm. A click on an event, or
"+ New event", opens the editor; while it is open the grid picks the date
(shift-click for the end). Repeat is none, daily, weekly, monthly or yearly
with an optional until-date; the alarm is none, 10m, 1h or 1d. An event with
something richer shows custom and an edit leaves it alone; its date cannot
move from here, since a rule like BYDAY would stay on the old day. An edit
rewrites only the fields that changed. Edit and delete act on the whole
series. Escape closes the editor, then the panel.
Everything goes through `calendar/calctl`, which reads the calendar list
from khal's config, writes the `.ics` files atomically and starts a
`vdirsyncer sync` after each write. The panel watches the calendar
directories, so khal edits and cron syncs show up live. Tests:
`/usr/bin/python3 calendar/test_calctl.py`, `node calendar/test_cal.js`.
## Privacy
`privacy.sh` prints `mic cam loc scr`, each 0 or 1, and `Privacy.qml` polls it
every 2s. It polls because Quickshell's `PwNode` exposes no running state,
which is what tells a capturing source from an idle one, so mic and screen go
through `pw-dump`, and camera and location through `fuser` and `pgrep`. A
failed run prints nothing and the last reading is kept, since no dump is a
broken run and not "nothing is capturing".
## 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, so switching
icon theme in the appearance panel changes the island. 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 idle sliver is never the accent,
because udt can snap the accent to the same colour as a capture kind.
## Blur
The window sets namespace `quickshell-island`. For frosting, add to
`~/.config/hypr/sections/decorations.lua`:
hl.layer_rule({
name = "blur-island",
match = { namespace = "^(quickshell-island)$" },
blur = true,
xray = false,
ignore_alpha = 0.1,
})
Without it the island renders flat translucent.
|