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.