- the card opens on the monitor that was clicked (keybinds: the focused monitor); the others keep their pill. moving the card to another monitor restarts its open animation there - only the focused monitor's window (and the one with the card) reads Spectrum, so the other pills show still bars and rest - hover lights only the pill under the pointer - measured with music on 3 monitors: pill 10.7% of a core (was 27.5%), open card 20.7% (was 58%), ~62 frames a second in total, 0-1 slow frames per open/close (was 7-14), back to one island's cost
9.7 KiB
AGENTS.md
Notes for agents continuing work on the Magic Island shell. Read README.md first for what it does and how it's laid out.
Ground rules from the user
- Branch per big change, and ask before merging. Create a new branch for each big change, show the result, and merge into
masteronly after the user explicitly approves. Small follow-ups the user asks for in the same breath can go on the same branch. - Never touch the user's real players' sound. Cider and Firefox are real; their volume, equalizer and output are the user's. Before any
island preset|outputcall or volume change, runamane ipc call island statusand make sureplayer=is a fake test player. Test audio goes to inaudible null sinks (see Testing). - The desktop is reached only by
ssh -o BatchMode=yes fabian@192.168.2.169(Hyprland, Intel iGPU, monitors DP-2 and eDP-1). Don't probe the network, SSH keys or other hosts; if SSH fails, report it and wait. - Leave the desktop clean: stop fake players and null sinks after testing, and restart the shell without frame logging (see "After testing").
- Be honest in summaries about what was and wasn't checked on the real desktop.
Constraints of Amane itself
- The shell can only depend on
amaneand the Rust standard library:amane compilegeneratesCargo.tomland no other crates can be added. External tools run as processes (std::process::Command,amane::output). - Amane docs:
/root/amane/docs/src/*.mdon the dev host; library source in/root/amane/src/. - Amane redraws the whole window every frame, so every effect in the view is paid at the frame rate.
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 dropdown, time-bar scrub. 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. Stops when it pauses. |
art.rs Art |
Service, 1 s poll | Cover URL → local file (curl for https), copied to palette-cover, which Palette (opened in main.rs, 8 colours) watches. |
parse.rs |
plain functions, no amane | pactl/cava/proc parsing, filter-chain config, pw-cli params, EQ store, middle ellipsis. Unit-tested on its own. |
The equalizer: one setsid -f pipewire -c eq-<key>.conf process 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 are moved 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 is first changed, or it has a saved non-flat curve.
The view (island.rs view)
- One
LayerWindowper monitor (window_per_monitor, namespacemagic-island,Layer::Overlaybecause the user's windows are fullscreen) sized for the open card; anInputArealets clicks outside the island through. - All windows share the
Islandstate.opened_onnames the monitor the card is open on (others show their pill),pointer_onthe monitor whose pill is hovered. Only the focused monitor's window (and the one with the card) readsSpectrum, so the others don't redraw for every cava frame: with music, pill ~11% of a core and open card ~21%, as with one island.focused_monitor()comes fromWorkspaces, which follows the mouse on Hyprland. Spectrum::listenfirstpkill -KILL -fs any cava running ourcava.conf: a stopped shell leaves its cava behind, and cava ignores SIGTERM.phases(open)splits the 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). Closing runs it backwards. At most one face (pill or card) is built per frame.veiled()fades a face by laying the backdrop gradient over it with alpha1 - showninstead of.opacity(): same picture, no GPU layer.backdrop_colors()/backdrop(): one 150° three-stop gradient from the palette (dominant,colors()[1],background()), each darkened bydeepen()to a max brightness of 0.22 / 0.15 / 0.07.pill_levels(): 7 bars, bar i = loudest of spectrum slice i of 7, × 1.4, linear, drawn up and down from the middle line.ring(): 40 candles around the CD, levels frommirrored()(bass at the top, highs down both sides),powf(0.75). Drawn as 3 paths (glow, body, flame tips).- Moving the pointer off the island collapses it (
on_hover).
Performance rules (measured on the Intel iGPU)
Current numbers: card open with music 60 fps, about 3.1 ms GPU per frame; pill with music 60 fps, about 1.7 ms GPU, about 9% of one core for shell + cava; paused, about 1 frame a second. Opening has 1 frame over 20 ms (where the card first appears), closing 0.
- Measure before and after every visual change with
tools/amane-remote.sh HOST fpsandanim island, baseline first. - 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.- Gradients are expensive in numbers: a gradient background plus 10 gradient slider fills halved the frame rate; the single backdrop gradient is affordable.
- A veil over a gradient must be the same gradient at the same size as the shape (inset 1 px for the rim): Amane places a gradient by each rectangle's own bounds, so a smaller veil shows patches.
- Blurred shadows cost every frame: the drop shadow only exists on the pill and during the animation.
- Many shapes cost: draw repeated marks (candles, bars) as one
Pathper look. - Streaming data needs
amane::request_frame()in the view while levels are live; a Service write alone only got about 11 redraws a second from Hyprland. Rectangle::childholds one child; stack layers withStack.
Develop
-
Type-check on the dev host without the desktop. A scratch crate outside the repo (don't commit a
Cargo.toml, Amane generates its own):[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 clippyin that crate. (It is at/root/.hermes/cache/scratch/amane-check/on the dev host.) -
Unit tests:
rustc --edition 2024 --test src/parse.rs -o /tmp/parse_test && /tmp/parse_test(5 tests).
Testing on the desktop
tools/amane-remote.sh HOST COMMAND; the full command list is in the comment at the top of the script. The main ones:
H=fabian@192.168.2.169
tools/amane-remote.sh $H deploy src # back up the remote src once, then replace it
tools/amane-remote.sh $H build # amane compile
tools/amane-remote.sh $H start # runs the shell WITH frame logging (AMANE_FRAMES=1)
tools/amane-remote.sh $H fps 3 # frames, average gap, gpu wait
tools/amane-remote.sh $H anim island # open/close 3x, every frame gap
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 (tools/fake_mpris.py), "Synth Test" makes real sound into island_test_a
tools/amane-remote.sh $H ipc island status
tools/amane-remote.sh $H sh 'COMMAND' # anything inside the graphical session's environment
- Pick a fake player with
ipc island nextuntilstatusnames it, before anything that changes sound. - The island follows the focused monitor: read its geometry from
hyprctl layers -j(namespacemagic-island) right before eachgrimcapture. - With one window per monitor,
fpsandanimcount every window's frames together; resting windows add ~1000 ms gaps (one frame a second for the time bar). Those aren't stalls. - The desktop's
$XDG_RUNTIME_DIR/hypr/keeps folders of dead Hyprland instances; the live one is the one with.socket.sock, whichamane-remote.shnow picks. A shell started with a dead signature gets an emptyWorkspaceslist. - Once, a fresh shell exited with "lost the gpu (Device is lost)" (Mesa warns the Xe driver is experimental); starting it again worked.
- The user's pointer may be over the island and open or collapse it during a test; check
statusand capture right afteropen. - To tune a visualiser, sample the real levels read-only (
pw-record --target <stream> | cava, withtimeout -s KILLon each stage; a plaintimeouton a pipeline orphans cava) and look at the distribution first. Typical music sits at 0.15–0.35. - In
pkill -fpatterns over SSH, use[p]ython3 ...so the pattern doesn't match the remote shell itself.
After testing
tools/amane-remote.sh $H players stop
tools/amane-remote.sh $H sinks stop
tools/amane-remote.sh $H sh 'kill $(cat shell.pid); sleep 0.5; rm -f shell.log
setsid -f sh -c "echo \$\$ > shell.pid; exec ~/.cache/amane/project/target/debug/amane-shell" > /dev/null 2>&1'
The desktop's amane CLI is older than the docs: no ~/.cache/amane/amane-shell is saved and amane run doesn't apply, so the shell runs from ~/.cache/amane/project/target/debug/amane-shell.
Open threads
- The candle ring is still mirrored (
mirrored()); the user had the pill bars unmirrored and may want the same for the ring. - See README "Known limits".