# Feature spec — dock favicons (app icons on preview cards)
> STATUS: **SPEC ONLY — not scheduled, no code yet.** Written so it can be picked
> up cold. Vocabulary is canonical: **favicon** = "the application icon shown on a
> preview, resolved from the toplevel's `app_id` via the XDG icon theme"
> (GLOSSARY:67). No new terms needed.
## Goal
Each minimized window's dock preview card shows its application icon (favicon),
resolved from the toplevel `app_id` through the freedesktop XDG icon theme. Today
a card shows preview + title; the b2 list-binding design already reserves a
per-row `favicon` string field — it is just unwired.
## Architecture (mechanism/policy split, per the constitution)
Three disjoint pieces across two units:
1. **Pure lookup core — ext-stage-dock (or a shared pure core).** Input
`(app_id, size, scale, theme, [inherited…])` → ordered candidate file paths,
per the freedesktop Icon Theme Specification. Pure input→output, doctest-hard,
ZERO I/O: the parsed theme index + a "does this file exist" predicate are
INJECTED, so it tests against a fake filesystem. Real `index.theme` parsing and
`stat`s live in a thin edge adapter.
2. **Decode + texture load — KERNEL / ui substrate (the GATING work).** The
substrate's RmlUi render interface must turn an icon FILE PATH into a sampled
GL texture. Today it only registers the custom `unbox-preview://N` texture; it
(VERIFY) does not implement RmlUi `LoadTexture` for on-disk images. Add a
decoder feeding RmlUi's texture loader so an `
` referencing an icon file
resolves. Needs PNG **and** SVG (see formats below).
3. **Dock wiring — ext-stage-dock.** Per slot: resolve `app_id` → favicon path
(piece 1) → bind the favicon `
` via the existing b2
`bind_list_string("slots","favicon", …)`. Show a generic fallback icon on miss.
## Dependencies to approve (AGENTS.md: no new deps without sign-off)
- **Themes (data, NOT a build dep):** `hicolor-icon-theme` + `adwaita-icon-theme`
are already installed and sufficient. Optional for coverage: `papirus-icon-theme`
(extra). No build wiring.
- **Decoders (the real new deps — vendored Meson subprojects):**
- **stb_image** — PNG (single header, trivial). App icons in hicolor are
commonly PNG (`foot.png`, `firefox.png` present).
- **lunasvg** — SVG. NON-OPTIONAL: modern themes are SVG-first (adwaita ships
**715 .svg vs 51 .png**; papirus/breeze all SVG), and the generic fallbacks
are SVG. lunasvg is light, vendorable, and is RmlUi's own blessed SVG plugin
backend. **Reject `librsvg`** (heavy GNOME/Rust dep).
## New contract surface (sketch — finalize when built)
- **Kernel `ui.hpp`:** prefer the minimal route — once `LoadTexture` decodes
`file://` image paths, `
` just
works with NO new public API. Only add a typed
`UiSubstrate::create_icon(path,size) -> source_uri` (mirroring `create_preview`)
if texture caching/lifetime forces kernel ownership. Decide at build time.
- **ext-stage-dock:** no public-header change — favicon is internal policy; the
b2 `favicon` list field already exists in the contract.
## Lookup core detail (freedesktop Icon Theme Spec, minimal)
- Resolve order: requested theme's size-matched dirs → inherited themes → `hicolor`
→ `/usr/share/pixmaps/.{png,svg,xpm}` → none.
- `app_id` normalization rules (pure): as-is; lowercased; reverse-DNS reduced to
last component (`org.foo.Bar` → `bar`); known aliases. Try in order.
- Size selection: exact size dir, else `scalable`, else nearest. Prefer full-color
over `-symbolic` unless only symbolic exists.
## Image-format reality (measured on this box)
- hicolor app icons: mostly PNG, some SVG. adwaita/papirus/breeze: SVG-first.
- ⇒ both decoders required; SVG is the one that can't be skipped.
## Caching / lifetime
Favicons are few and immutable per app (unlike refreshable per-window previews).
Decode on first use, cache by `(path,size)` for the session. Owner (substrate vs
dock) decided when built — leans substrate if route (a) above is taken.
## Test plan
- **Lookup core (doctest, fake fs, no I/O):** exact hit; reverse-DNS last-segment;
size 48 vs scalable; theme→inherited→hicolor fallback; pixmaps fallback; miss→none.
- **Substrate decode (headless+gles2):** load a known PNG and a known SVG → sampled
texture; position-aware known-color pixel readback (mirror the a1 preview color
test); decode failure degrades to none, never throws.
- **Dock glue:** resolvable `app_id` binds a non-empty favicon src; unresolvable
binds the fallback.
## Implementation order (when scheduled — two briefs, kernel FIRST)
1. **KERNEL:** vendor lunasvg + stb_image (Meson wraps), wire RmlUi `LoadTexture`
to decode PNG/SVG, cache, headless decode test, `ui.hpp` doc. (Gating.)
2. **ext-stage-dock:** pure lookup core + edge adapter; resolve per slot; bind the
favicon `
`; fallback icon; tests. Config (theme name + size) via
`unbox.toml` — coordinate with the ext-keybindings config pattern or a
`[dock]` block.
## Open questions
- Theme selection: hardcoded default vs `unbox.toml [dock] icon_theme = "…"`
(lean: config with a sane default — adwaita/hicolor).
- Does RmlUi 6.2 `
` accept `file://` once `LoadTexture` is wired, or do we
need a custom `unbox-icon://` scheme like the preview? Verify against the
vendored source.
- SVG rasterization size: render at the card's favicon box (dp==px at 1.0 ratio);
re-decode on the (rare) size change.