19 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) plus an eGPU (RX 6600). 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 shell uses: OpenRC services
iwd,bluetooth,openvpn.<name>,ollama(stopped by default;gemma4:e4b, listens on 0.0.0.0:11434, runs on the eGPU); doas rulepermit nopass fabianforrc-service,shandcat; gnome-keyring as the Secret Service (secret-tool);wl-copy,ip,lspci,ghostty,btm. iwd and bluetoothd are often stopped; the shell then greys their tiles.
Ground rules from the user
- Branch per big change, ask before merging. 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: their session starts one at login. 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. The user may be at the desktop while you test: if workspaces change or cards close by themselves, stop the on-screen tests.
- 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). - Blur strength is Hyprland's only (
decoration.blur.*; the user keeps size 1 and the layer rule'signore_alpha = 0.5). The shell never sets it. - D-Bus mostly for reading status. Actions go through the CLIs:
iwctl,bluetoothctl(a D-Bus agent version was reverted,afe963d). The shell serves only what the user chose:org.kde.StatusNotifierWatcher(tray) andorg.freedesktop.Notifications(Amane's own server; dunst is commented out in~/.config/hypr/autocmds.lua, backupautocmds.lua.before-amane-notifications). Ask before serving anything else. - Never wake iwd by accident: any D-Bus call to
net.connman.iwdstarts it. CheckNameHasOwnerfirst. - Root only through
doas -nwith fixed command lines. Strings from other programs go toCommandargs, never intoamane::spawn/output(sh -c). Secrets go on stdin:wl-copy,secret-tool store, curl's-K -for the calendar. Never put a password in a file, an argument or a log. - Local AI only: the AI card talks to Ollama on the desktop, never a cloud service. Phone stats are out for now.
- Edits to AGENTS.md prompt for approval (
security.protected_instruction_files); 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), an idle pill without players, one island per monitor (live bars only on the focused one), a workspace pill per monitor, wallpaper colours through matugen, and:
- Control centre (top right): status pill opening a panel with tiles (Wi-Fi, Bluetooth, VPN, Hotspot, Ethernet), volume, brightness, keyboard light, performance mode, eGPU programs, Settings (gear). The panel morphs into its menus as one shape: Wi-Fi and Bluetooth maps, VPN list, sound outputs, Ethernet details. Popups (
magic-prompt) ask for passwords, PINs and passkeys. - System tray right of the workspace pill.
- Bar hiding only on monitors showing a fullscreen window; the top edge brings it back. Peeks: the workspace pill on a workspace change; the island on a song change or new player, but not when the focused window's class is the playing player (
parse::same_app). - Workspaces: a removed empty workspace's slot shrinks and fades during the slide; a created one (
createworkspacev2) grows in. - Reserved space: windows that aren't fullscreen stay below the bar (44 px).
- Settings:
~/.config/amane/config.tomlholds every shell setting with its help; the config window edits them plus every Hyprland option. Saves are serialised byconfig::WRITTEN, and the file watch skips the shell's own saves (an earlier race reset settings to defaults). - Glass designs:
appearance.surface.design= solid / frosted / liquid, drawn bysurface::shell()on every surface. The user keeps all three and may want more. The old branchesglass-base,design-frosted,design-liquid(also on origin) are superseded; deleting them was asked, no answer.
Branch side-pills (pushed, not merged; the user tries it first): two one-icon pills left of the island (AI, System) and two right (Home, Notifications); see Architecture and README "Side pills".
On the desktop: the agent-started shell (pid in ~/.cache/amane-remote-test/shell.pid, stderr in shell.err) runs the side-pills build; the user's login shell was stopped with their approval. config.toml has pills.ai.model = gemma4:e4b, design frosted at 0.7. ~/.config/hypr/hyprland.lua did not require("amane") (checked 2026-10-15), so Hyprland options set in the window are lost on a reload; the user was told.
Theme pipeline: Skwd-wall v2's matugen integration renders ~/.config/amane/matugen/amane.colors into ~/.config/amane/theme.
Constraints of Amane itself
- Only
amaneand the Rust standard library:amane compilegeneratesCargo.toml, so no crates. External tools run as processes (Command,amane::output); HTTP is curl. - 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 [choices]; "help" } } }: a typed accessor plus an OPTIONS entry for the window. load()/save() of config.toml, set/reset; [hyprland.*] tables hold Hyprland overrides. |
settings.rs |
view, normal Window |
The config window: a page per part and per Hyprland category, search, a row per setting (switch, slider + field, text with swatch, chips), reset per row. Esc closes. config IPC. |
hypr.rs Hypr |
Service, worker | Hyprland's options from hyprctl descriptions -j and one --batch of j/getoption. set/reset apply with hl.config, rewrite ~/.config/hypr/amane.lua and config.toml. |
surface.rs |
widget function | shell(w, h, radius, colors, lift, content): the chosen glass design for every surface. |
island.rs Island |
input-only Service | The island: open: Animation, player selection, menus, scrub, hover spots, width_on(monitor) (for the pods), island IPC. expand() closes a pod card. |
pods.rs Pods |
view + input-only Service | Side pills: windows `magic-pods-left |
ai.rs Ai |
Service | Ollama /api/chat via curl, streamed line by line (parse::Chunk); starts ollama via doas when it doesn't answer. Stop and New chat bump round, so a late line from an old answer is dropped. |
stats.rs Stats |
Service, thread | CPU (/proc/stat), GPU clock (xe/i915 sysfs), memory, network, disk sectors of physical disks, hottest sensor per hwmon. All graphs in one canvas. Processes runs ghostty --class=amane.monitor -e btm via hl.exec_cmd on special:monitor, then toggles it. |
home.rs Clock, Forecast, Agenda |
Services, threads | Avatar (~/.face, else a glyph), clock, open-meteo weather (Munich by default), month grid, CalDAV (iCloud by default): discovery, REPORT with expand, parse::ics_events. Sign-in stores an app-specific password with secret-tool (service amane-calendar account <id>). |
notices.rs Notices |
Service, poll 250 ms | On top of Amane's Notifications server: popups (magic-notifications, top right of the focused monitor, timeouts by urgency, paused on hover), pictures found once per notification (tray::icon_file), unread count, the history card (cross = dismiss, Clear all, Mute = notifications.popups.enabled). A popup click without a default action only hides it; the server's click would delete it. The server itself never expires notifications. |
sound.rs Sound |
Service, poll | pactl sinks and sink-inputs → each player's streams, volume, mute, output, equalizer. Curves in ~/.config/amane/island-eq. |
spectrum.rs Spectrum |
Service | pw-record --target <stream> | cava → levels while the shown player plays; kills stale cava with -KILL. |
art.rs Art |
Service, poll | Cover → palette-cover for cover_colors. |
theme.rs Theme |
Service, watch_file |
10 colours from ~/.config/amane/theme faded on one Animation; also watches config.toml. Views start with theme::begin(), draw with theme::now(); theme::touch() redraws all. |
workspaces.rs |
view + Hover |
Workspace pill per monitor; slots by x in one Stack under one sliding disc; ghost and growing slots. |
parse.rs |
plain functions | All parsing, unit-tested: pactl/cava/proc, EQ store, colours, iwctl/bluetoothctl, hyprctl JSON, TOML (Value), JSON, Hyprland values, same_app, Ollama chunks, /proc stats, weather, WebDAV XML, iCalendar, civil dates, notification body markup. |
control.rs, map.rs, prompt.rs, system.rs |
views, Services | Control centre and its menus, the maps (one canvas each), the popups, the network/power/GPU services. |
glyph.rs |
shapes | Every icon as Shapes; draw many into ONE canvas (glyph_shapes), as each Canvas has a large fixed GPU cost. |
edge.rs Edge |
Service, Hyprland sockets | socket2 events, no polling: bar hiding, magic-edge, peeks, workspace slide (switched, created), hide_h(), magic-reserve. |
tray.rs, png.rs |
Services | StatusNotifierWatcher and host, icons from themes or pixmaps, dbusmenu menus. |
The island view
- One
LayerWindowper monitor (magic-island,Layer::Overlay), sized for the open card; anInputAreapasses clicks outside through. phases(open)splits one timeline: pill fades out, the empty shape grows, then the card fades in. At most one face per frame;veiled()fades with a backdrop-coloured veil.- Hover: 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, and
collapse()/set_menu()drop stale highlights.
Settings
- Add a setting in
config.rs'ssettings!block with a range and a one-line help; use the accessor in code. Derived values are functions over settings. Text sizes are roles (appearance.text.*). Remainingconsts are names and identities only. - The font and cava's bar count apply on restart; everything else on save.
amane ipc call config get|set|reset <path> [value](hyprland.<option.path>for Hyprland);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 }.
Hyprland 0.56 (Lua config) pitfalls
- Never return
visible(false)from a layer view: a hidden-at-start layer window stays blank forever. Returnwindow.click_through().child(Rectangle::new().width(0.0).height(0.0)). Workspaces::focusdoes nothing. Usehl.dsp.focus({ workspace = N }). Special workspaces:hl.exec_cmd(cmd, { workspace = "special:x silent" }),hl.dsp.workspace.toggle_special("x").hyprctl eval/replshare one Lua state; globals vanish on a config reload.- Layer-rule
match.namespaceis a full match:^magic-.*$. $XDG_RUNTIME_DIR/hypr/keeps folders of dead instances; the live one has.socket.sock.hyprctl layerslists hidden layer windows too (alpha 0).- Reserved space:
Zone::Reservereserves the whole window height, only on a window anchored to one edge. Check withhyprctl monitors(reserved: 0 44 0 0). - A peek only shows on the focused monitor. A monitor whose workspace has a fullscreen window hides the bar, pods included (an open pod card stays).
Performance rules
- Measure before and after every visual change, same player and monitor: Amane's per-frame
gputime andtopCPU. - 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: size it for the whole animation.
.clip()and.opacity(<1)each add a GPU layer; a shadow is two. Halos only while hovered.- One backdrop gradient is affordable, many aren't.
- Draw repeated marks as one
Pathper look; many icons or graphs in oneCanvas. - Streaming data needs
amane::request_frame()while it's live. Slow work (icon lookups, curl, sysfs walks) belongs on Service threads, never in a view.
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. Fetch the copy withssh … 'tar cf - --exclude=target --exclude=.git -C ~/git amane'. - Unit tests:
rustc --edition 2024 --test src/parse.rs -o /tmp/parse_test && /tmp/parse_test(21 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 # start a shell 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
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 openandipc pods open Xact on the focused monitor (pods take a monitor name too). Read geometry fromhyprctl layers -jright before eachgrimcapture, on a monitor without a fullscreen window.- Animations: capture a strip in a loop (
grim -g "3840,0 300x60") while dispatching the change, then tile the frames with ffmpeg. - Keys:
wtype -k Escape. Hover can't be faked withmovecursor; real motion moves the user's mouse, so ask first. - Without a mouse:
ipc control open|close|menu …|status,ipc tray list|menu|open N|…,ipc pods …. Notifications:notify-send -a App -i icon -u critical -A key=Label …, thenipc pods notices list. - Measure gaps and alignment from pixels (
ffmpeg … crop=… -f rawvideo), not by asking a vision model. - Check
uptimebefore trusting timings. Inpkill -fpatterns over SSH use[p]ython3 ....
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 2> shell.err" > /dev/null 2>&1'
Open threads
- Side pills, untested by the agent: the calendar (the user signs in later; CalDAV discovery and the iCloud answer have never run), clicks and typing in the cards (only IPC was used), the AI answer in the card (the API answered in 0.8 s; the card capture came after the user closed it), the popups' new right alignment, Processes (ghostty + btm), the avatar (
~/.facedoesn't exist yet). - During the notification test all three test notifications left the history within 6 s and the Firefox one's Open action fired, apparently from the user's clicks at the desk; not confirmed.
- The user adds
require("amane")tohyprland.lua(or asks the agent to). - The config window was only driven over IPC and Esc.
- The candle ring is still mirrored (
mirrored()); the pill's bars aren't. - Upstream Amane fixes for the two Hyprland 0.56 issues: not started.
island-eqis tracked but rewritten live; untracking it was proposed, no answer. Leftovers offered for deletion, no answer:src.before-test/and old screenshots on the desktop, merged and superseded local branches.- On a monitor narrower than about 1500 px the maps run under the island.
- The open/close stutter rise (25–40 slow frames per 3 opens and closes) is unexplained.
- See README "Known limits".