git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

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

 Cargo.toml    |   1 +
 src/config.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 163 insertions(+), 9 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 47ab128..bfba6ce 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ xkeysym = "0.2"
 resvg = "0.41.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
+kdl = "4.6"
 libc = "0.2"
 log = "0.4"
 rfd = "0.15"
diff --git a/src/config.rs b/src/config.rs
index c0ad2ff..3744676 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,6 +1,105 @@
 use std::fs;
 use serde_json::Value;
 
+fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
+    let mut map = serde_json::Map::new();
+    for node in doc.nodes() {
+        let name = node.name().value().to_string();
+        
+        let mut node_map = serde_json::Map::new();
+        let mut has_props = false;
+        for entry in node.entries() {
+            if let Some(prop_name) = entry.name() {
+                has_props = true;
+                let j_val = match entry.value() {
+                    kdl::KdlValue::Bool(b) => serde_json::Value::Bool(*b),
+                    kdl::KdlValue::Base2(i) |
+                    kdl::KdlValue::Base8(i) |
+                    kdl::KdlValue::Base10(i) |
+                    kdl::KdlValue::Base16(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
+                    kdl::KdlValue::Base10Float(f) => {
+                        if let Some(num) = serde_json::Number::from_f64(*f) {
+                            serde_json::Value::Number(num)
+                        } else {
+                            serde_json::Value::Null
+                        }
+                    }
+                    kdl::KdlValue::String(s) |
+                    kdl::KdlValue::RawString(s) => serde_json::Value::String(s.clone()),
+                    kdl::KdlValue::Null => serde_json::Value::Null,
+                };
+                node_map.insert(prop_name.value().to_string(), j_val);
+            }
+        }
+
+        let val = if let Some(children) = node.children() {
+            kdl_to_json(children)
+        } else if has_props {
+            serde_json::Value::Object(node_map)
+        } else if let Some(entry) = node.entries().first() {
+            match entry.value() {
+                kdl::KdlValue::Bool(b) => serde_json::Value::Bool(*b),
+                kdl::KdlValue::Base2(i) |
+                kdl::KdlValue::Base8(i) |
+                kdl::KdlValue::Base10(i) |
+                kdl::KdlValue::Base16(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
+                kdl::KdlValue::Base10Float(f) => {
+                    if let Some(num) = serde_json::Number::from_f64(*f) {
+                        serde_json::Value::Number(num)
+                    } else {
+                        serde_json::Value::Null
+                    }
+                }
+                kdl::KdlValue::String(s) |
+                kdl::KdlValue::RawString(s) => serde_json::Value::String(s.clone()),
+                kdl::KdlValue::Null => serde_json::Value::Null,
+            }
+        } else {
+            serde_json::Value::Null
+        };
+
+        if let Some(existing) = map.remove(&name) {
+            match existing {
+                serde_json::Value::Array(mut arr) => {
+                    arr.push(val);
+                    map.insert(name, serde_json::Value::Array(arr));
+                }
+                other => {
+                    map.insert(name, serde_json::Value::Array(vec![other, val]));
+                }
+            }
+        } else {
+            let list_names = ["keybind", "pointer_bind", "gesture_bind", "mode_rule", "tag_layout", "startup", "device"];
+            if list_names.contains(&name.as_str()) {
+                map.insert(name, serde_json::Value::Array(vec![val]));
+            } else {
+                map.insert(name, val);
+            }
+        }
+    }
+    serde_json::Value::Object(map)
+}
+
+pub fn parse_kdl_to_json(content: &str) -> serde_json::Value {
+    if let Ok(doc) = content.parse::<kdl::KdlDocument>() {
+        let val = kdl_to_json(&doc);
+        if let Some(obj) = val.as_object() {
+            if obj.is_empty() && (content.trim().starts_with('{') || content.trim().starts_with('[')) {
+                if let Ok(j) = serde_json::from_str::<serde_json::Value>(content) {
+                    return j;
+                }
+            }
+        }
+        val
+    } else {
+        if let Ok(j) = serde_json::from_str::<serde_json::Value>(content) {
+            j
+        } else {
+            serde_json::json!({})
+        }
+    }
+}
+
 pub fn update_json_in_memory(val_obj: &mut Value, key: &str, value: &str, default_section: &str) -> bool {
     let j_val = if let Ok(parsed_val) = serde_json::from_str::<Value>(value) {
         parsed_val
@@ -34,6 +133,58 @@ pub fn update_json_in_memory(val_obj: &mut Value, key: &str, value: &str, defaul
     updated
 }
 
+pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str, default_section: &str) -> bool {
+    let section_node = if let Some(node) = doc.nodes_mut().iter_mut().find(|n| n.name().value() == default_section) {
+        node
+    } else {
+        if let Ok(new_node) = format!("{}\n", default_section).parse::<kdl::KdlNode>() {
+            doc.nodes_mut().push(new_node);
+            doc.nodes_mut().last_mut().unwrap()
+        } else {
+            return false;
+        }
+    };
+
+    let children = section_node.ensure_children();
+
+    let child_node = if let Some(child) = children.nodes_mut().iter_mut().find(|n| n.name().value() == key) {
+        child
+    } else {
+        if let Ok(new_child) = format!("{}\n", key).parse::<kdl::KdlNode>() {
+            children.nodes_mut().push(new_child);
+            children.nodes_mut().last_mut().unwrap()
+        } else {
+            return false;
+        }
+    };
+
+    let (kdl_val, kdl_ty) = if let Ok(b) = value.parse::<bool>() {
+        (kdl::KdlValue::Bool(b), Some("bool"))
+    } else if value.starts_with('#') {
+        (kdl::KdlValue::String(value.to_string()), Some("color"))
+    } else if value.contains('.') {
+        if let Ok(f) = value.parse::<f64>() {
+            (kdl::KdlValue::Base10Float(f), Some("f64"))
+        } else {
+            (kdl::KdlValue::String(value.to_string()), None)
+        }
+    } else if let Ok(i) = value.parse::<i64>() {
+        (kdl::KdlValue::Base10(i), Some("i64"))
+    } else {
+        let s = value.trim_matches('"').to_string();
+        (kdl::KdlValue::String(s), None)
+    };
+
+    child_node.entries_mut().clear();
+    let mut entry = kdl::KdlEntry::new(kdl_val);
+    if let Some(ty) = kdl_ty {
+        entry.set_ty(ty);
+    }
+    child_node.entries_mut().push(entry);
+
+    true
+}
+
 pub fn get_config_path() -> std::path::PathBuf {
     let dir = if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
         if !xdg_config.is_empty() {
@@ -46,7 +197,7 @@ pub fn get_config_path() -> std::path::PathBuf {
         let home = std::env::var("HOME").unwrap_or_else(|_| "/home/lsgalante".to_string());
         std::path::PathBuf::from(home).join(".config")
     };
-    dir.join("cce").join("config.json")
+    dir.join("cce").join("config.kdl")
 }
 
 fn perform_rolling_backup(path: &str) {
@@ -62,13 +213,13 @@ fn perform_rolling_backup(path: &str) {
         return;
     }
     for i in (1..=4).rev() {
-        let src = backup_dir.join(format!("config.json.{}.bak", i));
-        let dst = backup_dir.join(format!("config.json.{}.bak", i + 1));
+        let src = backup_dir.join(format!("config.kdl.{}.bak", i));
+        let dst = backup_dir.join(format!("config.kdl.{}.bak", i + 1));
         if src.exists() {
             let _ = fs::rename(src, dst);
         }
     }
-    let dst = backup_dir.join("config.json.1.bak");
+    let dst = backup_dir.join("config.kdl.1.bak");
     let _ = fs::copy(path, dst);
 }
 
@@ -89,12 +240,14 @@ fn safe_write(path: &str, content: &str) -> bool {
 
 pub fn write_config_value(path: &str, key: &str, value: &str, default_section: &str) -> bool {
     let content = fs::read_to_string(path).unwrap_or_default();
-    let mut val: Value = serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}));
+    let mut doc = match content.parse::<kdl::KdlDocument>() {
+        Ok(d) => d,
+        Err(_) => kdl::KdlDocument::new(),
+    };
     
-    if update_json_in_memory(&mut val, key, value, default_section) {
-        if let Ok(updated_str) = serde_json::to_string_pretty(&val) {
-            return safe_write(path, &updated_str);
-        }
+    if update_kdl_in_memory(&mut doc, key, value, default_section) {
+        let updated_str = doc.to_string();
+        return safe_write(path, &updated_str);
     }
     false
 }