Files
amane-config/AGENTS.md
T
2026-10-09 04:29:41 +02:00

11 KiB
Raw Blame History

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 master only 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|output call or volume change, run amane ipc call island status and make sure player= 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 amane and the Rust standard library: amane compile generates Cargo.toml and no other crates can be added. External tools run as processes (std::process::Command, amane::output).
  • Amane docs: /root/amane/docs/src/*.md on 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, 250 ms 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, key = value settings and #rrggbb. Unit-tested on its own.
theme.rs Theme Service, 500 ms poll The shell's 10 colours from ~/.config/amane/theme (rendered by Skwd-wall from matugen/amane.colors) and shell.conf (cover_colors, transition_ms), faded together on one Animation<Colors>. Every view starts with theme::begin() (reads Theme, so it redraws on a change; returns cover_colors) and draws with theme::now(). No theme file: the original pink on purple.
workspaces.rs view + input-only Hover Service Workspace pill per monitor (namespace magic-workspaces, top left): Workspaces filtered by output() == monitor.name, special workspaces skipped, click = hyprctl dispatch 'hl.dsp.focus({ workspace = N })' (Hyprland 0.56 with a Lua config rejects the dispatch workspace N that amane's Workspaces::focus sends). Draws with theme::now(), reuses the island's backdrop() and HOVER_MS. Redraws only on Hyprland events and hover.

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 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.
  • Never visible(false). Hyprland 0.56 configures a layer surface before its first commit, so a window that starts hidden gets a size, Amane logs "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)).
  • All windows share the Island state. opened_on names the monitor the card is open on (others show their pill), pointer_on the monitor whose pill is hovered. Only the focused monitor's window (and the one with the card) reads Spectrum, 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 from Workspaces, which follows the mouse on Hyprland.
  • Spectrum::listen first pkill -KILL -fs any cava running our cava.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 alpha 1 - shown instead of .opacity(): same picture, no GPU layer.
  • backdrop_colors() / backdrop(): one 150° three-stop gradient from the palette (dominant, colors()[1], background()), each darkened by deepen() 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 from mirrored() (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 fps and anim 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 Path per 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::child holds one child; stack layers with Stack.

Develop

  1. 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 clippy in that crate. (It is at /root/.hermes/cache/scratch/amane-check/ on the dev host.)

  2. 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 next until status names it, before anything that changes sound.
  • The island follows the focused monitor: read its geometry from hyprctl layers -j (namespace magic-island) right before each grim capture.
  • With one window per monitor, fps and anim count 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, which amane-remote.sh now picks. A shell started with a dead signature gets an empty Workspaces list.
  • 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 status and capture right after open.
  • To tune a visualiser, sample the real levels read-only (pw-record --target <stream> | cava, with timeout -s KILL on each stage; a plain timeout on a pipeline orphans cava) and look at the distribution 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.

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".