Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions desktop/bundle/src/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const EXEC_PATH: &str = "Contents/MacOS";
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
const RESOURCES_PATH: &str = "Contents/Resources";
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
const GRAPHITE_DOCUMENT_TYPE: &str = "art.graphite.document";
const GRAPHITE_FILE_EXTENSION: &str = "graphite";
const GRAPHITE_MIME_TYPE: &str = "application/graphite+json";

pub fn main() -> Result<(), Box<dyn Error>> {
let app_bin = build_bin("graphite-desktop-platform-mac", None)?;
Expand Down Expand Up @@ -73,7 +76,7 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) ->
cf_bundle_identifier: id.to_string(),
cf_bundle_display_name: exec_name.to_string(),
cf_bundle_executable: exec_name.to_string(),
cf_bundle_icon_file: ICONS_FILE_NAME.to_string(),
cf_bundle_icon_file: if is_helper { None } else { Some(ICONS_FILE_NAME.to_string()) },
cf_bundle_info_dictionary_version: "6.0".to_string(),
cf_bundle_package_type: "APPL".to_string(),
cf_bundle_signature: "????".to_string(),
Expand All @@ -85,13 +88,56 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) ->
ls_minimum_system_version: "11.0".to_string(),
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
ns_supports_automatic_graphics_switching: true,
cf_bundle_document_types: (!is_helper).then(document_types),
ut_exported_type_declarations: (!is_helper).then(exported_type_declarations),
};

let plist_file = dir.join("Info.plist");
plist::to_file_xml(plist_file, &info)?;
Ok(())
}

fn document_types() -> Vec<DocumentType> {
vec![
DocumentType {
cf_bundle_type_name: "Graphite Document".to_string(),
cf_bundle_type_role: "Editor".to_string(),
cf_bundle_type_extensions: Some(vec![GRAPHITE_FILE_EXTENSION.to_string()]),
cf_bundle_type_icon_file: Some(ICONS_FILE_NAME.to_string()),
ls_handler_rank: Some("Owner".to_string()),
ls_item_content_types: vec![GRAPHITE_DOCUMENT_TYPE.to_string()],
},
DocumentType {
cf_bundle_type_name: "SVG Image".to_string(),
cf_bundle_type_role: "Editor".to_string(),
cf_bundle_type_extensions: Some(vec!["svg".to_string()]),
cf_bundle_type_icon_file: None,
ls_handler_rank: Some("Alternate".to_string()),
ls_item_content_types: vec!["public.svg-image".to_string()],
},
DocumentType {
cf_bundle_type_name: "Image".to_string(),
cf_bundle_type_role: "Editor".to_string(),
cf_bundle_type_extensions: None,
cf_bundle_type_icon_file: None,
ls_handler_rank: Some("Alternate".to_string()),
ls_item_content_types: vec!["public.image".to_string()],
},
]
}

fn exported_type_declarations() -> Vec<ExportedTypeDeclaration> {
vec![ExportedTypeDeclaration {
ut_type_identifier: GRAPHITE_DOCUMENT_TYPE.to_string(),
ut_type_description: "Graphite Document".to_string(),
ut_type_conforms_to: vec!["public.json".to_string()],
ut_type_tag_specification: TypeTagSpecification {
public_filename_extension: vec![GRAPHITE_FILE_EXTENSION.to_string()],
public_mime_type: GRAPHITE_MIME_TYPE.to_string(),
},
}]
}

