# 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, with a media pill left of the clock while something plays or is paused. 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 ` ` 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: headphones icon, volume icon, tray, layout flag. The performance panel that used to open from here is the drawer's Performance page now (`desktop/modules/perf/`). - **Headphones icon** shows while Sony headphones are connected (a BlueZ name starting `WH-`, `WF-` or `LinkBuds`). Click opens the headphones panel: battery (BlueZ), noise control tiles (ANC, ambient, off), the ambient level (1-20, drag or wheel, sent once it settles) and focus on voice (both shown in ambient mode only), the headphones' own sink volume (their `bluez_output` node, not the default sink; wheel steps 5%, the icon mutes), power off and refresh. The mode goes through `sonyctl`, which speaks Sony's v2 protocol over RFCOMM channel 18 and is read on every open, since the headphones' button changes it too. Each call is about 2s, so the panel takes one at a time. Test: `/usr/bin/python3 desktop/island/test_sonyctl.py` (codec only, no Bluetooth). - **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`. ## Media pill Left of the clock pill, while something is playing or paused. It shows the album art and `artist · album · title`, scrolling when the line is wider than 220px. The player is the sound module's `Player`, so the browser proxy duplicates are already filtered out. Hovering swaps the text for a progress bar; MPRIS does not push position, so the pill re-reads it every second while hovered. A click on the bar seeks, only when the player reports `canSeek` and a known length. The wheel over the pill goes to the previous track (up) or the next (down), one per notch. ## OBS pill Right of the status pill, while OBS Studio runs with its WebSocket server enabled (Tools > WebSocket Server Settings; auth stays on, the server listens on every interface). `Obs.qml` speaks obs-websocket v5 directly and reads the password from OBS's own `obs-websocket/config.json`, so no copy of it exists; `obs-auth.sh` computes the SHA256 handshake QML cannot. At rest it shows a record dot, with the elapsed time while recording. A click opens its panel in the capsule: Record or Stop, Pause or Resume (while recording), Webcam (the first `v4l2_input` item in the current scene; absent without one), Audio (mute the global "Desktop Audio" input), four corner buttons that move the webcam on the canvas, and a scene grid, four columns of icon tiles, current one highlighted, click to switch. In a scene whose only source is a PipeWire capture (the window scene), a Window button opens that source's properties, where OBS's portal picker chooses the window. The scene glyphs come from `~/.config/keystone/obs-scene-icons.json`, watched and edited from the settings window's "OBS scenes" page; an unmapped scene shows `?`. While recording the sliver pulses red, and holds dim while paused. QML's `WebSocket` does not expose the close code, so a wrong password reads the same as OBS quitting; it is logged once as "closed before Identified". ## Controls panel (removed) There was one, opened by SUPER alone. Each of its seven tiles opened a drawer page that already existed and it went unused, so it was removed and SUPER alone opens the drawer instead. `Prefs.qml` keeps only the pinned IP; a `controls` key left in `desktop.json` by the old panel is kept on write and otherwise ignored. ## 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 on the settings window's Icons page 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 `keystone-island`. For frosting, add to `~/.config/hypr/sections/decorations.lua`: hl.layer_rule({ name = "blur-island", match = { namespace = "^(keystone-island)$" }, blur = true, xray = false, ignore_alpha = 0.1, }) Without it the island renders flat translucent. ## File drop A file dragged onto the sliver opens a bubble, "Drop for the assistant"; dropping on it hands the file to the assistant canvas, which opens in its file state with a new conversation. Wayland reports a drag only once it enters one of the surface's input regions, so the receptor is the sliver's width at rest and grows to hold the bubble while a drag is over it. Only `file://` URLs are taken; a refused drop says why in the bubble for a moment. The file manager offers a trashed file as a `trash:` URL, which has no path to read: restore it first.