feat(io): .bak backup-on-save + open .bak as a supported type (#205)

- Save: overwriting an existing file first copies it to a sibling <name>.bak
  (default on; toggle with the ISAVEBAK command, persisted in settings).
- Open: .bak is a first-class type — the Open dialog lists it and the loader
  detects DWG vs DXF from the file's leading magic bytes (the extension hides
  the real format).
- File association registers .bak too, every launch (so users who associated
  earlier get it automatically): Windows via the registry; Linux via a
  high-priority shared-mime-info magic that resolves DWG/DXF content under the
  .bak extension, with the generic *.bak glob claimed only once the app is the
  default DWG handler — so it never hijacks other apps' backups.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-29 12:49:02 +03:00
1 parent b77187e672
commit 962268e16e
7 files changed
+148 -4

No files matched your search

+19
View File
@@ -1449,6 +1449,25 @@ impl OpenCADStudio {
.push_error("Requires 0 OR 1 OR MULTIPLE OR SINGLE");
}
}
"ISAVEBAK" => {
let v = if self.backup_on_save { 1 } else { 0 };
self.command_line.push_output(&format!("ISAVEBAK = {v}"));
}
cmd if cmd.starts_with("ISAVEBAK ") => {
match cmd.trim_start_matches("ISAVEBAK").trim() {
"0" => {
self.backup_on_save = false;
self.persist_settings_if_changed();
self.command_line.push_output("ISAVEBAK set to 0");
}
"1" => {
self.backup_on_save = true;
self.persist_settings_if_changed();
self.command_line.push_output("ISAVEBAK set to 1");
}
_ => self.command_line.push_error("Requires 0 or 1"),
}
}
"PDSIZE" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new(
+4
View File
@@ -247,6 +247,9 @@ pub(super) struct OpenCADStudio {
dyn_input: bool,
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
pub texteditmode: bool,
/// When true (default), saving over an existing file first writes a `.bak`
/// copy of it for recovery (#205). Toggle with the ISAVEBAK command.
pub backup_on_save: bool,
/// 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.
@@ -1862,6 +1865,7 @@ impl OpenCADStudio {
show_grid: false,
dyn_input: true,
texteditmode: false,
backup_on_save: true,
default_bg_color: None,
default_paper_bg_color: None,
awaiting_vports: false,
+7 -1
View File
@@ -103,6 +103,9 @@ pub struct UserSettings {
pub plugin_repos: Vec<String>,
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
pub texteditmode: bool,
/// When true, saving over an existing file first copies it to a sibling
/// `<name>.bak` so a faulty or accidental save can be recovered (#205).
pub backup_on_save: bool,
/// 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
@@ -133,6 +136,7 @@ impl Default for UserSettings {
disabled_plugins: Vec::new(),
plugin_repos: Vec::new(),
texteditmode: false,
backup_on_save: true,
bg_color: None,
paper_bg_color: None,
}
@@ -177,6 +181,7 @@ impl UserSettings {
s.texteditmode = v;
}
}
"backup_on_save" => s.backup_on_save = val == "1",
"disabled_plugins" => {
s.disabled_plugins = val
.split(',')
@@ -218,7 +223,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={}\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={}\nbg_color={}\npaper_bg_color={}\n",
b(self.dyn_input),
b(self.ortho),
b(self.polar),
@@ -230,6 +235,7 @@ impl UserSettings {
self.disabled_plugins.join(","),
self.plugin_repos.join(","),
self.texteditmode,
b(self.backup_on_save),
rgb_to_str(self.bg_color),
rgb_to_str(self.paper_bg_color),
);
+8
View File
@@ -40,6 +40,7 @@ impl OpenCADStudio {
},
plugin_repos: self.plugin_repos.clone(),
texteditmode: self.texteditmode,
backup_on_save: self.backup_on_save,
bg_color: self.default_bg_color.map(f4_to_u3),
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
}
@@ -58,6 +59,7 @@ impl OpenCADStudio {
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
self.plugin_repos = s.plugin_repos.clone();
self.texteditmode = s.texteditmode;
self.backup_on_save = s.backup_on_save;
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
@@ -536,6 +538,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.tabs[i].scene.document.header.user_real1 =
self.tabs[i].scene.annotation_scale as f64;
// A direct Save preserves the document's current version.
if self.backup_on_save {
crate::io::write_backup(&path);
}
match crate::io::save(&self.tabs[i].scene.document, &path) {
Ok(()) => {
self.command_line
@@ -597,6 +602,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
#[cfg(not(target_arch = "wasm32"))]
{
let path = self.save_dialog_folder.join(&filename);
if self.backup_on_save {
crate::io::write_backup(&path);
}
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
Ok(()) => {
self.command_line
+2 -1
View File
@@ -315,9 +315,10 @@ impl OpenCADStudio {
async {
let handle = rfd::AsyncFileDialog::new()
.set_title("Select External Reference File")
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
.add_filter("CAD Files", &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"])
.add_filter("DWG Files", &["dwg", "DWG"])
.add_filter("DXF Files", &["dxf", "DXF"])
.add_filter("Backup Files", &["bak", "BAK"])
.pick_file()
.await;
match handle {