Skip to content

Commit 1c2ac19

Browse files
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
1 parent 8ae8c47 commit 1c2ac19

7 files changed

Lines changed: 213 additions & 33 deletions

File tree

desktop/bundle/src/mac.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const EXEC_PATH: &str = "Contents/MacOS";
1313
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
1414
const RESOURCES_PATH: &str = "Contents/Resources";
1515
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
16+
const GRAPHITE_DOCUMENT_TYPE: &str = "art.graphite.document";
17+
const GRAPHITE_FILE_EXTENSION: &str = "graphite";
18+
const GRAPHITE_MIME_TYPE: &str = "application/graphite+json";
1619

1720
pub fn main() -> Result<(), Box<dyn Error>> {
1821
let app_bin = build_bin("graphite-desktop-platform-mac", None)?;
@@ -73,7 +76,7 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) ->
7376
cf_bundle_identifier: id.to_string(),
7477
cf_bundle_display_name: exec_name.to_string(),
7578
cf_bundle_executable: exec_name.to_string(),
76-
cf_bundle_icon_file: ICONS_FILE_NAME.to_string(),
79+
cf_bundle_icon_file: if is_helper { None } else { Some(ICONS_FILE_NAME.to_string()) },
7780
cf_bundle_info_dictionary_version: "6.0".to_string(),
7881
cf_bundle_package_type: "APPL".to_string(),
7982
cf_bundle_signature: "????".to_string(),
@@ -85,13 +88,56 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) ->
8588
ls_minimum_system_version: "11.0".to_string(),
8689
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
8790
ns_supports_automatic_graphics_switching: true,
91+
cf_bundle_document_types: (!is_helper).then(document_types),
92+
ut_exported_type_declarations: (!is_helper).then(exported_type_declarations),
8893
};
8994

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

