git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

WORKSPACE.md (30.1K)

  1 # The cce workspace
  2 
  3 Guidance for working anywhere in the `cce` Wayland desktop workspace — the layout,
  4 the multi-repo rule, the build/install entry point, config, and IPC.
  5 
  6 **This file lives here, not at the workspace root, because the root's repository
  7 is deliberately narrow** (see the multi-repo section below): it versions only
  8 what ties the crates together, and the guide to the crates belongs with the build
  9 tooling it documents. The root `CLAUDE.md` is a pointer to this file and repeats
 10 only the three rules that must not be acted against before reading it. Per-crate notes stay in each crate's own `CLAUDE.md`; compositor-specific
 11 detail is in the adjacent `CLAUDE.md`.
 12 
 13 **Paths below are relative to the workspace root** — the parent of this crate — so
 14 `cce-ui/src/` means `../cce-ui/src/` when read from here.
 15 
 16 ## What this is
 17 
 18 This is the **`cce` Cargo workspace** (`resolver = "2"`). It is a complete Wayland
 19 desktop environment written in Rust, split into two halves:
 20 
 21 - **`cce-compositor/`** — the compositor + tiling window manager (`cce-fx`, symlinked
 22   as `cce`), built on wlroots 0.20 via FFI with vendored scenefx. This crate is its own
 23   world: it has a `build.rs` native-build pipeline, a `Makefile`, and its own detailed
 24   **`cce-compositor/CLAUDE.md`** — read that before working inside `cce-compositor/`.
 25 - **~30 `cce-*` client apps and services** (`cce-status-interface`, `cce-system-interface`,
 26   `cce-designer`, `cce-files`, `cce-color-editor`, `cce-mail`, `cce-graph`, `cce-notifier`,
 27   `cce-authenticator`, `cce-display-manager`, `cce-text-editor`, `cce-data-editor`,
 28   `cce-fonts`, `cce-cloud`, `cce-browser`, `cce-notes`, `cce-model`, `cce-screenaver`,
 29   `cce-gallery`, `cce-terminal`, …; the root `Cargo.toml` lists them) — Wayland client GUIs that connect to the
 30   compositor and to each other over Unix sockets. (The desktop background is drawn
 31   natively by the compositor — the former `cce-wallpaper` client was retired.)
 32 
 33 The one thing tying every crate together is **`cce-ui`**, the shared GUI toolkit. Every
 34 client depends on it (a git pin that the root `[patch]` redirects to `../cce-ui`). Its
 35 GUI-free half is **`cce-core`** — config, input bindings, the animations switch, units, the
 36 socket IPC and the DE's spec parsers — which cce-ui re-exports at the old paths
 37 (`cce_ui::config`, …) and which a process that draws nothing depends on directly: the
 38 compositor (since 2026-10-07; it linked the whole toolkit for its config before), the
 39 browser's launch helper and cce-window-manager. The other shared crate is
 40 **`cce-window-manager`** — the compositor's
 41 pure-Rust window-management policy layer (arrange pass, `TilingMode`, saved state,
 42 slotmap; no FFI), extracted from `cce-compositor/` and consumed only by it. The compositor
 43 re-exports it as `crate::policy` / `crate::tiling` / `crate::slotmap`.
 44 
 45 ### Version control: this is a MULTI-repo, not a monorepo
 46 
 47 **Each member crate is its own independent git repository** with its own committed
 48 `Cargo.lock`. The workspace root is a git repository too, but a deliberately narrow
 49 one: it versions only `Cargo.toml` (the member list and the `[patch]` block),
 50 `Cargo.lock`, `.cargo/config.toml`, its `CLAUDE.md` and `bump-revs.sh`, and its
 51 `.gitignore` excludes every subdirectory by glob, so no crate can be swallowed as an
 52 embedded repo and adding a crate needs no change there. The crates sit
 53 side-by-side under this directory to form the build workspace, but are versioned and
 54 published separately.
 55 
 56 **Committing is not publishing — pushing is.** Every crate's `origin` is
 57 **GitHub** (`https://github.com/lsgalante/<crate>.git`), and it is the only
 58 remote: the local bare repos under `~/git/` and the `published` remote are gone
 59 (since 2026-09-20). A `post-commit` hook, symlinked into each repo by gitsite's
 60 `install-hooks.sh`, pushes the branch just committed, so in the normal case a
 61 commit is on GitHub seconds later — but a crate without the hook, or a push
 62 that failed, leaves the commit on no remote at all, and nothing warns.
 63 `git log origin/<branch>..` is the check.
 64 
 65 **git.lucas.co is a read-only mirror of GitHub**, not a publishing step.
 66 gitsite (`~/projects/gitsite`, run hourly by the `gitsite.timer` user unit via
 67 `autodeploy.sh`) `ls-remote`s every repo in its `repos.conf` — whose path
 68 field is now the GitHub URL — and when any HEAD moved, re-mirrors, renders and
 69 deploys the static site (browsable, clonable over dumb HTTP for `clone`-mode
 70 entries). So the chain is `git commit` → hook → GitHub → `gitsite.timer` →
 71 the site, with up to an hour of lag at the last step. New crates get a line in
 72 `repos.conf`. (The pre-2026-08-11 per-crate codeberg.org remotes are retired;
 73 those repos still exist server-side for old history.)
 74 
 75 The **Cargo git pins name GitHub too** (since 2026-09-21): every app declares
 76 `cce-ui` (and the compositor `cce-window-manager`) as
 77 `{ git = "https://github.com/lsgalante/<dep>.git", rev = "<sha>" }`, and the
 78 root `[patch]` block keys on that same URL to redirect it to the local crate.
 79 They pointed at git.lucas.co before, which meant a rev pinned right after a
 80 push was unfetchable for up to an hour of mirror lag. Keep the two URLs
 81 identical: a patch whose key does not match the pin is silently unused, and
 82 every crate then builds a fetched copy of the toolkit instead of the tree.
 83 `bump-revs.sh` at the root repins after a shared crate is pushed; it reads the
 84 rev from GitHub itself, and refuses while the dependency's work tree is dirty
 85 or ahead of origin.
 86 
 87 Consequences to respect:
 88 - **Commit crate changes inside the crate's own repo.** The root repo tracks only the
 89   workspace files above; its glob `.gitignore` is what keeps the crates out of it, so
 90   never force-add a crate's files there.
 91 - **Each crate must build standalone.** Do not introduce `[workspace.dependencies]` /
 92   `<dep>.workspace = true`: a standalone clone of a single crate's repo has no
 93   `[workspace]` parent, so inherited deps fail to resolve. Dependency versions are
 94   intentionally declared per-crate (minor drift between independent crates is fine).
 95 - A crate's `[profile.*]` is honored when it's built standalone (it is then its own
 96   workspace root) and ignored (with a warning) in the full-tree build — that warning is
 97   expected, not a bug to "fix" by deleting the profile.
 98 
 99 ## Build, test, run
