status bar
git clone https://git.lucas.co/cce-status-interface.git
refactor: delete json_find_key — config lookup is explicit pointers only
The warn-once fuzzy fallback went quiet once every key (including the
newly canonicalized disabled_color) resolved at its pointer, so the
legacy search — snake_case split across nesting plus a depth-first scan
that could land on unrelated same-named widget keys — is gone. A key at
a non-canonical location now simply does not resolve.
Co-Authored-By: Claude Fable 5 <[email protected]>
CLAUDE.md | 15 ++--
PROPOSAL.md | 8 +--
src/config.rs | 217 +++++++++++++---------------------------------------------
src/main.rs | 32 ++++-----
4 files changed, 72 insertions(+), 200 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 4857ebc..a0be9fd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -138,15 +138,14 @@ background_blur tint scaling still applies on top) and `module { text_color }`
(module text, raw-sRGB like every text color, falls back to the shared
`status_normal_color`). Everything is read through
`cce_ui::config::cached_config()`; KDL is converted to JSON
-(`cce_ui::config::parse_kdl_to_json`) and looked up **pointer-first**
-(`config.rs::pointer_or_fuzzy`): every key has an explicit JSON-pointer for its
-canonical nesting (`/style/status/background_color`, `/module/height`,
+(`cce_ui::config::parse_kdl_to_json`) and looked up by **explicit JSON
+pointer only**: every key names its canonical nesting
+(`/style/status/background_color`, `/module/height`,
`/window_manager/light_source_position`, `/layout/status_bar/<module>` for
-per-module sides, …), with the legacy fuzzy `json_find_key` (snake_case split
-across nesting, then depth-first search) kept only as a fallback that
-`log::warn!`s once per key when it alone hits. When adding a key, add its
-pointer; once the warnings stay quiet across a release the fuzzy fallback is
-scheduled for deletion. Shared keys used here: `bar_height`, `status_font`
+per-module sides, …), and a key parked anywhere else simply does not resolve.
+(The legacy fuzzy `json_find_key` — snake_case split across nesting, then
+depth-first search — was deleted 2026-08-18 after its fallback warnings went
+quiet; don't reintroduce it.) Shared keys used here: `bar_height`, `status_font`
(also via fontconfig alias `status-interface`), `status_font_size`,
`status_padding`, `status_module_spacing`, `status_normal_color`,
`status_background_color`, `status_background_blur`, `status_box_bevel`(`_depth`),
diff --git a/PROPOSAL.md b/PROPOSAL.md
index 17333b6..1ca858a 100644
--- a/PROPOSAL.md
+++ b/PROPOSAL.md
@@ -1,10 +1,10 @@
# Proposal: cce-status-interface cleanup
> **Status (2026-08-18): executed.** Phases 0–3 and 5 are done; phase 4 became
-> moot when the cce-cloud popups were replaced by in-surface menus. The one
-> open item from phase 2: `json_find_key` survives as the warn-once fallback
-> behind `pointer_or_fuzzy` — delete it after a release of quiet logs. Phase 6
-> follow-ups remain available.
+> moot when the cce-cloud popups were replaced by in-surface menus. Phase 2's
+> final step landed too: `json_find_key` and the warn-once fallback are
+> deleted — config lookup is explicit JSON pointers only. Phase 6 follow-ups
+> remain available.
Fixes for the issues identified in the 2026-07 review: `main.rs` carrying five jobs,
a fuzzy config-lookup layer that partially duplicates `cce-ui`, stringly-typed
diff --git a/src/config.rs b/src/config.rs
index 7f36302..20d20cc 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,135 +1,69 @@
//! Config access: cached KDL config lookup, color/font/dimension readers,
//! and resolution of the ccectl binary.
//!
-//! Every key has an explicit JSON-pointer location (the canonical nesting in
-//! config.kdl). Reads try the pointer first and fall back to the legacy fuzzy
-//! search ([`json_find_key`]), warning once per key when only the fallback
-//! hits — those warnings mean the key sits somewhere non-canonical in the
-//! user's config (or collides with an unrelated key of the same name).
+//! Every key is read by an explicit JSON pointer — the canonical nesting in
+//! config.kdl. A key at a non-canonical location simply does not resolve.
+//! (The legacy fuzzy search that split snake_case keys across nesting was
+//! deleted after a release of quiet fallback-warning logs.)
//!
//! Color space: text colors stay **raw sRGB** — they end up as `[u8; 3]` for
//! cosmic-text (see `StyledLabel`), which expects sRGB. Quad/box colors are
//! converted with [`cce_ui::color::srgb_to_linear`] because the Vulkan pipeline
//! samples them in linear space.
-use std::collections::HashSet;
-use std::sync::{Mutex, OnceLock};
-
pub(crate) fn get_cached_config() -> serde_json::Value {
cce_ui::config::cached_config()
}
-/// Legacy fuzzy lookup: exact key, then snake_case prefixes split across
-/// nesting, then a depth-first search of every object. Kept only as the
-/// fallback for configs that predate the canonical pointer locations.
-pub(crate) fn json_find_key<'a>(val: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> {
- fn find_recursive<'a>(val: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> {
- if let Some(obj) = val.as_object() {
- if let Some(v) = obj.get(key) {
- return Some(v);
- }
- let parts: Vec<&str> = key.split('_').collect();
- for i in 1..parts.len() {
- let (prefix_parts, suffix_parts) = parts.split_at(i);
- let prefix = prefix_parts.join("_");
- let suffix = suffix_parts.join("_");
- if let Some(sub_val) = obj.get(&prefix) {
- if let Some(found) = find_recursive(sub_val, &suffix) {
- return Some(found);
- }
- }
- }
- for (_, sub_val) in obj.iter() {
- if let Some(found) = find_recursive(sub_val, key) {
- return Some(found);
- }
- }
- }
- None
- }
- find_recursive(val, key)
-}
-
-fn warn_fuzzy_once(pointer: &str, legacy_key: &str) {
- static WARNED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
- let warned = WARNED.get_or_init(|| Mutex::new(HashSet::new()));
- if warned.lock().map_or(false, |mut set| set.insert(pointer.to_string())) {
- log::warn!(
- "config key '{}' not found at its canonical location {} — \
- resolved by fuzzy search instead; consider moving it in config.kdl",
- legacy_key, pointer
- );
- }
-}
-
-/// Look up `pointer` in `val`, falling back to the legacy fuzzy search for
-/// `legacy_key` (with a once-per-pointer warning when only the fallback hits).
-pub(crate) fn pointer_or_fuzzy<'a>(
- val: &'a serde_json::Value,
- pointer: &str,
- legacy_key: &str,
-) -> Option<&'a serde_json::Value> {
- if let Some(v) = val.pointer(pointer) {
- return Some(v);
- }
- let found = json_find_key(val, legacy_key);
- if found.is_some() {
- warn_fuzzy_once(pointer, legacy_key);
- }
- found
-}
-
-fn cfg_f32(pointer: &str, legacy_key: &str) -> Option<f32> {
- let val = get_cached_config();
- pointer_or_fuzzy(&val, pointer, legacy_key).and_then(|v| v.as_f64()).map(|n| n as f32)
+fn cfg_f32(pointer: &str) -> Option<f32> {
+ get_cached_config().pointer(pointer).and_then(|v| v.as_f64()).map(|n| n as f32)
}
-fn cfg_string(pointer: &str, legacy_key: &str) -> Option<String> {
- let val = get_cached_config();
- pointer_or_fuzzy(&val, pointer, legacy_key).and_then(|v| v.as_str()).map(|s| s.to_string())
+fn cfg_string(pointer: &str) -> Option<String> {
+ get_cached_config().pointer(pointer).and_then(|v| v.as_str()).map(|s| s.to_string())
}
/// A text color: raw sRGB RGB with alpha forced to 1.0 (cosmic-text consumes
/// text colors as sRGB `[u8; 3]`; alpha is not carried by the text path).
-pub(crate) fn text_color_from(val: &serde_json::Value, pointer: &str, legacy_key: &str) -> Option<[f32; 4]> {
- let s = pointer_or_fuzzy(val, pointer, legacy_key)?.as_str()?;
+pub(crate) fn text_color_from(val: &serde_json::Value, pointer: &str) -> Option<[f32; 4]> {
+ let s = val.pointer(pointer)?.as_str()?;
let [r, g, b, _] = cce_ui::color::parse_hex_rgba(s)?;
Some([r, g, b, 1.0])
}
/// A quad/box color: RGB converted sRGB→linear for the Vulkan pipeline, alpha
/// kept raw.
-pub(crate) fn quad_color_from(val: &serde_json::Value, pointer: &str, legacy_key: &str) -> Option<[f32; 4]> {
- let s = pointer_or_fuzzy(val, pointer, legacy_key)?.as_str()?;
+pub(crate) fn quad_color_from(val: &serde_json::Value, pointer: &str) -> Option<[f32; 4]> {
+ let s = val.pointer(pointer)?.as_str()?;
cce_ui::color::parse_hex_rgba_linear(s)
}
-fn cfg_text_color(pointer: &str, legacy_key: &str) -> Option<[f32; 4]> {
- text_color_from(&get_cached_config(), pointer, legacy_key)
+fn cfg_text_color(pointer: &str) -> Option<[f32; 4]> {
+ text_color_from(&get_cached_config(), pointer)
}
-fn cfg_quad_color(pointer: &str, legacy_key: &str) -> Option<[f32; 4]> {
- quad_color_from(&get_cached_config(), pointer, legacy_key)
+fn cfg_quad_color(pointer: &str) -> Option<[f32; 4]> {
+ quad_color_from(&get_cached_config(), pointer)
}
pub(crate) fn read_normal_color_from_config() -> Option<[f32; 4]> {
// App-native `module { text_color }` first (a TEXT color — raw sRGB, per
// the color-space split in CLAUDE.md), then the shared status key.
- cfg_text_color("/module/text_color", "module_text_color")
- .or_else(|| cfg_text_color("/style/status/normal_color", "status_normal_color"))
+ cfg_text_color("/module/text_color")
+ .or_else(|| cfg_text_color("/style/status/normal_color"))
}
pub(crate) fn read_disabled_color_from_config() -> Option<[f32; 4]> {
- cfg_text_color("/style/status/disabled_color", "disabled_color")
+ cfg_text_color("/style/status/disabled_color")
}
pub(crate) fn read_status_font_from_config() -> String {
// App-native `module { font }` first (a family; an embedded size ranks
// below module { font_size } in the size chain), then the shared key.
- if let Some(font_str) = cfg_string("/module/font", "module_font") {
+ if let Some(font_str) = cfg_string("/module/font") {
return font_str;
}
- if let Some(font_str) = cfg_string("/style/status/font", "status_font") {
+ if let Some(font_str) = cfg_string("/style/status/font") {
return font_str;
}
@@ -146,8 +80,8 @@ pub(crate) fn read_status_height_from_config() -> f32 {
// App-native `module { height }` first — the compositor reads the same
// key for the arrange pass's segment height and reserved strip, so the
// two sides always agree — then the shared layout { bar_height }.
- cfg_f32("/module/height", "module_height")
- .or_else(|| cfg_f32("/layout/bar_height", "bar_height"))
+ cfg_f32("/module/height")
+ .or_else(|| cfg_f32("/layout/bar_height"))
.unwrap_or(28.0)
}
@@ -155,30 +89,30 @@ pub(crate) fn read_status_font_size_from_config() -> f32 {
// App-native `module { font_size }` wins over everything — including the
// size embedded in the shared font string ("Chivo Mono 14"), which stays
// the fallback along with the shared status_font_size key.
- if let Some(size) = cfg_f32("/module/font_size", "module_font_size") {
+ if let Some(size) = cfg_f32("/module/font_size") {
return size;
}
- if let Some(font_str) = cfg_string("/module/font", "module_font") {
+ if let Some(font_str) = cfg_string("/module/font") {
let (_, parsed_size) = cce_ui::layout::parse_font_string(&font_str);
if let Some(size) = parsed_size {
return size;
}
}
- if let Some(font_str) = cfg_string("/style/status/font", "status_font") {
+ if let Some(font_str) = cfg_string("/style/status/font") {
let (_, parsed_size) = cce_ui::layout::parse_font_string(&font_str);
if let Some(size) = parsed_size {
return size;
}
}
- cfg_f32("/style/status/font_size", "status_font_size").unwrap_or(11.0)
+ cfg_f32("/style/status/font_size").unwrap_or(11.0)
}
pub(crate) fn read_status_padding_from_config() -> f32 {
// App-native `module { padding }` first (the text inset inside each
// module box — bar-side only), then the shared status key.
- cfg_f32("/module/padding", "module_padding")
- .or_else(|| cfg_f32("/style/status/padding", "status_padding"))
+ cfg_f32("/module/padding")
+ .or_else(|| cfg_f32("/style/status/padding"))
.unwrap_or(8.0)
}
@@ -186,8 +120,8 @@ pub(crate) fn read_status_module_spacing_from_config() -> f32 {
// App-native `module { spacing }` first (the same value the compositor
// reads for the gap BETWEEN segments — this reader only matters for the
// intra-surface layout of a multi-module bar), then the shared key.
- cfg_f32("/module/spacing", "module_spacing")
- .or_else(|| cfg_f32("/style/status/module_spacing", "status_module_spacing"))
+ cfg_f32("/module/spacing")
+ .or_else(|| cfg_f32("/style/status/module_spacing"))
.unwrap_or(8.0)
}
@@ -222,7 +156,8 @@ pub(crate) fn parse_font_for_alias(content: &str, alias: &str) -> Option<String>
/// and converted. (This unifies the two previous readers, one of which only
/// degree-converted integer values.)
pub(crate) fn light_source_position_from(val: &serde_json::Value) -> f32 {
- let raw = pointer_or_fuzzy(val, "/window_manager/light_source_position", "light_source_position")
+ let raw = val
+ .pointer("/window_manager/light_source_position")
.and_then(|v| v.as_f64())
.map(|f| f as f32)
.unwrap_or(2.356_194_5); // 135°, the compositor default
@@ -238,15 +173,15 @@ pub(crate) fn read_light_source_position_from_config() -> f32 {
}
pub(crate) fn read_status_background_blur_from_config() -> f32 {
- cfg_f32("/style/status/background_blur", "status_background_blur").unwrap_or(0.0)
+ cfg_f32("/style/status/background_blur").unwrap_or(0.0)
}
pub(crate) fn read_status_box_background_color_from_config() -> Option<[f32; 4]> {
// App-native `module { background_color }` first (a quad color — the
// sRGB→linear conversion applies, per the color-space split in
// CLAUDE.md), then the shared status key, then the built-in default.
- let mut color = cfg_quad_color("/module/background_color", "module_background_color")
- .or_else(|| cfg_quad_color("/style/status/background_color", "status_background_color"))
+ let mut color = cfg_quad_color("/module/background_color")
+ .or_else(|| cfg_quad_color("/style/status/background_color"))
.unwrap_or_else(|| {
let c = cce_ui::color::srgb_to_linear(0x15 as f32 / 255.0);
let b = cce_ui::color::srgb_to_linear(0x20 as f32 / 255.0);
@@ -269,7 +204,7 @@ pub(crate) fn read_status_box_corner_radius_from_config() -> f32 {
// Unlike the other module styling keys there is deliberately no shared
// `style { status ... }` fallback — the radius is purely bar-side
// cosmetics (the old `status_box_corner_radius` rung was removed).
- cfg_f32("/module/corner_radius", "module_corner_radius").unwrap_or(4.0)
+ cfg_f32("/module/corner_radius").unwrap_or(4.0)
}
/// Bevel treatment for the module boxes.
@@ -284,7 +219,7 @@ pub(crate) enum StatusBoxBevel {
/// `style { status box_bevel="raised"|"inset" }`; absent, `"none"`, or any
/// other value keeps the flat boxes.
pub(crate) fn read_status_box_bevel_from_config() -> Option<StatusBoxBevel> {
- match cfg_string("/style/status/box_bevel", "status_box_bevel")?.to_ascii_lowercase().as_str() {
+ match cfg_string("/style/status/box_bevel")?.to_ascii_lowercase().as_str() {
"raised" => Some(StatusBoxBevel::Raised),
"inset" => Some(StatusBoxBevel::Inset),
_ => None,
@@ -295,7 +230,7 @@ pub(crate) fn read_status_box_bevel_from_config() -> Option<StatusBoxBevel> {
/// in logical px. The DE-wide `bevel_width` (~9px) is window-scale; module
/// boxes in a ~24px bar want a much tighter lip.
pub(crate) fn read_status_box_bevel_depth_from_config() -> f32 {
- cfg_f32("/style/status/box_bevel_depth", "status_box_bevel_depth").unwrap_or(3.0)
+ cfg_f32("/style/status/box_bevel_depth").unwrap_or(3.0)
}
pub(crate) fn get_ccectl_cmd() -> String {
@@ -371,74 +306,14 @@ style {
);
}
- // --- json_find_key (legacy fuzzy fallback) ---
-
- #[test]
- fn find_key_exact_match_at_top_level() {
- let val = serde_json::json!({"bar_height": 30.0});
- assert_eq!(json_find_key(&val, "bar_height").and_then(|v| v.as_f64()), Some(30.0));
- }
-
- #[test]
- fn find_key_splits_snake_case_across_nesting() {
- // "status_background_color" matches status { background_color }.
- let val = serde_json::json!({"style": {"status": {"background_color": "#101010"}}});
- assert_eq!(
- json_find_key(&val, "status_background_color").and_then(|v| v.as_str()),
- Some("#101010")
- );
- }
-
- #[test]
- fn find_key_exact_match_wins_over_split() {
- // An exact "status_font" key beats descending into status { font }.
- let val = serde_json::json!({
- "status_font": "Exact Font",
- "status": {"font": "Split Font"}
- });
- assert_eq!(
- json_find_key(&val, "status_font").and_then(|v| v.as_str()),
- Some("Exact Font")
- );
- }
-
- #[test]
- fn find_key_recurses_into_unrelated_parents() {
- // The key is found even under a parent the key name never mentions.
- let val = serde_json::json!({"unrelated": {"deeply": {"bar_height": 42.0}}});
- assert_eq!(json_find_key(&val, "bar_height").and_then(|v| v.as_f64()), Some(42.0));
- }
-
- #[test]
- fn find_key_miss_is_none() {
- let val = serde_json::json!({"style": {"status": {}}});
- assert!(json_find_key(&val, "nonexistent_key").is_none());
- }
-
- // --- pointer_or_fuzzy ---
-
- #[test]
- fn pointer_wins_over_fuzzy_match() {
- // With both a canonical and a stray same-named key, the pointer wins.
- let val = serde_json::json!({
- "style": {"status": {"normal_color": "#111111"}},
- "stray": {"status_normal_color": "#222222"}
- });
- assert_eq!(
- pointer_or_fuzzy(&val, "/style/status/normal_color", "status_normal_color")
- .and_then(|v| v.as_str()),
- Some("#111111")
- );
- }
+ // --- pointer-only lookup ---
#[test]
- fn pointer_miss_falls_back_to_fuzzy() {
+ fn non_canonical_locations_do_not_resolve() {
+ // A key parked somewhere other than its canonical nesting is simply
+ // absent — the fuzzy search that used to find these is gone.
let val = serde_json::json!({"stray": {"status_normal_color": "#222222"}});
- assert_eq!(
- pointer_or_fuzzy(&val, "/style/status/normal_color", "status_normal_color")
- .and_then(|v| v.as_str()),
- Some("#222222")
- );
+ assert!(text_color_from(&val, "/style/status/normal_color").is_none());
}
// --- light_source_position ---
@@ -473,11 +348,11 @@ style {
let linear = cce_ui::color::srgb_to_linear(raw);
// Text color: raw sRGB, alpha forced to 1.0 (cosmic-text takes sRGB u8).
- let text = text_color_from(&val, "/style/status/normal_color", "status_normal_color").unwrap();
+ let text = text_color_from(&val, "/style/status/normal_color").unwrap();
assert_rgba_close(text, [raw, raw, raw, 1.0]);
// Quad color: RGB linearized for the Vulkan pipeline, alpha raw.
- let quad = quad_color_from(&val, "/style/status/background_color", "status_background_color").unwrap();
+ let quad = quad_color_from(&val, "/style/status/background_color").unwrap();
assert_rgba_close(quad, [linear, linear, linear, raw]);
}
}
diff --git a/src/main.rs b/src/main.rs
index 2ab14d1..91bd209 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -888,7 +888,7 @@ fn module_side_from_json(val: &serde_json::Value, name: &str) -> Side {
// Canonical: `layout { status_bar <name>="top-left" }` — the same key the
// compositor persists a super+drag snap into.
let pointer = format!("/layout/status_bar/{}", name);
- if let Some(side_val) = pointer_or_fuzzy(val, &pointer, name) {
+ if let Some(side_val) = val.pointer(&pointer) {
if let Some(side_str) = side_val.as_str() {
match side_str.to_lowercase().as_str() {
"left" | "top-left" | "bottom-left" | "top-center" | "bottom-center" => return Side::Left,
@@ -1867,11 +1867,14 @@ mod tests {
// --- module_side_from_json ---
+ fn side_cfg(name: &str, value: &str) -> serde_json::Value {
+ serde_json::json!({"layout": {"status_bar": {name: value}}})
+ }
+
#[test]
fn module_side_explicit_values() {
- let val = serde_json::json!({"clock": "left", "window": "right"});
- assert_eq!(module_side_from_json(&val, "clock"), Side::Left);
- assert_eq!(module_side_from_json(&val, "window"), Side::Right);
+ assert_eq!(module_side_from_json(&side_cfg("clock", "left"), "clock"), Side::Left);
+ assert_eq!(module_side_from_json(&side_cfg("window", "right"), "window"), Side::Right);
}
#[test]
@@ -1885,20 +1888,16 @@ mod tests {
("bottom-right", Side::Right),
("TOP-LEFT", Side::Left), // case-insensitive
] {
- let val = serde_json::json!({"cpu": snap});
- assert_eq!(module_side_from_json(&val, "cpu"), side, "snap {}", snap);
+ assert_eq!(module_side_from_json(&side_cfg("cpu", snap), "cpu"), side, "snap {}", snap);
}
}
#[test]
- fn module_side_canonical_location_wins() {
- // `layout { status_bar clock="left" }` beats a stray same-named key
- // the fuzzy fallback would otherwise find.
- let val = serde_json::json!({
- "layout": {"status_bar": {"clock": "left"}},
- "stray": {"clock": "right"}
- });
- assert_eq!(module_side_from_json(&val, "clock"), Side::Left);
+ fn module_side_only_canonical_location_resolves() {
+ // Only `layout { status_bar <name>=... }` counts; a same-named key
+ // anywhere else is ignored (the fuzzy search is gone).
+ let val = serde_json::json!({"stray": {"clock": "left"}});
+ assert_eq!(module_side_from_json(&val, "clock"), Side::Right);
}
#[test]
@@ -1911,9 +1910,8 @@ mod tests {
#[test]
fn module_side_unknown_value_falls_through_to_default() {
- let val = serde_json::json!({"window": "sideways", "clock": "sideways"});
- assert_eq!(module_side_from_json(&val, "window"), Side::Left);
- assert_eq!(module_side_from_json(&val, "clock"), Side::Right);
+ assert_eq!(module_side_from_json(&side_cfg("window", "sideways"), "window"), Side::Left);
+ assert_eq!(module_side_from_json(&side_cfg("clock", "sideways"), "clock"), Side::Right);
}
#[test]