# tasks.md — live status
> The orchestrator updates this after EVERY milestone. Keep it terse:
> slice status + the single next action. History lives in git.
## Now
**Just landed — screenshots + wallpaper (user-driven, real-seat/nested VERIFIED):**
- **Screenshots (grim):** kernel creates `wlr_screencopy_manager_v1` +
`wlr_xdg_output_manager_v1` (policy-free plumbing, like data-device). grim
captures the standard `wlr_scene_output_commit` composite (RML docs are
scene-buffer nodes → captured). Verified nested: `grim` → valid 1280×720 PNG.
- **Arbitrary image decode:** vendored `stb_image` (user-approved); substrate
`LoadTexture` now decodes PNG/JPEG/… (was TGA-only), and a SubstrateSystemInterface
`JoinPath` override stops RmlUi stripping the leading `/` of an absolute path, so
`decorator: image('/abs')` AND `
` both load. Kernel ui_pixel tests.
- **`ext-wallpaper` (NEW, standard tier):** `[wallpaper]` in unbox.toml
(path/fit/color), shown in the `background` layer, hot-reloaded (drop+recreate
inline doc); bundled default `assets/ext-wallpaper/default.jpg` when no path set.
Needed a new kernel **`UiSurfaceSpec::input_transparent`** flag (a full-screen
background surface must not steal clicks — substrate press-ownership is per-rect,
pre-bus). Verified nested: configured image + bundled default both render; input
passes through. swaybg (layer-shell) still works too. GAP: multi-output (primary
only). Commits on feat/rml-compositing.
**ACTIVE (core, user-driven) — Slice 13: RML COMPOSITING (Phase 0 GO → Phase 2 impl).** Big direction
change: RMLUi becomes the content compositor — toplevels + layer-shell (incl.
wallpaper) + chrome are RML elements backed by LIVE, SHARED GL textures, with
layout/animation/3D effects in RCSS; wlroots stays foundation + cursor plane +
(deferred) fullscreen scanout bypass. Lost wlr_scene damage/scanout is mitigated
by OUR dirty-gated rendering (NOT a RMLUi built-in) + a deferred scanout bypass.
GATED BY A SPIKE before commit. Full spec + acceptance criteria:
`notes/rml-compositing.md`; decision row in `notes/plan.md` §2.
SPIKE RESULT: **PHASE 0 CLOSED — GO, real-seat CONFIRMED.** All 7 criteria
`ALL PASS` headless on Haswell+crocus (CF-AX3 GPU class); on the real seat:
input accurate through the 3D transform (after the `Element::Project()` routing
fix), and ~30fps under the 4-window `--demo` load. Stage-0 instrumentation
(per-phase split + GPU timer) shows it is **fill-bound** (~10–15ms whole-output
composite, ~2ms CPU) → damage limiting is the recovery lever, built properly in
Phase 1 (not the throwaway). Surface trees = **per-subsurface elements** (RTT
hook); present = FBO→dmabuf swapchain→wlr_scene_buffer + EGL fence. Throwaway
target `packages/kernel/rml-compositing-spike` (`--verify`/`--run`/`--demo`),
out of the shipped binary. **CONTRACT DECISION (user): RCSS is the single source
of truth for ALL layout + animation; C++ drives the document via a TYPED
substrate API.** PHASE 2 on `feat/rml-compositing` (off main; spike sources carried
as in-tree reference, `build_by_default:false`, deleted when the waves land).
**Waves 1 + 1b DONE + verified** (kernel): `SurfaceElement` (live sibling of
`Preview`) — zero-copy seq-gated import, frame-callback duty, dirty-gate, public
`create_surface_element(wlr_surface*)`; **surface trees** (subsurface/popup child
elements, whole-tree frame-done, parent-relative placement); **input-back**
(pointer/touch → surface-local via `Element::Project`, pure inversion core
doctested) + `SurfaceElement::focus_keyboard()` primitive. Kernel suite (72c/375a)
+ asan green; test-only `wayland-client`/xdg-shell-client codegen accepted
(kernel-tests scope). Wave plan: **W1/W1b** done; **W2** = ext-xdg-shell (ADD
`Toplevel::wl_surface()`; keep scene compositing for now — retire behind the flag
in W3) + ext-layer-shell (expose its surface); **W3** = NEW `ext-window-field`
(window list + RCSS layout + focus policy via `focus_keyboard()`, flips the flag);
**W4** = ext-stage-dock; **W5** = damage limiting (Option B) + scanout bypass.
**Waves 1b/2/3 + click-to-focus DONE + verified + committed.** Core RML
compositing is FUNCTIONAL behind `--rml-compositing` / `UNBOX_RML_COMPOSITING`:
- W2 ext-xdg-shell: `Toplevel::wl_surface()` (additive; scene compositing intact).
- Kernel: fixed the W1 test-seam listener-lifetime bug (root cause of the
`ext-stage-dock-glue`/`ext-xdg-shell-client` teardown aborts — both now GREEN).
- W3 NEW `ext-window-field` (core, `--rml-compositing`-gated): toplevels become
RCSS surface elements in ONE window-field ui surface; `bind_list("wins")` +
RCSS flex layout (`assets/ext-window-field/field.{rml,rcss}`); `Toplevel::hide()`
takes them out of wlr_scene; focus via `on_toplevel_focused`.
- Click/tap-to-focus: kernel `SurfaceElement::on_pressed` + window-field wires it
to `Toplevel::focus()`.
All unit suites + build-asan green; no regressions.
**POST-W3 polish + W3.5 (user-driven, real-seat CONFIRMED on the CF-AX3):**
- **Resize-to-tile** (real-seat verified, "resolution is great"): kernel
`SurfaceElement::rendered_width()/height()` (reads back the RCSS-resolved
box — substrate already computes it for popup placement), ext-xdg-shell
`Toplevel::set_size()`, and an ext-window-field frame-pumped feedback loop that
configures each client to its on-screen box so the live texture maps 1:1.
Policy is config-driven (`unbox.toml [window-field] resize_mode` =
off|settle|continuous|debounced, hot-reloaded; pure doctested core).
- **ROOT-CAUSE FIX:** the field now shows each window via `
`,
NOT a `data-style-decorator`. RmlUi DOES bind `src` (data-attr); the decorator
had silently disabled everything the substrate keys off the `src` attribute —
pointer/touch input-back, click-to-focus, popup/subsurface placement, AND size
readback. (The dock keeps its decorator: frozen previews need none of that.)
- **W3.5 — FLOATING WINDOWS** (this wave, inserted before the dock per user): the
field is now a floating desktop — move (titlebar drag), resize (two bottom
corner grips), and a close button per window. New kernel primitive
`UiSurface::bind_list_drag` (per-row drag, the list analogue of bind_drag).
ext-window-field: per-window geometry STATE (x,y,w,h,z) bound + applied as RCSS
data-style (C++ owns interactive state, RCSS renders — contract-clean); pure
doctested geometry core (`src/geometry.cpp`: move / resize_bl / resize_br with
anchored-opposite-edge + min-size, field clamp); z-order raise on focus; cascade
placement. The earlier focused-dominates TILING is dropped — tiled / sidebar
containers return LATER as containers windows migrate between (captured-state
animations), per the user's staged plan. All suites + build-asan green.
- **SERVER-SIDE DECORATIONS** (CSD fix for floating windows): ext-xdg-shell now
advertises the xdg-decoration manager and forces SERVER_SIDE when RML
compositing is on (host-bin passes the flag to create()), so clients drop their
own titlebars and only the field's RML chrome shows. Classic path keeps CSD.
Kernel wrapper exposes wlr_xdg_decoration_v1. (Caveat: GTK/libadwaita ignore
SSD and always draw CSD — a client limitation.)
NEXT ACTIONS (need USER): (1) **real-seat check of floating windows** —
`UNBOX_RML_COMPOSITING=1 UNBOX_ASSET_DIR=/assets ./build/packages/host-bin/unbox`
on the CF-AX3: drag titlebars to move, drag bottom grips to resize, tap × to
close. (2) **Wave 4 (now W5) decision** — how minimize-to-dock coordinates with
the floating window-field. (3) **Wave 6 decision** — damage limiting has real
unknowns (RmlUi exposes no per-element screen damage), may need a spike.
DEFERRED: Wave 3b (layer-shell wallpaper as surface element — wallpaper/panels
already work via wlr_scene background/overlay bands); spike-target deletion +
final doc reconcile (after W5).
Tiling (slice 7) is DEFERRED behind this (becomes RCSS over surface elements;
pure layout core in `notes/tiling-spec.md` carries over). Stage dock (slice 10)
real-seat feel check is paused under this pivot.
**DEV WORKFLOW (RML/RCSS hot-reload — use this):** UI documents are external assets
under `assets//` (e.g. `assets/ext-stage-dock/dock.rml` + `dock.rcss`), loaded
via `UiSurfaceSpec::rml_path`. Launch unbox with
`UNBOX_ASSET_DIR=/assets UNBOX_DEV=1` (reads the source tree + arms an inotify
watcher). Then editing a .rml/.rcss and saving HOT-RELOADS the live surface — NO
recompile, NO restart (bindings/geometry preserved; a broken file keeps the old doc).
Real-seat verified. Installed builds find assets via `-DUNBOX_ASSET_DIR_DEFAULT`.
(9c0c0bf kernel, f852141 dock+build)
**CONFIG HOT-RELOAD (always-on, user feature):** editing `~/.config/unbox/unbox.toml`
re-applies keybindings LIVE — no restart. Backed by a general kernel primitive
`Host::watch_file(path, cb) -> FileWatch` (RAII, coalesced, editor-save/create-safe,
error-isolated; ONE session inotify also backs the UI hot-reload above). A
malformed/mid-edit file keeps the current bindings + logs a warning (keys never
drop). Real-seat verified. (3c1bde9 kernel watch_file, 9f7dc09 ext-keybindings)
**Just landed — usability slice (user-driven, real-seat verified on the CF-AX3):**
`ext-keybindings` (new core ext) reads keybindings from `unbox.toml`: tap-Super →
spawn fuzzel, Alt+Tab / Alt+Shift+Tab → stable focus rotation over all toplevels,
Alt+F1, Ctrl+Alt+Backspace → quit. ext-xdg-shell's hardcoded keybinds migrated
out. Kernel now exports `WAYLAND_DISPLAY` so extension-spawned clients reach unbox
(was the fuzzel "no monitors" root cause). build + build-asan both green.
Follow-up: Ctrl+Alt+F1..F12 VT switching is now kernel-hardwired (the session
escape hatch).
**Active — Slice 10: stage dock** (user-driven; supersedes slice 6 as the next
UI work). The Stage-Manager-style left-edge dock of minimized-window **previews**,
revealed by a left-edge **swipe**. **Fork B** (see plan.md §2): previews are
toplevel snapshots imported as textures into the RMLUi context, shown as `
`
in one RML document. Waves (a number per wave runs in parallel = disjoint units):
**Landed a1–d1 (committed; code-complete, all green build + build-asan 10/10 suites;
real-seat feel pending):**
- a1 kernel SPIKE — Fork-B GO on crocus: `Preview` + `create_preview(wlr_scene_tree*)`,
wlr pixels → dmabuf → EGLImage → sampled RMLUi texture → `
`. (7fed564)
- b1 ext-xdg-shell — `Toplevel::hide()/show()` (≠ unmap) + `geometry()` + `scene_tree()`. (bdce81a)
- b2 kernel — UiSurface list bindings (`bind_list`/`bind_list_string`/`bind_list_event`). (74c8071)
- b4 ext-stage-dock (new unit) — skeleton + pure cores (reveal recognizer, dock layout). (d6535e8)
- c2 ext-stage-dock + host-bin — Super+M minimize → preview slot → hide; tap → restore. (3376100)
- d1 ext-stage-dock — RCSS dock slide-in + per-slot settle; restore instant. (b578327)
- fix ext-stage-dock — dock previews were blank: `data-attr-src` (RmlUi binds
attrs, not `{{}}`) + font `Noto Sans` + valid `transform-origin`. REAL-SEAT
VERIFIED: minimizing 2 foot windows shows 2 live preview snapshots in the dock;
Super+M repeats with >1 window. (5ebd45a)
- TRANSPARENCY + usability pass (REAL-SEAT VERIFIED): kernel — ui surfaces now
composite per-pixel alpha (stray opaque `Clear()` removed) AND `set_size` resizes
the render target (was logical-only; the slice-5 change-request) so a surface can
grow. (f1e12a3). ext-stage-dock — strip background transparent (windows show
through; cards keep their panel), surface hugs the card stack (no full-height
input capture), and re-minimize-after-empty fixed (stale `focused_`: restore now
sets it directly since a non-defocused window's `focus()` is a seat no-op). (661166a)
- CARD = ROUNDED THUMBNAIL (REAL-SEAT VERIFIED): the card IS the window preview,
rounded on all four corners — a full-bleed `image(... cover center)` decorator on
a child of a rounded `overflow:hidden` slot (RmlUi won't clip an element's OWN
decorator to its OWN radius → decorator lives on the clipped child). First use of
the substrate's RmlUi clipping path (scissor + stencil clip-mask); kernel verified
it correct + added 4 regression tests (6519ebf). Title overlay parked
(`display:none`, binding kept) for a later text redesign — user's call. (a743f44)
**NEXT (needs user):**
1. REAL-SEAT feel check (covers c2+d1): `~/start-unbox.sh -s foot`, Super+M minimizes
foot → its preview card slides into the 240px left dock; tap the card → foot
restores; minimizing the last window slides the dock out.
2. BOUNDARY DECISION — the full cross-screen "window flies into the dock" flight (and
e1's drag-out grow-back) needs exactly ONE new kernel primitive: an
**input-transparent UiSurface flag** on `UiSurfaceSpec` (d1 proved animation-end is
already observable via `bind_event`; no timer/frame-tick needed). Approve it →
then c1 gesture-claim + e1 (edge reveal + drag-out), the config-driven
minimize-keybind migration (ext-keybindings action + a stage-dock Service), and
favicon (needs an XDG icon-theme dep) follow.
Slice 6 re-scoped: the **window-list taskbar is CUT** (overlaps the stage dock,
conflicts with the touch/iPad direction; its contract-exercise purpose was met by
the stage dock). Replaced by two future, not-yet-designed features sequenced
AFTER slice 7 (tiling): **status bar** (slice 11) and **home screen** (slice 12)
— ideas + open questions captured in `notes/status-bar-home-screen.md`. Launching
is covered by fuzzel today (and the home screen later).
Still queued whenever UI work resumes: keyboard-into-ui-surfaces, removing the
deprecated no-op `Options::ui_spike`, retiring host-bin's demo ui.
## Slices
| # | Slice | Status | Acceptance |
|---|---|---|---|
| 0 | Harness skeleton | **DONE** 2026-06-12 | All harness md files in place |
| 1 | Bootstrap: toolchain, Meson skeleton, RMLUi subproject compiles, empty kernel links wlroots-0.20 from C++ via the extern-"C" wrapper | **DONE** 2026-06-12 | met: build green; tests 1/1; binary prints wlroots 0.20.1 + RmlUi 6.2, exits 0 |
| 2 | tinywl port: kernel skeleton runs nested under labwc | **DONE** 2026-06-12 | met: nested output WL-1, foot toplevel mapped+focused, GLES2 renderer; touch handlers added (tinywl lacks them); headless boot test green |
| 3 | **THE SPIKE:** RMLUi→scene bridge | **DONE — GO** 2026-06-12 | met: Plan A (dmabuf FBO→wlr_buffer→wlr_scene_buffer) verified nested+headless on HD 4400; Plan B fallback verified; orientation fixed + position-aware guard; input proof on-screen; RSS ≈83 MiB; ASan/UBSan clean in our code (known noise: Mesa leak reports + 2 benign UBSan downcasts inside vendored RMLUi). glFinish→fence and format negotiation deferred to the real substrate (slice 4+) |
| 4 | Extension host + contracts: bus, manifests, static registration; xdg-shell/layer-shell refactored OUT of kernel into core extensions | **DONE** 2026-06-12 | met: kernel boots featureless (names no feature); typed Event/Filter bus error-isolated + topo activation; ext-xdg-shell (toplevels, focus, grabs via pure GrabMachine, button/axis routing, Ctrl+Alt+Backspace quit) + ext-layer-shell (fuzzel verified, pure arrangement core) pass suites; typed surface→scene-tree registry replaced the data-field convention; first protocol codegen (wlr-layer-shell XML vendored); user hands-on: all input paths verified incl. touch; 68 cases green + ASan clean; idle RSS ≈73 MiB |
| 5 | Input routing + ergonomics contract: unified pointer/touch→RMLUi events, keybinding filter chain, touch-mode RCSS variables | **DONE** 2026-06-13 | met (user hands-on): real ui substrate (`Host::ui()` → UiSurface, scalar+event bindings, dmabuf+fence+swapchain); same demo surface driven by mouse AND finger; consume-or-pass with implicit-grab ownership (press owner gets release, per touch point too); touch-mode = state+notification only, NO visual scaling (user decision); touch-initiated grabs incl. pointer/touch alternation (seat release-leak fixed); keybinding chain satisfied by slice-4 Filter (ext-keybindings deferred); 113 doctest cases green, ASan clean, idle RSS ≈78 MiB |
| 5b | Usability: `ext-keybindings` (config-driven `unbox.toml`) — Super→fuzzel, Alt+Tab focus rotation; ext-xdg-shell keybinds migrated; kernel exports `WAYLAND_DISPLAY` for spawned clients | **DONE** 2026-06-13 | met (real-seat, user-confirmed): fuzzel opens on Super, Alt+Tab cycles all windows, quit works; build + build-asan both green (3rd-party Mesa/RmlUi sanitizer noise suppressed; a real libwayland leak in the layer-shell client test fixed) |
| 6 | ~~ext-taskbar~~ + ext-launcher | **taskbar CUT / re-scoped** | window-list taskbar dropped (overlaps the stage dock + conflicts with the touch/iPad direction). Replaced by slices 11–12. Launching is covered by fuzzel today and the home screen later. The contract-exercise purpose was met by the stage dock. |
| 7 | ext-window-tiling: pure layout core + thin scene glue | **DEFERRED pending slice 13** (baseline designed) | layout math 100% doctest-covered, zero wlroots types in core. Baseline = `primary` (right) + `stack` (left), auto-tile, 1=full/2=50-50/3+=stack-left; see `notes/tiling-spec.md` (+ `notes/tiling-layouts-reference.md`). Held until RML compositing (slice 13) lands — tiling then becomes RCSS layout over surface elements; the pure core is renderer-agnostic and carries over. (`Toplevel::set_box` prototype reverted; recreate from `prompts/ext-xdg-shell.md` when tiling resumes.) |
| 8 | ext-osk: RML keyboard ui surface injecting via wlr_seat | pending | type into foot via touch only; auto-show on text-input focus |
| 9 | Session hardening: s6 user service, TTY launch on seat0, layout persistence (append-only state + pure reconcile on boot) | pending | survives `kill -9` + s6 restart with workspaces restored |
| 10 | **Stage dock** (ext-stage-dock): minimized-window previews on a left-edge swipe (Fork B) | **a1–d1 landed; previews real-seat-verified** | DONE: Super+M minimize→RMLUi-imported preview snapshot→dock slot→hide (previews confirmed rendering on hardware); RCSS dock slide-in + slot settle. NEXT: confirm tap-to-restore + animation feel; 1 boundary call (input-transparent UiSurface flag) → c1 gesture-claim → e1 gesture reveal/drag-out; then config-driven minimize keybind + favicon (XDG icon dep) |
| 11 | **Status bar** (tent. ext-statusbar): iPad/iOS top bar — clock (left), configurable left/middle/right sections, tray (right) wifi/volume/battery | **IDEA — needs design** | sequenced AFTER slice 7 (tiling); replaces cut taskbar. Details + open questions: `notes/status-bar-home-screen.md` |
| 12 | **Home screen** (tent. ext-home, iPad springboard): app grid; tap = launch-or-raise (instance picker if >1 open); add/remove apps; swipe-up-from-bottom to enter | **IDEA — needs design** | sequenced AFTER slice 7 (tiling); replaces cut taskbar. Details + open questions: `notes/status-bar-home-screen.md` |
| 13 | **RML compositing** — RMLUi becomes the content compositor (toplevels + layer-shell incl. wallpaper + chrome = RML elements backed by LIVE, SHARED GL textures; layout/animation/3D effects in RCSS). wlroots = foundation + cursor plane + (deferred) fullscreen scanout bypass. | **Phase 0 spike CLOSED — GO, real-seat CONFIRMED; Phase 1 design DONE (`notes/rml-compositing-phase1.md`); Phase 2 impl NEXT** | All 7 criteria `ALL PASS` headless on Haswell+crocus: (1) zero-copy live dmabuf texture (cached when unchanged); (2) RCSS perspective+rotateY on live pixels (readback); (3) screen→surface-local inversion through the transform = 0.000000px; (4) surface tree composited → **per-subsurface elements** (RTT hook for tree-spanning effects); (5) wallpaper via identical import path; (6) idle dirty-gate = 0 idle renders / 1-per-commit (frame-time @load = real-seat); (7) FBO→dmabuf→wlr_scene_buffer + EGL fence. Spike target `rml-compositing-spike` (`--verify`/`--run`). Report + runbook: `reports/rml-compositing-spike.md`. |
## Deferred decisions (decide when reached — see notes/plan.md §7)
dlopen extensions · remote builds on a fast box · xwayland default ·
OSK virtual-keyboard protocol vs direct seat injection · workspace model ·
clang-format style details