collective

Browse / github.com/storytold/deckcraft / patch

macOS: open documents from Finder (double-click, Open With, Dock)

macOS: open documents from Finder (double-click, Open With, Dock) Finder hands files to an app through a kAEOpenDocuments ('odoc') Apple event, not argv. DeckCraft declared no CFBundleDocumentTypes and winit 0.30 doesn't handle the event, so AppKit refused every double-click with "DeckCraft cannot open files in the Office Open XML presentation format", even with DeckCraft set as the default app for .pptx. - Info.plist: declare .deckcraft/.slidecraft (Owner) and .pptx, .potx, .ppsx (Alternate) as document types. - apple_events.rs: register an NSAppleEventManager handler before the event loop through fmv-macos-events (as GridCraft does, since the workspace fo

Status
open
Score
0.00
Adopters
0 other operators
Author
fedehsq (imported from pull request #1072, open)
Base
a31b990d32f2c8562f1588562f922960c3fde48b
Size
+154 −4 in 5 files
Review
pending
Shared
2026-10-28
Id
08eaaa92e5c5789271e74afe

How it went

No other operator has reported using it yet.

Diff

raw · Cargo.lock apps/deckcraft/Cargo.toml apps/deckcraft/src/apple_events.rs apps/deckcraft/src/main.rs packaging/macos/Info.plist.in

diff --git a/Cargo.lock b/Cargo.lock--- a/Cargo.lock+++ b/Cargo.lock@@ -901,6 +901,7 @@ dependencies = [  "deckcraft-ui-egui",  "eframe",  "egui",+ "fmv-macos-events",  "image",  "log",  "rfd",@@ -1631,6 +1632,17 @@ dependencies = [  "num-traits", ] +[[package]]+name = "fmv-macos-events"+version = "0.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "90642cf66dde7f205dc8b42dfe57b47d0f3a29f571b832742883ef512e23fe55"+dependencies = [+ "objc2 0.6.5",+ "objc2-app-kit 0.3.2",+ "objc2-foundation 0.3.2",+]+ [[package]] name = "fnv" version = "1.0.7"diff --git a/apps/deckcraft/Cargo.toml b/apps/deckcraft/Cargo.toml--- a/apps/deckcraft/Cargo.toml+++ b/apps/deckcraft/Cargo.toml@@ -29,5 +29,12 @@ arboard = { workspace = true, features = ["image-data"] } image = { workspace = true } cpal = "0.16" +[target.'cfg(target_os = "macos")'.dependencies]+# Finder double-clicks, Open With and Dock drops arrive as an 'odoc' Apple event, not argv. This+# small audited crate (also used by GridCraft, PdfCraft and PhotoCraft) registers the handler+# (Objective-C interop needs unsafe, which our workspace forbids) behind a safe API. Pinned+# exactly: review the diff before bumping.+fmv-macos-events = "=0.1.0"+ [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] winit = { version = "0.30", default-features = false, features = ["x11"] }diff --git a/apps/deckcraft/src/apple_events.rs b/apps/deckcraft/src/apple_events.rsnew file mode 100644--- /dev/null+++ b/apps/deckcraft/src/apple_events.rs@@ -0,0 +1,52 @@+//! macOS open-documents and quit Apple events.+//!+//! Finder double-clicks, Open With, drops on the Dock icon and `open -a DeckCraft talk.pptx` don't+//! pass paths on the command line: LaunchServices sends the running (or just-launched) app a+//! `kAEOpenDocuments` ('odoc') Apple event. winit 0.30 doesn't handle it and owns the+//! `NSApplicationDelegate`, so AppKit answered "DeckCraft cannot open files in the Office Open XML+//! presentation format". Handling it ourselves needs Objective-C class declarations, i.e. `unsafe`,+//! which this workspace forbids. The audited `fmv-macos-events` crate (also used by GridCraft,+//! PdfCraft and PhotoCraft) wraps exactly that, an `NSAppleEventManager` handler registered before+//! Finder's launch event that leaves winit's delegate alone, behind a safe main-thread API.++use deckcraft_ui_egui::SlideApp;+use fmv_macos_events::{Event, Inbox, Registration};++/// Keeps the Apple-event handlers registered; hold it until the event loop returns.+pub struct AppleEvents {+    _registration: Registration,+    inbox: Inbox,+}++impl AppleEvents {+    /// Register the handlers. Call on the main thread before the event loop starts, so the event+    /// that launched the app (a Finder double-click) is caught too.+    pub fn install() -> Self {+        let (registration, inbox) = Registration::install();+        Self { _registration: registration, inbox }+    }++    /// The queue the app drains every frame ([`poll`]); events arriving later wake `ctx`.+    pub fn connect(&self, ctx: &egui::Context) -> Inbox {+        let ctx = ctx.clone();+        self.inbox.set_wake(move || ctx.request_repaint());+        self.inbox.clone()+    }+}++/// Open the documents and act on the quit requests that arrived since the last frame. Documents go+/// through [`SlideApp::open_from_system`], so a double-clicked `.ppsx` starts its slide show as it+/// does from the command line.+pub fn poll(inbox: &Inbox, app: &mut SlideApp, ctx: &egui::Context) {+    for e in inbox.drain() {+        match e {+            Event::Open(paths) => {+                let paths: Vec<String> = paths.iter().filter_map(|p| p.to_str()).map(str::to_string).collect();+                for (f, e) in app.open_from_system(&paths) {+                    log::error!("{f}: {e}");+                }+            }+            Event::Quit => ctx.send_viewport_cmd(egui::ViewportCommand::Close),+        }+    }+}diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs--- a/apps/deckcraft/src/main.rs+++ b/apps/deckcraft/src/main.rs@@ -14,6 +14,8 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] +#[cfg(target_os = "macos")]+mod apple_events; mod audio; mod control_server; #[cfg(all(unix, not(target_os = "macos")))]@@ -27,10 +29,12 @@ mod pasteboard; use deckcraft_engine::Session; use deckcraft_ui_egui::{Services, SlideApp}; -struct App(SlideApp);+struct App(SlideApp, #[cfg(target_os = "macos")] fmv_macos_events::Inbox);  impl eframe::App for App {     fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {+        #[cfg(target_os = "macos")]+        apple_events::poll(&self.1, &mut self.0, ctx);         self.0.logic(ctx);     }     fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {@@ -234,6 +238,12 @@ fn main() -> eframe::Result {     // Before eframe creates the wgpu instance: default Windows to DirectX 12 with FXC (see graphics.rs).     #[cfg(target_os = "windows")]     graphics::configure(&mut options, eframe::wgpu::Backends::from_env(), eframe::wgpu::Dx12Compiler::from_env());+    // Registered before the event loop starts, so it catches the Finder event that launched us as+    // well as later ones. Lives until the event loop returns; the app creator only borrows it.+    #[cfg(target_os = "macos")]+    let apple_events = apple_events::AppleEvents::install();+    #[cfg(target_os = "macos")]+    let apple_events = &apple_events;     eframe::run_native(         "DeckCraft",         options,@@ -264,7 +274,11 @@ fn main() -> eframe::Result {             if show {                 app.start_show(0, false);             }-            Ok(Box::new(App(app)))+            Ok(Box::new(App(+                app,+                #[cfg(target_os = "macos")]+                apple_events.connect(&cc.egui_ctx),+            )))         }),     ) }@@ -278,7 +292,11 @@ mod tests {     #[test]     fn on_exit_ends_the_process_on_macos() {         if std::env::var_os("DECKCRAFT_TEST_ON_EXIT").is_some() {-            let mut app = App(SlideApp::new(Session::new(), Services::default()));+            let mut app = App(+                SlideApp::new(Session::new(), Services::default()),+                #[cfg(target_os = "macos")]+                fmv_macos_events::Inbox::default(),+            );             println!("calling on_exit");             eframe::App::on_exit(&mut app);             println!("on_exit returned");diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in--- a/packaging/macos/Info.plist.in+++ b/packaging/macos/Info.plist.in@@ -63,7 +63,7 @@       <key>UTTypeTagSpecification</key>       <dict>         <key>public.filename-extension</key>-        <array><string>deckcraft</string></array>+        <array><string>deckcraft</string><string>slidecraft</string></array>         <key>public.mime-type</key>         <array><string>application/x-deckcraft</string></array>       </dict>@@ -91,5 +91,66 @@     </dict>   </array> +  <!-- What Finder may open with DeckCraft (double-click, Open With, default app). Without these+       AppKit refuses the 'odoc' event with "DeckCraft cannot open files in the … format";+       apple_events.rs hands the paths to the app. -->+  <key>CFBundleDocumentTypes</key>+  <array>+    <dict>+      <key>CFBundleTypeName</key>+      <string>DeckCraft Presentation</string>+      <key>CFBundleTypeRole</key>+      <string>Editor</string>+      <key>LSHandlerRank</key>+      <string>Owner</string>+      <key>LSItemContentTypes</key>+      <array><string>ai.storyteller.deckcraft.presentation</string></array>+      <key>CFBundleTypeExtensions</key>+      <array><string>deckcraft</string><string>slidecraft</string></array>+    </dict>+    <dict>+      <key>CFBundleTypeName</key>+      <string>PowerPoint Presentation</string>+      <key>CFBundleTypeRole</key>+      <string>Editor</string>+      <key>LSHandlerRank</key>+      <string>Alternate</string>+      <key>LSItemContentTypes</key>+      <array><string>org.openxmlformats.presentationml.presentation</string></array>+      <key>CFBundleTypeExtensions</key>+      <array><string>pptx</string></array>+      <key>CFBundleTypeMIMETypes</key>+      <array><string>application/vnd.openxmlformats-officedocument.presentationml.presentation</string></array>+    </dict>+    <dict>+      <key>CFBundleTypeName</key>+      <string>PowerPoint Template</string>+      <key>CFBundleTypeRole</key>+      <string>Editor</string>+      <key>LSHandlerRank</key>+      <string>Alternate</string>+      <key>LSItemContentTypes</key>+      <array><string>org.openxmlformats.presentationml.template</string></array>+      <key>CFBundleTypeExtensions</key>+      <array><string>potx</string></array>+      <key>CFBundleTypeMIMETypes</key>+      <array><string>application/vnd.openxmlformats-officedocument.presentationml.template</string></array>+    </dict>+    <dict>+      <key>CFBundleTypeName</key>+      <string>PowerPoint Show</string>+      <key>CFBundleTypeRole</key>+      <string>Viewer</string>+      <key>LSHandlerRank</key>+      <string>Alternate</string>+      <key>LSItemContentTypes</key>+      <array><string>org.openxmlformats.presentationml.slideshow</string></array>+      <key>CFBundleTypeExtensions</key>+      <array><string>ppsx</string></array>+      <key>CFBundleTypeMIMETypes</key>+      <array><string>application/vnd.openxmlformats-officedocument.presentationml.slideshow</string></array>+    </dict>+  </array>+ </dict> </plist>