From 6cd8b2d6c2268af79915f8fd1655e2224147f71e Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 8 May 2026 15:28:19 +0000 Subject: [PATCH 1/5] linux --- .nix/pkgs/graphite.nix | 5 ++++- desktop/assets/art.graphite.Graphite.desktop | 3 ++- desktop/assets/art.graphite.Graphite.xml | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 desktop/assets/art.graphite.Graphite.xml diff --git a/.nix/pkgs/graphite.nix b/.nix/pkgs/graphite.nix index d60d3667f5..d2bbca81bf 100644 --- a/.nix/pkgs/graphite.nix +++ b/.nix/pkgs/graphite.nix @@ -134,7 +134,10 @@ deps.crane.lib.buildPackage ( cp target/${if dev then "debug" else "release"}/graphite $out/bin/graphite mkdir -p $out/share/applications - cp $src/desktop/assets/*.desktop $out/share/applications/ + cp $src/desktop/assets/art.graphite.Graphite.desktop $out/share/applications/ + + mkdir -p $out/share/mime/packages + cp $src/desktop/assets/art.graphite.Graphite.xml $out/share/mime/packages/ mkdir -p $out/share/icons/hicolor/scalable/apps cp ${branding}/app-icons/graphite.svg $out/share/icons/hicolor/scalable/apps/art.graphite.Graphite.svg diff --git a/desktop/assets/art.graphite.Graphite.desktop b/desktop/assets/art.graphite.Graphite.desktop index 2e17c64fec..48d53531ce 100644 --- a/desktop/assets/art.graphite.Graphite.desktop +++ b/desktop/assets/art.graphite.Graphite.desktop @@ -2,10 +2,11 @@ Name=Graphite GenericName=Vector & Raster Graphics Editor Comment=Open-source vector & raster graphics editor. Featuring node based procedural nondestructive editing workflow. -Exec=graphite +Exec=graphite %F Terminal=false Type=Application Icon=art.graphite.Graphite Categories=Graphics;VectorGraphics;RasterGraphics; Keywords=graphite;editor;vector;raster;procedural;design; StartupWMClass=art.graphite.Graphite +MimeType=application/graphite+json;image/svg+xml;image/png;image/jpeg;image/gif;image/bmp;image/tiff;image/webp;image/x-portable-pixmap;image/x-portable-graymap;image/x-portable-bitmap;image/vnd.microsoft.icon; diff --git a/desktop/assets/art.graphite.Graphite.xml b/desktop/assets/art.graphite.Graphite.xml new file mode 100644 index 0000000000..7c087ee356 --- /dev/null +++ b/desktop/assets/art.graphite.Graphite.xml @@ -0,0 +1,9 @@ + + + + Graphite Document + + + + + From a4885a5f0c38523bf1a0f4a212106c8d80971c8c Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 8 May 2026 15:28:19 +0000 Subject: [PATCH 2/5] windows --- Cargo.lock | 14 +++++- desktop/platform/win/Cargo.toml | 3 ++ desktop/platform/win/src/file_associations.rs | 48 +++++++++++++++++++ desktop/platform/win/src/main.rs | 7 +++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 desktop/platform/win/src/file_associations.rs diff --git a/Cargo.lock b/Cargo.lock index dcc71e997b..c8a4a75c6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2138,6 +2138,7 @@ name = "graphite-desktop-platform-win" version = "0.0.0" dependencies = [ "graphite-desktop", + "windows-registry 0.6.1", "winres", ] @@ -2439,7 +2440,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry", + "windows-registry 0.5.3", ] [[package]] @@ -6848,6 +6849,17 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.2.0" diff --git a/desktop/platform/win/Cargo.toml b/desktop/platform/win/Cargo.toml index e268622cca..34cf4871b4 100644 --- a/desktop/platform/win/Cargo.toml +++ b/desktop/platform/win/Cargo.toml @@ -15,5 +15,8 @@ path = "src/main.rs" [dependencies] graphite-desktop = { path = "../.." } +[target.'cfg(target_os = "windows")'.dependencies] +windows-registry = "0.6" + [target.'cfg(target_os = "windows")'.build-dependencies] winres = "0.1" diff --git a/desktop/platform/win/src/file_associations.rs b/desktop/platform/win/src/file_associations.rs new file mode 100644 index 0000000000..18eee49124 --- /dev/null +++ b/desktop/platform/win/src/file_associations.rs @@ -0,0 +1,48 @@ +use std::env; +use std::error::Error; + +use windows_registry::CURRENT_USER; + +const PROG_ID: &str = "Graphite.Document"; +const EXECUTABLE_NAME: &str = "Graphite.exe"; +const APP_FRIENDLY_NAME: &str = "Graphite"; +const DOCUMENT_FRIENDLY_NAME: &str = "Graphite Document"; +const MIME_TYPE: &str = "application/graphite+json"; +const FILE_EXTENSION: &str = ".graphite"; +const SUPPORTED_EXTENSIONS: &[&str] = &[FILE_EXTENSION, ".svg", ".png", ".jpg", ".jpeg"]; + +pub fn register() { + if let Err(e) = register_inner() { + eprintln!("Failed to register file associations: {e}"); + } +} + +fn register_inner() -> Result<(), Box> { + let exe = env::current_exe()?; + let exe_string = exe.to_string_lossy(); + let open_command = format!("\"{exe_string}\" \"%1\""); + let icon_value = format!("{exe_string},0"); + + let prog_id = CURRENT_USER.create(format!("Software\\Classes\\{PROG_ID}"))?; + prog_id.set_string("", DOCUMENT_FRIENDLY_NAME)?; + let prog_id_icon = CURRENT_USER.create(format!("Software\\Classes\\{PROG_ID}\\DefaultIcon"))?; + prog_id_icon.set_string("", &icon_value)?; + let prog_id_command = CURRENT_USER.create(format!("Software\\Classes\\{PROG_ID}\\shell\\open\\command"))?; + prog_id_command.set_string("", &open_command)?; + + let app_base = format!("Software\\Classes\\Applications\\{EXECUTABLE_NAME}"); + let app = CURRENT_USER.create(&app_base)?; + app.set_string("FriendlyAppName", APP_FRIENDLY_NAME)?; + let app_command = CURRENT_USER.create(format!("{app_base}\\shell\\open\\command"))?; + app_command.set_string("", &open_command)?; + let supported = CURRENT_USER.create(format!("{app_base}\\SupportedTypes"))?; + for extension in SUPPORTED_EXTENSIONS { + supported.set_string(extension, "")?; + } + + let extension = CURRENT_USER.create(format!("Software\\Classes\\{FILE_EXTENSION}"))?; + extension.set_string("", PROG_ID)?; + extension.set_string("Content Type", MIME_TYPE)?; + + Ok(()) +} diff --git a/desktop/platform/win/src/main.rs b/desktop/platform/win/src/main.rs index 2864480316..34a9762387 100644 --- a/desktop/platform/win/src/main.rs +++ b/desktop/platform/win/src/main.rs @@ -1,4 +1,11 @@ #![windows_subsystem = "windows"] + +#[cfg(target_os = "windows")] +mod file_associations; + fn main() { + #[cfg(target_os = "windows")] + file_associations::register(); + graphite_desktop::start(); } From 0ae71a89c524e00d545f54e29a9974f26ee2f342 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 8 May 2026 15:28:19 +0000 Subject: [PATCH 3/5] Fix --- desktop/assets/art.graphite.Graphite.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/assets/art.graphite.Graphite.desktop b/desktop/assets/art.graphite.Graphite.desktop index 48d53531ce..2e715a4f7d 100644 --- a/desktop/assets/art.graphite.Graphite.desktop +++ b/desktop/assets/art.graphite.Graphite.desktop @@ -9,4 +9,4 @@ Icon=art.graphite.Graphite Categories=Graphics;VectorGraphics;RasterGraphics; Keywords=graphite;editor;vector;raster;procedural;design; StartupWMClass=art.graphite.Graphite -MimeType=application/graphite+json;image/svg+xml;image/png;image/jpeg;image/gif;image/bmp;image/tiff;image/webp;image/x-portable-pixmap;image/x-portable-graymap;image/x-portable-bitmap;image/vnd.microsoft.icon; +MimeType=application/graphite+json;image/svg+xml;image/png;image/jpeg; From 0dacec4f1618818af80ecd852861021ffb0eedfb Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 9 May 2026 19:25:50 +0000 Subject: [PATCH 4/5] Improve for windows --- Cargo.lock | 1 + desktop/platform/win/Cargo.toml | 1 + desktop/platform/win/src/file_associations.rs | 108 +++++++++++++----- desktop/platform/win/src/main.rs | 2 +- 4 files changed, 83 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8a4a75c6b..6ba448b8be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2138,6 +2138,7 @@ name = "graphite-desktop-platform-win" version = "0.0.0" dependencies = [ "graphite-desktop", + "windows 0.62.2", "windows-registry 0.6.1", "winres", ] diff --git a/desktop/platform/win/Cargo.toml b/desktop/platform/win/Cargo.toml index 34cf4871b4..de23c8e774 100644 --- a/desktop/platform/win/Cargo.toml +++ b/desktop/platform/win/Cargo.toml @@ -17,6 +17,7 @@ graphite-desktop = { path = "../.." } [target.'cfg(target_os = "windows")'.dependencies] windows-registry = "0.6" +windows = { version = "0.62", features = ["Win32_UI_Shell"] } [target.'cfg(target_os = "windows")'.build-dependencies] winres = "0.1" diff --git a/desktop/platform/win/src/file_associations.rs b/desktop/platform/win/src/file_associations.rs index 18eee49124..0e6f26a66d 100644 --- a/desktop/platform/win/src/file_associations.rs +++ b/desktop/platform/win/src/file_associations.rs @@ -1,6 +1,7 @@ use std::env; -use std::error::Error; +use std::path::Path; +use windows::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify}; use windows_registry::CURRENT_USER; const PROG_ID: &str = "Graphite.Document"; @@ -11,38 +12,89 @@ const MIME_TYPE: &str = "application/graphite+json"; const FILE_EXTENSION: &str = ".graphite"; const SUPPORTED_EXTENSIONS: &[&str] = &[FILE_EXTENSION, ".svg", ".png", ".jpg", ".jpeg"]; -pub fn register() { - if let Err(e) = register_inner() { +pub fn write() { + if let Err(e) = FileAssociationWriter::new(&env::current_exe().unwrap()) + .document_type(PROG_ID, DOCUMENT_FRIENDLY_NAME) + .application(EXECUTABLE_NAME, APP_FRIENDLY_NAME, SUPPORTED_EXTENSIONS) + .extension(FILE_EXTENSION, PROG_ID, MIME_TYPE) + .write() + { eprintln!("Failed to register file associations: {e}"); } } -fn register_inner() -> Result<(), Box> { - let exe = env::current_exe()?; - let exe_string = exe.to_string_lossy(); - let open_command = format!("\"{exe_string}\" \"%1\""); - let icon_value = format!("{exe_string},0"); - - let prog_id = CURRENT_USER.create(format!("Software\\Classes\\{PROG_ID}"))?; - prog_id.set_string("", DOCUMENT_FRIENDLY_NAME)?; - let prog_id_icon = CURRENT_USER.create(format!("Software\\Classes\\{PROG_ID}\\DefaultIcon"))?; - prog_id_icon.set_string("", &icon_value)?; - let prog_id_command = CURRENT_USER.create(format!("Software\\Classes\\{PROG_ID}\\shell\\open\\command"))?; - prog_id_command.set_string("", &open_command)?; - - let app_base = format!("Software\\Classes\\Applications\\{EXECUTABLE_NAME}"); - let app = CURRENT_USER.create(&app_base)?; - app.set_string("FriendlyAppName", APP_FRIENDLY_NAME)?; - let app_command = CURRENT_USER.create(format!("{app_base}\\shell\\open\\command"))?; - app_command.set_string("", &open_command)?; - let supported = CURRENT_USER.create(format!("{app_base}\\SupportedTypes"))?; - for extension in SUPPORTED_EXTENSIONS { - supported.set_string(extension, "")?; +struct FileAssociationWriter { + open_command: String, + icon_value: String, + entries: Vec, +} + +struct RegistryEntry { + path: String, + name: String, + value: String, +} + +impl FileAssociationWriter { + fn new(executable: &Path) -> Self { + let exe_string = executable.to_string_lossy(); + Self { + open_command: format!("\"{exe_string}\" \"%1\""), + icon_value: format!("{exe_string},0"), + entries: Vec::new(), + } } - let extension = CURRENT_USER.create(format!("Software\\Classes\\{FILE_EXTENSION}"))?; - extension.set_string("", PROG_ID)?; - extension.set_string("Content Type", MIME_TYPE)?; + fn document_type(mut self, prog_id: &str, friendly_name: &str) -> Self { + let base = format!("Software\\Classes\\{prog_id}"); + self.push(&base, "", friendly_name); + self.push(&format!("{base}\\DefaultIcon"), "", &self.icon_value.clone()); + self.push(&format!("{base}\\shell\\open\\command"), "", &self.open_command.clone()); + self + } + + fn application(mut self, executable_name: &str, friendly_name: &str, supported_extensions: &[&str]) -> Self { + let base = format!("Software\\Classes\\Applications\\{executable_name}"); + self.push(&base, "FriendlyAppName", friendly_name); + self.push(&format!("{base}\\shell\\open\\command"), "", &self.open_command.clone()); + + let supported_path = format!("{base}\\SupportedTypes"); + for extension in supported_extensions { + self.push(&supported_path, extension, ""); + } + self + } - Ok(()) + fn extension(mut self, extension: &str, prog_id: &str, mime_type: &str) -> Self { + let path = format!("Software\\Classes\\{extension}"); + self.push(&path, "", prog_id); + self.push(&path, "Content Type", mime_type); + self + } + + fn push(&mut self, path: &str, name: &str, value: &str) { + self.entries.push(RegistryEntry { + path: path.to_owned(), + name: name.to_owned(), + value: value.to_owned(), + }); + } + + fn write(self) -> windows_registry::Result<()> { + let mut changed = false; + + for entry in &self.entries { + let key = CURRENT_USER.create(&entry.path)?; + if key.get_string(&entry.name).ok().as_deref() == Some(entry.value.as_str()) { + continue; + } + key.set_string(&entry.name, &entry.value)?; + changed = true; + } + + if changed { + unsafe { SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None) }; + } + Ok(()) + } } diff --git a/desktop/platform/win/src/main.rs b/desktop/platform/win/src/main.rs index 34a9762387..646b77a349 100644 --- a/desktop/platform/win/src/main.rs +++ b/desktop/platform/win/src/main.rs @@ -5,7 +5,7 @@ mod file_associations; fn main() { #[cfg(target_os = "windows")] - file_associations::register(); + file_associations::write(); graphite_desktop::start(); } From b116b9f3fdf620fffa21a2a5ae466395f9d86a12 Mon Sep 17 00:00:00 2001 From: Timon Date: Sun, 10 May 2026 13:44:05 +0000 Subject: [PATCH 5/5] Add TODO --- desktop/platform/win/src/file_associations.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/desktop/platform/win/src/file_associations.rs b/desktop/platform/win/src/file_associations.rs index 0e6f26a66d..a7a63d1075 100644 --- a/desktop/platform/win/src/file_associations.rs +++ b/desktop/platform/win/src/file_associations.rs @@ -1,3 +1,6 @@ +// This is a bit of a hack to get file associations working without an installer. +// TODO: Replace this with a proper installer that can set up file associations. + use std::env; use std::path::Path;