Files
amane-config/AGENTS.md
T

19 KiB
Raw Blame History

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-config on the dev host; remote git@server1:fabolous005/amane-config.git.
  • Amane library: /root/amane (docs in docs/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.md and references/remote-visual-testing.md cover this project; scripts/ holds copies of tools/ (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/amane on 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 rule permit nopass fabian for rc-service, sh and cat; 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 master only 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|output call or volume change, check amane ipc call island status names 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 start refuses 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's ignore_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) and org.freedesktop.Notifications (Amane's own server; dunst is commented out in ~/.config/hypr/autocmds.lua, backup autocmds.lua.before-amane-notifications). Ask before serving anything else.
  • Never wake iwd by accident: any D-Bus call to net.connman.iwd starts it. Check NameHasOwner first.
  • Root only through doas -n with fixed command lines. Strings from other programs go to Command args, never into amane::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.toml holds every shell setting with its help; the config window edits them plus every Hyprland option. Saves are serialised by config::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 by surface::shell() on every surface. The user keeps all three and may want more. The old branches glass-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 amane and the Rust standard library: amane compile generates Cargo.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 compile saves ~/.cache/amane/amane-shell; amane run starts 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 LayerWindow per monitor (magic-island, Layer::Overlay), sized for the open card; an InputArea passes 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's settings! 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.*). Remaining consts 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); status says open=… page=… errors=… search=….
  • hl.config value 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. Return window.click_through().child(Rectangle::new().width(0.0).height(0.0)).
  • Workspaces::focus does nothing. Use hl.dsp.focus({ workspace = N }). Special workspaces: hl.exec_cmd(cmd, { workspace = "special:x silent" }), hl.dsp.workspace.toggle_special("x").
  • hyprctl eval / repl share one Lua state; globals vanish on a config reload.
  • Layer-rule match.namespace is a full match: ^magic-.*$.
  • $XDG_RUNTIME_DIR/hypr/ keeps folders of dead instances; the live one has .socket.sock.
  • hyprctl layers lists hidden layer windows too (alpha 0).
  • Reserved space: Zone::Reserve reserves the whole window height, only on a window anchored to one edge. Check with hyprctl 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 gpu time and top CPU.
  • 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 Path per look; many icons or graphs in one Canvas.
  • 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

  1. 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]. Then cargo clippy. Fetch the copy with ssh … 'tar cf - --exclude=target --exclude=.git -C ~/git amane'.
  2. 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 next until status names it, before anything that changes sound.
  • ipc island open and ipc pods open X act on the focused monitor (pods take a monitor name too). Read geometry from hyprctl layers -j right before each grim capture, 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 with movecursor; 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 …, then ipc pods notices list.
  • Measure gaps and alignment from pixels (ffmpeg … crop=… -f rawvideo), not by asking a vision model.
  • Check uptime before trusting timings. In pkill -f patterns 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 (~/.face doesn'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") to hyprland.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-eq is 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".