Desktop: Register the Graphite app as a file handler on Mac (#4106)

* Desktop: Implement native file open handler

* Desktop: Register file types on Mac

* Review
This commit is contained in:
Timon
2026-05-06 13:05:30 +00:00
committed by GitHub
parent 8ae8c47fe1
commit 1c2ac19b16
7 changed files with 213 additions and 33 deletions
+36 -20
View File
@@ -2,6 +2,7 @@ use rand::Rng;
use rfd::AsyncFileDialog;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender, SyncSender};
@@ -14,7 +15,6 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::WindowId;
use crate::cef;
use crate::cli::Cli;
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
use crate::event::{AppEvent, AppEventScheduler};
use crate::persist;
@@ -47,7 +47,7 @@ pub(crate) struct App {
web_communication_startup_buffer: Vec<Vec<u8>>,
#[cfg_attr(not(target_os = "macos"), expect(unused))]
preferences: Preferences,
cli: Cli,
launch_documents: Option<Vec<PathBuf>>,
startup_time: Option<Instant>,
exiting: Arc<AtomicBool>,
exit_reason: ExitReason,
@@ -58,6 +58,7 @@ impl App {
Window::init();
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
cef_context: Box<dyn cef::CefContext>,
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
@@ -65,7 +66,7 @@ impl App {
app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler,
preferences: Preferences,
cli: Cli,
launch_documents: Vec<PathBuf>,
) -> Self {
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
ctrlc::set_handler(move || {
@@ -115,7 +116,7 @@ impl App {
web_communication_initialized: false,
web_communication_startup_buffer: Vec::new(),
preferences,
cli,
launch_documents: Some(launch_documents),
startup_time: None,
exiting,
exit_reason: ExitReason::Shutdown,
@@ -307,22 +308,11 @@ impl App {
responses.push(message);
}
DesktopFrontendMessage::OpenLaunchDocuments => {
if self.cli.files.is_empty() {
let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else {
tracing::error!("OpenLaunchDocuments should only be sent once");
return;
}
let app_event_scheduler = self.app_event_scheduler.clone();
let launch_documents = std::mem::take(&mut self.cli.files);
let _ = thread::spawn(move || {
for path in launch_documents {
tracing::info!("Opening file from command line: {}", path.display());
if let Ok(content) = fs::read(&path) {
let message = DesktopWrapperMessage::OpenFile { path, content };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
} else {
tracing::error!("Failed to read file: {}", path.display());
}
}
});
};
self.open_files(launch_documents);
}
DesktopFrontendMessage::UpdateMenu { entries } => {
if let Some(window) = &self.window {
@@ -476,11 +466,37 @@ impl App {
event_loop.exit();
}
#[cfg(target_os = "macos")]
AppEvent::AddLaunchDocuments(paths) => {
if let Some(launch_documents) = &mut self.launch_documents {
launch_documents.extend(paths);
} else {
self.open_files(paths);
}
}
#[cfg(target_os = "macos")]
AppEvent::MenuEvent { id } => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
}
}
}
fn open_files(&mut self, paths: Vec<PathBuf>) {
if paths.is_empty() {
return;
}
let app_event_scheduler = self.app_event_scheduler.clone();
let _ = thread::spawn(move || {
for path in paths {
tracing::info!("Opening file: {}", path.display());
if let Ok(content) = fs::read(&path) {
let message = DesktopWrapperMessage::OpenFile { path, content };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
} else {
tracing::error!("Failed to read file: {}", path.display());
}
}
});
}
}
impl ApplicationHandler for App {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
@@ -570,7 +586,7 @@ impl ApplicationHandler for App {
}
if !self.cef_init_successful
&& !self.cli.disable_ui_acceleration
&& !self.preferences.disable_ui_acceleration
&& self.web_communication_initialized
&& let Some(startup_time) = self.startup_time
&& startup_time.elapsed() > Duration::from_secs(3)
+2 -2
View File
@@ -131,13 +131,13 @@ fn platform_settings(instance_dir: &Path) -> Settings {
{
let exe = std::env::current_exe().expect("cannot get current exe path");
let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure");
return Settings {
Settings {
main_bundle_path: app_root.to_str().map(CefString::from).unwrap(),
multi_threaded_message_loop: 0,
external_message_pump: 1,
no_sandbox: 1, // GPU helper crashes when running with sandbox
..base
};
}
}
#[cfg(not(target_os = "macos"))]
+2
View File
@@ -10,6 +10,8 @@ pub(crate) enum AppEvent {
NodeGraphExecutionResult(NodeGraphExecutionResult),
Exit,
#[cfg(target_os = "macos")]
AddLaunchDocuments(Vec<std::path::PathBuf>),
#[cfg(target_os = "macos")]
MenuEvent {
id: String,
},
+7 -5
View File
@@ -67,7 +67,7 @@ pub fn start() {
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
dirs::delete_old_cef_browser_directory();
let prefs = preferences::read();
let mut prefs = preferences::read();
// Must be called before event loop initialization or native window integrations will break
App::init();
@@ -80,13 +80,15 @@ pub fn start() {
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
let disable_ui_acceleration = prefs.disable_ui_acceleration || cli.disable_ui_acceleration;
if disable_ui_acceleration {
if cli.disable_ui_acceleration {
prefs.disable_ui_acceleration = true;
}
if prefs.disable_ui_acceleration {
println!("UI acceleration is disabled");
}
let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver);
let cef_context = match cef_context_builder.create(cef_handler, disable_ui_acceleration) {
let cef_context = match cef_context_builder.create(cef_handler, prefs.disable_ui_acceleration) {
Ok(context) => {
tracing::info!("CEF initialized successfully");
context
@@ -102,7 +104,7 @@ pub fn start() {
}
};
let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli);
let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files);
let exit_reason = app.run(event_loop);
+1 -1
View File
@@ -26,8 +26,8 @@ impl super::NativeWindow for NativeWindowImpl {
}
fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self {
app::setup(app_event_scheduler.clone());
let menu = menu::Menu::new(app_event_scheduler);
NativeWindowImpl { menu }
}
+70 -3
View File
@@ -1,6 +1,22 @@
use objc2::{ClassType, define_class, msg_send};
use objc2_app_kit::{NSApplication, NSEvent, NSEventType, NSResponder};
use objc2_foundation::NSObject;
use std::ffi::CStr;
use std::ffi::OsStr;
use std::ops::Deref;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::sync::{Mutex, Once};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2::{ClassType, MainThreadMarker, MainThreadOnly, define_class, msg_send};
use objc2_app_kit::{NSApplication, NSApplicationDelegate, NSEvent, NSEventType, NSResponder};
use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSURL};
use crate::event::{AppEvent, AppEventScheduler};
static APP_EVENT_SCHEDULER: Mutex<Option<AppEventScheduler>> = Mutex::new(None);
static INSTALL_DELEGATE: Once = Once::new();
static LAUNCH_DOCUMENTS: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
define_class!(
#[unsafe(super(NSApplication, NSResponder, NSObject))]
@@ -20,12 +36,63 @@ define_class!(
}
);
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "GraphiteApplicationDelegate"]
struct GraphiteApplicationDelegate;
unsafe impl NSObjectProtocol for GraphiteApplicationDelegate {}
unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate {
#[unsafe(method(application:openURLs:))]
fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
let app_event_scheduler = APP_EVENT_SCHEDULER.lock().unwrap();
let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap();
for index in 0..urls.count() {
let url = urls.objectAtIndex(index);
if !url.isFileURL() {
tracing::error!("Ignoring macOS open URL event for non-file URL: {:?}", url);
continue;
}
let path = unsafe { CStr::from_ptr(url.fileSystemRepresentation().as_ptr()) };
let path = PathBuf::from(OsStr::from_bytes(path.to_bytes()));
pending_paths_to_open.push(path);
}
if let Some(app_event_scheduler) = app_event_scheduler.deref() {
app_event_scheduler.schedule(AppEvent::AddLaunchDocuments(std::mem::take(&mut pending_paths_to_open)));
}
}
}
);
fn instance() -> objc2::rc::Retained<NSApplication> {
unsafe { msg_send![GraphiteApplication::class(), sharedApplication] }
}
pub(super) fn init() {
let _ = instance();
INSTALL_DELEGATE.call_once(|| {
let mtm = MainThreadMarker::new().expect("only ever called from main thread");
let delegate: Retained<GraphiteApplicationDelegate> = unsafe { msg_send![super(GraphiteApplicationDelegate::alloc(mtm).set_ivars(())), init] };
instance().setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
std::mem::forget(delegate);
});
}
pub(super) fn setup(app_event_scheduler: AppEventScheduler) {
let mut app_event_scheduler_guard = APP_EVENT_SCHEDULER.lock().unwrap();
let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap();
app_event_scheduler.schedule(AppEvent::AddLaunchDocuments(std::mem::take(&mut pending_paths_to_open)));
*app_event_scheduler_guard = Some(app_event_scheduler);
}
pub(super) fn hide() {