git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commitf9bd017b18011cca0d1aad29c307db02af94d6e4
parente5f60e715c
authorLucas Galante <[email protected]>
date2026-07-05 20:38
feat: use dynamic cce-ui metrics and safe process spawning helpers

 src/main.rs         | 42 ++++++++++++++++++++++++++----------------
 src/pages/browse.rs | 47 +++++++++++++++++++++++++++++++----------------
 src/services/fs.rs  | 10 +++++-----
 3 files changed, 62 insertions(+), 37 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 7fa6b91..c23703f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -240,14 +240,17 @@ impl FilesystemApp {
             let accent = [0.36, 0.56, 0.38, 1.0];
             let text_fg = [0.83, 0.83, 0.83, 1.0];
 
+            let btn_h = cce_ui::layout::button_height();
+            let btn_y = bar_y + (select_bar_h - btn_h) / 2.0;
+
             // Cancel button
             let cancel_x = self.width as f32 - 180.0;
             pc.button(
                 "Cancel",
                 cancel_x,
-                bar_y + 10.0,
+                btn_y,
                 70.0,
-                28.0,
+                btn_h,
                 [0.25, 0.12, 0.12, 0.5],
                 [0.35, 0.15, 0.15, 0.8],
                 text_fg,
@@ -260,9 +263,9 @@ impl FilesystemApp {
             pc.button(
                 button_label,
                 open_x,
-                bar_y + 10.0,
+                btn_y,
                 80.0,
-                28.0,
+                btn_h,
                 accent,
                 [0.46, 0.66, 0.48, 1.0],
                 [0.10, 0.16, 0.11, 1.0],
@@ -307,7 +310,7 @@ impl FilesystemApp {
 
         // Gather open-with dialog backdrop & dialog panel if active (open_with_dialog uses textbox rendering manually but we can gather its other quads/texts)
         let mut dialog_pc = pages::PageContent::new();
-        if let Some((_path, _textbox)) = &mut self.open_with_dialog {
+        if let Some((_path, textbox)) = &mut self.open_with_dialog {
             let dialog_w = 400.0;
             let dialog_h = 160.0;
             let dialog_x = (self.width as f32 - dialog_w) / 2.0;
@@ -325,18 +328,24 @@ impl FilesystemApp {
             // Description
             dialog_pc.text("Enter command:", dialog_x + 20.0, dialog_y + 42.0, 11.0, [0.54, 0.54, 0.58, 1.0]);
 
-            // Textbox quads & labels are rendered via textbox.all_quads inside the layout, so we'll grab them from the textbox child widget since textbox is linked to root_window.
+            // Set textbox position dynamically using configured textbox height
+            let tb_x = dialog_x + 20.0;
+            let tb_y = dialog_y + 60.0;
+            let tb_w = dialog_w - 40.0;
+            let tb_h = cce_ui::layout::textbox_height();
+            textbox.set_rect(tb_x, tb_y, tb_w, tb_h);
 
             // Render Buttons: Cancel & Open
+            let btn_h = cce_ui::layout::button_height();
             let btn_cancel_x = dialog_x + dialog_w - 180.0;
-            let btn_cancel_y = dialog_y + dialog_h - 44.0;
+            let btn_cancel_y = dialog_y + dialog_h - btn_h - 16.0;
             let btn_cancel_w = 70.0;
-            let btn_cancel_h = 28.0;
+            let btn_cancel_h = btn_h;
 
             let btn_open_x = dialog_x + dialog_w - 100.0;
-            let btn_open_y = dialog_y + dialog_h - 44.0;
+            let btn_open_y = dialog_y + dialog_h - btn_h - 16.0;
             let btn_open_w = 80.0;
-            let btn_open_h = 28.0;
+            let btn_open_h = btn_h;
 
             let cancel_hover = self.cursor_x >= btn_cancel_x && self.cursor_x <= btn_cancel_x + btn_cancel_w
                 && self.cursor_y >= btn_cancel_y && self.cursor_y <= btn_cancel_y + btn_cancel_h;
@@ -759,7 +768,7 @@ impl Application for FilesystemApp {
                                 command.arg(arg);
                             }
                             command.arg(&path);
-                            let _ = command.spawn();
+                            let _ = cce_ui::process::spawn_detached(command);
                         }
                     }
                 }
@@ -925,17 +934,18 @@ impl Application for FilesystemApp {
             let tb_x = dialog_x + 20.0;
             let tb_y = dialog_y + 60.0;
             let tb_w = dialog_w - 40.0;
-            let tb_h = 28.0;
+            let tb_h = cce_ui::layout::textbox_height();
 
+            let btn_h = cce_ui::layout::button_height();
             let btn_cancel_x = dialog_x + dialog_w - 180.0;
-            let btn_cancel_y = dialog_y + dialog_h - 44.0;
+            let btn_cancel_y = dialog_y + dialog_h - btn_h - 16.0;
             let btn_cancel_w = 70.0;
-            let btn_cancel_h = 28.0;
+            let btn_cancel_h = btn_h;
 
             let btn_open_x = dialog_x + dialog_w - 100.0;
-            let btn_open_y = dialog_y + dialog_h - 44.0;
+            let btn_open_y = dialog_y + dialog_h - btn_h - 16.0;
             let btn_open_w = 80.0;
-            let btn_open_h = 28.0;
+            let btn_open_h = btn_h;
 
             if state == ElementState::Pressed {
                 let clicked_inside = pos.x >= dialog_x && pos.x <= dialog_x + dialog_w && pos.y >= dialog_y && pos.y <= dialog_y + dialog_h;
diff --git a/src/pages/browse.rs b/src/pages/browse.rs
index 64f31c4..aa0daa2 100644
--- a/src/pages/browse.rs
+++ b/src/pages/browse.rs
@@ -43,7 +43,7 @@ impl Default for BrowseState {
             search_box: cce_ui::widget::TextBox::new(String::new())
                 .with_max_width(None)
                 .with_placeholder("Search"),
-            list_box: cce_ui::widget::List::new(28.0, 2.0),
+            list_box: cce_ui::widget::List::new(cce_ui::layout::button_height(), 2.0),
             selected: None,
             breadcrumb,
             save_name_box: cce_ui::widget::TextBox::new(String::new()).with_max_width(None),
@@ -88,7 +88,7 @@ pub enum BrowseNavigation {
 }
 
 pub fn is_project_dir(path: &Path) -> bool {
-    path.is_dir() && path.join("state.json").exists()
+    path.is_dir() && (path.join("state.json").exists() || path.join("state.kdl").exists())
 }
 
 // ── Helpers ─────────────────────────────────────────────────────────
@@ -174,7 +174,7 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_
     layout.init(client_x, client_y, client_w, client_h);
 
     let breadcrumb_h = 24.0;
-    let textbox_h = 28.0;
+    let textbox_h = cce_ui::layout::textbox_height();
 
     // 1. Allocate and render Breadcrumb
     let (bx, by, bw, bh) = layout.allocate(client_w, breadcrumb_h);
@@ -265,7 +265,7 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_
                 list_x + 4.0,
                 draw_y,
                 list_w - 24.0,
-                28.0,
+                cce_ui::layout::button_height(),
                 bg,
                 hover_bg,
                 fg,
@@ -273,7 +273,11 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_
             );
 
             // Draw contents inside the button boundary:
-            pc.text(icon, list_x + 12.0, draw_y + 7.0, 13.0, fg);
+            let row_h = cce_ui::layout::button_height();
+            let y_text_13 = cce_ui::layout::center_text_y(draw_y, row_h, 13.0);
+            let y_text_11 = cce_ui::layout::center_text_y(draw_y, row_h, 11.0);
+
+            pc.text(icon, list_x + 12.0, y_text_13, 13.0, fg);
             
             let show_size = list_w > 400.0;
             let show_perm = list_w > 480.0;
@@ -300,15 +304,15 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_
                 entry.name.clone()
             };
 
-            pc.text(&name_truncated, list_x + 32.0, draw_y + 7.0, 13.0, fg);
+            pc.text(&name_truncated, list_x + 32.0, y_text_13, 13.0, fg);
             if show_size {
-                pc.text(&size_str, list_x + list_w - 290.0, draw_y + 8.0, 11.0, text_dim);
+                pc.text(&size_str, list_x + list_w - 290.0, y_text_11, 11.0, text_dim);
             }
             if show_perm {
-                pc.text(&perm_str, list_x + list_w - 210.0, draw_y + 8.0, 11.0, text_dim);
+                pc.text(&perm_str, list_x + list_w - 210.0, y_text_11, 11.0, text_dim);
             }
             if show_modified {
-                pc.text(&entry.modified, list_x + list_w - 120.0, draw_y + 8.0, 11.0, text_dim);
+                pc.text(&entry.modified, list_x + list_w - 120.0, y_text_11, 11.0, text_dim);
             }
         }
     }
@@ -437,7 +441,7 @@ mod tests {
                 })
                 .collect(),
             search_box: cce_ui::widget::TextBox::new(String::new()).with_max_width(None),
-            list_box: cce_ui::widget::List::new(28.0, 2.0),
+            list_box: cce_ui::widget::List::new(cce_ui::layout::button_height(), 2.0),
             ..BrowseState::default()
         }
     }
@@ -511,21 +515,32 @@ mod tests {
         let unique_dir = std::env::temp_dir().join(format!("clear_test_dir_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)));
         std::fs::create_dir_all(&unique_dir).unwrap();
         
-        // Initially, path is a directory but doesn't have state.json
+        // Initially, path is a directory but doesn't have state.json or state.kdl
         assert!(!is_project_dir(&unique_dir));
         
         // Create state.json
-        let file_path = unique_dir.join("state.json");
-        std::fs::write(&file_path, "{}").unwrap();
+        let file_path_json = unique_dir.join("state.json");
+        std::fs::write(&file_path_json, "{}").unwrap();
         
         // Now it should be recognized as a project dir
         assert!(is_project_dir(&unique_dir));
         
-        // If it's a file rather than a directory, even if named state.json, it shouldn't be a project dir itself
-        assert!(!is_project_dir(&file_path));
+        // Remove state.json and verify it's not a project dir
+        std::fs::remove_file(&file_path_json).unwrap();
+        assert!(!is_project_dir(&unique_dir));
+
+        // Create state.kdl
+        let file_path_kdl = unique_dir.join("state.kdl");
+        std::fs::write(&file_path_kdl, "name \"test\"").unwrap();
+
+        // Now it should be recognized as a project dir
+        assert!(is_project_dir(&unique_dir));
+
+        // If it's a file rather than a directory, even if named state.kdl, it shouldn't be a project dir itself
+        assert!(!is_project_dir(&file_path_kdl));
 
         // Clean up
-        let _ = std::fs::remove_file(&file_path);
+        let _ = std::fs::remove_file(&file_path_kdl);
         let _ = std::fs::remove_dir(&unique_dir);
     }
 
diff --git a/src/services/fs.rs b/src/services/fs.rs
index 79b9a2a..ffe5aa4 100644
--- a/src/services/fs.rs
+++ b/src/services/fs.rs
@@ -387,7 +387,7 @@ pub fn save_last_dir_internal(dir: &Path) {
 }
 
 pub fn get_mime_type(path: &Path) -> Option<String> {
-    if path.is_dir() && path.join("state.json").exists() {
+    if path.is_dir() && (path.join("state.json").exists() || path.join("state.kdl").exists()) {
         return Some("application/x-cce-project".to_string());
     }
     if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
@@ -539,7 +539,7 @@ pub fn open_file(path: &Path) {
                         command.arg(arg);
                     }
                     command.arg(path);
-                    if command.spawn().is_ok() {
+                    if cce_ui::process::spawn_detached(command).is_ok() {
                         opened = true;
                     }
                 }
@@ -547,9 +547,9 @@ pub fn open_file(path: &Path) {
         }
     }
     if !opened {
-        let _ = std::process::Command::new("xdg-open")
-            .arg(path)
-            .spawn();
+        let mut command = std::process::Command::new("xdg-open");
+        command.arg(path);
+        let _ = cce_ui::process::spawn_detached(command);
     }
 }