status bar
git clone https://git.lucas.co/cce-status-interface.git
CLAUDE.md (25.8K)
1 # CLAUDE.md
2
3 This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
5 ## What this is
6
7 `cce-status-interface` is the status bar of the `cce` Wayland desktop environment. It
8 is one crate in the multi-repo `cce` workspace — see `../cce-compositor/WORKSPACE.md` for
9 the workspace layout, the multi-repo git rules (commit here, never `git init` at the root),
10 and the `cce-ui` toolkit this app is built on. This crate is deliberately small:
11 `src/main.rs` (the `StatusApp` application, layout/input, launcher daemon),
12 `src/modules.rs` (the `StatusModule` trait and its ten implementations),
13 `src/config.rs` (pointer-first config readers), `src/tray.rs` (SNI host),
14 `src/cloud.rs` (menu page building), `src/stats.rs` (system stat readers —
15 numbers, not strings; the modules do the formatting), `src/icons.rs` (tinted
16 cce-icons glyph textures), `src/listeners.rs` (status/switcher socket tasks).
17
18 ## Build, test, run
19
20 ```sh
21 cargo build --release # standalone build (or `-p cce-status-interface` from the workspace root)
22 cargo test # 48 tests: main.rs (contrast, parsers), config.rs, tray.rs
23 make install # release build, then `ccebuild install --no-build cce-status-interface`
24 ```
25
26 Running it requires a live cce compositor session (`$WAYLAND_DISPLAY` plus the cce
27 sockets); there is no meaningful headless mode.
28
29 ## Process model (the most important thing to know)
30
31 One binary, three modes, selected by CLI args in `main()`:
32
33 - **No args — launcher daemon.** Spawns one child process per module
34 (`--module window`, `--module clock`, …), polls every 500ms and restarts crashed
35 children with exponential backoff (500ms doubling to 30s; 30s of healthy uptime
36 resets it). This is the normal production mode: each module is its own process and
37 its own Wayland surface.
38 - **`--module <name>`** — a single-module bar segment. Valid names: `window`, `tray`,
39 `stats`, `cpu`, `memory`, `brightness`, `volume`, `battery`, `clock`, `light_source`
40 (the daemon launches `stats`, not the five it combines).
41 - **`--trigger-switcher`** — one-shot: writes `trigger` to the switcher socket of the
42 running instance and exits (used as a keybinding target).
43
44 (The old `--monolithic` all-modules-in-one-window mode is gone, along with the
45 app-side super+drag module reordering that only made sense there.)
46
47 The compositor places each segment by its Wayland `app_id`, computed in
48 `StatusApp::get_app_id()`: `cce-status-{side}-{name}` (e.g. `cce-status-left-window`). If
49 `/tmp/cce-status-interface-{WAYLAND_DISPLAY}.sock` exists, the `cce-status-interface-`
50 prefix is used instead — keep both spellings in mind when matching app_ids. A module's
51 side comes from the config (`get_module_side`, which also maps snap positions like
52 `top-left`/`bottom-right` to left/right); default is `window` → left, everything else →
53 right. **`light_source` is the exception**: it short-circuits ahead of all of
54 that and takes its side from `/window_manager/light_source_position` — the
55 angle points at a side — so a `layout { status_bar light_source=… }` entry is
56 read and then ignored, which looks like the key not working.
57
58 ## Rendering
59
60 The app implements `cce_ui::engine::Application` on the **`display_list()` paint path**
61 (Phase 6ak) — the legacy `view*()`/`text_items()` methods are gone. The flow:
62
63 1. `rebuild_layout()` runs the two-pass module layout — for each module first
64 `StatusModule::width()`, then `StatusModule::render()` — filling retained buffers on
65 `StatusApp`: `rects`, `rounded_boxes`, `text_prims`
66 (the `TextPrim` tuple type; build them with `draw_label()` from a
67 `cce_ui::widget::StyledLabel`), `icon_prims` (`IconPrim` — a tinted
68 cce-icons glyph texture at a logical rect), plus `input_regions`,
69 `module_bounds`, `tray_item_bounds`.
70 2. `display_list()` replays those buffers into a `PaintCtx` each frame (and triggers
71 `rebuild_layout()` when size/scale changed or `needs_rebuild` is set).
72 `overlay_quads()` remains a separate on-top pass (used for drag feedback).
73
74 **Module boxes hug their content.** `StatusModule::width()` is the STABLE slot
75 width — widest-plausible templates for the stat modules, 24px title buckets for
76 the window module — and it alone sizes the surface, which is what keeps the
77 compositor's configure-echo jitter out of the loop; `content_width()` (default:
78 `width()`) measures the live text, and the drawn bubble takes that width,
79 centered in the slot, so the padding on each side of the text is
80 `module { padding }` rather than padding-plus-template-surplus. The drawn width
81 is eased over ~120ms in `tick` (`bubble_w_now`/`bubble_w_target` — one pair of
82 fields, sound because a `StatusApp` hosts exactly one module), and the
83 in-surface menu expansion grows out of `collapsed_box` — the bubble actually
84 drawn — not out of the slot, so the box-grows-into-the-menu continuity holds.
85
86 Orientation is dynamic: `is_vertical()` compares the surface size against the
87 configured bar thickness; every module renders along one axis using `bar_h`/`coord`
88 accordingly.
89
90 **The stat modules read out as a glyph with the number beside it, not a
91 label.** `cpu`, `memory`, `brightness`, `volume` and `battery` are
92 `IconStat` implementations: each names a cce-icons glyph, the bare number
93 and a color, and `IconReadout` draws the glyph (tinted that color, at
94 `module { icon_alpha }`) with the number `module { icon_gap }` to its right
95 — no unit symbol, since the glyph IS the unit ("87" beside the battery, not
96 "Bat 87%"). **The launcher runs them as ONE segment**, `stats`
97 (`StatsModule`): every readout in a single bubble, `module { icon_spacing }`
98 apart, in the order cpu, memory, brightness, volume, battery — the order the
99 compositor's `RIGHT_ORDER` gave the five separate segments, and `stats` has
100 its own slot there between `tray` and `clock` (cce-window-manager
101 2026-09-16). The five single names stay valid `--module` values for a bar
102 that wants them apart; a blanket `impl<T: IconStat> StatusModule for T`
103 lays a lone readout out through the same `readouts_width` /
104 `render_readouts` the combined segment uses. (Superimposing the number on a
105 ghosted glyph, with a bold weight and a dark pocket under the digits, was
106 tried first on 2026-09-16 and replaced the same day by the side-by-side
107 form; `icon_weight` survives as an opt-in, the pocket is gone.) The muted
108 sink swaps to `volume-muted`, the charging battery to `battery-charging`;
109 the battery also keeps its accent color while charging or under 10%. A
110 reader with nothing (no battery, no backlight, no pactl) returns `None`
111 and drops out of the row — a lone module with nothing has width 0, i.e. it
112 is hidden rather than an empty bubble; a reader that answers without a
113 number (cpu with no /proc/stat, a sink with no level) draws the glyph
114 alone.
115
116 The glyphs come from the **cce-icons** crate via `cce_ui::icons_dir()`
117 (`$CCE_ICONS_DIR`, else `~/projects/cce/cce-icons/svg`) — but NOT through
118 `cce_ui::upload_icon`: a `Prim::Image` has alpha and no color, and the
119 artwork is white, so `icons.rs::tinted_icon` rasterizes the SVG itself
120 (`cce_ui::rasterize_svg`), multiplies it by the readout's raw-sRGB color and
121 uploads it, cached per `(name, px, color)` — for the life of the RENDERER,
122 not the process: the cache holds renderer image ids, and a reconnect
123 (cce-ui repairs a lost transport by opening a new session around the same
124 `Application`) rebuilds the renderer and its image table, leaving every
125 cached id naming nothing. A draw for an unknown id is skipped rather than
126 reported, so a reconnected bar came back with its numbers and no glyphs at
127 all; `renderer_init` now calls `icons::drop_textures()` on every renderer
128 after the first, and the rebuild it forces re-uploads them. A
129 glyph that fails to load falls back to the old text readout ("Cpu 45%"), so
130 a bar started without the icon set is still attributable; **a shadow session
131 needs `CCE_ICONS_DIR` exported into the spawn**, its HOME being elsewhere,
132 exactly as it needs `CCE_FONTS_DIR`. Slot stability holds as before: the
133 stable width sizes every number at the "100" template, so a value crossing
134 a digit boundary never resizes the surface, and the bubble eases to the
135 live row. `memory` reads as a percentage of the total in use (used = total
136 less free, buffers and page cache) since 2026-09-16 — the "Mem 10/62G"
137 gigabyte form went with the label.
138
139 ## Events and IPC
140
141 `update()` consumes `CustomEvent`s sent over a calloop channel from tokio tasks spawned
142 in `new()` — which tasks run depends on the selected module, so a clock process doesn't
143 listen to tray D-Bus, etc.:
144
145 - **Compositor status feed** (`spawn_status_listener`): connects to
146 `/tmp/cce-status[-interface]-{WAYLAND_DISPLAY}.sock` and subscribes, one task
147 per topic, line-oriented — `layout` and `title` only in the process that owns
148 the window module, `dismiss` and `backdrop` in every one. Reconnects back off
149 1s doubling to 30s, reset the moment a connection delivers a line: a
150 compositor that does not know a topic drops the subscription on sight, so a
151 flat retry made a bar running ahead of its compositor reconnect once a second
152 from every module process, forever. The compositor also offers `modifiers`
153 (`status_server.rs`), but nothing here subscribes to it and the match over
154 topics ends in `unreachable!()` — adding a subscription means adding its arm
155 first. (The old `viewport` topic is gone with the viewport-tag feature.)
156 - **System stats** (`spawn_system_stats`): `/proc/stat`, `/proc/meminfo`,
157 `/sys/class/power_supply/BAT*`, `/sys/class/backlight`, and `pactl` for volume/mute.
158 `SystemStats` carries numbers (`cpu_pct`, `memory`, `battery: (capacity,
159 charging)`, `volume: (level, muted)`, `brightness`), each `Option` where
160 the source can be absent; only the clock arrives pre-formatted. The loop is
161 once a second, which is fine for a clock or a load average and far too slow
162 for the two values a KEYPRESS moves — so the backlight and the sink have a
163 fast path beside it (`spawn_level_watchers`), each pushing its own
164 one-field event (`BrightnessUpdated` / `VolumeUpdated`) that patches
165 `stats` in place. `watch_brightness` polls `/sys/class/backlight` every
166 100ms — `brightnessctl` writes the attribute directly, so there is nothing
167 to subscribe to, and two small sysfs reads are cheap enough that the
168 interval is not worth tuning; `watch_volume` follows `pactl subscribe` and
169 re-reads only on a `sink`/`server` event (NOT `sink-input`, which fires
170 throughout playback, and NOT `client`, which the bar's own `pactl` runs
171 generate — matching either would put the reader in a loop with itself).
172 Both send only a CHANGED value, so an idle desktop never wakes the event
173 loop, and `update()` asks `paints_stat` whether this module shows the field
174 before redrawing — the one-field counterpart to `stats_signature`, and a
175 test holds the two in agreement. The subscription burst is coalesced for
176 30ms before the read (a held volume key emits a stream of events, and one
177 `pactl` spawn per event would fall behind); the child carries
178 `PR_SET_PDEATHSIG` as well as `kill_on_drop`, because a subscription whose
179 reader was killed outright is reparented to init and sits there rather than
180 noticing. The one-second loop still reads both values, so it remains the
181 safety net when `pactl subscribe` cannot run at all. Measured in a shadow:
182 ~45ms for the backlight, ~55ms for the sink, against a second before.
183 - **Tray** (`spawn_status_tray`): a full StatusNotifierItem/Watcher host over `zbus`,
184 including DBusMenu fetching. Icons arrive as pixmaps or theme names (rendered via
185 `resvg`/`png`).
186 - **Backdrop** (`spawn_status_listener("backdrop <app_id>")`): what THIS segment
187 is composited over, measured compositor-side and pushed as `<luma> <spread>`
188 (0-100 each) or `unknown`. Every module process subscribes, naming itself with
189 `status_app_id()`. A Wayland client cannot see behind its own surface, so this
190 is the only source of the fact — see `module { text_contrast }` below.
191 - **Switcher** (`spawn_switcher_listener`): binds
192 `/tmp/cce-status-interface-switcher-{WAYLAND_DISPLAY}.sock`; a line on it fires
193 `SwitcherTriggered`.
194
195 Outbound actions shell out to `ccectl` (`windows --json`, `focus-window`,
196 `window-switcher`, `status-hide-mode`, `adjust-position-mode`), resolved from `~/.local/bin` first (`get_ccectl_cmd`).
197 `ccectl windows --json` returns one JSON object per line; the text format is kept only
198 as a parse fallback for older compositors (`parse_ccectl_window_any_line` handles
199 both). Keyboard alt-tab switching is delegated to the compositor
200 (`ccectl window-switcher`) — don't reimplement it here.
201
202 **Right-click menus are IN-SURFACE** (`ModuleContextMenu`): the module's own
203 surface expands below the bar strip to contain the menu — the module box
204 literally grows into the menu (one continuous rounded box; the expansion and
205 contraction are ANIMATED over ~140ms, `menu_anim`/`menu_closing` stepped in
206 `tick`, eased in `rebuild_layout`, surface resized per-frame via
207 `desired_size`; the menu object drops only when the contraction lands) —
208 module context menus and tray icon DBusMenus alike (fetched/flattened by `cloud.rs::
209 fetch_tray_menu_pages` into `MenuPage`/`MenuRow` pages riding a
210 `CustomEvent::TrayMenuFetched`; submenus paginate in place; row clicks send the
211 DBusMenu "clicked" via `send_tray_menu_event`). The compositor treats a status
212 segment thicker than the bar as expanded: frozen slot, no size enforcement,
213 raised above overlapped windows; the bar must reset its own height on close.
214 In droplet style the expanded panel is a FLAT glass sheet: `spec_at_reference_height`
215 fades `dome` and `gleam` to zero (continuously in the growth factor, gone by
216 twice the bar height) because the SDF-gradient dome creases on a long-sided
217 box — full strength drew a blocky lit picture-frame with the band pinned, and
218 envelope folds across the body with the band grown; both were tried. The panel
219 keeps the silhouette-hugging water terms (clarity, rim crest, core, contact
220 shadow), and the hovered row's highlight is a rounded pill inset from the
221 panel edge (`menu_hover_rect`, drawn post-scrim), not a full-width rect.
222
223 **No cce-cloud popups remain in this app**: the window picker (window-module
224 click → `MenuReady` rows of `Ccectl(["focus-window", id])`) is an in-surface
225 menu too. Menu width sizes to
226 the longest row label. Expanded segments stack in the compositor's popups
227 layer (cce-fx@74a0f75) so click-away-close works across the whole surface,
228 including the strip band over neighboring segments. Plain Escape closes open
229 menus too — compositor-side like click-away (cce-fx@7db8c03), arriving here as
230 the same `dismiss` push; this app never sees the key itself, since status
231 segments hold no keyboard focus.
232
233 ## Config
234
235 Config comes from the shared `~/.config/cce/config.kdl` with the app's own
236 `~/.config/cce/cce-status-interface/config.kdl` merged over it (cce-ui does the
237 merge by executable name; both files' mtimes drive the live-reload poll via
238 `config_files_modified`). App-native keys live in the app file — currently
239 `module { corner_radius }` (overall module box radius; deliberately NO shared
240 fallback — the old `status_box_corner_radius` rung was removed) and `module { spacing }` (the gap between segments;
241 the COMPOSITOR reads this one for its arrange pass — bar-side it only affects a
242 multi-module surface — applied on `ccectl reload`) and `module { height }` (the
243 bar height; read by BOTH sides — bar surfaces live via the mtime poll, the
244 compositor's segment height + reserved strip on `ccectl reload` — falls back to
245 the shared `layout { bar_height }`) and `module { padding }` (text inset inside
246 each module box, bar-side only, falls back to the shared
247 `/style/status/padding`) and
248 `module { font_size }` (module text size, bar-side only; beats even the size
249 embedded in the shared font string, which remains the fallback) and
250 `module { font }` (module text family; an embedded size ranks below
251 module { font_size } in the size chain) and `module { background_color }` (the
252 module box fill, rgba; linearized like every quad color, and the
253 background_blur tint scaling still applies on top) and `module { text_color }`
254 (module text, raw-sRGB like every text color, falls back to the shared
255 `/style/status/normal_color`) and `module { droplet }` (the water-droplet module
256 style — cce-ui's `Prim::Droplet`, shader mode 10; the key's PRESENCE enables
257 it, its value is whitespace-separated `k=v` pairs onto `DropletSpec` — sag,
258 belly, belly_w, blend, sheet_r, attach, clarity, dome, band, gleam, shine,
259 rim, bow, curve, core, refr, ghost, shadow; defaults = the oval dewdrop (no
260 belly; attach 0.42 + sheet_r 0.58 fill the height so there is NO straight
261 side; bow arcs the bottom; curve 2.6 = superellipse joins, so everything but
262 the flat top is one continuous curve), belly>0 brings back the pendant-pool
263 look — warn-and-skip on unknown keys. Three of those knobs are not this
264 side's: `refr` (rim refraction, logical px) and `ghost` (the inverted lens
265 image in the belly) are read by the COMPOSITOR, whose scenefx droplet node
266 bends the backdrop behind the drop — a Wayland client cannot see behind its
267 own surface, so this side parses them and draws nothing. `shadow` (0-1, the
268 contact shadow under the drop's lower arc) IS drawn here, and it is why the
269 drop box does not fill the surface: the box is inset by 1px for the
270 silhouette's AA feather plus `DropletSpec::shadow_gap()` for the shadow's
271 falloff (`main.rs`, three call sites — left, right, and the expanded menu
272 box, which becomes the drop growing))
273 and `module { icon_size }` (glyph height for the icon readouts, logical px,
274 default 16 = the tray's fixed icon size, so the two read as one set) and `module { icon_font_size }` (the number beside
275 the glyph, default `module { font_size }`) and `module { icon_gap }` (glyph
276 to number, logical px, default 4) and `module { icon_spacing }` (between
277 readouts in the `stats` bubble, default `module { spacing }`) and
278 `module { icon_alpha }` (glyph opacity 0-1, default 1) and
279 `module { icon_weight }` (OpenType weight of the number, unset = regular)
280 and `module { text_raise }` (lifts module text above vertical center, logical
281 px, bar-side only — every module funnels through `centered_text_y`) and
282 `module { text_scrim }` (0-1 resting opacity of a
283 feathered pool filling each module box, the DE's one text-contrast treatment)
284 and `module { text_scrim_feather }` (that pool's falloff in logical px,
285 default a quarter of the box height) and `module { text_contrast }` (adaptive
286 contrast 0-1, default 0 = off — the compositor's `backdrop` measurement
287 deepens the pool through it, so the ground darkens only as far as a backdrop
288 the configured text color cannot carry demands; on its own, with no
289 `text_scrim`, it makes the pool appear ONLY when the backdrop earns it).
290 (The glyph-decorating treatments this replaced — `text_relief`'s letterpress
291 underlay and `text_halo`'s four-copy outline — were deleted 2026-08-28 once
292 the scrim superseded both; don't reintroduce a per-letterform treatment
293 without a reason the ground cannot serve.) Everything is read through
294 `cce_ui::config::cached_config()`; KDL is converted to JSON
295 (`cce_ui::config::parse_kdl_to_json`) and looked up by **explicit JSON
296 pointer only**: every key names its canonical nesting
297 (`/style/status/background_color`, `/module/height`,
298 `/window_manager/light_source_position`, `/layout/status_bar/<module>` for
299 per-module sides, …), and a key parked anywhere else simply does not resolve.
300 (The legacy fuzzy `json_find_key` — snake_case split across nesting, then
301 depth-first search — was deleted 2026-08-18 after its fallback warnings went
302 quiet; don't reintroduce it.) Shared keys used here, written as the pointers
303 they are actually looked up by — the flat snake_case spellings this list used
304 to carry (`status_padding`, `status_font`, …) appear nowhere in the config or
305 the code: `/layout/bar_height`, `/style/status/font` (also via fontconfig alias
306 `status-interface`), `/style/status/font_size`, `/style/status/padding`,
307 `/style/status/module_spacing`, `/style/status/normal_color`,
308 `/style/status/disabled_color`, `/style/status/background_color`,
309 `/style/status/background_blur`, `/style/status/box_bevel`(`_depth`),
310 `/window_manager/light_source_position`, and `/layout/status_bar/<module>` for
311 the per-module sides. (The whole-bar
312 background chain is gone: a `StatusApp` is always a single `--module` segment,
313 so the surface bg is permanently transparent and only module boxes paint.)
314
315 Color space (one rule, enforced in `config.rs`): **text colors stay raw sRGB**
316 (`text_color_from` — cosmic-text consumes sRGB `[u8; 3]`), **quad/box colors
317 are linearized** (`quad_color_from` via `cce_ui::color::parse_hex_rgba_linear`,
318 for the Vulkan pipeline). No local gamma math — the old scattered `.powf(2.2)`
319 is gone; `text_colors_stay_srgb_and_quad_colors_are_linearized`, in
320 `config.rs`, is the spec. Config changes are picked up by polling the file
321 mtime in `tick()`, so there is no reload event to wire up.
322
323 ## Adaptive text contrast
324
325 The bar draws into its own buffer and can never see what it is composited
326 over, so a module box at `background_color` alpha `30` leaves its text at the
327 mercy of whatever the desktop shows through it. `module { text_contrast }`
328 closes that loop with the compositor, which CAN see:
329
330 1. `cce-fx` measures each segment's backdrop per frame (`backdrop.rs`) and
331 pushes `<luma> <spread>` on the status socket's `backdrop` topic.
332 2. `contrast_demand()` checks the configured text color's WCAG contrast
333 against that backdrop at three points — the mean AND both ends of the
334 spread — and takes the worst. Checking only the mean is the trap: a segment
335 half on a black grid cell and half on a light gap averages to a comfortable
336 mid-gray while the text is invisible over one half.
337 3. `tick` eases `contrast_now` toward that demand over ~120ms. Stepping
338 straight to it makes the scrim pulse as the desktop pans under the segment.
339
340 `module { text_scrim }` is the treatment itself: a feathered pool (`cce_ui`'s
341 `Prim::Glow` — solid through a core rect, falling off to nothing across
342 `text_scrim_feather` px, tessellated as per-vertex-alpha rings so there is no
343 banding) filling each module box. It darkens the ground the glyphs sit on
344 rather than decorating the letterforms. It rests at the configured opacity and
345 `text_contrast` deepens it from there, so it is a constant when that knob is
346 off — and either knob alone is meaningful.
347
348 One pool per bubble, not per text run, so a segment reads as one darkened
349 lozenge rather than a pill inside a pill. Its shape is the bubble's ACTUAL
350 shape, which means two paths: a droplet module gets `Prim::DropletScrim`
351 (cce-ui shader mode 12 — the droplet's own SDF under the same `DropletSpec`,
352 filled flat and feathered inward, so the vignette's edge is the drop's edge by
353 construction), while a plain rounded box gets `Prim::Glow` with its core inset
354 by exactly the feather, which lands the gradient's outer edge on the box edge.
355 The rounded-rect pool is clipped to its box; the droplet one needs no clip,
356 since the shader cannot draw outside the silhouette it is evaluating.
357
358 The pool's color comes from `dominant_run_color` — the widest run inside the
359 box, using the width that rides `TextPrim`'s last field — with
360 `dominant_icon_color` as the fallback for a box that holds tray icons and no
361 run: the icons read as light glyphs (dark pixmaps are recolored toward white),
362 so the tray is grounded as a white run would be, with a black pool. Before
363 2026-09-05 a box with no measured run got no pool, which left the tray the
364 one bare bubble in the strip — visibly lighter than its neighbors, and the
365 one segment the backdrop feed could not deepen. A box holding neither text
366 nor icons still gets no pool. Width is the tiebreak because a module mixing
367 colors is led by its longest label.
368
369 The pool takes its color from `treatment_rgb` — whichever of black/white the
370 run reads against, by contrast ratio (the WCAG crossover is near 0.18, not
371 0.5). Chosen from the run's OWN color, not the configured module color: a
372 module may paint a run in something else entirely, and the volume module's
373 muted state uses the shared `disabled_color`. A black pool behind black text
374 is not a weaker treatment — it is an eraser. (That `disabled_color` is a light
375 red as of 2026-08-28, chosen so the muted run keeps the same dark pool as
376 every other bubble instead of inverting to a light one.)
377
378 A window covering part of a segment is measured too — the compositor reads
379 that window's own content over the overlapping strip and blends it with the
380 desktop reading for the rest. `unknown` is left for content it genuinely
381 cannot read (no committed buffer, an unsupported read format).
382
383 Failures resolve toward legible in every direction: an unparseable or absent
384 line reads as `(50, 100)` — mid luminance, full spread — which drives the
385 scrim rather than switching it off.
386
387 ## Interactions worth knowing before touching input code
388
389 - **Super + left-drag on a segment is handled by the compositor**, not this app: it
390 starts the same segment drag as adjust-position mode (snap to an edge on release,
391 persisted to `layout.status_bar.<module>` in config.kdl). This app never sees those
392 presses and no longer tracks the super key — with two exceptions since
393 2026-09-16 (cce-fx `cursor.rs`): a press that travels under 6px is a CLICK,
394 replayed to the segment as press+release instead of snapped (a still click on
395 a top-edge segment used to re-home it to top-center), and an EXPANDED segment
396 (menu open) is never grabbed at all, so the "Done" row can end adjust mode.
397 - Tray icons left-click activate / right-click open their DBusMenu. (The old
398 layout-mode menu and viewport tabs are gone with the viewport-tag feature.)
399 - `ToggleHideModules` / `ToggleAdjustPositionMode` mirror their state to the compositor
400 via `ccectl status-hide-mode|adjust-position-mode true|false`; the adjust-mode state
401 is read back with `ccectl adjust-position-mode query` (the compositor is the single
402 source of truth — the old `/tmp/cce-status-interface-adjust-mode` sentinel file is
403 no longer consulted).