#[derive(serde::Serialize)]
struct InfoPlist {
#[serde(rename = "CFBundleName")]
Expand All @@ -103,7 +149,8 @@ struct InfoPlist {
#[serde(rename = "CFBundleExecutable")]
cf_bundle_executable: String,
#[serde(rename = "CFBundleIconFile")]
cf_bundle_icon_file: String,
#[serde(skip_serializing_if = "Option::is_none")]
cf_bundle_icon_file: Option<String>,
#[serde(rename = "CFBundleInfoDictionaryVersion")]
cf_bundle_info_dictionary_version: String,
#[serde(rename = "CFBundlePackageType")]
Expand All @@ -123,7 +170,53 @@ struct InfoPlist {
#[serde(rename = "LSMinimumSystemVersion")]
ls_minimum_system_version: String,
#[serde(rename = "LSUIElement")]
#[serde(skip_serializing_if = "Option::is_none")]
ls_ui_element: Option<String>,
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
ns_supports_automatic_graphics_switching: bool,
#[serde(rename = "CFBundleDocumentTypes")]
#[serde(skip_serializing_if = "Option::is_none")]
cf_bundle_document_types: Option<Vec<DocumentType>>,
#[serde(rename = "UTExportedTypeDeclarations")]
#[serde(skip_serializing_if = "Option::is_none")]
ut_exported_type_declarations: Option<Vec<ExportedTypeDeclaration>>,
}

#[derive(serde::Serialize)]
struct DocumentType {
#[serde(rename = "CFBundleTypeName")]
cf_bundle_type_name: String,
#[serde(rename = "CFBundleTypeRole")]
cf_bundle_type_role: String,
#[serde(rename = "CFBundleTypeExtensions")]
#[serde(skip_serializing_if = "Option::is_none")]
cf_bundle_type_extensions: Option<Vec<String>>,
#[serde(rename = "CFBundleTypeIconFile")]
#[serde(skip_serializing_if = "Option::is_none")]
cf_bundle_type_icon_file: Option<String>,
#[serde(rename = "LSHandlerRank")]
#[serde(skip_serializing_if = "Option::is_none")]
ls_handler_rank: Option<String>,
#[serde(rename = "LSItemContentTypes")]
ls_item_content_types: Vec<String>,
}

#[derive(serde::Serialize)]
struct ExportedTypeDeclaration {
#[serde(rename = "UTTypeIdentifier")]
ut_type_identifier: String,
#[serde(rename = "UTTypeDescription")]
ut_type_description: String,
#[serde(rename = "UTTypeConformsTo")]
ut_type_conforms_to: Vec<String>,
#[serde(rename = "UTTypeTagSpecification")]
ut_type_tag_specification: TypeTagSpecification,
}

#[derive(serde::Serialize)]
struct TypeTagSpecification {
#[serde(rename = "public.filename-extension")]
public_filename_extension: Vec<String>,
#[serde(rename = "public.mime-type")]
public_mime_type: String,
}
59 changes: 39 additions & 20 deletions desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -47,7 +47,8 @@ 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>>,
disable_ui_acceleration: bool,
startup_time: Option<Instant>,
exiting: Arc<AtomicBool>,
exit_reason: ExitReason,
Expand All @@ -58,14 +59,16 @@ 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>,
wgpu_context: WgpuContext,
app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler,
preferences: Preferences,
cli: Cli,
launch_documents: Vec<PathBuf>,
disable_ui_acceleration: bool,
) -> Self {
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
ctrlc::set_handler(move || {
Expand Down Expand Up @@ -115,7 +118,8 @@ impl App {
web_communication_initialized: false,
web_communication_startup_buffer: Vec::new(),
preferences,
cli,
launch_documents: Some(launch_documents),
disable_ui_acceleration,
startup_time: None,
exiting,
exit_reason: ExitReason::Shutdown,
Expand Down Expand Up @@ -307,22 +311,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 send once");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a small typo in the error message: "send" should be "sent".

Suggested change
tracing::error!("OpenLaunchDocuments should only be send once");
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 {
Expand Down Expand Up @@ -476,11 +469,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) {
Expand Down Expand Up @@ -570,7 +589,7 @@ impl ApplicationHandler for App {
}

if !self.cef_init_successful
&& !self.cli.disable_ui_acceleration
&& !self.disable_ui_acceleration
&& self.web_communication_initialized
&& let Some(startup_time) = self.startup_time
&& startup_time.elapsed() > Duration::from_secs(3)
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
11 changes: 10 additions & 1 deletion desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,16 @@ 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,
cli.disable_ui_acceleration,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: App::new is passed cli.disable_ui_acceleration, which ignores the prefs.disable_ui_acceleration value. This makes the app think acceleration is enabled even when prefs disabled it, so the UI-acceleration failure watchdog can still exit the app unnecessarily. Pass the computed disable_ui_acceleration instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/src/lib.rs, line 113:

<comment>App::new is passed `cli.disable_ui_acceleration`, which ignores the `prefs.disable_ui_acceleration` value. This makes the app think acceleration is enabled even when prefs disabled it, so the UI-acceleration failure watchdog can still exit the app unnecessarily. Pass the computed `disable_ui_acceleration` instead.</comment>

<file context>
@@ -102,7 +102,16 @@ pub fn start() {
+		app_event_scheduler,
+		prefs,
+		cli.files,
+		cli.disable_ui_acceleration,
+	);
 
</file context>
Suggested change
cli.disable_ui_acceleration,
disable_ui_acceleration,

);

let exit_reason = app.run(event_loop);

Expand Down
2 changes: 1 addition & 1 deletion desktop/src/window/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Expand Down
76 changes: 73 additions & 3 deletions desktop/src/window/mac/app.rs
Original file line number Diff line number Diff line change
@@ -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};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import msg_send_id to properly handle the initialization of the application delegate.

Suggested change
use objc2::{ClassType, MainThreadMarker, MainThreadOnly, define_class, msg_send};
use objc2::{ClassType, MainThreadMarker, MainThreadOnly, define_class, msg_send, msg_send_id};

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))]
Expand All @@ -20,12 +36,66 @@ 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 Some(app_event_scheduler) = APP_EVENT_SCHEDULER.lock().ok() else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The error message here is misleading: .lock().ok() returns None only when the mutex is poisoned, not when the scheduler is uninitialized (which is the inner Option being None). More importantly, returning early here means the file paths from this event are never buffered into LAUNCH_DOCUMENTS, so they are lost entirely if the mutex is poisoned. Consider collecting paths into LAUNCH_DOCUMENTS before attempting to acquire this lock, so that paths are always buffered regardless of the scheduler lock state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/src/window/mac/app.rs, line 50:

<comment>The error message here is misleading: `.lock().ok()` returns `None` only when the mutex is **poisoned**, not when the scheduler is uninitialized (which is the inner `Option` being `None`). More importantly, returning early here means the file paths from this event are never buffered into `LAUNCH_DOCUMENTS`, so they are lost entirely if the mutex is poisoned. Consider collecting paths into `LAUNCH_DOCUMENTS` before attempting to acquire this lock, so that paths are always buffered regardless of the scheduler lock state.</comment>

<file context>
@@ -20,12 +36,66 @@ define_class!(
+	unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate {
+		#[unsafe(method(application:openURLs:))]
+		fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
+			let Some(app_event_scheduler) = APP_EVENT_SCHEDULER.lock().ok() else {
+				tracing::error!("Received macOS open URL event before the app event scheduler was initialized");
+				return;
</file context>

tracing::error!("Received macOS open URL event before the app event scheduler was initialized");
return;
};
Comment on lines +50 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The error message here is misleading. APP_EVENT_SCHEDULER.lock().ok() only returns None if the mutex is poisoned, not if the scheduler is uninitialized. If the scheduler is uninitialized (i.e., the inner Option is None), the lock succeeds and the code correctly proceeds to buffer the paths in LAUNCH_DOCUMENTS.

Additionally, returning early on a poisoned lock prevents the paths from being added to the buffer. It is better to collect the paths first and then attempt to schedule the event if the scheduler is available.

fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
			let mut paths = Vec::new();

			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()));

				paths.push(path);
			}

			if paths.is_empty() {
				return;
			}

			let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap();
			pending_paths_to_open.extend(paths);

			if let Ok(scheduler_guard) = APP_EVENT_SCHEDULER.lock() {
				if let Some(app_event_scheduler) = scheduler_guard.as_ref() {
					app_event_scheduler.schedule(AppEvent::AddLaunchDocuments(std::mem::take(&mut pending_paths_to_open)));
				}
			}
		}


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] };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The use of super(...) inside msg_send! is incorrect here as it is intended for calling superclass implementations within a class method override. Additionally, msg_send_id! should be used for init to correctly handle the memory management of the returned object.

Suggested change
let delegate: Retained<GraphiteApplicationDelegate> = unsafe { msg_send![super(GraphiteApplicationDelegate::alloc(mtm).set_ivars(())), init] };
let delegate: Retained<GraphiteApplicationDelegate> = unsafe { msg_send_id![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() {
Expand Down
Loading