forked from 1b-agent/opencadstudio-upstream
feat(io): FILEASSOC command to toggle file association on/off (#205)
`FILEASSOC 1` (re)registers the app as a .dwg/.dxf/.bak handler; `FILEASSOC 0` unregisters it (removes the .desktop + CAD mime package on Linux, the per-user registry keys on Windows); bare `FILEASSOC` reports the current state. The choice persists (file_assoc_enabled) and gates the every-launch re-registration, so turning it off stays off across restarts. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
962268e16e
commit
9ea800f049
5 files changed
+113
-4
No files matched your search
@@ -1468,6 +1468,39 @@ impl OpenCADStudio {
|
||||
_ => self.command_line.push_error("Requires 0 or 1"),
|
||||
}
|
||||
}
|
||||
"FILEASSOC" => {
|
||||
let v = if self.file_assoc_enabled { 1 } else { 0 };
|
||||
self.command_line.push_output(&format!("FILEASSOC = {v}"));
|
||||
}
|
||||
cmd if cmd.starts_with("FILEASSOC ") => {
|
||||
match cmd.trim_start_matches("FILEASSOC").trim() {
|
||||
"1" => {
|
||||
self.file_assoc_enabled = true;
|
||||
self.persist_settings_if_changed();
|
||||
match crate::io::file_association::register_as_handler() {
|
||||
Ok(()) => self.command_line.push_output(
|
||||
"FILEASSOC set to 1 — registered as a .dwg/.dxf/.bak handler",
|
||||
),
|
||||
Err(e) => self
|
||||
.command_line
|
||||
.push_error(&format!("FILEASSOC: registration failed: {e}")),
|
||||
}
|
||||
}
|
||||
"0" => {
|
||||
self.file_assoc_enabled = false;
|
||||
self.persist_settings_if_changed();
|
||||
match crate::io::file_association::unregister_handler() {
|
||||
Ok(()) => self
|
||||
.command_line
|
||||
.push_output("FILEASSOC set to 0 — unregistered as a file handler"),
|
||||
Err(e) => self
|
||||
.command_line
|
||||
.push_error(&format!("FILEASSOC: unregister failed: {e}")),
|
||||
}
|
||||
}
|
||||
_ => self.command_line.push_error("Requires 0 or 1"),
|
||||
}
|
||||
}
|
||||
"PDSIZE" => {
|
||||
use crate::command::ValuePromptCommand;
|
||||
let c = ValuePromptCommand::new(
|
||||
|
||||
+13
-2
@@ -250,6 +250,9 @@ pub(super) struct OpenCADStudio {
|
||||
/// 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,
|
||||
/// 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,
|
||||
/// 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.
|
||||
@@ -1866,6 +1869,7 @@ impl OpenCADStudio {
|
||||
dyn_input: true,
|
||||
texteditmode: false,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
default_bg_color: None,
|
||||
default_paper_bg_color: None,
|
||||
awaiting_vports: false,
|
||||
@@ -2151,8 +2155,15 @@ impl OpenCADStudio {
|
||||
// portable .exe / AppImage that no installer has registered. Detached so
|
||||
// it never delays startup.
|
||||
std::thread::spawn(|| {
|
||||
if let Err(e) = crate::io::file_association::register_as_handler() {
|
||||
eprintln!("file association: handler registration failed: {e}");
|
||||
// Honour the FILEASSOC toggle: skip (re)registration when the user
|
||||
// has turned file association off.
|
||||
let enabled = settings::UserSettings::load()
|
||||
.map(|s| s.file_assoc_enabled)
|
||||
.unwrap_or(true);
|
||||
if enabled {
|
||||
if let Err(e) = crate::io::file_association::register_as_handler() {
|
||||
eprintln!("file association: handler registration failed: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
let state = Self::new();
|
||||
|
||||
+7
-1
@@ -106,6 +106,9 @@ pub struct UserSettings {
|
||||
/// 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,
|
||||
/// 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,
|
||||
/// 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
|
||||
@@ -137,6 +140,7 @@ impl Default for UserSettings {
|
||||
plugin_repos: Vec::new(),
|
||||
texteditmode: false,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
}
|
||||
@@ -182,6 +186,7 @@ impl UserSettings {
|
||||
}
|
||||
}
|
||||
"backup_on_save" => s.backup_on_save = val == "1",
|
||||
"file_assoc_enabled" => s.file_assoc_enabled = val == "1",
|
||||
"disabled_plugins" => {
|
||||
s.disabled_plugins = val
|
||||
.split(',')
|
||||
@@ -223,7 +228,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={}\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={}\nbg_color={}\npaper_bg_color={}\n",
|
||||
b(self.dyn_input),
|
||||
b(self.ortho),
|
||||
b(self.polar),
|
||||
@@ -236,6 +241,7 @@ impl UserSettings {
|
||||
self.plugin_repos.join(","),
|
||||
self.texteditmode,
|
||||
b(self.backup_on_save),
|
||||
b(self.file_assoc_enabled),
|
||||
rgb_to_str(self.bg_color),
|
||||
rgb_to_str(self.paper_bg_color),
|
||||
);
|
||||
|
||||
@@ -41,6 +41,7 @@ impl OpenCADStudio {
|
||||
plugin_repos: self.plugin_repos.clone(),
|
||||
texteditmode: self.texteditmode,
|
||||
backup_on_save: self.backup_on_save,
|
||||
file_assoc_enabled: self.file_assoc_enabled,
|
||||
bg_color: self.default_bg_color.map(f4_to_u3),
|
||||
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
||||
}
|
||||
@@ -60,6 +61,7 @@ impl OpenCADStudio {
|
||||
self.plugin_repos = s.plugin_repos.clone();
|
||||
self.texteditmode = s.texteditmode;
|
||||
self.backup_on_save = s.backup_on_save;
|
||||
self.file_assoc_enabled = s.file_assoc_enabled;
|
||||
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
|
||||
|
||||
@@ -60,6 +60,27 @@ pub fn register_as_handler() -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove this app's file-handler registration (the inverse of
|
||||
/// [`register_as_handler`]) so it no longer claims .dwg/.dxf/.bak. Best-effort.
|
||||
pub fn unregister_handler() -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
windows_impl::unregister_handler()
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
linux_impl::unregister_handler()
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to make this app the default handler for .dwg and .dxf. Returns a short
|
||||
/// human-readable status string on success, or an error message on failure.
|
||||
pub async fn set_default_app() -> Result<String, String> {
|
||||
@@ -189,10 +210,29 @@ mod windows_impl {
|
||||
}
|
||||
|
||||
use windows_sys::Win32::System::Registry::{
|
||||
RegCloseKey, RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CURRENT_USER,
|
||||
RegCloseKey, RegCreateKeyExW, RegDeleteTreeW, RegSetValueExW, HKEY, HKEY_CURRENT_USER,
|
||||
KEY_WRITE, REG_OPTION_NON_VOLATILE, REG_SZ,
|
||||
};
|
||||
|
||||
/// Remove the per-user keys this app created — undoes [`register_handler`].
|
||||
/// Best-effort: leaves the shared `.dwg/.dxf/.bak` OpenWithProgids values
|
||||
/// (they point at now-deleted ProgIDs, which Windows simply ignores).
|
||||
pub(super) fn unregister_handler() -> Result<(), String> {
|
||||
for key in [
|
||||
r"Software\Classes\Applications\OpenCADStudio.exe",
|
||||
r"Software\Classes\OpenCADStudio.DWG",
|
||||
r"Software\Classes\OpenCADStudio.DXF",
|
||||
r"Software\Classes\OpenCADStudio.BAK",
|
||||
r"Software\Open CAD Studio",
|
||||
] {
|
||||
let w = wide(key);
|
||||
unsafe {
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, w.as_ptr());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wide(s: &str) -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect()
|
||||
}
|
||||
@@ -395,6 +435,23 @@ mod linux_impl {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the .desktop entry and the CAD mime package, then refresh the
|
||||
/// caches — undoes [`register_handler`]. Best-effort; missing files are fine.
|
||||
pub(super) fn unregister_handler() -> Result<(), String> {
|
||||
let apps = data_home()?.join("applications");
|
||||
let _ = std::fs::remove_file(apps.join(format!("{APP_ID}.desktop")));
|
||||
let _ = std::process::Command::new("update-desktop-database")
|
||||
.arg(&apps)
|
||||
.status();
|
||||
|
||||
let home = data_home()?;
|
||||
let _ = std::fs::remove_file(home.join("mime/packages").join(format!("{APP_ID}-cad.xml")));
|
||||
let _ = std::process::Command::new("update-mime-database")
|
||||
.arg(home.join("mime"))
|
||||
.status();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a high-priority shared-mime-info magic rule so DWG/DXF *content*
|
||||
/// is recognised regardless of extension. Without it a `.bak` copy matches
|
||||
/// the freedesktop `*.bak` glob (application/x-trash) and never reaches us;
|
||||
|
||||
Reference in new issue
Block a user