AGENTS.md: side pills, glass designs on master, notification daemon, Ollama; condensed
This commit is contained in:
@@ -7,47 +7,47 @@ Handoff for agents continuing work on the Magic Island shell (Hermes, Claude Cod
|
|||||||
- **This repo:** `/root/amane-config` on the dev host; remote `git@server1:fabolous005/amane-config.git`.
|
- **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).
|
- **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).
|
- **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). 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:** `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 control centre uses:** OpenRC services `iwd` (Wi-Fi), `bluetooth` (bluez), `openvpn.<name>` (VPN); doas rule `permit nopass fabian` for `rc-service`, `sh` and `cat`; `wl-copy`, `ip`, `lspci`. iwd and bluetoothd are often stopped; the shell then greys their tiles.
|
- **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
|
## Ground rules from the user
|
||||||
|
|
||||||
- **Branch per big change, ask before merging.** New branch for each big change; 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.**
|
- **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.
|
- **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:** they sometimes run it themselves. Don't kill it without asking; `tools/amane-remote.sh start` refuses when it finds a shell it didn't start.
|
- **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.
|
- **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").
|
- **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.
|
- **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.
|
- **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 shell never sets it. A shell blur setting was added once and reverted at the user's request.
|
- **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 only for reading status.** Actions go through the CLIs: `iwctl` to join, scan and disconnect, `bluetoothctl` to pair and connect (the user had a D-Bus agent version reverted, `afe963d`). The one exception, chosen by the user: the shell serves `org.kde.StatusNotifierWatcher` and calls tray apps' dbusmenus. Ask before serving anything else on the bus.
|
- **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 (it then joins Wi-Fi and adds a route). Check `org.freedesktop.DBus.NameHasOwner` first; only talk to it while it has an owner.
|
- **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** (`rc-service <svc> start|stop`, `cat /var/lib/iwd/<profile>.psk`). Strings from other programs (SSIDs, device names, D-Bus metadata) go to `Command` args, never into `amane::spawn`/`output` (`sh -c`). Passwords go to `wl-copy` on stdin, never as an argument; iwctl's `--passphrase` is visible in `ps` for about a second, which the user accepted.
|
- **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.
|
||||||
- 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.
|
- **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.
|
- Report honestly what was and wasn't checked on the real desktop.
|
||||||
|
|
||||||
## Current state
|
## 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:
|
**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): 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.
|
- **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.
|
- **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).
|
- **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`).
|
||||||
- **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.
|
- **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).
|
- **Reserved space:** windows that aren't fullscreen stay below the bar (44 px).
|
||||||
- **Settings** (branch `config-window`, merged 2026-10-15): `~/.config/amane/config.toml` holds 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.
|
- **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.
|
||||||
|
|
||||||
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.
|
**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".
|
||||||
|
|
||||||
**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 (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.
|
||||||
|
|
||||||
**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 renders `~/.config/amane/matugen/amane.colors` into `~/.config/amane/theme`.
|
||||||
|
|
||||||
**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
|
## Constraints of Amane itself
|
||||||
|
|
||||||
- Only `amane` and the Rust standard library: `amane compile` generates `Cargo.toml`, so no crates. External tools run as processes (`std::process::Command`, `amane::output`).
|
- 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 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 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.
|
- `amane compile` saves `~/.cache/amane/amane-shell`; `amane run` starts it.
|
||||||
@@ -56,68 +56,64 @@ On 2026-10-13 the user tried the mouse-only parts (tray clicks, top-edge reveal,
|
|||||||
|
|
||||||
| Piece | Kind | Job |
|
| 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. |
|
| `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 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`). |
|
| `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 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. |
|
| `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`. |
|
||||||
| `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. |
|
| `surface.rs` | widget function | `shell(w, h, radius, colors, lift, content)`: the chosen glass design for every surface. |
|
||||||
| `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`. |
|
| `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. |
|
||||||
| `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 -KILL`s cava left by an earlier shell (cava ignores SIGTERM). |
|
| `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`). |
|
||||||
| `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`. |
|
| `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. |
|
||||||
| `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. |
|
| `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. |
|
||||||
| `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. |
|
| `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>`). |
|
||||||
| `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. |
|
| `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. |
|
||||||
| `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. |
|
| `sound.rs` `Sound` | Service, poll | `pactl` sinks and sink-inputs → each player's streams, volume, mute, output, equalizer. Curves in `~/.config/amane/island-eq`. |
|
||||||
| `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.*`. |
|
| `spectrum.rs` `Spectrum` | Service | `pw-record --target <stream> \| cava` → levels while the shown player plays; kills stale cava with `-KILL`. |
|
||||||
| `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. |
|
| `art.rs` `Art` | Service, poll | Cover → `palette-cover` for `cover_colors`. |
|
||||||
| `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). |
|
| `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. |
|
||||||
| `glyph.rs` | shapes | Every icon as `Shape`s; maps draw all of theirs into ONE canvas (`glyph_shapes`), as each `Canvas` has a large fixed GPU cost. |
|
| `workspaces.rs` | view + `Hover` | Workspace pill per monitor; slots by x in one `Stack` under one sliding disc; ghost and growing slots. |
|
||||||
| `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). |
|
| `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. |
|
||||||
| `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. |
|
| `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. |
|
||||||
| `png.rs` | plain functions | Stored-deflate PNG writer for pixmap tray icons. |
|
| `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 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 island view
|
||||||
|
|
||||||
### The view (island.rs `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.
|
||||||
- One `LayerWindow` per monitor (`magic-island`, `Layer::Overlay` because the user's windows are fullscreen), sized for the open card; an `InputArea` lets clicks outside through. No player: `idle()`, a click-through pill.
|
- 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.
|
||||||
- `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 read `Spectrum` (reading subscribes), so the rest stay idle.
|
|
||||||
- `phases(open)` splits one timeline (`island.animation.open_ms`): pill fades out, the empty shape grows until `shape_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()`); with `cover_colors` the card blends to the cover's colours as it opens. White-alpha overlays (`appearance.overlays.*`, `CLEAR`) stay unthemed.
|
|
||||||
- Hover: `hovering(spot)` eases a `Spot` over `appearance.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, and `collapse()`/`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
|
## Settings
|
||||||
|
|
||||||
- **Add a setting** by declaring it in `config.rs`'s `settings!` 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. Remaining `const`s are names and identities only (D-Bus names, `CLEAR`, `TAU`).
|
- **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.
|
- 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`); `status` says `open=… page=… errors=… search=…`.
|
- `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 }`. `hyprctl --batch` of all `j/getoption` prints plain "invalid type (internal error)" lines between objects for a few options.
|
- `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
|
## 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"). Return `window.click_through().child(Rectangle::new().width(0.0).height(0.0))`.
|
- **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** (sends `dispatch workspace N`). Use `hl.dsp.focus({ workspace = N })`.
|
- **`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 (a rule kept in a variable) vanish on a config reload.
|
- `hyprctl eval` / `repl` share one Lua state; globals vanish on a config reload.
|
||||||
- Layer-rule `match.namespace` is a full match: `^magic-.*$`. Layer rules can only switch blur on; strength is global.
|
- Layer-rule `match.namespace` is a full match: `^magic-.*$`.
|
||||||
- `$XDG_RUNTIME_DIR/hypr/` keeps folders of dead instances; the live one has `.socket.sock`. A shell started with a dead signature gets an empty `Workspaces` list.
|
- `$XDG_RUNTIME_DIR/hypr/` keeps folders of dead instances; the live one has `.socket.sock`.
|
||||||
- `hyprctl layers` lists hidden layer windows too (alpha 0).
|
- `hyprctl layers` lists hidden layer windows too (alpha 0).
|
||||||
- **Reserved space:** `Zone::Reserve` reserves 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 keep `Zone::Ignore`. Check with `hyprctl monitors` (`reserved: 0 44 0 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; test with a workspace hop there, then restore the focus.
|
- 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).
|
||||||
- `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
|
## Performance rules
|
||||||
|
|
||||||
- **Measure before and after every visual change**, baseline first, same player and monitor: Amane's per-frame `gpu` time and `top` CPU (the iGPU busy counter reads 100% even with the shell paused).
|
- **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: give it the largest size for the animation (the workspace ghost does).
|
- 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 clip over the whole card once took the iGPU from 47 to 9 fps); a shadow is two. Halos only while hovered.
|
- `.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. A veil over a gradient must be the same gradient at the same size.
|
- One backdrop gradient is affordable, many aren't.
|
||||||
- Draw repeated marks (candles, bars) as one `Path` per look.
|
- Draw repeated marks as one `Path` per look; many icons or graphs in one `Canvas`.
|
||||||
- Streaming data needs `amane::request_frame()` in the view while levels are live.
|
- 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
|
## 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` (first build takes minutes).
|
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` (12 tests).
|
2. Unit tests: `rustc --edition 2024 --test src/parse.rs -o /tmp/parse_test && /tmp/parse_test` (21 tests).
|
||||||
|
|
||||||
## Testing on the desktop
|
## Testing on the desktop
|
||||||
|
|
||||||
@@ -127,23 +123,21 @@ The equalizer: one `setsid -f pipewire -c eq-<key>.conf` per player with `libpip
|
|||||||
H=fabian@192.168.2.169
|
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 deploy src # back up the remote src once, then replace it
|
||||||
tools/amane-remote.sh $H build # amane compile
|
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 start # start a shell WITH frame logging
|
||||||
tools/amane-remote.sh $H fps 3 # frames, average gap, gpu wait
|
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 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 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 players start 4 # fake MPRIS players
|
||||||
tools/amane-remote.sh $H sh 'COMMAND' # anything inside the graphical session's environment
|
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.
|
- Pick a fake player with `ipc island next` until `status` names it, before anything that changes sound.
|
||||||
- `ipc island open` opens on the **focused** monitor. Read geometry from `hyprctl layers -j` right before each `grim` capture. With one window per monitor the frame log mixes windows.
|
- `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"` takes ~20 ms) while dispatching the change, then tile the frames with ffmpeg (`tile=2x12`) and look at one sheet.
|
- **Animations:** capture a strip in a loop (`grim -g "3840,0 300x60"`) while dispatching the change, then tile the frames with ffmpeg.
|
||||||
- Keys to a focused window: `wtype -k Escape`. Hover can't be faked with `movecursor`; real motion moves the user's mouse, so ask first.
|
- Keys: `wtype -k Escape`. Hover can't be faked with `movecursor`; 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` (`status` ends with `prompt=…`), `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.
|
- 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`.
|
||||||
- Maps with many nodes: deploy a scratch `src/` whose `map::map` injects fake nodes (never commit it), then redeploy the real one.
|
- Measure gaps and alignment from pixels (`ffmpeg … crop=… -f rawvideo`), not by asking a vision model.
|
||||||
- Measure gaps and alignment from pixels (`ffmpeg … crop=1920:1:0:Y -f rawvideo`), not by asking a vision model.
|
- Check `uptime` before trusting timings. In `pkill -f` patterns over SSH use `[p]ython3 ...`.
|
||||||
- To tune a visualiser, sample real levels read-only first (`pw-record --target <stream> | cava`, `timeout -s KILL` on each stage); typical music sits at 0.15–0.35.
|
|
||||||
- Check `uptime` before trusting timings. In `pkill -f` patterns over SSH use `[p]ython3 ...`. A fresh shell once died with "lost the gpu"; starting again worked.
|
|
||||||
|
|
||||||
### After testing
|
### After testing
|
||||||
|
|
||||||
@@ -151,17 +145,18 @@ tools/amane-remote.sh $H sh 'COMMAND' # anything inside the graphical sessi
|
|||||||
tools/amane-remote.sh $H players stop
|
tools/amane-remote.sh $H players stop
|
||||||
tools/amane-remote.sh $H sinks 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
|
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'
|
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
|
## Open threads
|
||||||
|
|
||||||
- The user reviews the glass designs, ported onto master as settings; then merge the chosen one.
|
- **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 user adds `require("amane")` to `hyprland.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 config window was only driven over IPC and Esc.
|
||||||
- The candle ring is still mirrored (`mirrored()`); the pill's bars aren't.
|
- 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.
|
- 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 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.
|
- `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 stop at 600 px and run under the island.
|
- 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 on every build) is unexplained.
|
- The open/close stutter rise (25–40 slow frames per 3 opens and closes) is unexplained.
|
||||||
- See README "Known limits".
|
- See README "Known limits".
|
||||||
|
|||||||
Reference in New Issue
Block a user