Files
amane-config/AGENTS.md
T

163 lines
19 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
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|right` per monitor beside the island, moving out as it opens; a pill morphs into its card (one timeline, `pills.layout.open_ms`). One card open across the shell. `header`, `chip`, `button`, `rule` helpers for the cards. `pods` IPC (`open|toggle <pod> [monitor]`, `close`, `ask`, `processes`, `notices …`, `status`). |
| `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 `Shape`s; 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 `const`s 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.
```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 # 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
```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); 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".