git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitd7ad17d168cd17ee36c2b549d62aa23adb55ea8b
parentf16bc6e322
authorLucas Galante <[email protected]>
date2026-08-23 22:30
fix: file choosers open at the loaded project's directory

Save As / Open spawned cce-files with no path, so the chooser landed in its
remembered last-visited directory — wherever the file manager was last used
(the actual remembered value was cce-icons/svg), not where the current
project lives. Both choosers now pass the loaded project's parent directory
positionally, which cce-files already honors; a scratch project passes
nothing and keeps the remembered fallback. Verified live: with a project
loaded from ~/designs/gears, Save As opens showing ~/designs with the
project selected and the name box prefilled.

Passing a start dir also skips the chooser's remembered-dir restore step
entirely. The reported open latency did not reproduce headless — click-to-
mapped measured 0.12s, and 0.35s cold with the real remembered directory —
so no speculative fix beyond that; if it persists it is environmental
(compositor flight, cold caches) rather than in either app's startup path.

 src/app.rs  | 16 ++++++++++++++++
 src/main.rs | 15 +++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/src/app.rs b/src/app.rs
index f2b0436..ffaf3b5 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1747,8 +1747,21 @@ impl State {
         names
     }
 
+    /// Where a file chooser should open: the loaded project's parent directory
+    /// (the current working context — sibling projects live there), or None to
+    /// let cce-files use its remembered location. Passing it also skips the
+    /// chooser's remembered-dir restore outright.
+    pub(crate) fn chooser_start_dir(&self) -> Option<std::path::PathBuf> {
+        self.loaded_project_path
+            .as_ref()
+            .and_then(|p| p.parent())
+            .filter(|d| d.is_dir())
+            .map(|d| d.to_path_buf())
+    }
+
     pub fn open_file_chooser(&self) {
         let Some(sender) = self.event_sender.clone() else { return };
+        let start_dir = self.chooser_start_dir();
         std::thread::spawn(move || {
             use std::process::{Command, Stdio};
 
@@ -1786,6 +1799,7 @@ impl State {
 
             let child = match Command::new(exe_path)
                 .arg("--select")
+                .args(start_dir.as_deref())
                 .stdout(Stdio::piped())
                 .spawn()
             {
@@ -1815,6 +1829,7 @@ impl State {
 
     pub fn save_file_chooser(&self) {
         let Some(sender) = self.event_sender.clone() else { return };
+        let start_dir = self.chooser_start_dir();
         std::thread::spawn(move || {
             use std::process::{Command, Stdio};
 
@@ -1852,6 +1867,7 @@ impl State {
 
             let child = match Command::new(exe_path)
                 .arg("--save")
+                .args(start_dir.as_deref())
                 .stdout(Stdio::piped())
                 .spawn()
             {
diff --git a/src/main.rs b/src/main.rs
index dde00da..6c4a921 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -69,6 +69,21 @@ mod tests {
     use crate::shortcut::{Shortcut, ShortcutManager, Action};
     use crate::geometry::{GAttribute, GVertex, Geometry, line_vertices};
 
+    /// The choosers open in the loaded project's parent — the "current view" —
+    /// and fall back to cce-files' remembered location only when nothing is
+    /// loaded (a scratch project has no place to point at).
+    #[test]
+    fn test_chooser_opens_at_the_loaded_projects_parent() {
+        let mut state = State::new(false);
+        assert_eq!(state.chooser_start_dir(), None, "scratch project must not pin a dir");
+
+        let dir = std::env::temp_dir().join("cce-designer-test-projects").join("gears");
+        std::fs::create_dir_all(&dir).unwrap();
+        state.loaded_project_path = Some(dir.clone());
+        assert_eq!(state.chooser_start_dir().as_deref(), dir.parent(),
+            "chooser must start where the current project lives");
+    }
+
     /// The default-project pointer must survive the KDL round trip state.kdl
     /// actually goes through — serde alone passing means nothing if
     /// json_to_kdl_string / parse_kdl_to_json drop or retype the field.