git.lucas.co / cce-data-editor
structured data editor
git clone https://git.lucas.co/cce-data-editor.git

CLAUDE.md (9K)

  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-data-editor` is the KDL config editor for the CCE Wayland desktop environment: a
  8 GUI app for browsing and editing KDL files (primarily `~/.config/cce/config.kdl` and
  9 `input.kdl`). It is one crate of the multi-repo `cce` workspace — this repo is cloned
 10 side-by-side with its siblings and depends on `cce-ui = { path = "../cce-ui" }`, so a
 11 checkout of `../cce-ui` must exist to build. The workspace-level
 12 `../cce-compositor/WORKSPACE.md` (when present) covers the whole desktop environment; commit
 13 in THIS repo, never at the workspace root.
 14 
 15 The entire app is **one file: `src/main.rs`** (~3.1k lines) — a `DataEditorApp` struct
 16 implementing `cce_ui::engine::Application`, run by `cce_ui::engine::run::<DataEditorApp>()`.
 17 There are no modules to navigate; use the section landmarks below.
 18 
 19 ## Build, run, test
 20 
 21 ```sh
 22 cargo build --release          # binary lands in ../target/release/ (shared workspace target)
 23 cargo run -- <file.kdl>        # optional file argument auto-loads on startup
 24 make install                   # release build, then `ccebuild install --no-build cce-data-editor`
 25 cargo test                     # see caveat below
 26 ```
 27 
 28 Running requires a live Wayland session (it is a raw Wayland client, not X11/toolkit).
 29 
 30 **Test caveat**: both `#[cfg(test)]` tests read the user's real
 31 `~/.config/cce/config.kdl` (via `cce_ui::config::get_config_path()`) and
 32 `test_kdl_roundtrip` additionally asserts a specific key
 33 (`style.status.background_color`) exists in it. They are environment-dependent
 34 integration checks of the flatten/unflatten/span machinery, not hermetic unit tests —
 35 failures may mean the local config changed, not that code broke.
 36 
 37 ## Architecture
 38 
 39 ### Data model: KDL ⇄ JSON ⇄ flat keys
 40 
 41 The core state is `flat_keys: Vec<(String, serde_json::Value)>` — the KDL document
 42 parsed to JSON (`cce_ui::config::parse_kdl_to_json`) then flattened to dotted paths
 43 with bracket indices (`style.status.background_color`, `outputs[0].mode`). Three
 44 representations are kept in sync:
 45 
 46 - **Raw KDL text** — the right-pane multiline `TextBox` (`raw_json_editor`; the name
 47   is historical, it holds KDL). Ground truth for save/format.
 48 - **`flat_keys`** — drives the left-pane `TreeList` and all value edits.
 49 - **On-screen selection** — `selected_key_idx` into `flat_keys`.
 50 
 51 Edits flow one of two ways:
 52 - Tree/editor side: mutate `flat_keys[idx].1` → `update_raw_from_flat()` →
 53   `unflatten_json` → `json_to_kdl_string_with_annotations` regenerates the raw text
 54   (preserving KDL type annotations harvested from the current text).
 55 - Raw side: `raw_json_editor.take_change()` in `tick()` reparses the text and rebuilds
 56   `flat_keys` (only if the KDL parses), re-resolving the selection by key name.
 57 
 58 `parse_path` / `find_kdl_span` map a flat path to a byte span in the KDL source. This
 59 powers bidirectional selection sync: clicking a tree row highlights its span in the raw
 60 editor (`sync_preview_selection`), and clicking in the raw editor selects the tree row
 61 whose span most tightly contains the cursor (smallest-span-wins search in
 62 `handle_mouse_input`).
 63 
 64 Save/format refuse to run when the text is not valid KDL; an invalid document still
 65 loads (best-effort parse) with an error in the status bar.
 66 
 67 ### Type-driven inline value editors
 68 
 69 Eleven editor widgets exist permanently as fields (`selected_*_editor`); exactly one is
 70 positioned inline over the selected tree row, the rest are parked off-screen at
 71 `(-1000, -1000)` (the "hide" convention — there is no visibility flag). Which editor
 72 appears is decided in two places that must stay in agreement: the layout block in
 73 `display_list` and the click handler in `handle_mouse_input`. The decision keys off:
 74 
 75 - **KDL type annotations** in the document (read via
 76   `cce_ui::config::get_kdl_type_annotation`): `menu:a,b,c` → `Dropdown`,
 77   `button` / `button:<shell-cmd>` → `Button` (clicking spawns the command),
 78   `keybind` → `KeybindRecorder`, `f64:min-max` → clamps applied values,
 79   `bevel` → `BevelPreview` (a mini relief cross-section of the
 80   "shoulder,base,bias" value; clicking opens `cce-relief`, whose Save is picked
 81   up by the disk-sync watch). The annotation stays `bevel` — it is a config key,
 82   and did not follow the tool's rename from `cce-bevel`.
 83   `relief` → the same `BevelPreview`, seeded from the `(relief)` value's `k=`
 84   knob triple (`cce_ui::relief_spec::ReliefSpec`), but clicking opens
 85   `cce-relief --key <flat.path>` so Save rewrites that single value instead of
 86   the file's shared material. Keys NAMED `line_relief` get this treatment even
 87   while their value is still a plain integer (the annotation only exists once
 88   a material was saved) — same key-name-heuristic convention as fonts.
 89   `ramp` → `RampPreview` (the spec's value curve as a polyline); clicking
 90   opens `cce-ramp --key <flat.path>`, whose Save writes the spec back with
 91   the `(ramp)` annotation. Ramp-named keys (`ramp` / `*_ramp`) whose string
 92   value parses as a ramp spec get the treatment before the annotation exists
 93   (`overview_ramp` shipped unannotated).
 94   A **unit annotation** (`px` / `mm` / `cm` / `in` / `pt`, e.g. `width=(mm)2.0`), or
 95   any string value `cce_ui::units::Len` parses (`"2mm"` — which is exactly what
 96   `kdl_to_json` makes of the annotated number) → a two-decimal `Spinbox`
 97   (`selected_len_editor`) beside a unit `Dropdown` (`selected_unit_editor`).
 98   Changing the number writes `"<n><unit>"`, which the KDL writer emits as
 99   `(<unit>)<n>`; changing the unit keeps the LENGTH — converted through
100   `cce_ui::units::metric()`, the display's px-per-mm — and rewrites the number,
101   so 2 mm switched to inches reads 0.08 and a px value switched to mm reads what
102   it measures on this panel. Both sites (`display_list`, click) key off
103   `is_len_type`; the commit paths are `commit_len` / `sync_len_editors`.
104 - **Key-name heuristics**: `font` / `*_font` / `*.font` → `FontSelector`; keybind-ish
105   names (`key`, `shortcut`, `brightness_up`, …) → `KeybindRecorder`.
106 - **Value shape**: `#`-prefixed string → `ColorSelector`, bool → `Checkbox`,
107   integer → `Spinbox`, everything else → plain `TextBox` (values are entered as JSON;
108   bare words fall back to strings).
109 
110 Editor commits are polled in `tick()` via each widget's `take_change()`, all funneling
111 into the same mutate-`flat_keys` → `update_raw_from_flat()` → `sync_preview_selection()`
112 sequence.
113 
114 ### cce-ui "Phase 6" conventions (post-container)
115 
116 This app tracks the current cce-ui architecture; mirror these patterns when touching UI
117 code, and don't reintroduce the retired ones:
118 
119 - **No root container.** The root plate container and the `SplitBox` are dissolved.
120   Top-level widgets are parentless, registered once with
121   `ui_context.register_widget(id, ptr)` in the first `display_list` call, and painted
122   as separate roots via `cce_ui::scene::painter::paint_root_into` (shared borrows).
123 - **Single paint path.** Everything renders through `display_list` into one `PaintCtx`
124   — window plate quad, widget walk, splitter divider quad, the toolbar "File:" label,
125   then popovers and the context menu drawn last directly into the list (the engine's
126   xdg-popup path is gone; `display_list_text()` returns `true`).
127 - **`SplitPane`** (app-owned struct at the top of the file) replaces `SplitBox`: it
128   keeps only the divider drag/hover state and fraction; pane rects come from the scene
129   solver.
130 - **Layout via the scene solver**: `display_list` builds a small
131   `cce_ui::scene::layout` arena (column: menubar 42px / content row with the two panes
132   growing by `split.frac` / statusbar 30px) and assigns solved rects with `set_rect`.
133   The inline value editors are hand-positioned at `row_x + 245.0` over the tree row.
134 - **Routed events**: input handlers build a `cce_ui::widget::Event` and call
135   `ui_context.propagate_event(&ev, root_id)` per root. Note the ordering contracts
136   documented inline: the key-input chain short-circuits on first handled;
137   Enter-applies-value is gated on `value_was_editing` captured *before* dispatch;
138   mouse events check `editor_handled` before letting the tree list see the click.
139 - **TextBox editing model**: while `editing`, live content is `edit_buffer`, not
140   `text` — hence the recurring
141   `let content = if editing { &edit_buffer } else { &text }` idiom. Keep it when
142   reading editor content.
143 - **Text shaping**: `refresh_widget_text()` calls `prepare_text(&mut self.font_system)`
144   on every text-bearing widget each rebuild — this is load-bearing for cursor↔pixel
145   mapping, not just rendering.
146 
147 ### Shortcuts and app config
148 
149 Keyboard shortcuts resolve once at startup (`DataEditorKeys::load`) from
150 `~/.config/cce/input.kdl` through the `cce-data-editor` domain (falling back to
151 `cce-ui`, then the hardcoded defaults: ctrl+o / ctrl+s / ctrl+q / ctrl+shift+f) via
152 `cce_ui::input::app_chord`. The font-size chords (ctrl +/-) are deliberately hardcoded.
153 The recent-files list (File dropdown) is shared toolkit state via
154 `cce_ui::config::load_recent_files` / `save_recent_files`, capped at 10 entries.