100+
fn document_types() -> Vec<DocumentType> {
101+
vec![
102+
DocumentType {
103+
cf_bundle_type_name: "Graphite Document".to_string(),
104+
cf_bundle_type_role: "Editor".to_string(),
105+
cf_bundle_type_extensions: Some(vec![GRAPHITE_FILE_EXTENSION.to_string()]),
106+
cf_bundle_type_icon_file: Some(ICONS_FILE_NAME.to_string()),
107+
ls_handler_rank: Some("Owner".to_string()),
108+
ls_item_content_types: vec![GRAPHITE_DOCUMENT_TYPE.to_string()],
109+
},
110+
DocumentType {
111+
cf_bundle_type_name: "SVG Image".to_string(),
112+
cf_bundle_type_role: "Editor".to_string(),
113+
cf_bundle_type_extensions: Some(vec!["svg".to_string()]),
114+
cf_bundle_type_icon_file: None,
115+
ls_handler_rank: Some("Alternate".to_string()),
116+
ls_item_content_types: vec!["public.svg-image".to_string()],
117+
},
118+
DocumentType {
119+
cf_bundle_type_name: "Image".to_string(),
120+
cf_bundle_type_role: "Editor".to_string(),
121+
cf_bundle_type_extensions: None,
122+
cf_bundle_type_icon_file: None,
123+
ls_handler_rank: Some("Alternate".to_string()),
124+
ls_item_content_types: vec!["public.image".to_string()],
125+
},
126+
]
127+
}
128+
129+
fn exported_type_declarations() -> Vec<ExportedTypeDeclaration> {
130+
vec![ExportedTypeDeclaration {
131+
ut_type_identifier: GRAPHITE_DOCUMENT_TYPE.to_string(),
132+
ut_type_description: "Graphite Document".to_string(),
133+
ut_type_conforms_to: vec!["public.json".to_string()],
134+
ut_type_tag_specification: TypeTagSpecification {
135+
public_filename_extension: vec![GRAPHITE_FILE_EXTENSION.to_string()],
136+
public_mime_type: GRAPHITE_MIME_TYPE.to_string(),
137+
},
138+
}]
139+
}
140+
95141
#[derive(serde::Serialize)]
96142
struct InfoPlist {
97143
#[serde(rename = "CFBundleName")]
@@ -103,7 +149,8 @@ struct InfoPlist {
103149
#[serde(rename = "CFBundleExecutable")]
104150
cf_bundle_executable: String,
105151
#[serde(rename = "CFBundleIconFile")]
106-
cf_bundle_icon_file: String,
152+
#[serde(skip_serializing_if = "Option::is_none")]
153+
cf_bundle_icon_file: Option<String>,
107154
#[serde(rename = "CFBundleInfoDictionaryVersion")]
108155
cf_bundle_info_dictionary_version: String,
109156
#[serde(rename = "CFBundlePackageType")]
@@ -123,7 +170,53 @@ struct InfoPlist {
123170
#[serde(rename = "LSMinimumSystemVersion")]
124171
ls_minimum_system_version: String,
125172
#[serde(rename = "LSUIElement")]
173+
#[serde(skip_serializing_if = "Option::is_none")]
126174
ls_ui_element: Option<String>,
127175
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
128176
ns_supports_automatic_graphics_switching: bool,
177+
#[serde(rename = "CFBundleDocumentTypes")]
178+
#[serde(skip_serializing_if = "Option::is_none")]
179+
cf_bundle_document_types: Option<Vec<DocumentType>>,
180+
#[serde(rename = "UTExportedTypeDeclarations")]
181+
#[serde(skip_serializing_if = "Option::is_none")]
182+
ut_exported_type_declarations: Option<Vec<ExportedTypeDeclaration>>,
183+
}
184+
185+
#[derive(serde::Serialize)]
186+
struct DocumentType {
187+
#[serde(rename = "CFBundleTypeName")]
188+
cf_bundle_type_name: String,
189+
#[serde(rename = "CFBundleTypeRole")]
190+
cf_bundle_type_role: String,
191+
#[serde(rename = "CFBundleTypeExtensions")]
192+
#[serde(skip_serializing_if = "Option::is_none")]
193+
cf_bundle_type_extensions: Option<Vec<String>>,
194+
#[serde(rename = "CFBundleTypeIconFile")]
195+
#[serde(skip_serializing_if = "Option::is_none")]
196+
cf_bundle_type_icon_file: Option<String>,
197+
#[serde(rename = "LSHandlerRank")]
198+
#[serde(skip_serializing_if = "Option::is_none")]
199+
ls_handler_rank: Option<String>,
200+
#[serde(rename = "LSItemContentTypes")]
201+
ls_item_content_types: Vec<String>,
202+
}
203+
204+
#[derive(serde::Serialize)]
205+
struct ExportedTypeDeclaration {
206+
#[serde(rename = "UTTypeIdentifier")]
207+
ut_type_identifier: String,
208+
#[serde(rename = "UTTypeDescription")]
209+
ut_type_description: String,
210+
#[serde(rename = "UTTypeConformsTo")]
211+
ut_type_conforms_to: Vec<String>,
212+
#[serde(rename = "UTTypeTagSpecification")]
213+
ut_type_tag_specification: TypeTagSpecification,
214+
}
215+
216+
#[derive(serde::Serialize)]
217+
struct TypeTagSpecification {
218+
#[serde(rename = "public.filename-extension")]
219+
public_filename_extension: Vec<String>,
220+
#[serde(rename = "public.mime-type")]
221+
public_mime_type: String,
129222
}

desktop/src/app.rs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use rand::Rng;
22
use rfd::AsyncFileDialog;
33
use std::fs;
44
use std::io::Read;
5+
use std::path::PathBuf;
56
use std::sync::Arc;
67
use std::sync::atomic::{AtomicBool, Ordering};
78
use std::sync::mpsc::{Receiver, Sender, SyncSender};
@@ -14,7 +15,6 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
1415
use winit::window::WindowId;
1516

1617
use crate::cef;
17-
use crate::cli::Cli;
1818
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
1919
use crate::event::{AppEvent, AppEventScheduler};
2020
use crate::persist;
@@ -47,7 +47,7 @@ pub(crate) struct App {
4747
web_communication_startup_buffer: Vec<Vec<u8>>,
4848
#[cfg_attr(not(target_os = "macos"), expect(unused))]
4949
preferences: Preferences,
50-
cli: Cli,
50+
launch_documents: Option<Vec<PathBuf>>,
5151
startup_time: Option<Instant>,
5252
exiting: Arc<AtomicBool>,
5353
exit_reason: ExitReason,
@@ -58,14 +58,15 @@ impl App {
5858
Window::init();
5959
}
6060

61+
#[allow(clippy::too_many_arguments)]
6162
pub(crate) fn new(
6263
cef_context: Box<dyn cef::CefContext>,
6364
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
6465
wgpu_context: WgpuContext,
6566
app_event_receiver: Receiver<AppEvent>,
6667
app_event_scheduler: AppEventScheduler,
6768
preferences: Preferences,
68-
cli: Cli,
69+
launch_documents: Vec<PathBuf>,
6970
) -> Self {
7071
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
7172
ctrlc::set_handler(move || {
@@ -115,7 +116,7 @@ impl App {
115116
web_communication_initialized: false,
116117
web_communication_startup_buffer: Vec::new(),
117118
preferences,
118-
cli,
119+
launch_documents: Some(launch_documents),
119120
startup_time: None,
120121
exiting,
121122
exit_reason: ExitReason::Shutdown,
@@ -307,22 +308,11 @@ impl App {
307308
responses.push(message);
308309
}
309310
DesktopFrontendMessage::OpenLaunchDocuments => {
310-
if self.cli.files.is_empty() {
311+
let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else {
312+
tracing::error!("OpenLaunchDocuments should only be sent once");
311313
return;
312-
}
313-
let app_event_scheduler = self.app_event_scheduler.clone();
314-
let launch_documents = std::mem::take(&mut self.cli.files);
315-
let _ = thread::spawn(move || {
316-
for path in launch_documents {
317-
tracing::info!("Opening file from command line: {}", path.display());
318-
if let Ok(content) = fs::read(&path) {
319-
let message = DesktopWrapperMessage::OpenFile { path, content };
320-
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
321-
} else {
322-
tracing::error!("Failed to read file: {}", path.display());
323-
}
324-
}
325-
});
314+
};
315+
self.open_files(launch_documents);
326316
}
327317
DesktopFrontendMessage::UpdateMenu { entries } => {
328318
if let Some(window) = &self.window {
@@ -476,11 +466,37 @@ impl App {
476466
event_loop.exit();
477467
}
478468
#[cfg(target_os = "macos")]
469+
AppEvent::AddLaunchDocuments(paths) => {
470+
if let Some(launch_documents) = &mut self.launch_documents {
471+
launch_documents.extend(paths);
472+
} else {
473+
self.open_files(paths);
474+
}
475+
}
476+
#[cfg(target_os = "macos")]
479477
AppEvent::MenuEvent { id } => {
480478
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
481479
}
482480
}
483481
}
482+
483+
fn open_files(&mut self, paths: Vec<PathBuf>) {
484+
if paths.is_empty() {
485+
return;
486+
}
487+
let app_event_scheduler = self.app_event_scheduler.clone();
488+
let _ = thread::spawn(move || {
489+
for path in paths {
490+
tracing::info!("Opening file: {}", path.display());
491+
if let Ok(content) = fs::read(&path) {
492+
let message = DesktopWrapperMessage::OpenFile { path, content };
493+
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
494+
} else {
495+
tracing::error!("Failed to read file: {}", path.display());
496+
}
497+
}
498+
});
499+
}
484500
}
485501
impl ApplicationHandler for App {
486502
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
@@ -570,7 +586,7 @@ impl ApplicationHandler for App {
570586
}
571587

572588
if !self.cef_init_successful
573-
&& !self.cli.disable_ui_acceleration
589+
&& !self.preferences.disable_ui_acceleration
574590
&& self.web_communication_initialized
575591
&& let Some(startup_time) = self.startup_time
576592
&& startup_time.elapsed() > Duration::from_secs(3)

desktop/src/cef/context/builder.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,13 @@ fn platform_settings(instance_dir: &Path) -> Settings {
131131
{
132132
let exe = std::env::current_exe().expect("cannot get current exe path");
133133
let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure");
134-
return Settings {
134+
Settings {
135135
main_bundle_path: app_root.to_str().map(CefString::from).unwrap(),
136136
multi_threaded_message_loop: 0,
137137
external_message_pump: 1,
138138
no_sandbox: 1, // GPU helper crashes when running with sandbox
139139
..base
140-
};
140+
}
141141
}
142142

143143
#[cfg(not(target_os = "macos"))]

desktop/src/event.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ pub(crate) enum AppEvent {
1010
NodeGraphExecutionResult(NodeGraphExecutionResult),
1111
Exit,
1212
#[cfg(target_os = "macos")]
13+
AddLaunchDocuments(Vec<std::path::PathBuf>),
14+
#[cfg(target_os = "macos")]
1315
MenuEvent {
1416
id: String,
1517
},

desktop/src/lib.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub fn start() {
6767
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
6868
dirs::delete_old_cef_browser_directory();
6969

70-
let prefs = preferences::read();
70+
let mut prefs = preferences::read();
7171

7272
// Must be called before event loop initialization or native window integrations will break
7373
App::init();
@@ -80,13 +80,15 @@ pub fn start() {
8080

8181
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
8282

83-
let disable_ui_acceleration = prefs.disable_ui_acceleration || cli.disable_ui_acceleration;
84-
if disable_ui_acceleration {
83+
if cli.disable_ui_acceleration {
84+
prefs.disable_ui_acceleration = true;
85+
}
86+
if prefs.disable_ui_acceleration {
8587
println!("UI acceleration is disabled");
8688
}
8789

8890
let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver);
89-
let cef_context = match cef_context_builder.create(cef_handler, disable_ui_acceleration) {
91+
let cef_context = match cef_context_builder.create(cef_handler, prefs.disable_ui_acceleration) {
9092
Ok(context) => {
9193
tracing::info!("CEF initialized successfully");
9294
context
@@ -102,7 +104,7 @@ pub fn start() {
102104
}
103105
};
104106

105-
let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli);
107+
let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files);
106108

107109
let exit_reason = app.run(event_loop);
108110

desktop/src/window/mac.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ impl super::NativeWindow for NativeWindowImpl {
2626
}
2727

2828
fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self {
29+
app::setup(app_event_scheduler.clone());
2930
let menu = menu::Menu::new(app_event_scheduler);
30-
3131
NativeWindowImpl { menu }
3232
}
3333

0 commit comments

Comments
 (0)