Files
amane-config/AGENTS.md
T

125 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. Unit-tested on its own. |
| `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). Reuses the island's `pub` colours, `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 -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".