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
@@ -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"@@ -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"] }new file mode 100644@@ -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),+ }+ }+}@@ -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");@@ -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>