forked from 1b-agent/opencadstudio-upstream
feat(io): autosave to .sv$ recovery files (SAVETIME command) (#205)
A periodic subscription (every SAVETIME minutes — default 10, 0 disables) writes a <name>.sv$ recovery copy of each dirty, already-saved drawing at its own DWG version. Non-destructive: it never touches the original file or the dirty flag, and skips unsaved (path-less) tabs. The SAVETIME command sets/queries the interval and persists it. Recovery files (.sv$ and .bak) open through the normal Open dialog — the loader detects DWG vs DXF from the leading magic bytes, and a "Backup / Autosave" filter lists them. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
54186309b2
commit
02d2764e45
7 files changed
+93
-6
No files matched your search
@@ -1501,6 +1501,27 @@ impl OpenCADStudio {
|
||||
_ => self.command_line.push_error("Requires 0 or 1"),
|
||||
}
|
||||
}
|
||||
"SAVETIME" => {
|
||||
self.command_line
|
||||
.push_output(&format!("SAVETIME = {}", self.savetime_min));
|
||||
}
|
||||
cmd if cmd.starts_with("SAVETIME ") => {
|
||||
match cmd.trim_start_matches("SAVETIME").trim().parse::<i32>() {
|
||||
Ok(v) if v >= 0 => {
|
||||
self.savetime_min = v;
|
||||
self.persist_settings_if_changed();
|
||||
let msg = if v == 0 {
|
||||
"SAVETIME set to 0 (autosave off)".to_string()
|
||||
} else {
|
||||
format!("SAVETIME set to {v} minute(s)")
|
||||
};
|
||||
self.command_line.push_output(&msg);
|
||||
}
|
||||
_ => self
|
||||
.command_line
|
||||
.push_error("Requires a non-negative number of minutes (0 = off)"),
|
||||
}
|
||||
}
|
||||
"PDSIZE" => {
|
||||
use crate::command::ValuePromptCommand;
|
||||
let c = ValuePromptCommand::new(
|
||||
|
||||
@@ -253,6 +253,9 @@ pub(super) struct OpenCADStudio {
|
||||
/// When true (default), the app registers itself as a .dwg/.dxf/.bak file
|
||||
/// handler on each launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command);
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
/// Persisted default viewport background, restored from settings and applied
|
||||
/// to every drawing tab (new and opened) so a chosen background survives
|
||||
/// restarts (#188). `None` = the built-in dark-grey / off-white defaults.
|
||||
@@ -1120,6 +1123,8 @@ pub enum Message {
|
||||
OverwriteConfirm,
|
||||
/// Go back to the Save dialog from the overwrite warning.
|
||||
OverwriteCancel,
|
||||
/// Periodic autosave tick — write `.sv$` recovery files for dirty tabs.
|
||||
AutoSave,
|
||||
/// Save-as path picked for the unsaved-changes → save → close flow.
|
||||
UnsavedPickedSavePath(Option<std::path::PathBuf>),
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
@@ -1870,6 +1875,7 @@ impl OpenCADStudio {
|
||||
texteditmode: false,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
savetime_min: 10,
|
||||
default_bg_color: None,
|
||||
default_paper_bg_color: None,
|
||||
awaiting_vports: false,
|
||||
|
||||
+11
-1
@@ -109,6 +109,9 @@ pub struct UserSettings {
|
||||
/// When true (default), the app (re)registers itself as a .dwg/.dxf/.bak
|
||||
/// handler on every launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command).
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
/// Persisted viewport background colours (0–255 RGB); `None` = app default
|
||||
/// (dark grey model / off-white paper). Applied to every drawing tab on
|
||||
/// launch and to tabs opened later, so a chosen background survives restarts
|
||||
@@ -141,6 +144,7 @@ impl Default for UserSettings {
|
||||
texteditmode: false,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
savetime_min: 10,
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
}
|
||||
@@ -187,6 +191,11 @@ impl UserSettings {
|
||||
}
|
||||
"backup_on_save" => s.backup_on_save = val == "1",
|
||||
"file_assoc_enabled" => s.file_assoc_enabled = val == "1",
|
||||
"savetime_min" => {
|
||||
if let Ok(v) = val.parse::<i32>() {
|
||||
s.savetime_min = v.max(0);
|
||||
}
|
||||
}
|
||||
"disabled_plugins" => {
|
||||
s.disabled_plugins = val
|
||||
.split(',')
|
||||
@@ -228,7 +237,7 @@ impl UserSettings {
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let body = format!(
|
||||
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\ntexteditmode={}\nbackup_on_save={}\nfile_assoc_enabled={}\nbg_color={}\npaper_bg_color={}\n",
|
||||
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\ntexteditmode={}\nbackup_on_save={}\nfile_assoc_enabled={}\nsavetime_min={}\nbg_color={}\npaper_bg_color={}\n",
|
||||
b(self.dyn_input),
|
||||
b(self.ortho),
|
||||
b(self.polar),
|
||||
@@ -242,6 +251,7 @@ impl UserSettings {
|
||||
self.texteditmode,
|
||||
b(self.backup_on_save),
|
||||
b(self.file_assoc_enabled),
|
||||
self.savetime_min,
|
||||
rgb_to_str(self.bg_color),
|
||||
rgb_to_str(self.paper_bg_color),
|
||||
);
|
||||
|
||||
@@ -42,6 +42,7 @@ impl OpenCADStudio {
|
||||
texteditmode: self.texteditmode,
|
||||
backup_on_save: self.backup_on_save,
|
||||
file_assoc_enabled: self.file_assoc_enabled,
|
||||
savetime_min: self.savetime_min,
|
||||
bg_color: self.default_bg_color.map(f4_to_u3),
|
||||
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
||||
}
|
||||
@@ -62,6 +63,7 @@ impl OpenCADStudio {
|
||||
self.texteditmode = s.texteditmode;
|
||||
self.backup_on_save = s.backup_on_save;
|
||||
self.file_assoc_enabled = s.file_assoc_enabled;
|
||||
self.savetime_min = s.savetime_min;
|
||||
self.default_bg_color = s.bg_color.map(u3_to_f4);
|
||||
self.default_paper_bg_color = s.paper_bg_color.map(u3_to_f4);
|
||||
// Push the restored background onto every drawing tab that exists now
|
||||
@@ -675,6 +677,41 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.on_save_dialog_confirm()
|
||||
}
|
||||
|
||||
/// Periodic autosave (SAVETIME): write a `.sv$` recovery copy for every
|
||||
/// dirty tab that has a path, at the document's own DWG version. Best-effort
|
||||
/// and non-destructive — it never touches the original file or the dirty
|
||||
/// flag, and skips unsaved (path-less) tabs.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
||||
let mut n = 0;
|
||||
for i in 0..self.tabs.len() {
|
||||
if !self.tabs[i].dirty {
|
||||
continue;
|
||||
}
|
||||
let Some(path) = self.tabs[i].current_path.clone() else {
|
||||
continue;
|
||||
};
|
||||
let version = self.tabs[i].scene.document.version;
|
||||
if let Ok(bytes) =
|
||||
crate::io::save_to_bytes(&self.tabs[i].scene.document, "dwg", version)
|
||||
{
|
||||
if std::fs::write(path.with_extension("sv$"), bytes).is_ok() {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
self.command_line
|
||||
.push_output(&format!("Autosaved {n} drawing(s) to .sv$"));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_page_setup_commit(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let layout_name = self.tabs[i].scene.current_layout.clone();
|
||||
|
||||
@@ -2540,6 +2540,7 @@ impl OpenCADStudio {
|
||||
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
|
||||
Task::none()
|
||||
}
|
||||
Message::AutoSave => self.on_autosave(),
|
||||
|
||||
Message::UnsavedPickedSavePath(Some(path)) => {
|
||||
self.on_unsaved_picked_save_path_some(path)
|
||||
|
||||
@@ -1423,6 +1423,17 @@ impl OpenCADStudio {
|
||||
iced::time::every(std::time::Duration::from_millis(300)).map(|_| Message::PollWebFonts);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let web_fonts = Subscription::none();
|
||||
// Periodic autosave to a `.sv$` recovery file (native only). SAVETIME is
|
||||
// the interval in minutes; 0 disables it.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let autosave = if self.savetime_min > 0 {
|
||||
iced::time::every(std::time::Duration::from_secs(self.savetime_min as u64 * 60))
|
||||
.map(|_| Message::AutoSave)
|
||||
} else {
|
||||
Subscription::none()
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let autosave = Subscription::none();
|
||||
iced::Subscription::batch([
|
||||
frames,
|
||||
history_tick,
|
||||
@@ -1430,6 +1441,7 @@ impl OpenCADStudio {
|
||||
hover_dwell,
|
||||
caret_blink,
|
||||
web_fonts,
|
||||
autosave,
|
||||
event::listen_with(|ev, status, win_id| {
|
||||
use iced::event::Status;
|
||||
match ev {
|
||||
|
||||
+5
-5
@@ -38,10 +38,10 @@ const PHASE_FINALIZING: u8 = 3;
|
||||
pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
|
||||
let handle = rfd::AsyncFileDialog::new()
|
||||
.set_title("Open CAD file")
|
||||
.add_filter("CAD Files", &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"])
|
||||
.add_filter("CAD Files", &["dwg", "dxf", "bak", "sv$", "DWG", "DXF", "BAK"])
|
||||
.add_filter("DWG Files", &["dwg", "DWG"])
|
||||
.add_filter("DXF Files", &["dxf", "DXF"])
|
||||
.add_filter("Backup Files", &["bak", "BAK"])
|
||||
.add_filter("Backup / Autosave", &["bak", "sv$", "BAK"])
|
||||
.add_filter("All Files", &["*"])
|
||||
.pick_file()
|
||||
.await?;
|
||||
@@ -169,9 +169,9 @@ pub fn load_file(path: &Path) -> Result<CadDocument, String> {
|
||||
.map(|e| e.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
// A `.bak` backup holds a verbatim DWG or DXF copy — detect the real format
|
||||
// from the file's leading bytes rather than the `.bak` extension.
|
||||
let effective = if ext == "bak" {
|
||||
// A `.bak` backup or `.sv$` autosave holds a verbatim DWG/DXF copy — detect
|
||||
// the real format from the file's leading bytes, not the extension.
|
||||
let effective = if ext == "bak" || ext == "sv$" {
|
||||
sniff_dwg_or_dxf(path)
|
||||
} else {
|
||||
ext.clone()
|
||||
|
||||
Reference in new issue
Block a user