100 
101 The workspace `target/` dir is shared at the repo root (`./target/`). There is no root
102 Makefile, but **`ccebuild` is the DE-wide entry point** — do not hand-roll a loop over
103 the crates. It ships in `cce-compositor/scripts/ccebuild` and installs to
104 `~/.local/bin`:
105 
106 ```sh
107 ccebuild install            # build the workspace, install every binary + unit
108 ccebuild install cce-mail  # just one package (what each crate's `make install` runs)
109 ccebuild restart            # restart user services left on a replaced binary
110 ccebuild status             # built-vs-installed drift, AND running-vs-installed
111 ccebuild prune              # target/ artifacts of crates cargo no longer knows
112 ccebuild install-system     # the root-owned binaries, units, PAM stacks, udev rules (one sudo prompt; --dry-run to preview)
113 ```
114 
115 The full deploy loop is `ccebuild install && ccebuild restart`. `ccebuild` derives
116 every binary from `cargo metadata`, which is the point: the per-crate Makefiles used
117 to name their binaries by hand, so crates with extra `[[bin]]` targets shipped
118 incomplete for weeks (`cce-ui` without `cce-relief`, `cce-display-manager` without its
119 three `cce-keyring-unlock*` helpers). Each crate's `make install` is now a thin
120 wrapper around `ccebuild install --no-build <pkg>`; `make build/run/clean` are
121 unchanged. **Never add a binary name to a Makefile** — cargo already knows it.
122 
123 ### Desktop entries
124 
125 A crate that should appear in the launcher — or be selectable as an XDG default —
126 ships **`<crate>/<name>.desktop` at its own root**, next to `Cargo.toml` and beside
127 any `*.service` it ships. `ccebuild install` copies those into
128 `$XDG_DATA_HOME/applications` and runs `update-desktop-database`, filtered by
129 package the same way units are.
130 
131 Two rules, both learned the hard way when these files lived only in
132 `~/.local/share/applications` and were hand-edited there:
133 
134 - **`Exec=` is a bare binary name**, never an absolute path. `~/.local/bin` is the
135   first entry on the session PATH, and the launcher (`cce-cloud`) spawns through
136   `sh -c`, so the name resolves. Nine of the ten imported entries had baked in
137   `/home/lsgalante/.local/bin/…`.
138 - **An app is only reachable as a default handler if it declares `MimeType=`.** The
139   settings app's Default Apps page builds each dropdown by scanning installed
140   entries for the ones claiming that category's MIME types, so an app with no
141   `MimeType=` line simply never appears as a candidate — which is why `cce-files`
142   could not be chosen as the file manager despite having an entry. Declaring a type
143   also means honoring it: the app has to accept the path or URL argv the field code
144   (`%f`/`%u`) passes it.
145 
146 ### App icons
147 
148 An entry's `Icon=` should be the app's own name (`Icon=cce-files`), backed by
149 `cce-icons/hicolor/scalable/apps/cce-files.svg`. `ccebuild install` mirrors any
150 crate's `hicolor/` tree into `$XDG_DATA_HOME/icons/hicolor/` and refreshes the GTK
151 icon cache; `cce-icons/hicolor/README.md` documents the naming and the symlink
152 convention that keeps `svg/` the sole source of the artwork.
153 
154 Before 2026-08-16 the entries borrowed generic freedesktop names
155 (`preferences-system`, `system-file-manager`), which resolved only if some other
156 installed theme happened to provide them, and `cce-preview` "worked" only because
157 five PNGs had been hand-copied into `~/.local/share/icons` — unversioned, and gone
158 on a fresh clone. The same failure as the `.desktop` files themselves.
159 
160 Note that **nothing displayed an `Icon=` key at all** until the launcher was taught
161 to: `cce-cloud`'s `AppInfo` had no icon field. `cce_ui::icon` is the shared
162 resolver (theme name or absolute path → file); it is distinct from
163 `cce_ui::upload_icon`, which loads a *bundled* cce-icons glyph for in-widget use.
164 
165 `ccebuild status` is the tool for "is what's running actually the code I built?".
166 Because `install` unlinks before writing, a process still on the old inode reports its
167 exe as `(deleted)`, which is how both `status` and `restart` detect drift. It also
168 catches apps launched straight out of `target/` rather than `~/.local/bin`.
169 
170 **But it cannot see a client that is stale against `cce-ui`.** The toolkit is a
171 static Rust library, so committing and installing `cce-ui` itself changes nothing
172 about the ~20 crates that link it — each has to be rebuilt and reinstalled before
173 it carries the change. `status` compares each binary's mtime in `target/release`
174 against the one in `~/.local/bin`, so a client nobody rebuilt has both old and
175 equal and reads as up to date. It is stale against a *dependency*, the one kind of
176 staleness that check has no notion of.
177 
178 Then a **running** process keeps its old inode until it is relaunched, and
179 `cce-fx` keeps its own until the next login. So a toolkit fix lands in three
180 stages — commit, rebuild dependents, relaunch — and it is the middle one that
181 gets skipped. Learned from cce-ui@2416904, which raised each client's
182 `RLIMIT_NOFILE`: the fix was committed and cce-ui installed, and every client
183 still ran at the old limit until its own crate was rebuilt, thirteen of them.
184 
185 Sweep with one cargo invocation over the dependents (`cargo build --release -p …
186 -p …` — one shape, since alternating with a bare `--workspace` build re-resolves
187 features and invalidates crates, as below), then `ccebuild install
188 --no-build <crate>` for each. Verify by looking *inside* the installed binary for
189 something the change introduced — `strings ~/.local/bin/<crate> | grep -q
190 '<new log string>'` — rather than trusting that the build ran. (`cce-browser` belongs in the sweep
191 too, but for a different reason than previously recorded here: since
192 2026-08-30 its **default build is WPE WebKit** against the system
193 `libWPEWebKit` — seconds, ~14 MB, no Servo compiled at all. The old
194 leave-it-out rule dated from Servo being the default engine — always the
195 crates.io package, never vendored — which cost more than the rest of the
196 workspace combined; that backend still exists behind `--no-default-features
197 --features servo` and is still that expensive, so only build it deliberately.
198 The default flip is itself a lesson for sweeps: while WPE was opt-in, a
199 featureless sweep rebuild silently reverted the installed browser to the
200 wrong engine. Defaults are what sweeps build; an opt-in variant of a binary
201 does not survive one.)
202 
203 **A hit proves freshness; a miss proves nothing.** Not every string literal in
204 the source survives into the binary, and the two cases are not distinguishable
205 from the outside. Measured against a current `cce-fx` on 2026-08-28: the live
206 `Action` names `mode_next_shared` and `toggle_overview` appear (twice and once),
207 while `overlay_right`, `brightness_down` and `focus_up` — same file, same kind
208 of literal, all reachable in `cce-window-manager/src/api.rs` — report zero.
209 Probably link-time constant merging; recorded as observed, not explained. So
210 prefer a long distinctive log string over a short match-arm literal, and never
211 read a zero as "the build didn't take" — that false negative has already cost a
212 session an afternoon of chasing a build that had worked.
213 
214 When the answer actually matters, test the behavior instead of a proxy for it:
215 put a deliberately bogus value where the real one goes (a made-up action name in
216 a shadow session's `input.kdl`) and watch for the code path that rejects it —
217 the compositor's "unknown window-manager action" warning firing for the bogus
218 name and staying quiet for yours proves the running binary knows yours.
219 
220 For plain cargo work:
221 
222 ```sh
223 cargo build --release                       # build every crate
224 cargo build -p cce-status-interface         # build one client
225 cargo run  -p cce-system-interface           # run one client
226 cargo test --workspace                       # all tests (tests are sparse)
227 cargo test -p cce-fx --lib config::          # tests in one module of one crate
228 ```
229 
230 Building the workspace compiles the **compositor** too, which triggers its `build.rs`
231 (meson/ninja to build vendored scenefx, wayland-scanner for protocols, bindgen over
232 wlroots). That needs native system deps — see `cce-compositor/CLAUDE.md` for the full list. If you
233 only touch a client, prefer `-p <crate>` to avoid rebuilding the compositor — but note
234 that alternating `cargo build --release` with `cargo build --release -p <crate>`
235 resolves different unified feature sets, so each invocation re-invalidates a few
236 crates (~11s). Pick one shape and stay with it.
237 
238 Binary names do not reliably match the crate: `cce-fx` lives in `cce-compositor/`,
239 `cce-system-interface` and `cce-files` declare explicit `[[bin]]` names, and several
240 crates ship extra bins (`cce-relief` → `cce-ramp`, `cce-compositor` →
241 `ccectl`). Ask cargo rather than guessing:
242 `cargo metadata --no-deps --format-version 1 | jq -r '.packages[].targets[] | select(.kind|index("bin")) | .name'`.
243 
244 ## The `cce-ui` toolkit (start here for any client work)
245 
246 `cce-ui` is a **custom retained-mode GUI toolkit**, not a wrapper around an existing
247 framework. Understanding it is the prerequisite for touching any client.
248 
249 - **Transport**: raw `wayland-client` 0.31 + `smithay-client-toolkit` 0.19, driven by a
250   `calloop` event loop. Clients are real Wayland surfaces, not toolkit windows.
251 - **Rendering**: raw Vulkan via **ash** (`cce-ui/src/vk/` — `VkRenderer`; the wgpu
252   path was retired), with **cosmic-text** for text shaping (depended on directly
253   since the wgpu retirement — it used to be reached through glyphon, whose only
254   other export was the wgpu renderer nothing here used). Widgets emit
255   vertex batches (quads, rounded rects, vectors, arcs, circles) — see the re-export
256   list in `cce-ui/src/engine.rs`. There is no HTML/DOM; the UI is drawn as GPU
257   primitives.
258 - **The `Application` trait** (`cce-ui/src/backend/app.rs`, re-exported from
259   `cce_ui::engine`) is the contract every client implements. Key methods:
260   `create(sender)`, `settings`, `update(msg)`, `tick(dt)`,
261   `display_list` (the single paint path) plus `overlay_quads` / `custom_vertices`,
262   and the input hooks (`handle_pointer_move`, `handle_mouse_input`, …). Apps needing
263   direct renderer access (3D scenes, app-shaped text, non-rect window chrome) use the
264   extended hooks `renderer_init` / `stage_renderer` / `standard_csd` /
265   `take_window_action` — `cce-designer` is the reference consumer. A client's
266   `main.rs` is typically a struct implementing `Application` plus a one-line
267   `cce_ui::engine::run::<MyApp>();`.
268 - **Modules**: `widget/` (containers, inputs, display, editors), `scene/` (the arena,
269   box-model layout, display list and paint walk), `backend/` (the `Application` contract,
270   the driver, frame building, the Wayland shell), `draw/` and `vk/` (what a renderer draws,
271   and the Vulkan renderer), `web/` and `mac/` (the browser and AppKit shells), `layout/`
272   (style getters — fonts + sizing, lots of `*_font_parsed()` — plus the legacy layout
273   engines), `color/`, `context.rs` (`UiContext`), `protocol.rs` (talking to the compositor),
274   `file_dialog.rs`, `scale.rs` (HiDPI), `mcp.rs` (tools-only MCP server over Streamable
275   HTTP so apps can expose their state/actions to AI agents — `cce-designer` is the
276   reference consumer, see its CLAUDE.md). `config`, `input`, `motion`, `units` (lengths
277   with units — `(mm)` config values — and the display metric from EDID) and `ipc` live in
278   `cce-core` and are re-exported at the same paths. cce-ui's own `CLAUDE.md` has the full
279   module map.
280 
281 When adding a widget or a client, mirror an existing client (e.g.
282 `cce-status-interface`) rather than inventing a new structure.
283 
284 ## Configuration (shared across the whole DE)
285 
286 Config is **KDL** (`kdl` crate), loaded from `~/.config/cce/` (honoring
287 `XDG_CONFIG_HOME`), via `cce-core/src/config.rs` (re-exported as `cce_ui::config`):
288 
289 - **`~/.config/cce/config.kdl`** — the shared/global config (`get_config_path()`).
290 - **`~/.config/cce/<app-name>/config.kdl`** — per-app override
291   (`get_app_config_path(app_name)`).
292 - **`~/.config/cce/input.kdl`** — DE-wide keybindings and pointer input settings,
293   domain-scoped (`cce-core/src/input.rs`, re-exported as `cce_ui::input`): top-level nodes are domains
294   (`cce-window-manager` for compositor actions, `cce-ui` for toolkit-wide widget
295   defaults, `cce-<app>` for per-app bindings), children are `name "chord"`
296   bindings. Resolution for an app is `<app>.<name>` → `cce-ui.<name>` (the
297   toolkit-wide `undo` / `redo` chords live here — `cce-ui/src/history.rs`); the
298   compositor maps its domain onto `cce-window-manager::api::Action` via the
299   policy crate's `bindings` module. Legacy keybind entries in `config.kdl` still
300   load; `input.kdl` wins on conflict. `ccectl migrate-input` extracts config.kdl
301   keybindings into input.kdl (with backup; config.kdl is never rewritten).
302   A top-level `input { }` block holds global pointer hardware defaults with
303   per-device-class sub-blocks (`mouse` / `trackpad` / `trackpoint`: accel,
304   scroll_factor…), consumed by the compositor; an `input { }` child inside an
305   app domain holds that app's scroll overrides, applied client-side by cce-ui
306   (pixel deltas scale as trackpad, discrete wheel clicks as mouse). Smooth
307   scrolling is tuned by the same keys on both sides — `smooth_scroll`,
308   `scroll_ease`, `kinetic_scroll`, `scroll_friction` — read by cce-ui from
309   the app/`cce-ui` domain (`cce-ui/src/widget/scroll_motion.rs`, the one
310   wheel→offset model every scrolling widget and app-owned list drives) and
311   by the compositor from the global block for its own desktop pans. Keybinding
312   and input edits are made directly on the file (e.g. via cce-data-editor) —
313   there is deliberately no dedicated settings UI.
314 - Config edits are backed up under `~/.config/cce/backups/config.kdl.<n>.bak`.
315 
316 The compositor additionally runs `~/.config/cce/init` on startup and persists window
317 state to `~/.local/state/cce/state.json` — details in `cce-compositor/CLAUDE.md`.
318 
319 ## How the pieces talk (IPC)
320 
321 Clients and compositor communicate over Unix sockets keyed by `$WAYLAND_DISPLAY`:
322 
323 - **Control**: `/tmp/cce-{WAYLAND_DISPLAY}.sock` — line-oriented request/reply. The
324   `ccectl` binary (in `cce-compositor/`) is the CLI client; run `ccectl` with no args for the
325   command list.
326 - **Status**: `/tmp/cce-status-{WAYLAND_DISPLAY}.sock` — subscribe to `layout` /
327   `title` / `modifiers` / `adjust` / `dismiss` / `selection`
328   and receive push updates. `adjust` is "on"/"off" as window-adjust mode
329   (overview, or Super held) comes and goes — what `cce-grid` keys its image
330   resize handles on. `selection` is the other grid-facing topic: the overview
331   drag-selection carrying the grid's images tells it where they went
332   (`move <id>:<x>:<y> ...`, then `drop`); the grid reports the images it holds
333   the other way, with `grid-items` on the control socket.
334   This feeds `cce-status-interface` (the status bar). (The `viewport` topic
335   went with the viewport-tag feature; the per-segment `backdrop` topic went
336   2026-10-01, when the bar's text contrast moved to compositor-side backdrop
337   compression.)
338 - **Per-app instance sockets**: `/tmp/<app>-{WAYLAND_DISPLAY}.sock`, the same
339   convention (`cce_ui::ipc::socket_path`). An app that runs once per session
340   uses **`cce_ui::ipc::instance`**: `forward_or_claim(prefix, line)` in
341   `main()` before any Wayland work (true = a running instance took it, exit),
342   `serve(handler)` once the loop's sender exists, `cleanup()` after `run`
343   returns. It owns the connect-before-bind race, stale-socket replacement and
344   bounded reads; the app owns only its line protocol. cce-browser, cce-notes
345   and cce-graph's vault mode use it. A listener of any other shape reads
346   requests with `cce_ui::ipc::read_request_line` (a total deadline and a size
347   cap), never a bare `read_line`: one silent client otherwise wedges the
348   listener for every client after it. Don't copy either into an app again.
349 
350 ## Window fades (DE-wide open/close dissolve)
351 
352 Every window and overlay the user opens dissolves in when it maps and out when
353 it closes. **The fade is the compositor's, in both directions** — it ramps the
354 opacity of the client's scene subtree
355 (`river_scene_node_set_opacity`, which also carries the scenefx backdrop blur,
356 drop shadow and bevel), so a whole window crossfades against the desktop rather
357 than each of its elements crossfading against each other.
358 
359 - **In** is automatic and needs nothing from the client: `Window::map` starts
360   the ramp for toplevels, `handle_layer_surface_map` for Overlay-layer
361   surfaces. Desktop furniture opts out — status segments, the wallpaper, the
362   grid layer (`Window::wants_map_fade`), and the Background/Bottom/Top layers —
363   because those map once at login, where a dissolve reads as the desktop
364   failing to draw.
365 - **Out** needs one thing from the client, because a surface that is already
366   destroyed cannot be faded: it sends `fade-out` on the control socket, is
367   answered with a duration in ms, and keeps its surface mapped and its process
368   alive for exactly that long before exiting. `cce_ui::ipc::request_close_fade()`
369   is that call, and `window_runner` already makes it for every `Application`, so
370   an ordinary cce client gets the close fade for free. An app driving its own
371   event loop calls it itself — `cce-cloud` is the worked example.
372 - The duration is `surface { fade in_ms=140 out_ms=120 }`, clamped to 2s. It is
373   answered back over the socket rather than duplicated in the client, so the
374   two halves cannot drift when the config changes. `0` disables that direction,
375   and a client that gets `0` exits immediately.
376 - The target is resolved from the caller's **pid** (SO_PEERCRED on the control
377   socket), not from a name in the command: the kernel vouches for it, and a
378   client always knows its own pid even when it has no app_id.
379 
380 **Do not fade a window from inside the client.** It cannot work: the surface
381 stays fully present to the compositor however transparent the client draws
382 itself, so the blur behind it hangs at full strength over a dissolving window —
383 and in cce-ui specifically, shader-lit output (SDF plate rims, specular) is not
384 vertex-alpha and does not fade with the geometry at all. `cce-cloud` carried
385 exactly that for months; its close fade dropped the plate batches to hide the
386 un-fading rims, which deleted the window's whole background on the fade's first
387 frame, since a plate batch **is** its cover quad.
388 
389 Note the usual toolkit-staleness trap (above): the fade-IN is entirely
390 compositor-side and appears the moment a new `cce-fx` is running, but the
391 fade-OUT rides `cce-ui`, so a client nobody rebuilt fades in and then vanishes.
392 That asymmetry is the symptom of a missed sweep, not of a broken fade.
393 
394 ## Animations switch (DE-wide, per power mode)
395 
396 `cce_ui::motion::enabled()` is the one question every easing in the DE asks
397 before it steps; when it answers no, the motion lands on its target in the
398 same frame. That means snap, not freeze: a dropdown still opens and a scroll
399 still moves. Its source is **`/run/cce/animations`** (`on`/`off`, missing
400 means on), written as root by `cce-power-apply` when the Power page's
401 **Animations** lever is part of the mode that is running. It lives under
402 /run, not `~/.config`, because the writer runs from udev with no session and
403 no `$HOME`. It also runs after every wake (`cce-power-apply-resume.service`,
404 which `ccebuild install-system` enables through its `X-CceEnable=yes` key),
405 because the kernel's resume event for a charger plugged in during sleep is
406 unreliable. `enabled()` re-reads it at most every 500 ms, so a plug or unplug
407 reaches running clients and the compositor without a reload. Set
408 `CCE_ANIMATIONS=0` (or `1`) to force it for one process, for testing.
409 
410 What follows the switch: in cce-ui, the dropdown open/close, the toggle
411 slide, the scrollbar raise/sink fade, the wheel glide and kinetic coast
412 (`scroll_settings()` reports both off), slider and ramp wheel inertia, and
413 the hover highlight. In the compositor, the open/close fades (window,
414 overlay layer, and the `fade-out` reply, which answers 0), the camera eases
415 (`advance_camera_animation`: overview ramp, focus pans, kinetic pan), the
416 border-reveal/adjust-dim fades, and the fullscreen-toggle resize. **A new
417 animation should ask `enabled()` too.** Like the close fade, the cce-ui half
418 reaches a client only once that client has been rebuilt against the toolkit.
419 
420 ## Idle timeouts per power mode
421 
422 The compositor's display-off and sleep countdowns come from `idle { }` in
423 config.kdl, and the Power page can override either per power mode with the
424 **Display Off After** and **Sleep After** levers (`idle_display_off_secs`,
425 `idle_sleep_secs` in `/etc/cce/power.kdl`). Same mechanism as the
426 animations switch: `cce-power-apply` writes **`/run/cce/idle_display_off`**
427 and **`/run/cce/idle_sleep`** as root (seconds on a line, 0 = never), and the
428 idle manager (`idle.rs`, `PLAN_DIR` + `PLAN_DISPLAY_OFF_FILE` /
429 `PLAN_SLEEP_FILE`) polls both once a second, so an unplug shortens the
430 countdown within a second and nothing reloads. `CCE_IDLE_PLAN_DIR=<dir>`
431 moves the directory for one compositor, for testing in a shadow, which must
432 not follow the live machine's files. A file that is absent means the config's value, and
433 `ccectl idle timeouts` edits that config base, which the plan keeps
434 overriding while its mode holds the lever; `ccectl idle status` reports the
435 values in force plus `plan_display_off=` / `plan_sleep=` (`none` or seconds).
436 The paths are spelled in both crates (the app cannot be a compositor
437 dependency), so a rename must land on both sides.
438 
439 ## Repo hygiene
440 
441 The repo root and `cce-compositor/scratch/` are littered with **ad-hoc debugging artifacts** — many
442 `screenshot_*.png`, `*.log` (some enormous, e.g. `debug.txt`, `dropbox_strace.log`),
443 and one-off `*.py` inspection scripts (`patch*.py`, `scan_*.py`, `inspect_*.py`). These
444 are **not part of the build**. Don't treat them as source, and don't add more to the
445 root; use the scratchpad directory for temporary files.
446 
447 ## Concurrent sessions (multiple agents in this workspace)
448 
449 Several Claude Code sessions may be working in sibling crates **at the same
450 time**. The workspace shares one `target/`, one `~/.local/bin`, and one live
451 compositor session between them, so an unscoped command in one session damages
452 the others. The rules:
453 
454 - **Scope builds and installs to your crate.** `cargo build --release -p
455   <crate>` and `ccebuild install --no-build <crate>` — never a bare
456   `ccebuild install`, which deploys *every* crate's most recent build,
457   including another session's half-finished work. A concurrent build blocking
458   on cargo's build-directory lock ("Blocking waiting for file lock") is
459   normal — wait it out; don't kill it or conclude the build is broken.
460 - **Restart with `ccebuild restart <crate>`, never the bare form.** Bare
461   `ccebuild restart` is unscoped: it restarts every user service running a
462   replaced binary, including apps another session has installed but is not
463   ready to restart. The per-crate form restarts only units shipped by the
464   named crate(s). Apps that are not services restart by pid, not name:
465   `pkill -x` matches the kernel comm name, which is truncated to 15
466   characters, so it silently matches NOTHING for most `cce-*` binary
467   names ("cce-status-interface" is 20) — a "kill then relaunch" built on
468   it relaunches beside the survivor and doubles the app. Find the pid
469   with `ps -eo pid,ppid,cmd`, confirm it is yours via
470   `/proc/<pid>/cgroup` (a unit's processes name their unit), `kill` it
471   explicitly, then relaunch detached.
472 - **Shared crates are exclusive.** Before editing `cce-ui`, `cce-core`,
473   `cce-window-manager`, or `cce-icons`, run `git status` there. Foreign dirt
474   means another session owns that crate right now — coordinate or stop; don't
475   edit around it. Commit your own crate's work promptly so other sessions
476   always see clean repos.
477 - **Verify in your own shadow session.** `cce-shadow start --new` gives each
478   agent a private headless compositor. Driving the *live* session (`ccectl`
479   pointer injection, screenshots, app restarts) is only safe when you know
480   you are the sole session doing so — two agents share one pointer and one
481   screen, and each contaminates the other's observations.
482 - **Coordinate through the harness.** `ListAgents` shows the other local
483   Claude sessions; `SendMessage` reaches them. Before touching a shared crate
484   that shows foreign dirt, ask the session that owns it instead of guessing.