status bar
git clone https://git.lucas.co/cce-status-interface.git
PROPOSAL.md (9.9K)
1 # Proposal: cce-status-interface cleanup
2
3 > **Status (2026-08-19): closed.** Phases 0–3 and 5 are done; phase 4 became
4 > moot when the cce-cloud popups were replaced by in-surface menus. Phase 2's
5 > final step landed too: `json_find_key` and the warn-once fallback are
6 > deleted — config lookup is explicit JSON pointers only. Phase 6: the
7 > launcher backoff shipped (4353b94), the fuzzy upstream was rejected here
8 > and is moot since the deletion, and the `cce-tray` extraction stays
9 > deliberately gated on a second SNI consumer existing (user-confirmed
10 > 2026-08-19) — `src/tray.rs` keeps that extraction cheap whenever one
11 > appears. Nothing on this document remains actionable.
12
13 Fixes for the issues identified in the 2026-07 review: `main.rs` carrying five jobs,
14 a fuzzy config-lookup layer that partially duplicates `cce-ui`, stringly-typed
15 coordination with the compositor, inconsistent color gamma handling, mixed
16 `eprintln!`/`log::` output, and near-zero test coverage.
17
18 Ordered so that each phase is independently commitable and the risky changes land on
19 top of a test safety net. Phases 1, 2, 5, and 6 touch only this repo; phase 3 also
20 touches `cce/`; phase 4 also touches `cce-ui/`. Per the multi-repo rules, each repo
21 gets its own commits and must keep building standalone.
22
23 ---
24
25 ## Phase 0 — Characterization tests (safety net)
26
27 Today there is one test (`test_status_config`). Before moving anything, pin down the
28 behavior the later phases will refactor:
29
30 - `parse_viewport_text`: pango spans, JSON-wrapped payloads, malformed/unterminated
31 spans, plain-text fallback.
32 - `json_find_key`: exact match, snake_case split across nesting
33 (`status_background_color` → `status { background_color }`), collision/traversal
34 order, miss → `None`. These tests become the spec for phase 2's replacement.
35 - `get_module_side`: config side values, snap-position aliases
36 (`top-left`/`bottom-right`/…), defaults (`window` → left, rest → right).
37 - The `ccectl windows` line parser in `trigger_switcher` (extract the per-line parse
38 into a free function first so it's testable): `app_id=`/`title="…"`/`focused=`/
39 `window id=` extraction, filtered app_ids. These tests get retired in phase 3 when
40 the parser is replaced by JSON, but until then they document the wire format.
41 - Color parsing: one test asserting which keys are gamma-corrected and which are raw
42 sRGB, so phase 2's fix is a deliberate, visible change rather than an accident.
43
44 **Effort:** small. **Risk:** none (test-only).
45
46 ## Phase 1 — Split `main.rs` (mechanical, no behavior change)
47
48 `main.rs` is ~3,400 lines. Split by existing seams, keeping `modules.rs` as-is:
49
50 | New file | Contents (moved, not rewritten) | ~lines |
51 |---|---|---|
52 | `src/tray.rs` | zbus proxies/traits, `StatusNotifierWatcher`/host impl, `spawn_status_tray`, `fetch_tray_item`, icon decode | ~850 |
53 | `src/cloud.rs` | `show_cce_cloud_menu`, `MenuItem` parsing/paging, window-picker spawn body from `trigger_switcher` | ~500 |
54 | `src/stats.rs` | `spawn_system_stats`, `read_cpu_ticks`, `read_memory_usage`, `read_battery_details`, `read_volume`, brightness | ~300 |
55 | `src/config.rs` | all `read_*_from_config`, `parse_*_color_from_key`, `json_find_key`, `parse_font_for_alias`, `get_*_cmd` | ~350 |
56 | `src/listeners.rs` | `spawn_status_listener`, `spawn_switcher_listener` | ~100 |
57 | `main.rs` (remains) | `StatusApp`, `Application` impl, layout/input, `CustomEvent`, launcher-daemon `main()` | ~1,300 |
58
59 Rule for the phase: `git diff` should show only moves, `use` changes, and visibility
60 bumps (`fn` → `pub(crate) fn`). No logic edits — those come later, reviewable on their
61 own.
62
63 **Effort:** medium (mostly mechanical). **Risk:** low with phase 0 in place.
64
65 ## Phase 2 — Config: replace the local layer with `cce-ui` accessors
66
67 `cce_ui::config` already provides `cached_config()`, JSON-pointer accessors
68 (`get_f32`, `get_bool`, `get_string`, `get_color`) and a recursive `find_key`. The
69 local layer in this crate re-implements the lookup with an extra behavior — splitting
70 snake_case keys across nesting — and hand-rolls gamma with `.powf(2.2)`.
71
72 1. **Make each config key an explicit pointer.** Replace
73 `json_find_key(&val, "status_background_color")` with
74 `cce_ui::config::get_color("/style/status/background_color")` (etc.), encoding the
75 real nesting once instead of discovering it by recursive search. Keys whose actual
76 KDL location is unclear get resolved by looking at a real `config.kdl` and the
77 compositor's reader — that's the point: today nobody can grep where a key lives.
78 2. **Keep a thin fallback during migration.** A local
79 `get_color_fuzzy(key)` that first tries the pointer, then falls back to the old
80 `json_find_key`, with a `log::warn!` when only the fallback hits. After one release
81 of quiet logs, delete the fallback and `json_find_key` entirely.
82 3. **Fix gamma in one place.** `get_color` returns raw sRGB by its own doc; apply
83 `cce_ui::color::srgb_to_linear` at the single point where colors enter the render
84 state (`rebuild_layout`), replacing the scattered `.powf(2.2)` and the
85 `status_normal_color`-is-linear-but-background-isn't inconsistency. Verify visually
86 against the current bar before/after (screenshot compare) since this may shift
87 perceived colors that users have tuned; if `status_normal_color` was correct as-is,
88 document that in the code rather than leaving it implicit.
89 4. **Delete local duplicates:** `parse_hex`, `parse_hex_rgba` wrappers,
90 `parse_srgb_color_from_key`, `parse_rgba_color_from_key`, `parse_json` — all become
91 calls into `cce_ui::config`/`cce_ui::color`.
92
93 **Effort:** medium. **Risk:** medium (visible color shifts possible — mitigated by the
94 phase-0 gamma test and a manual screenshot check). Cross-crate impact: none; other
95 clients using the fuzzy pattern can migrate later on their own schedule.
96
97 ## Phase 3 — Structured `ccectl` output (cross-repo: `cce/`)
98
99 The window picker parses `ccectl windows` free text with `find("app_id=")` and
100 friends; titles containing `"` or spaces in unexpected places break it silently.
101
102 1. **In `cce/` (owns `run_cce_ctl` and the control socket):** add `--json` to the
103 read commands this crate consumes — `windows` first; `viewports`/others as needed.
104 Output: one JSON object per line or a single array —
105 `{"id": …, "app_id": "…", "title": "…", "focused": bool}`. Text output stays the
106 default so nothing else breaks.
107 2. **In this crate:** replace the line parser with `serde_json` deserialization into a
108 small `WindowEntry` struct; fall back to the text parser if `--json` is rejected
109 (running against an older compositor), so the two repos can ship independently.
110 3. **Same pass, smaller items:**
111 - Replace `Command::new("kill").arg(pid)` with a direct `SIGTERM` via `libc::kill`
112 (or the `nix` crate) — no shell-out, and an error result instead of a silent
113 failure.
114 - Replace the `/tmp/cce-status-interface-adjust-mode` sentinel-file read in
115 `ToggleAdjustPositionMode` with a `ccectl` query, so mode state has one source of
116 truth (the compositor).
117
118 **Effort:** medium, split across two repos. **Risk:** low (fallback keeps old/new
119 combinations working).
120
121 ## Phase 4 — A shared popup helper (cross-repo: `cce-ui/`)
122
123 The `cce-cloud` popup pattern (spawn, write JSON pages to stdin, track pid via
124 `CloudSpawned`/`CloudClosed`, toggle-off by kill, restore focus on close) is
125 hand-rolled here in three places (window picker, tray menus, layout menu) and will be
126 wanted by other clients.
127
128 - Add `cce_ui::process::CloudPopup` (the crate already has `process.rs`):
129 `spawn(pages, position, source) -> CloudPopup`, `toggle_off()`, `is_running()`,
130 completion callback for focus-restore. Internals lifted from this crate's working
131 implementation — it's extraction, not redesign.
132 - Port the three call sites here; `active_cloud_pid`/`active_cloud_source` and the
133 `/proc/<pid>/comm` checks move behind the helper.
134
135 Deliberately **not** proposing a socket protocol between bar and popups: the
136 pid-plus-stdin model is working, and the goal is to stop *re-implementing* it, not to
137 replace it. Revisit only if popups need richer two-way communication.
138
139 **Effort:** medium. **Risk:** low-medium (behavior-preserving extraction, three call
140 sites to verify by hand: window picker, a tray menu, layout menu).
141
142 ## Phase 5 — Logging unification
143
144 51 `eprintln!` vs 12 `log::` calls today, and `env_logger` is already initialized.
145
146 - Convert `eprintln!` → `log::debug!` (chatty per-event traces: `[status-listener]`,
147 `[cloud-event]`, stats updates) or `log::info!`/`log::warn!` (lifecycle, failures).
148 - Keep the existing bracketed subsystem tags as message prefixes; they're useful.
149 - Default filter stays `Info`, so the net effect is a much quieter stderr with
150 `RUST_LOG=debug` restoring today's firehose.
151
152 **Effort:** small, mechanical. **Risk:** none.
153
154 ## Phase 6 — Follow-ups (explicitly out of scope for now)
155
156 - **Tray as its own crate** (`cce-tray`): worth doing only when a second client needs
157 SNI. Phase 1's `tray.rs` makes the later extraction cheap.
158 - **Upstreaming the fuzzy key-split into `cce_ui::config::find_key`**: rejected —
159 phase 2 goes the other way (explicit pointers), and two lookup semantics in the
160 toolkit is worse than one.
161 - **Launcher-daemon supervision polish** (backoff on crash-looping modules instead of
162 unconditional 500ms restarts): cheap, but wait until after phase 1 so it lands in a
163 small `main()`.
164
165 ---
166
167 ## Sequencing and verification
168
169 ```
170 0 tests ─→ 1 split ─→ 2 config/gamma ─→ 5 logging
171 └────→ 3 ccectl --json (needs a cce/ commit first)
172 └────→ 4 CloudPopup (needs a cce-ui/ commit first)
173 ```
174
175 Phases 3 and 4 are independent of 2 and of each other; 5 can land any time after 1.
176
177 Each phase ends with: `cargo test` green, `cargo build --release` standalone, and a
178 manual smoke test in a live session — bar renders in both orientations, viewport tabs
179 switch, a tray menu opens and closes, the window picker toggles, super+drag moves a
180 module, and a config edit hot-reloads.