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/Cargo.lock b/Cargo.lock
index dcc71e997b..6ba448b8be 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2138,6 +2138,8 @@ name = "graphite-desktop-platform-win"
version = "0.0.0"
dependencies = [
"graphite-desktop",
+ "windows 0.62.2",
+ "windows-registry 0.6.1",
"winres",
]
@@ -2439,7 +2441,7 @@ dependencies = [
"tokio",
"tower-service",
"tracing",
- "windows-registry",
+ "windows-registry 0.5.3",
]
[[package]]
@@ -6848,6 +6850,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/assets/art.graphite.Graphite.desktop b/desktop/assets/art.graphite.Graphite.desktop
index 2e17c64fec..2e715a4f7d 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;
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
+
+
+
+
+
diff --git a/desktop/platform/win/Cargo.toml b/desktop/platform/win/Cargo.toml
index e268622cca..de23c8e774 100644
--- a/desktop/platform/win/Cargo.toml
+++ b/desktop/platform/win/Cargo.toml
@@ -15,5 +15,9 @@ path = "src/main.rs"
[dependencies]
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
new file mode 100644
index 0000000000..0e6f26a66d
--- /dev/null
+++ b/desktop/platform/win/src/file_associations.rs
@@ -0,0 +1,100 @@
+use std::env;
+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";
+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 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}");
+ }
+}
+
+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(),
+ }
+ }
+
+ 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
+ }
+
+ 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 2864480316..646b77a349 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::write();
+
graphite_desktop::start();
}