git.lucas.co / cce-graph
node-based graph editor
git clone https://git.lucas.co/cce-graph.git

commit43e864cb3d2c966845bc3135aff5af80ac18537e
parenta04dfac7c9
authorLucas Galante <[email protected]>
date2026-07-04 16:06
feat: save/load project state as KDL instead of JSON

 src/main.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 48 insertions(+), 5 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index ded96e4..c24f84b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -348,6 +348,39 @@ fn load_project_from_kdl_path(path: &std::path::Path) -> Result<GraphProjectStat
     })
 }
 
+fn save_project_to_kdl_path(path: &std::path::Path, state: &GraphProjectState) -> Result<(), Box<dyn std::error::Error>> {
+    let mut kdl = String::new();
+    kdl.push_str(&format!("name {:?}\n", state.name));
+    kdl.push_str(&format!("show_grid {}\n", state.show_grid));
+    kdl.push_str(&format!("uniform_background {}\n", state.uniform_background));
+    kdl.push_str(&format!("opacity {}\n\n", state.opacity));
+
+    for node in &state.nodes {
+        kdl.push_str(&format!("node {:?} {{\n", node.name));
+        kdl.push_str(&format!("    position {} {}\n", node.position.0, node.position.1));
+        kdl.push_str(&format!("    inputs {}\n", node.inputs));
+        kdl.push_str(&format!("    outputs {}\n", node.outputs));
+        kdl.push_str(&format!("    geom_visible {}\n", node.geom_visible));
+        if !node.node_type.is_empty() {
+            kdl.push_str(&format!("    node_type {:?}\n", node.node_type));
+        }
+        for (p_name, p_val, p_type) in &node.parameters {
+            kdl.push_str(&format!("    parameter {:?} value={:?} type={:?}\n", p_name, p_val, p_type));
+        }
+        kdl.push_str("}\n\n");
+    }
+
+    for img in &state.images {
+        kdl.push_str(&format!("image {:?} {{\n", img.path));
+        kdl.push_str(&format!("    position {} {}\n", img.position.0, img.position.1));
+        kdl.push_str(&format!("    size {} {}\n", img.size.0, img.size.1));
+        kdl.push_str("}\n\n");
+    }
+
+    std::fs::write(path, kdl)?;
+    Ok(())
+}
+
 fn get_view_options(show_grid: bool, uniform_bg: bool, opacity: f32) -> Vec<String> {
     vec![
         format!("{} Show Grid", if show_grid { "✓" } else { "  " }),
@@ -556,7 +589,7 @@ impl GraphApp {
         std::fs::create_dir_all(project_dir.join("assets"))?;
         std::fs::create_dir_all(project_dir.join("code"))?;
 
-        let state_file_path = project_dir.join("state.json");
+        let state_file_path = project_dir.join("state.kdl");
 
         let project_images: Vec<GraphProjectImage> = self.loaded_images.iter()
             .map(|img| GraphProjectImage {
@@ -578,8 +611,13 @@ impl GraphApp {
             opacity: self.opacity,
         };
 
-        let content = serde_json::to_string_pretty(&state)?;
-        std::fs::write(&state_file_path, content)?;
+        save_project_to_kdl_path(&state_file_path, &state)?;
+
+        // Clean up old state.json if it exists
+        let old_json_path = project_dir.join("state.json");
+        if old_json_path.exists() {
+            let _ = std::fs::remove_file(old_json_path);
+        }
         
         self.loaded_project_path = Some(project_dir.to_path_buf());
         self.needs_rebuild = true;
@@ -628,9 +666,14 @@ impl GraphApp {
 
     fn load_project_from_path(&mut self, path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
         let (state_file_path, project_dir) = if path.is_dir() {
-            (path.join("state.json"), path.to_path_buf())
+            let kdl_path = path.join("state.kdl");
+            if kdl_path.exists() {
+                (kdl_path, path.to_path_buf())
+            } else {
+                (path.join("state.json"), path.to_path_buf())
+            }
         } else {
-            if path.file_name().map_or(false, |name| name == "state.json") {
+            if path.file_name().map_or(false, |name| name == "state.json" || name == "state.kdl") {
                 (path.to_path_buf(), path.parent().unwrap_or(path).to_path_buf())
             } else {
                 (path.to_path_buf(), path.parent().unwrap_or(path).to_path_buf())