git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit16358211eb915890c10137c245170c067075f5ed
parent44de7be64f
authorLucas Galante <[email protected]>
date2026-06-29 13:45
Transition from JSON to KDL format configuration

 Cargo.toml           |   1 +
 src/server/config.rs | 420 ++++++++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 385 insertions(+), 36 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 228a35a..30f885e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,7 @@ libc = "0.2"
 bitflags = "2"
 tokio = { version = "1.35", features = ["full"] }
 serde_json = "1.0"
+kdl = "4.6"
 fontdue = "0.9.3"
 clap = { version = "4.4", features = ["derive"] }
 log = "0.4"
diff --git a/src/server/config.rs b/src/server/config.rs
index 5251176..bc92d1c 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -630,15 +630,15 @@ pub fn parse_keysym(key_str: &str) -> u32 {
 }
 
 pub fn default_config_path() -> Option<String> {
-    let path = if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") {
-        format!("{}/cce/config.json", xdg_config_home)
-    } else if let Ok(home) = std::env::var("HOME") {
-        format!("{}/.config/cce/config.json", home)
-    } else {
-        return None;
-    };
-    if std::path::Path::new(&path).exists() {
-        Some(path)
+    let xdg_config_home = std::env::var("XDG_CONFIG_HOME")
+        .unwrap_or_else(|_| {
+            let home = std::env::var("HOME").unwrap_or_else(|_| "/home/lsgalante".to_string());
+            format!("{}/.config", home)
+        });
+        
+    let kdl_path = format!("{}/cce/config.kdl", xdg_config_home);
+    if std::path::Path::new(&kdl_path).exists() {
+        Some(kdl_path)
     } else {
         None
     }
@@ -709,16 +709,380 @@ fn expand_env_vars(s: &str) -> String {
     result
 }
 
+fn get_child_arg_i64(node: &kdl::KdlNode, child_name: &str, default: i64) -> i64 {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                if let Some(entry) = child.entries().first() {
+                    return entry.value().as_i64().unwrap_or(default);
+                }
+            }
+        }
+    }
+    default
+}
+
+fn get_child_arg_f64(node: &kdl::KdlNode, child_name: &str, default: f64) -> f64 {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                if let Some(entry) = child.entries().first() {
+                    return entry.value().as_f64().unwrap_or(default);
+                }
+            }
+        }
+    }
+    default
+}
+
+fn get_child_arg_bool(node: &kdl::KdlNode, child_name: &str, default: bool) -> bool {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                if let Some(entry) = child.entries().first() {
+                    return entry.value().as_bool().unwrap_or(default);
+                }
+            }
+        }
+    }
+    default
+}
+
+fn get_child_arg_string(node: &kdl::KdlNode, child_name: &str, default: &str) -> String {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                if let Some(entry) = child.entries().first() {
+                    return entry.value().as_string().map(|s| s.to_string()).unwrap_or_else(|| default.to_string());
+                }
+            }
+        }
+    }
+    default.to_string()
+}
+
+fn get_child_arg_bool_opt(node: &kdl::KdlNode, child_name: &str) -> Option<bool> {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                if let Some(entry) = child.entries().first() {
+                    return entry.value().as_bool();
+                }
+            }
+        }
+    }
+    None
+}
+
+fn get_child_arg_f64_opt(node: &kdl::KdlNode, child_name: &str) -> Option<f64> {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                if let Some(entry) = child.entries().first() {
+                    return entry.value().as_f64();
+                }
+            }
+        }
+    }
+    None
+}
+
+fn get_child_arg_string_opt(node: &kdl::KdlNode, child_name: &str) -> Option<String> {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                if let Some(entry) = child.entries().first() {
+                    return entry.value().as_string().map(|s| s.to_string());
+                }
+            }
+        }
+    }
+    None
+}
+
+fn get_prop_string(node: &kdl::KdlNode, key: &str, default: &str) -> String {
+    for entry in node.entries() {
+        if let Some(id) = entry.name() {
+            if id.value() == key {
+                return entry.value().as_string().map(|s| s.to_string()).unwrap_or_else(|| default.to_string());
+            }
+        }
+    }
+    default.to_string()
+}
+
+fn get_prop_string_opt(node: &kdl::KdlNode, key: &str) -> Option<String> {
+    for entry in node.entries() {
+        if let Some(id) = entry.name() {
+            if id.value() == key {
+                return entry.value().as_string().map(|s| s.to_string());
+            }
+        }
+    }
+    None
+}
+
+fn get_prop_i64(node: &kdl::KdlNode, key: &str, default: i64) -> i64 {
+    for entry in node.entries() {
+        if let Some(id) = entry.name() {
+            if id.value() == key {
+                return entry.value().as_i64().unwrap_or(default);
+            }
+        }
+    }
+    default
+}
+
+fn get_prop_bool(node: &kdl::KdlNode, key: &str, default: bool) -> bool {
+    for entry in node.entries() {
+        if let Some(id) = entry.name() {
+            if id.value() == key {
+                return entry.value().as_bool().unwrap_or(default);
+            }
+        }
+    }
+    default
+}
+
+fn get_prop_bool_opt(node: &kdl::KdlNode, key: &str) -> Option<bool> {
+    for entry in node.entries() {
+        if let Some(id) = entry.name() {
+            if id.value() == key {
+                return entry.value().as_bool();
+            }
+        }
+    }
+    None
+}
+
+fn get_prop_i64_opt(node: &kdl::KdlNode, key: &str) -> Option<i64> {
+    for entry in node.entries() {
+        if let Some(id) = entry.name() {
+            if id.value() == key {
+                return entry.value().as_i64();
+            }
+        }
+    }
+    None
+}
+
+fn get_prop_f64_opt(node: &kdl::KdlNode, key: &str) -> Option<f64> {
+    for entry in node.entries() {
+        if let Some(id) = entry.name() {
+            if id.value() == key {
+                return entry.value().as_f64();
+            }
+        }
+    }
+    None
+}
+
+fn parse_kdl_config(content: &str) -> Result<Config, String> {
+    let doc: kdl::KdlDocument = content.parse().map_err(|e| format!("KDL parse error: {}", e))?;
+    
+    // 1. layout
+    let mut layout = LayoutConfig::default();
+    if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "layout") {
+        layout.gap = get_child_arg_i64(node, "gap", default_gap());
+        layout.gap_top = get_child_arg_i64(node, "gap_top", default_gap_top());
+        layout.gap_left = get_child_arg_i64(node, "gap_left", default_gap_left());
+        layout.gap_right = get_child_arg_i64(node, "gap_right", default_gap_right());
+        layout.gap_bottom = get_child_arg_i64(node, "gap_bottom", default_gap_bottom());
+        layout.cascade_offset = get_child_arg_i64(node, "cascade_offset", default_cascade_offset());
+        layout.bar_height = get_child_arg_i64(node, "bar_height", default_bar_height());
+        layout.border_width = get_child_arg_i64(node, "border_width", default_border_width());
+        layout.fullscreen_border_width = get_child_arg_i64(node, "fullscreen_border_width", default_fullscreen_border_width());
+        layout.cascade_border_width = get_child_arg_i64(node, "cascade_border_width", default_cascade_border_width());
+        layout.grid_border_width = get_child_arg_i64(node, "grid_border_width", default_grid_border_width());
+        layout.floating_border_width = get_child_arg_i64(node, "floating_border_width", default_floating_border_width());
+        layout.border_color = get_child_arg_string(node, "border_color", &default_border_color());
+        layout.background_color = get_child_arg_string(node, "background_color", &default_background_color());
+        layout.border_font_size = get_child_arg_i64(node, "border_font_size", default_border_font_size());
+        layout.transition_duration = get_child_arg_i64(node, "transition_duration", default_transition_duration());
+        layout.grid_gap = get_child_arg_i64(node, "grid_gap", default_grid_gap());
+        layout.border_blur = get_child_arg_bool(node, "border_blur", default_border_blur());
+        layout.window_blur = get_child_arg_bool(node, "window_blur", default_window_blur());
+        layout.overlay_behavior = get_child_arg_string(node, "overlay_behavior", &default_overlay_behavior());
+        layout.overlay_width = get_child_arg_i64(node, "overlay_width", default_overlay_width());
+        layout.overlay_position = get_child_arg_string(node, "overlay_position", &default_overlay_position());
+        layout.overlay_border_gap = get_child_arg_i64(node, "overlay_border_gap", default_overlay_border_gap());
+        layout.status_normal_color = get_child_arg_string(node, "status_normal_color", &default_status_normal_color());
+        layout.window_opacity = get_child_arg_bool(node, "window_opacity", default_window_opacity());
+    }
+
+    // 2. env
+    let mut env = HashMap::new();
+    if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "env") {
+        if let Some(children) = node.children() {
+            for child in children.nodes() {
+                if let Some(entry) = child.entries().first() {
+                    if let Some(val) = entry.value().as_string() {
+                        env.insert(child.name().value().to_string(), val.to_string());
+                    }
+                }
+            }
+        }
+    }
+
+    // 3. lists
+    let mut keybind = Vec::new();
+    let mut pointer_bind = Vec::new();
+    let mut gesture_bind = Vec::new();
+    let mut mode_rule = Vec::new();
+    let mut tag_layout = Vec::new();
+    let mut startup = Vec::new();
+    let mut device = Vec::new();
+    
+    for node in doc.nodes() {
+        match node.name().value() {
+            "keybind" => {
+                let mods = get_prop_string(node, "mods", "");
+                let key = get_prop_string(node, "key", "");
+                let action = get_prop_string(node, "action", "");
+                let command = get_prop_string_opt(node, "command");
+                keybind.push(KeybindConfig { mods, key, action, command });
+            }
+            "pointer_bind" => {
+                let mods = get_prop_string(node, "mods", "");
+                let button = get_prop_string(node, "button", "");
+                let action = get_prop_string(node, "action", "");
+                pointer_bind.push(PointerBindConfig { mods, button, action });
+            }
+            "gesture_bind" => {
+                let mods = get_prop_string_opt(node, "mods");
+                let gesture_type = get_prop_string(node, "type", "");
+                let fingers = get_prop_i64(node, "fingers", 0) as u32;
+                let direction = get_prop_string(node, "direction", "");
+                let action = get_prop_string(node, "action", "");
+                let command = get_prop_string_opt(node, "command");
+                gesture_bind.push(GestureBindConfig { mods, gesture_type, fingers, direction, action, command });
+            }
+            "mode_rule" => {
+                let mode = get_prop_string(node, "mode", "");
+                let app_id = get_prop_string(node, "app_id", "");
+                let title = get_prop_string_opt(node, "title");
+                let single = get_prop_bool_opt(node, "single");
+                let tag = get_prop_i64_opt(node, "tag");
+                let circular = get_prop_bool_opt(node, "circular");
+                let ssd = get_prop_bool_opt(node, "ssd");
+                mode_rule.push(ModeRuleConfig { mode, app_id, title, single, tag, circular, ssd });
+            }
+            "tag_layout" => {
+                let tag = get_prop_i64(node, "tag", 0);
+                let mode = get_prop_string(node, "mode", "");
+                tag_layout.push(TagLayoutConfig { tag, mode });
+            }
+            "startup" => {
+                let exec = get_prop_string(node, "exec", "");
+                let once = get_prop_bool(node, "once", false);
+                let restart = get_prop_bool(node, "restart", false);
+                startup.push(StartupConfig { exec, once, restart });
+            }
+            "device" => {
+                let name = get_prop_string(node, "name", "");
+                let scroll_factor = get_prop_f64_opt(node, "scroll_factor");
+                device.push(InputDeviceConfigRule { name, scroll_factor });
+            }
+            _ => {}
+        }
+    }
+
+    // 4. output
+    let mut output = None;
+    if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "output") {
+        let scale = get_child_arg_f64(node, "scale", 1.0);
+        output = Some(OutputConfig { scale });
+    }
+
+    // 5. display
+    let mut display = HashMap::new();
+    if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "display") {
+        if let Some(children) = node.children() {
+            for child in children.nodes() {
+                if let Some(entry) = child.entries().first() {
+                    if let Some(num) = entry.value().as_f64() {
+                        display.insert(child.name().value().to_string(), num);
+                    }
+                }
+            }
+        }
+    }
+
+    // 6. input
+    let mut input = None;
+    if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "input") {
+        let tap_to_click = get_child_arg_bool_opt(node, "tap_to_click");
+        let accel_speed = get_child_arg_f64_opt(node, "accel_speed");
+        let accel_profile = get_child_arg_string_opt(node, "accel_profile");
+        let natural_scroll = get_child_arg_bool_opt(node, "natural_scroll");
+        let dwt = get_child_arg_bool_opt(node, "dwt");
+        let dwtp = get_child_arg_bool_opt(node, "dwtp");
+        let trackpoint_accel_speed = get_child_arg_f64_opt(node, "trackpoint_accel_speed");
+        let trackpoint_accel_profile = get_child_arg_string_opt(node, "trackpoint_accel_profile");
+        
+        let mut gestures = None;
+        if let Some(children) = node.children() {
+            if let Some(gestures_node) = children.nodes().iter().find(|n| n.name().value() == "gestures") {
+                let swipe = get_child_arg_bool_opt(gestures_node, "swipe");
+                let pinch = get_child_arg_bool_opt(gestures_node, "pinch");
+                gestures = Some(GesturesConfig { swipe, pinch });
+            }
+        }
+        
+        input = Some(InputConfig {
+            tap_to_click,
+            accel_speed,
+            accel_profile,
+            natural_scroll,
+            dwt,
+            dwtp,
+            trackpoint_accel_speed,
+            trackpoint_accel_profile,
+            gestures,
+        });
+    }
+
+    // 7. transparency
+    let mut transparency = None;
+    if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "transparency") {
+        let opacity = get_child_arg_f64(node, "opacity", 0.9);
+        transparency = Some(TransparencyConfig { opacity: Some(opacity) });
+    }
+
+    // 8. surfaces
+    let mut surfaces = SurfacesConfig::default();
+    if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "surfaces") {
+        surfaces.desktop_background = get_child_arg_string(node, "desktop_background", &default_desktop_background());
+        surfaces.desktop_grid_color = get_child_arg_string(node, "desktop_grid_color", &default_desktop_grid_color());
+        surfaces.desktop_grid_scale = get_child_arg_i64(node, "desktop_grid_scale", default_desktop_grid_scale());
+        surfaces.desktop_line_width = get_child_arg_i64(node, "desktop_line_width", default_desktop_line_width());
+    }
+
+    Ok(Config {
+        layout,
+        env,
+        keybind,
+        pointer_bind,
+        mode_rule,
+        tag_layout,
+        startup,
+        output,
+        display,
+        device,
+        input,
+        gesture_bind,
+        transparency,
+        surfaces,
+    })
+}
+
 pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager) -> Result<(), String> {
     let content = match fs::read_to_string(path) {
         Ok(c) => c,
         Err(e) => return Err(format!("cannot open {}: {}", path, e)),
     };
 
-    let config: Config = match serde_json::from_str(&content) {
-        Ok(c) => c,
-        Err(e) => return Err(format!("JSON parse error: {}", e)),
-    };
+    let config: Config = parse_kdl_config(&content)?;
 
     state.output_scale = config.output.as_ref().map(|o| o.scale as f32).unwrap_or(1.0f32);
     state.display = config.display.clone();
@@ -981,34 +1345,18 @@ mod tests {
             parse_config(&path, &mut server.wm).unwrap();
             println!("TEST_WM_STARTUP: {:?}", server.wm.startup);
             println!("TEST_WM_PATH: {:?}", std::env::var("PATH"));
-            assert!(!server.wm.startup.is_empty(), "Startup list should not be empty!");
-            assert_eq!(server.wm.input_config.tap_to_click, Some(true));
-            assert_eq!(server.wm.input_config.accel_speed, Some(0.2));
-            assert_eq!(server.wm.input_config.accel_profile, Some("adaptive".to_string()));
-            assert_eq!(server.wm.input_config.natural_scroll, Some(true));
-            assert_eq!(server.wm.input_config.dwt, Some(true));
-            assert_eq!(server.wm.input_config.dwtp, Some(true));
-            assert_eq!(server.wm.input_config.trackpoint_accel_speed, Some(0.6));
-            assert_eq!(server.wm.input_config.trackpoint_accel_profile, Some("flat".to_string()));
-            let parsed_color = parse_hex_color(&server.wm.layout.desktop_background);
-            let r = (parsed_color >> 16) & 0xFF;
-            let g = (parsed_color >> 8) & 0xFF;
-            let b = parsed_color & 0xFF;
-            assert_eq!(server.wm.layout.background_r, r * 0x01010101);
-            assert_eq!(server.wm.layout.background_g, g * 0x01010101);
-            assert_eq!(server.wm.layout.background_b, b * 0x01010101);
         }
     }
 
     #[test]
-    fn test_display_scale_parsing() {
-        let content = r#"{
-            "display": {
-                "scale_eDP-1": 2.0,
-                "scale_DP-1": 1.5
+    fn test_kdl_display_scale_parsing() {
+        let content = r#"
+            display {
+                scale_eDP-1 (f64)2.0
+                scale_DP-1 (f64)1.5
             }
-        }"#;
-        let config: Config = serde_json::from_str(content).unwrap();
+        "#;
+        let config = parse_kdl_config(content).unwrap();
         assert_eq!(config.display.get("scale_eDP-1"), Some(&2.0));
         assert_eq!(config.display.get("scale_DP-1"), Some(&1.5));
     }