Open Save As when saving a session that has never been saved

Cmd+S and File > Save Session on an untitled session only left "no path
yet: pass `path` (Save As)" in the status line. From a menu, the command
palette or the keyboard, Save now opens the Save Session As prompt until
the session has a file. Calls from the control channel, MCP and the CLI
still need a `path`.

The prompt suggests ~/<name>/<name>.scraft, but saving a session without
audio there failed with "No such file or directory", because only
writing audio created the session folder. Saving now creates the folder.
This commit is contained in:
grovebit committed 2026-10-10 14:20:55 +01:00
1 parent 3bc2d92b4e
commit 718bb5c7fb
4 files changed
+35 -2

No files matched your search

+10
View File
@@ -391,6 +391,16 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn saving_a_session_without_audio_creates_its_folder() {
let dir = std::env::temp_dir().join(format!("soundcraft-new-folder-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("Song").join("Song.scraft");
Engine::default().execute("session.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
assert!(path.exists());
let _ = std::fs::remove_dir_all(&dir);
}
fn tone_engine() -> (Engine, u64) {
let mut e = Engine::default();
e.execute("track.new", &json!({"count": 1, "name": "Tone", "format": "mono"})).unwrap();
+1
View File
@@ -519,6 +519,7 @@ fn write_session(session: &Session, path: &str, overwrite: bool) -> Result<(Sess
s.name = stem.to_string();
}
let text = s.to_json().map_err(|err| EngineError::Io(err.to_string()))?;
std::fs::create_dir_all(&dir).map_err(|err| EngineError::Io(format!("{}: {err}", dir.display())))?;
// Atomic-ish write: temp file then rename.
let tmp = p.with_extension("scraft.tmp");
std::fs::write(&tmp, text).map_err(|err| EngineError::Io(format!("{}: {err}", tmp.display())))?;
+3 -2
View File
@@ -72,8 +72,9 @@ impl Dialogs {
"file.import_audio" => Dialog::PathPrompt { cmd: id.into(), title: "Import Audio".into(), path: home, key: "path".into() },
"file.import_midi" => Dialog::PathPrompt { cmd: id.into(), title: "Import MIDI".into(), path: home, key: "path".into() },
"session.open" => Dialog::PathPrompt { cmd: id.into(), title: "Open Session".into(), path: home, key: "path".into() },
"session.save_as" | "session.save_copy" => Dialog::PathPrompt {
cmd: id.into(),
"session.save" if e.path.is_some() => return false,
"session.save" | "session.save_as" | "session.save_copy" => Dialog::PathPrompt {
cmd: if id == "session.save_copy" { id } else { "session.save_as" }.into(),
title: "Save Session As".into(),
path: format!("{home}/{name}/{name}.scraft"),
key: "path".into(),
+21
View File
@@ -149,6 +149,7 @@ const DIALOG_COMMANDS: &[&str] = &[
"file.import_audio",
"file.import_midi",
"session.open",
"session.save",
"session.save_as",
"session.save_copy",
"track.group",
@@ -618,4 +619,24 @@ mod tests {
invoke_menu(&mut app, "ui.bounce_dialog", "File > Bounce Mix...");
assert_eq!(app.dialogs.open_name(), Some("bounce"));
}
#[test]
fn save_asks_for_a_path_until_the_session_has_one() {
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
invoke_menu(&mut app, "session.save", "File > Save Session");
assert!(matches!(&app.dialogs.open, Some(crate::dialogs::Dialog::PathPrompt { cmd, .. }) if cmd == "session.save_as"));
app.dialogs.open = None;
assert!(invoke_shortcut_dialog(&mut app, "session.save"));
app.dialogs.open = None;
let dir = std::env::temp_dir().join(format!("soundcraft-save-{}", std::process::id()));
let path = dir.join("Song.scraft");
app.run("session.save_as", json!({"path": path.to_string_lossy()})).unwrap();
app.run("track.new", json!({})).unwrap();
assert!(!invoke_shortcut_dialog(&mut app, "session.save"));
invoke_menu(&mut app, "session.save", "File > Save Session");
assert!(app.dialogs.open.is_none());
assert!(!app.engine.is_dirty());
let _ = std::fs::remove_dir_all(&dir);
}
}