7844a98828
- 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
123 lines
9.7 KiB
Markdown
123 lines
9.7 KiB
Markdown
# 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, 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 `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.
|
||
- 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 -f`s 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):
|
||
|
||
```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` 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:
|
||
|
||
```sh
|
||
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
|
||
|
||
```sh
|
||
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".
|