22 KiB
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-15.
Where things are
- This repo:
/root/amane-configon the dev host; remotegit@server1:fabolous005/amane-config.git. - Amane library:
/root/amane(docs indocs/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.mdandreferences/remote-visual-testing.mdcover this project;scripts/holds copies oftools/(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.~/.config/amaneon 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 rulepermit nopass fabianforrc-service,shandcat;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
masteronly 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|outputcall or volume change, checkamane ipc call island statusnames a fake test player. Test audio goes to the inaudible null sinks. - The user's own amane: they sometimes run it themselves. Don't kill it without asking;
tools/amane-remote.sh startrefuses 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.
- 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:
iwctlto join, scan and disconnect,bluetoothctlto pair and connect (the user had a D-Bus agent version reverted,afe963d). The one exception, chosen by the user: the shell servesorg.kde.StatusNotifierWatcherand calls tray apps' dbusmenus. Ask before serving anything else on the bus. - Never wake iwd by accident: any D-Bus call to
net.connman.iwdstarts it (it then joins Wi-Fi and adds a route). Checkorg.freedesktop.DBus.NameHasOwnerfirst; only talk to it while it has an owner. - Root only through
doas -nwith 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 toCommandargs, never intoamane::spawn/output(sh -c). Passwords go towl-copyon stdin, never as an argument; iwctl's--passphraseis visible inpsfor about a second, which the user accepted. - 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, wallpaper colours through matugen, and:
- 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 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.tomlholds 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 tried the mouse-only parts (tray clicks, top-edge reveal, Copy buttons, PIN pairing): "All works". The agent itself only drives them over IPC.
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).
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.
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
amaneand the Rust standard library:amane compilegeneratesCargo.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 it needs a Hyprland layer rule.
amane compilesaves~/.cache/amane/amane-shell;amane runstarts it.
Architecture
| Piece | Kind | Job |
|---|---|---|
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 -KILLs 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 Shapes; 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 | .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 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
LayerWindowper monitor (magic-island,Layer::Overlaybecause the user's windows are fullscreen), sized for the open card; anInputArealets 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 readSpectrum(reading subscribes), so the rest stay idle.phases(open)splits one timeline (island.animation.open_ms): pill fades out, the empty shape grows untilshape_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()); withcover_colorsthe card blends to the cover's colours as it opens. White-alpha overlays (appearance.overlays.*,CLEAR) stay unthemed. - Hover:
hovering(spot)eases aSpotoverappearance.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, andcollapse()/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'ssettings!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. Remainingconsts 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);statussaysopen=… page=… errors=… search=….hl.configvalue forms: numbers and bools plain, gradients{ colors = { "rgba(…)" }, angle = 45.0 }, vec2{ x, y }, css gaps a number or{ top, right, bottom, left }.hyprctl --batchof allj/getoptionprints 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: a hidden-at-start layer window stays blank forever ("has no child to draw"). Returnwindow.click_through().child(Rectangle::new().width(0.0).height(0.0)). Workspaces::focusdoes nothing (sendsdispatch workspace N). Usehl.dsp.focus({ workspace = N }).hyprctl eval/replshare one Lua state; globals (a rule kept in a variable) vanish on a config reload.- Layer-rule
match.namespaceis 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 emptyWorkspaceslist.hyprctl layerslists hidden layer windows too (alpha 0).- Reserved space:
Zone::Reservereserves 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 keepZone::Ignore. Check withhyprctl 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, same player and monitor: Amane's per-frame
gputime andtopCPU (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
Pathper look. - Streaming data needs
amane::request_frame()in the view while levels are live.
Develop
- 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]. Thencargo clippy(first build takes minutes). - 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. Its output carries "bash -c uid" noise: grep -v it.
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 # 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 sinks start # null sinks island_test_a/_b
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 nextuntilstatusnames it, before anything that changes sound. ipc island openopens on the focused monitor. Read geometry fromhyprctl layers -jright before eachgrimcapture. 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 withmovecursor; 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(statusends withprompt=…),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/whosemap::mapinjects 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 KILLon each stage); typical music sits at 0.15–0.35. - Check
uptimebefore trusting timings. Inpkill -fpatterns over SSH use[p]ython3 .... A fresh shell once died with "lost the gpu"; starting again worked.
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); 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 reviews the glass designs, ported onto master as settings; then merge the chosen one.
- The user adds
require("amane")tohyprland.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. - Upstream Amane fixes for the two Hyprland 0.56 issues (hidden layer at start,
Workspaces::focus): not started. island-eqis 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".