AGENTS.md: settings, config window, workspace ghost; condensed

This commit is contained in:
server2
2026-10-15 02:59:52 +02:00
parent 3b9929aff4
commit 7d8525e95c
+84 -117
View File
@@ -1,154 +1,127 @@
# AGENTS.md
Handoff for agents continuing work on the Magic Island shell (Hermes, Claude Code, Codex, a new session). Read README.md first for what the shell does and how to use it; this file is how it's built, how to work on it, and where things stand. Last updated 2026-10-13.
Handoff for agents continuing work on the Magic Island shell (Hermes, Claude Code, Codex, a new session). Read README.md first for what the shell does and how to use it; this file is how it's built, how to work on it, and where things stand. Last updated 2026-10-15.
## Where things are
- **This repo:** `/root/amane-config` on the dev host; remote `git@server1:fabolous005/amane-config.git`.
- **Amane library and docs:** `/root/amane` (`docs/src/*.md`, source in `src/`). The desktop has its own copy at `~/git/amane`.
- **Amane library:** `/root/amane` (docs in `docs/src/*.md`). The desktop builds against its own, different copy at `~/git/amane`; type-check against a copy of that one (see Develop).
- **Hermes skill `amane`** (`~/.hermes/skills/software-development/amane/`): distilled Amane docs plus lessons. `references/recipe-media-island.md` and `references/remote-visual-testing.md` cover this project; `scripts/` holds copies of `tools/` (keep them in sync).
- **Desktop:** `ssh -o BatchMode=yes fabian@192.168.2.169`. Hyprland 0.56.2 with a **Lua** config (`~/.config/hypr/*.lua`; API stubs in `/usr/share/hypr/stubs/hl.meta.lua`). Intel Xe iGPU (Raptor Lake). Monitors: eDP-1 1920×1200 at 1920,620; HDMI-A-1 1920×1080 at 0,620; DP-2 1920×1080 at 3840,0; HDMI-A-4 at 5760,0 only when connected (on 2026-10-13 three were connected, all 1920 wide). `~/.config/amane` on the desktop is a git checkout of this repo.
- **Desktop:** `ssh -o BatchMode=yes fabian@192.168.2.169`. Hyprland 0.56.2 with a **Lua** config (`~/.config/hypr/*.lua`; API stubs in `/usr/share/hypr/stubs/hl.meta.lua`). Intel Xe iGPU (Raptor Lake). Monitors: eDP-1 1920×1200 at 1920,620; HDMI-A-1 1920×1080 at 0,620; DP-2 1920×1080 at 3840,0; HDMI-A-4 at 5760,0 only when connected. `~/.config/amane` on the desktop is a git checkout of this repo.
- **Desktop daemons the control centre uses:** OpenRC services `iwd` (Wi-Fi), `bluetooth` (bluez), `openvpn.<name>` (VPN); doas rule `permit nopass fabian` for `rc-service`, `sh` and `cat`; `wl-copy`, `ip`, `lspci`. iwd and bluetoothd are often stopped; the shell then greys their tiles.
## Ground rules from the user
- **Branch per big change, ask before merging.** New branch for each big change; merge into `master` only after explicit approval. "In main branch" or "merge directly to main" means commit straight to master. Small follow-ups in the same request can share a branch. **Push only when the user asks.**
- **Never touch the user's real players' sound** (Cider, Firefox). Before any `island preset|output` call or volume change, check `amane ipc call island status` names a fake test player. Test audio goes to the inaudible null sinks.
- **The user's own amane:** they sometimes run it from a terminal. Don't kill it without asking; `tools/amane-remote.sh start` refuses when it finds a shell it didn't start.
- **Desktop access is SSH only.** Don't probe the network, SSH keys or other hosts. If SSH fails, report it and wait (the user once had to restart sshd).
- **The user's own amane:** they sometimes run it themselves. Don't kill it without asking; `tools/amane-remote.sh start` refuses when it finds a shell it didn't start.
- **Desktop access is SSH only.** Don't probe the network, SSH keys or other hosts. If SSH fails, report it and wait.
- **Leave the desktop clean** after testing: fake players and null sinks stopped, shell running without the frame log (see "After testing").
- **Edit files with the patch/write tools, not scripts that rewrite files.** The user installs missing tools themselves. Scripts take instance values (hosts, names) as arguments.
- **D-Bus only for reading status.** Actions go through the CLIs: `iwctl` to join, scan and disconnect, `bluetoothctl` to pair and connect (the user had a D-Bus agent version reverted, `afe963d`). The one exception, chosen by the user: the shell serves `org.kde.StatusNotifierWatcher` and calls tray apps' dbusmenus for the tray. Ask before serving anything else on the bus.
- **No hardcoded tuning values:** every size, timing, text size, colour or interval is a setting in `config.rs` (see Settings). The user asked for this explicitly.
- **Blur strength is Hyprland's only** (`decoration.blur.*`); the shell never sets it. A shell blur setting was added once and reverted at the user's request.
- **D-Bus only for reading status.** Actions go through the CLIs: `iwctl` to join, scan and disconnect, `bluetoothctl` to pair and connect (the user had a D-Bus agent version reverted, `afe963d`). The one exception, chosen by the user: the shell serves `org.kde.StatusNotifierWatcher` and calls tray apps' dbusmenus. Ask before serving anything else on the bus.
- **Never wake iwd by accident:** any D-Bus call to `net.connman.iwd` starts it (it then joins Wi-Fi and adds a route). Check `org.freedesktop.DBus.NameHasOwner` first; only talk to it while it has an owner.
- **Root only through `doas -n` with fixed command lines** (`rc-service <svc> start|stop`, `cat /var/lib/iwd/<profile>.psk`). Strings from other programs (SSIDs, device names, D-Bus metadata) go to `Command` args, never into `amane::spawn`/`output` (`sh -c`). Passwords go to `wl-copy` on stdin, never as an argument; iwctl's `--passphrase` is visible in `ps` for about a second, which the user accepted.
- Hermes approval prompts are switched off; edits to AGENTS.md may still prompt (`security.protected_instruction_files`), and the user approves AGENTS.md rewrites first.
- Edits to AGENTS.md prompt for approval (`security.protected_instruction_files`); the user approves them at the screen, and a timed-out prompt is a no: don't retry by another path. Keep this file near 20k characters, Hermes's load limit.
- Report honestly what was and wasn't checked on the real desktop.
## Current state
**master** (pushed): the island (pill, card, CD ring, EQ, output and player switching, hover animations, instant transport buttons), an idle pill when no player is open, one island per monitor (card on one monitor, live bars only on the focused one), a workspace pill per monitor, and wallpaper colours through matugen. Merged from `control-centre` on 2026-10-13, then extended on master:
**master** (pushed): the island (pill, card, CD ring, EQ, output and player switching, hover animations, instant transport buttons), an idle pill when no player is open, one island per monitor (card on one monitor, live bars only on the focused one), a workspace pill per monitor, wallpaper colours through matugen, and:
- **Control centre** (top right): a status pill (Wi-Fi or hotspot, VPN, Bluetooth, Ethernet, battery; icons of stopped services hidden) that opens a panel: tiles for Wi-Fi, Bluetooth, VPN, Hotspot, Ethernet; volume with the output, brightness, keyboard light, performance mode, external-GPU programs. The panel morphs into its menus as one shape: Wi-Fi and Bluetooth maps (devices floating round this computer, click to join/pair, right-click a saved network for its password, click the centre to search again), VPN list, sound outputs, Ethernet details with copyable addresses. The maps reach from the right edge to the island's pill, less 16 px. Popups (`magic-prompt`) ask for passwords, PINs and passkeys and say why something failed.
- **Control centre** (top right): a status pill (Wi-Fi or hotspot, VPN, Bluetooth, Ethernet, battery) that opens a panel: tiles for Wi-Fi, Bluetooth, VPN, Hotspot, Ethernet; volume with the output, brightness, keyboard light, performance mode, external-GPU programs; a Settings button (gear) at its foot. The panel morphs into its menus as one shape: Wi-Fi and Bluetooth maps (click to join/pair, right-click a saved network for its password, click the centre to search again), VPN list, sound outputs, Ethernet details with copyable addresses. Popups (`magic-prompt`) ask for passwords, PINs and passkeys and say why something failed.
- **System tray** right of the workspace pill.
- **Bar hiding:** only on monitors showing a fullscreen window; the pointer resting on the top edge brings it back. Events bring one part in for a moment (a peek): the workspace pill alone on a workspace change, the island on a song change or a new player. A peek starts from where the part is, so switching from a shown bar to a fullscreen workspace keeps the workspace pill still (no dip) while the tray leaves on its own.
- **Reserved space:** windows that aren't fullscreen stay below the bar (44 px at the top of every monitor).
- **Bar hiding:** only on monitors showing a fullscreen window; the pointer on the top edge brings it back. Peeks: the workspace pill alone on a workspace change, the island on a song change or a new player. A peek starts from where the part is (no dip).
- **Workspace switch:** Hyprland deletes an empty workspace the moment it's left; its slot stays for the slide, shrinking and fading, so the row closes up with the disc's move instead of jumping.
- **Reserved space:** windows that aren't fullscreen stay below the bar (44 px).
- **Settings** (branch `config-window`, merged 2026-10-15): `~/.config/amane/config.toml` holds all 184 shell settings with their help; the config window (`Magic Island settings`, a normal window; Esc closes it) edits them plus every Hyprland option. See Settings.
On 2026-10-13 the user was asked to try the mouse-only parts (tray clicks, top-edge hover reveal, Copy buttons, PIN pairing) and answered "All works"; the agent itself only drove them over IPC.
On 2026-10-13 the user tried the mouse-only parts (tray clicks, top-edge reveal, Copy buttons, PIN pairing): "All works". The agent itself only drives them over IPC.
**Open design branches** (pushed, not merged; the user is reviewing them). They branched from `036b326`, 13 commits before master's control-centre work; merging one will need a rebase (conflicts not checked yet):
**Glass designs** (`glass-base`, `design-frosted`, `design-liquid`, pushed, not merged): `glass-base` adds `src/surface.rs` (`shell()` draws the island, idle pill and workspace pill; with glass below 1 the open/close fade uses `.opacity()`, as a veil can't hide content on see-through glass) and a `glass` opacity; `design-frosted` (0.62, thin tint, hairline white rim, top sheen, soft black shadow) and `design-liquid` (0.4, specular gradient, light rim warming to `flame` on hover, accent glow) build on it. They branched from `036b326`, before the control centre and the settings, and read `glass` from `shell.conf`, which master no longer has: port them as settings (`appearance.surface.*`), not as a revived `shell.conf`. Measured 2026-10-09 (pill / open card GPU ms per frame): solid 1.6 / 3.3, liquid 2.2 / 3.3, frosted 2.9 / 3.3. `~/.config/hypr/windowrules.lua` has the glass layer rule (`^magic-.*$`, blur, `ignore_alpha = 0.5`).
| Branch | What it is |
|---|---|
| `glass-base` | `glass` (0.05 to 1, default 1 = solid) in `shell.conf`; new `src/surface.rs` with `shell()`, which draws the island, idle pill and workspace pill; with glass below 1 the open/close fade uses `.opacity()` (a veil can't hide content on see-through glass). README "Glass" section with the Hyprland layer rule. |
| `design-frosted` | glass-base plus an Apple-style surface: default glass 0.62, thin tint, hairline white rim (accent when hovered), white sheen along the top third, soft low black shadow instead of the accent glow. |
| `design-liquid` | glass-base plus clear glass: default 0.4, denser toward the bottom right, specular gradient (bright top left, faint bottom right), 1.5 px light rim warming to `flame` when hovered, keeps the accent glow. |
**On the desktop:** the agent-started shell runs the `config-window` build (pid in `~/.cache/amane-remote-test/shell.pid`, no frame log). `config.toml` was made from the old `shell.conf` (still there, unread). `~/.config/hypr/hyprland.lua` does **not** `require("amane")` yet (checked 2026-10-15), so Hyprland options set in the window are lost on a Hyprland reload; the user was told to add it. amane is not autostarted: after a reboot the user starts it.
Each branch has review images in `docs/design/`. A `blur` value was added to `shell.conf` on all three and then reverted: **the user wants the blur strength set only in Hyprland** (`~/.config/hypr/looks.lua` has `decoration.blur.size = 3`). When the user picks a design, merge `glass-base` plus that design into master.
Measured 2026-10-09 with the silent "Synth Test" player playing, 4 monitors (Amane GPU time per frame, CPU of amane-shell):
| | Pill | Open card | Open/close |
|---|---|---|---|
| master (solid) | 1.6 ms, 12% | 3.3 ms, 25% | 2.5 ms |
| glass-base at 0.5 | 1.7 ms, 13.5% | 3.0 ms, 25.5% | 2.4 ms |
| design-liquid at 0.4 | 2.2 ms, 16% | 3.3 ms, 23.5% | 2.9 ms |
| design-frosted at 0.62 | 2.9 ms, 22% | 3.3 ms, 24% | 2.9 ms |
Hyprland's CPU stayed at about 9.5% in every run, blur rule on or off. All builds, master included, had 25–40 stutter frames (20–500 ms gaps) per 3 opens and closes, against 0–1 measured earlier with fewer monitors; the cause wasn't found (Cider and Firefox were running).
**On the desktop right now:** the agent-started shell runs a master build (`11646ca`; pid in `~/.cache/amane-remote-test/shell.pid`, no frame log). `~/.config/amane` is a checkout at `036b326` with master's `src/` deployed over it, so `git status` shows changed files. `shell.conf` has `glass = 0.70` (set by the user). amane is not autostarted by Hyprland: after a reboot the user starts it.
**Theme pipeline:** Skwd-wall v2's matugen integration `amane` renders `~/.config/amane/matugen/amane.colors` into `~/.config/amane/theme` (Skwd scheme: `scheme-fidelity`). `~/.config/hypr/windowrules.lua` has the glass layer rule (`^magic-.*$`, blur, `ignore_alpha = 0.5`).
**Theme pipeline:** Skwd-wall v2's matugen integration `amane` renders `~/.config/amane/matugen/amane.colors` into `~/.config/amane/theme` (scheme `scheme-fidelity`).
## Constraints of Amane itself
- Only `amane` and the Rust standard library: `amane compile` generates `Cargo.toml`, so no crates. External tools run as processes (`std::process::Command`, `amane::output`).
- Amane redraws the whole window every frame, so every effect is paid at the frame rate.
- Amane can only blur what the shell draws itself; frosting the desktop behind the shell needs a Hyprland layer rule.
- Amane can only blur what the shell draws itself; frosting the desktop behind it needs a Hyprland layer rule.
- `amane compile` saves `~/.cache/amane/amane-shell`; `amane run` starts it.
## Architecture
| Piece | Kind | Job |
|---|---|---|
| `island.rs` `Island` | input-only Service | `open: Animation` (0 pill, 1 card), `expanded`, `selected` player (bus name; `None` follows the playing one), open `menu`, time-bar `scrub`, `hover` (per-`Spot` animations), `opened_on` / `pointer_on` (monitors), `playing_now` (a clicked play/pause until the player answers). Also the view and the `island` IPC handler (`command`). |
| `sound.rs` `Sound` | Service, 1 s poll | `pactl list sinks` + `sink-inputs` → each player's streams (matched by process ancestry, then app name), volume, mute, output, equalizer. Curves saved by player identity to `~/.config/amane/island-eq`. |
| `spectrum.rs` `Spectrum` | Service, 200 ms check | While the shown player plays: `pw-record --target <its stream> \| cava -p $XDG_RUNTIME_DIR/magic-island/cava.conf` → 24 levels. On start it `pkill -KILL`s cava left by an earlier shell (cava ignores SIGTERM). |
| `art.rs` `Art` | Service, 250 ms poll | Cover URL → local file (curl for https), copied to `palette-cover`, which `Palette` (opened in `main.rs`, 8 colours) watches. Only used for `cover_colors`. |
| `theme.rs` `Theme` | Service, `amane::watch_file` | 10 colours from `~/.config/amane/theme` and `shell.conf` (`cover_colors`, `transition_ms`; `glass` on the glass branches), faded together on one `Animation<Colors>`. Every view starts with `theme::begin()` (subscribes, returns `cover_colors`) and draws with `theme::now()`. No theme file: the original pink on purple. `watch_file` watches the folder, so matugen's rename-over saves count (55–68 ms to redraw). |
| `workspaces.rs` | view + input-only `Hover` | Workspace pill per monitor (namespace `magic-workspaces`, top left): `Workspaces` filtered by `output() == monitor.name`, special workspaces skipped. Click runs `hyprctl dispatch 'hl.dsp.focus({ workspace = N })'` on a thread. |
| `parse.rs` | plain functions | pactl/cava/proc parsing, filter-chain config, `pw-cli` params, EQ store, middle ellipsis, `key = value` settings, `#rrggbb`; for the control centre also iwctl/bluetoothctl output (`plain_lines`, `bluez_reason`, `pairing_event`), iwd profile names and psk files, Wi-Fi bars, link speeds, `hyprctl monitors/clients`, `ip` addresses and routes, tray icon scoring and menu mnemonics. Unit-tested on its own. |
| `control.rs` `Control` | view + input-only Service | The control centre (`magic-control`, top right) and its menu window (`magic-control-menu`, no shape of its own: the panel morphs to the menu's size, then the menu's content fades in). `Menu` (Wifi, Bluetooth, Vpn, Sound, Ethernet), `menu_content(menu, map_w, ..)` for both windows, `map_width(monitor_w)`, the `control` IPC handler. |
| `system.rs` `Links`, `Power`, `Gpu` | Services, threads | iwd (D-Bus reads, only while it has an owner), openvpn services, bluez service state, wired cards from sysfs, battery, brightness, keyboard light, power profile, eGPU programs from fdinfo. Actions: `join_wifi`, `scan_wifi`, `disconnect_wifi` (iwctl), `scan_bluetooth`, `rc-service` via doas, `saved_password`, `wired_details`. |
| `prompt.rs` `Prompt` | view + input-only Service | The popup (`magic-prompt`): password, PIN, passkey, confirm, show a code, saved password (Copy), failure reason. Runs the `bluetoothctl` pairing session. Only popups with a text field take the keyboard. `prompt::copy(text, done)` is the one wl-copy. |
| `map.rs` | widget functions | The Wi-Fi/Bluetooth maps: nodes on an ellipse round the centre bubble (nearer = stronger), floating, lines (wobbling, running out while connecting, solid with a pulse), search rings. `AREA_H` 480, `NODES` 8 (a 9th makes labels meet at 822 px wide; the sizes were picked by simulating the drift). |
| `config.rs` | settings | Every setting declared once in `settings! { part { group { name: type = default, range; "help" } } }`: a typed accessor (`config::island::pill::width()`) plus an `OPTIONS` entry (path, help, kind, range, default) for the window. Values live in a `RwLock`; reading per draw costs nothing measurable. `load()` reads `config.toml` (first start: writes it, moving `shell.conf`'s 4 values over); `save()` rewrites it with every setting and its help. `[hyprland.*]` tables hold Hyprland overrides. |
| `settings.rs` | view, normal `Window` | The config window: a page per part and per Hyprland category, search over everything, a row per setting (switch, slider + field, text with colour swatch, choice chips), reset per row, `config.toml` errors at the top. Esc closes. The `config` IPC handler (`command`). |
| `hypr.rs` `Hypr` | Service, worker thread | Hyprland's options from Hyprland itself (`hyprctl descriptions -j`, one `--batch` of `j/getoption`), so no option is named in code. `set`/`reset` queue jobs (superseded ones dropped); the worker applies with `hl.config`, rewrites `~/.config/hypr/amane.lua` and `config.toml`; a reset reloads Hyprland's config so the user's value returns. |
| `island.rs` `Island` | input-only Service | `open: Animation` (0 pill, 1 card), `expanded`, `selected` player (`None` follows the playing one), open `menu`, time-bar `scrub`, `hover` (per-`Spot`), `opened_on` / `pointer_on` (monitors), `playing_now`. Also the view and the `island` IPC handler. |
| `sound.rs` `Sound` | Service, poll | `pactl list sinks` + `sink-inputs` → each player's streams (by process ancestry, then app name), volume, mute, output, equalizer; `presets()` from settings. Curves saved by player identity to `~/.config/amane/island-eq`. |
| `spectrum.rs` `Spectrum` | Service | While the shown player plays: `pw-record --target <stream> \| cava -p $XDG_RUNTIME_DIR/magic-island/cava.conf` → levels (`sound.spectrum.bars`). On start it `pkill -KILL`s cava left by an earlier shell (cava ignores SIGTERM). |
| `art.rs` `Art` | Service, poll | Cover URL → local file, copied to `palette-cover`, which `Palette` (opened in `main.rs`) watches. Only used for `cover_colors`. |
| `theme.rs` `Theme` | Service, `amane::watch_file` | 10 colours from `~/.config/amane/theme`, faded together on one `Animation<Colors>`; also watches `config.toml` (own thread) and reloads it. Views start with `theme::begin()` and draw with `theme::now()`; `theme::touch()` redraws every view. No theme file: `appearance.fallback.*`. `watch_file` watches the folder, so matugen's rename-over saves count. |
| `workspaces.rs` | view + input-only `Hover` | Workspace pill per monitor (`magic-workspaces`, top left), slots placed by x in one `Stack` under one sliding accent disc. Click runs `hyprctl dispatch 'hl.dsp.focus({ workspace = N })'` on a thread. |
| `parse.rs` | plain functions | pactl/cava/proc parsing, filter-chain config, EQ store, `key = value`, colours, iwctl/bluetoothctl output, Wi-Fi bars, `hyprctl` JSON, `ip`, tray icons; the TOML reader/writer (`Value`, `toml`), a JSON reader (`json`, skips non-JSON lines between values), Hyprland option values (`hypr_option`, `hypr_lua`). Unit-tested on its own. |
| `control.rs` `Control` | view + input-only Service | The control centre (`magic-control`) and its menu window (`magic-control-menu`: the panel morphs to the menu's size, then its content fades in). `menu_content(menu, map_w, ..)`, `map_width(monitor_w)`, the `control` IPC handler. |
| `system.rs` `Links`, `Power`, `Gpu` | Services, threads | iwd (D-Bus reads, only while it has an owner), openvpn, bluez state, wired cards from sysfs, battery, brightness, keyboard light, power profile, eGPU programs from fdinfo; actions via CLIs. Intervals in `control.polling.*`. |
| `prompt.rs` `Prompt` | view + input-only Service | The popup (`magic-prompt`): password, PIN, passkey, confirm, code, saved password, failure reason. Runs the `bluetoothctl` pairing session. `prompt::copy(text, done)` is the one wl-copy. |
| `map.rs` | widget functions | The Wi-Fi/Bluetooth maps: nodes on an ellipse round the centre bubble (nearer = stronger), floating, lines, search rings. Sizes in `control.maps.*` (8 nodes at most: a 9th makes labels meet at 822 px; picked by simulating the drift). |
| `glyph.rs` | shapes | Every icon as `Shape`s; maps draw all of theirs into ONE canvas (`glyph_shapes`), as each `Canvas` has a large fixed GPU cost. |
| `edge.rs` `Edge` | Service, Hyprland sockets | Reads `.socket2.sock` events (fullscreen, workspace, focus, windows) and asks `.socket.sock` for `monitors`/`clients`; no polling. Hides the bar on fullscreen monitors (`edge.part(Part, monitor)`, `hidden_by`), the 1 px `magic-edge` strip that brings it back, peeks and the island's pop (`edge.pop`), the workspace highlight slide (`switched`), and `spacer`, the `magic-reserve` strip that reserves 44 px for the bar. |
| `tray.rs` `Tray`, `TrayUi` | Services, bus threads | Owns `org.kde.StatusNotifierWatcher` (only if no other watcher runs) and is its host; items by bus name or path (path-only owners found by asking all connections at once); icons from themes or `IconPixmap`; dbusmenu menus in `magic-tray-menu`; the `tray` IPC handler. |
| `png.rs` | plain functions | Stored-deflate PNG writer for pixmap tray icons (amane draws images from files). |
| `edge.rs` `Edge` | Service, Hyprland sockets | `.socket2.sock` events and `.socket.sock` queries, no polling. Hides the bar on fullscreen monitors, the 1 px `magic-edge` strip, peeks, the island's pop, the workspace slide (`switched`), `hide_h()`, and `spacer` (`magic-reserve`, 44 px). |
| `tray.rs` `Tray`, `TrayUi` | Services, bus threads | Owns `org.kde.StatusNotifierWatcher` (only if no other watcher runs) and is its host; icons from themes or `IconPixmap`; dbusmenu menus in `magic-tray-menu`; the `tray` IPC handler. |
| `png.rs` | plain functions | Stored-deflate PNG writer for pixmap tray icons. |
The equalizer: one `setsid -f pipewire -c eq-<key>.conf` per player with `libpipewire-module-filter-chain`, 10 biquads (low shelf 31 Hz, peaking 63 Hz–8 kHz, high shelf 16 kHz). The player's streams move into sink `magic_island_eq.<key>`; gains change live with `pw-cli set-param magic_island_eq.<key> Props '{ params = [ "b1:Gain" 6.0 ... ] }'`. A player only gets one when its curve or output first changes, or it has a saved non-flat curve.
The equalizer: one `setsid -f pipewire -c eq-<key>.conf` per player with `libpipewire-module-filter-chain`, 10 biquads at `sound.equalizer.bands`. The player's streams move into sink `magic_island_eq.<key>`; gains change live with `pw-cli set-param`. A player only gets one when its curve or output first changes, or it has a saved non-flat curve.
### The view (island.rs `view`)
- One `LayerWindow` per monitor (`window_per_monitor`, namespace `magic-island`, `Layer::Overlay` because the user's windows are fullscreen), sized for the open card; an `InputArea` lets clicks outside the island through.
- No player: `idle()`, a static click-through pill with a drawn note and "Nothing playing".
- Monitors: `tap(monitor)` opens the card where it was clicked, `expand()` (IPC) on the focused monitor; `open_on` restarts the animation when the card moves. Other monitors render `phases(0.0)`, their pill. Only the focused window and the one with the card read `Spectrum` (reading a Service is what subscribes a window), so the rest stay idle.
- `phases(open)` splits one linear 560 ms timeline: pill fades out over 0–0.18, the empty shape grows over 0–0.6 (cubic ease-out), the card fades in over 0.6–1 (smoothstep). At most one face is built per frame. `veiled()` fades a face with a backdrop-coloured veil instead of `.opacity()`.
- Background: the wallpaper surfaces as one 150° three-stop gradient (`backdrop()`); with `cover_colors` the card's background blends to `backdrop_colors(Palette)` (dominant, `colors()[1]`, `background()`, darkened by `deepen()` to 0.22 / 0.15 / 0.07 brightness) as it opens. White-alpha overlays (`CHIP`, `TRACK`, `CHIP_HOVER`, `CLEAR`) stay unthemed.
- Hover: `hovering(spot)` returns the `on_hover` closure easing that `Spot` to 0/1 over 220 ms; the view reads `island.lit(spot)`. Amane matches hover targets by their index in the last frame, so a vanished button never hears the pointer leave: entering a spot fades all others, and `collapse()`/`set_menu()` drop stale highlights. Effects are colour blends and `.scale()`/`.translate()`; transport buttons go bold (`bold_icon`: the glyph's own outline, 0–2 px).
- Transport: play/pause flips at once through `playing_now`; after any transport click `refresh_soon()` re-reads Media at 150 and 600 ms (Media polls once a second). `island play-pause` over IPC takes the same path.
- `pill_levels()`: 7 bars, bar *i* = loudest of spectrum slice *i*, ×1.4, linear, drawn up and down from the middle. `ring()`: 40 candles, levels from `mirrored()`, 3 paths (glow, body, flame tips).
- Moving the pointer off the open card collapses it.
- One `LayerWindow` per monitor (`magic-island`, `Layer::Overlay` because the user's windows are fullscreen), sized for the open card; an `InputArea` lets clicks outside through. No player: `idle()`, a click-through pill.
- `tap(monitor)` opens the card where it was clicked, `expand()` (IPC) on the focused monitor. Only the focused window and the one with the card read `Spectrum` (reading subscribes), so the rest stay idle.
- `phases(open)` splits one timeline (`island.animation.open_ms`): pill fades out, the empty shape grows until `shape_done`, then the card fades in. At most one face per frame; `veiled()` fades with a backdrop-coloured veil instead of `.opacity()`.
- Background: one 150° three-stop gradient (`backdrop()`); with `cover_colors` the card blends to the cover's colours as it opens. White-alpha overlays (`appearance.overlays.*`, `CLEAR`) stay unthemed.
- Hover: `hovering(spot)` eases a `Spot` over `appearance.motion.hover_ms`. Amane matches hover targets by index in the last frame, so a vanished button never hears the pointer leave: entering a spot fades all others, and `collapse()`/`set_menu()` drop stale highlights.
- Transport: play/pause flips at once through `playing_now`; `refresh_soon()` re-reads Media at 150 and 600 ms.
## Settings
- **Add a setting** by declaring it in `config.rs`'s `settings!` block in the right part and group, with a range and a one-line help; use the accessor in code. Derived values (`inner_w()` and the like) are functions over settings, not constants. Text sizes are roles (`appearance.text.tiny|caption|small|label|body|heading|section|title|popup`), not numbers. Remaining `const`s are names and identities only (D-Bus names, `CLEAR`, `TAU`).
- The font and cava's bar count apply on restart; everything else on save.
- `amane ipc call config get|set|reset <path> [value]` drives everything without the mouse (`hyprland.<option.path>` for Hyprland, e.g. `hyprland.decoration.blur.size`); `status` says `open=… page=… errors=… search=…`.
- `hl.config` value forms: numbers and bools plain, gradients `{ colors = { "rgba(…)" }, angle = 45.0 }`, vec2 `{ x, y }`, css gaps a number or `{ top, right, bottom, left }`. `hyprctl --batch` of all `j/getoption` prints plain "invalid type (internal error)" lines between objects for a few options.
## Hyprland 0.56 (Lua config) pitfalls
- **Never return `visible(false)` from a layer view.** Hyprland configures a layer surface before its first commit, so a hidden-at-start window gets a size, Amane logs "`<name>` has no child to draw" and the window stays blank forever. With nothing to show, return `window.click_through().child(Rectangle::new().width(0.0).height(0.0))`.
- **`Workspaces::focus` does nothing:** it sends `dispatch workspace N`, which the Lua config rejects. Use `hl.dsp.focus({ workspace = N })`. Test dispatches harmlessly on the already-focused workspace.
- `hyprctl eval` / `hyprctl repl` run Lua in one shared state; globals (e.g. a layer rule kept in a variable) vanish when Hyprland reloads its config.
- Layer-rule `match.namespace` is a full match: `^magic-` matches nothing, `^magic-.*$` works. Layer rules can only switch blur on; blur strength (`decoration.blur.size`, `passes`) is global.
- `$XDG_RUNTIME_DIR/hypr/` keeps folders of dead instances; the live one has `.socket.sock` (`amane-remote.sh` picks it). A shell started with a dead signature gets an empty `Workspaces` list.
- `hyprctl layers` lists hidden layer windows too (alpha 0); filter on `alpha > 0`.
- **Reserved space and fullscreen:** a layer with `Zone::Reserve` reserves its whole height (amane's `measure_thickness`), and only on a window anchored to one edge (or an edge plus both sides), so corner-anchored or card-sized windows can't reserve. Hyprland keeps tiled and maximised (fullscreen 1) windows out of the reserved strip and lets fullscreen 2/3 cover it. Bar windows must keep `Zone::Ignore`, otherwise they get pushed down below the strip. Check with `hyprctl monitors` (`reserved: 0 44 0 0`) and a tiled window's `at:`.
- **Hyprland state comes from its sockets, not polling:** `.socket2.sock` streams events, `.socket.sock` answers `monitors`/`clients`. Fullscreen 2 and 3 hide the bar; 1 (maximised) keeps it.
- A peek (workspace or island) only shows on the **focused** monitor, and only matters on a fullscreen one; test with a workspace hop between two fullscreen workspaces there, then restore the focus.
- **Never return `visible(false)` from a layer view**: a hidden-at-start layer window stays blank forever ("has no child to draw"). Return `window.click_through().child(Rectangle::new().width(0.0).height(0.0))`.
- **`Workspaces::focus` does nothing** (sends `dispatch workspace N`). Use `hl.dsp.focus({ workspace = N })`.
- `hyprctl eval` / `repl` share one Lua state; globals (a rule kept in a variable) vanish on a config reload.
- Layer-rule `match.namespace` is a full match: `^magic-.*$`. Layer rules can only switch blur on; strength is global.
- `$XDG_RUNTIME_DIR/hypr/` keeps folders of dead instances; the live one has `.socket.sock`. A shell started with a dead signature gets an empty `Workspaces` list.
- `hyprctl layers` lists hidden layer windows too (alpha 0).
- **Reserved space:** `Zone::Reserve` reserves the whole window height and only on a window anchored to one edge. Tiled and maximised windows stay out of the strip; fullscreen 2/3 cover it. Bar windows keep `Zone::Ignore`. Check with `hyprctl monitors` (`reserved: 0 44 0 0`).
- A peek only shows on the **focused** monitor; test with a workspace hop there, then restore the focus.
- `hl.dsp.window.float(...)` acts on the focused window. To screenshot the config window floating, add a temporary rule: `hyprctl eval 'r = hl.window_rule({ name = "amane-settings", match = { title = "^Magic Island settings$" }, float = true, center = true, size = { 1100, 760 } })'`.
## Performance rules
- **Measure before and after every visual change**, baseline first, with the same player and monitor. The GPU-busy counter (`gt0/gtidle/idle_residency_ms`) reads 100% even with the shell paused, so compare Amane's per-frame `gpu` time and `top` CPU instead.
- Don't resize a shape with the full card inside it; grow the empty shape, then fade the content in.
- `.clip()` and `.opacity(<1)` each add a GPU layer (a clip over the whole card once took the iGPU from 47 to 9 fps). A shadow is two layers per frame: halos only while hovered.
- Gradients cost: a gradient background plus 10 gradient fills halved the frame rate; one backdrop gradient is affordable. A veil over a gradient must be the same gradient at the same size (Amane places gradients by each rectangle's own bounds).
- **Measure before and after every visual change**, baseline first, same player and monitor: Amane's per-frame `gpu` time and `top` CPU (the iGPU busy counter reads 100% even with the shell paused).
- Don't resize a shape with the full card inside it; grow the empty shape, then fade the content in. Don't resize a layer window every frame: give it the largest size for the animation (the workspace ghost does).
- `.clip()` and `.opacity(<1)` each add a GPU layer (a clip over the whole card once took the iGPU from 47 to 9 fps); a shadow is two. Halos only while hovered.
- One backdrop gradient is affordable, many aren't. A veil over a gradient must be the same gradient at the same size.
- Draw repeated marks (candles, bars) as one `Path` per look.
- Streaming data needs `amane::request_frame()` in the view while levels are live; a Service write alone got about 11 redraws a second from Hyprland.
- `Rectangle::child` holds one child; stack layers with `Stack`.
- Streaming data needs `amane::request_frame()` in the view while levels are live.
## Develop
1. Type-check on the dev host without the desktop, in a scratch crate outside the repo (don't commit a `Cargo.toml`). Scratch at `/root/.hermes/cache/scratch/amane-check/` (pruned after 24 h idle; recreate it):
```toml
[package]
name = "amane-shell"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "amane-shell"
path = '/root/amane-config/src/main.rs'
[dependencies]
amane = { path = '/root/amane' }
[workspace]
```
Then `cargo clippy` there. The first build takes minutes.
2. Unit tests: `rustc --edition 2024 --test src/parse.rs -o /tmp/parse_test && /tmp/parse_test` (10 tests).
1. Type-check in a scratch crate outside the repo (don't commit a `Cargo.toml`), at `/root/.hermes/cache/scratch/amane-check/` (pruned after 24 h idle): `[package] name = "amane-shell"`, edition 2024, `[[bin]] path = '/root/amane-config/src/main.rs'`, `amane = { path = '<copy of the desktop's ~/git/amane>' }`, an empty `[workspace]`. Then `cargo clippy` (first build takes minutes).
2. Unit tests: `rustc --edition 2024 --test src/parse.rs -o /tmp/parse_test && /tmp/parse_test` (12 tests).
## Testing on the desktop
`tools/amane-remote.sh HOST COMMAND`; the full list is at the top of the script.
`tools/amane-remote.sh HOST COMMAND`; the full list is at the top of the script. Its output carries "bash -c uid" noise: `grep -v` it.
```sh
H=fabian@192.168.2.169
@@ -157,44 +130,38 @@ tools/amane-remote.sh $H build # amane compile
tools/amane-remote.sh $H start # stop the shell it started earlier, start one WITH frame logging
tools/amane-remote.sh $H fps 3 # frames, average gap, gpu wait
tools/amane-remote.sh $H anim island # open/close 3x; stutter = 20-500 ms gaps
tools/amane-remote.sh $H shot magic-island out.png
tools/amane-remote.sh $H sinks start # null sinks island_test_a/_b
tools/amane-remote.sh $H players start 4 # fake MPRIS players; "Synth Test" plays a synth into island_test_a
tools/amane-remote.sh $H ipc island status
tools/amane-remote.sh $H players start 4 # fake MPRIS players; "Synth Test" plays into island_test_a
tools/amane-remote.sh $H sh 'COMMAND' # anything inside the graphical session's environment
```
- Pick a fake player with `ipc island next` until `status` names it, before anything that changes sound; stopping the fake players lets the island fall back to the user's player.
- `ipc island open` opens on the **focused** monitor (the user's, if they're at the desk). Read the geometry from `hyprctl layers -j` on that monitor right before each `grim` capture. The user's pointer can open or collapse the island mid-test.
- With one window per monitor the frame log mixes windows; resting ones show ~1000 ms gaps.
- Design screenshots: set `glass` in the remote `shell.conf` (applies live), wait ~1 s, capture pill, workspace pill and the card (`ipc island open`, wait ~2.5 s).
- Hover can't be tested with `hyprctl dispatch movecursor` (no motion events reach the surface); real motion needs a virtual pointer and moves the user's mouse, so ask first.
- Before trusting timings, check `uptime`: a background Gentoo build once made every build look broken.
- A fresh shell once exited with "lost the gpu (Device is lost)" (Mesa warns Xe is experimental); starting again worked.
- To tune a visualiser, sample real levels read-only (`pw-record --target <stream> | cava`, `timeout -s KILL` on each stage; a plain `timeout` on the pipeline orphans cava) first. Typical music sits at 0.15–0.35.
- In `pkill -f` patterns over SSH, use `[p]ython3 ...` so the pattern doesn't match the remote shell itself (a plain `pgrep -f fake_tray.py` once killed the test script).
- **Control centre and tray without a mouse:** `ipc control open|close|menu <wifi|bluetooth|vpn|sound>|ethernet|wifi search|status`, `ipc tray list|menu|open N|activate N|choose ID|enter ID|back|close`. `control status` ends with `prompt=<what the popup shows>`. A fake tray app: `python3 tools/fake_tray.py name|path` (by bus name, or path-only like libappindicator; it prints every call it gets). Kill it by pid.
- **Maps with many nodes:** with Wi-Fi off the maps are empty. Deploy a scratch copy of `src/` whose `map::map` injects fake nodes (never commit it), then redeploy the real `src`. Pick sizes by simulating the drift in Python, not from single screenshots.
- **Measure gaps and alignment from pixels**, not by asking a vision model: `ffmpeg -i shot.png -vf crop=1920:1:0:Y -f rawvideo -pix_fmt rgb24 row.raw` and look for the border pixels.
- To see the island and the control centre together, open them on a monitor without a fullscreen window (focus one of its workspaces first, restore after).
- Pick a fake player with `ipc island next` until `status` names it, before anything that changes sound.
- `ipc island open` opens on the **focused** monitor. Read geometry from `hyprctl layers -j` right before each `grim` capture. With one window per monitor the frame log mixes windows.
- **Animations:** capture a strip in a loop (`grim -g "3840,0 300x60"` takes ~20 ms) while dispatching the change, then tile the frames with ffmpeg (`tile=2x12`) and look at one sheet.
- Keys to a focused window: `wtype -k Escape`. Hover can't be faked with `movecursor`; real motion moves the user's mouse, so ask first.
- Control centre and tray without a mouse: `ipc control open|close|menu <wifi|bluetooth|vpn|sound>|ethernet|status` (`status` ends with `prompt=…`), `ipc tray list|menu|open N|activate N|choose ID|close`. Fake tray app: `python3 tools/fake_tray.py name|path`; kill it by pid.
- Maps with many nodes: deploy a scratch `src/` whose `map::map` injects fake nodes (never commit it), then redeploy the real one.
- Measure gaps and alignment from pixels (`ffmpeg … crop=1920:1:0:Y -f rawvideo`), not by asking a vision model.
- To tune a visualiser, sample real levels read-only first (`pw-record --target <stream> | cava`, `timeout -s KILL` on each stage); typical music sits at 0.15–0.35.
- Check `uptime` before trusting timings. In `pkill -f` patterns over SSH use `[p]ython3 ...`. A fresh shell once died with "lost the gpu"; starting again worked.
### After testing
```sh
tools/amane-remote.sh $H players stop
tools/amane-remote.sh $H sinks stop
tools/amane-remote.sh $H sh 'P=$(pgrep -f "^/usr/lib/python-exec/python3[.0-9]*/python3 fake_tray"); [ -n "$P" ] && kill $P' # fake tray apps
tools/amane-remote.sh $H sh 'kill $(cat shell.pid); for i in $(seq 50); do pgrep -F shell.pid > /dev/null || break; sleep 0.1; done
rm -f shell.log; setsid -f sh -c "echo \$\$ > shell.pid; exec ~/.cache/amane/amane-shell" > /dev/null 2>&1'
```
## Open threads
- The user picks a glass design; then rebase `glass-base` plus that design onto master (they predate the control centre, whose windows draw their own surfaces), merge, and update README/AGENTS.md.
- The user reviews the glass designs, ported onto master as settings; then merge the chosen one.
- The user adds `require("amane")` to `hyprland.lua` (or asks the agent to).
- The config window was only driven over IPC and Esc; clicking, dragging and typing in it are untested by the agent.
- The candle ring is still mirrored (`mirrored()`); the pill's bars aren't.
- The Amane library could fix the two Hyprland 0.56 issues upstream (handle a configure on a hidden layer window; use the Lua dispatch in `Workspaces::focus`). Not started.
- `island-eq` is tracked in git but rewritten live by the island, so the desktop checkout always shows it changed; untracking it was proposed, no answer yet.
- Leftovers offered for deletion, no answer yet: `src.before-test/` on the desktop, seven merged local branches on the dev host. New since: the merged local `control-centre` branch, and old screenshots in the desktop's `~/.cache/amane-remote-test/` (`dimtest.png`, `grab.png`, `shot.png`, `ws-after.png`, `shots/`).
- On a monitor narrower than about 1500 px the maps stop at 600 px (`MIN_MAP_W`) and run under the island.
- Upstream Amane fixes for the two Hyprland 0.56 issues (hidden layer at start, `Workspaces::focus`): not started.
- `island-eq` is tracked but rewritten live; untracking it was proposed, no answer yet. Leftovers offered for deletion, no answer: `src.before-test/` and old screenshots in `~/.cache/amane-remote-test/` on the desktop, merged local branches on the dev host.
- On a monitor narrower than about 1500 px the maps stop at 600 px and run under the island.
- The open/close stutter rise (25–40 slow frames per 3 opens and closes on every build) is unexplained.
- See README "Known limits".