Desktop: Add support for opening files through the already-running instance via a local socket (#4123)
* Desktop: Forward file-open args from a second launch to the running instance Also adds socket infrastructure that can be used in the future to allow dispatching actions from another process * Use socket instead of ipc terminologie * Fix * Fix * Better pipe name on windows
This commit is contained in:
parent
a28b9437aa
commit
98daa75c26
|
|
@ -1210,6 +1210,12 @@ dependencies = [
|
||||||
"libloading 0.8.8",
|
"libloading 0.8.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doctest-file"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
|
@ -2075,6 +2081,7 @@ dependencies = [
|
||||||
"glam",
|
"glam",
|
||||||
"graphite-desktop-embedded-resources",
|
"graphite-desktop-embedded-resources",
|
||||||
"graphite-desktop-wrapper",
|
"graphite-desktop-wrapper",
|
||||||
|
"interprocess",
|
||||||
"lzma-rust2",
|
"lzma-rust2",
|
||||||
"muda",
|
"muda",
|
||||||
"objc2 0.6.3",
|
"objc2 0.6.3",
|
||||||
|
|
@ -2695,6 +2702,19 @@ dependencies = [
|
||||||
"wgpu-executor",
|
"wgpu-executor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "interprocess"
|
||||||
|
version = "2.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "069323743400cb7ab06a8fe5c1ed911d36b6919ec531661d034c89083629595b"
|
||||||
|
dependencies = [
|
||||||
|
"doctest-file",
|
||||||
|
"libc",
|
||||||
|
"recvmsg",
|
||||||
|
"widestring",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-uring"
|
name = "io-uring"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
|
|
@ -4370,6 +4390,12 @@ dependencies = [
|
||||||
"font-types 0.11.0",
|
"font-types 0.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "recvmsg"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.17"
|
version = "0.5.17"
|
||||||
|
|
@ -6613,6 +6639,12 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
accepted = [
|
accepted = [
|
||||||
|
"0BSD", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
|
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
|
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`
|
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ ignore = [
|
||||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||||
#
|
#
|
||||||
allow = [
|
allow = [
|
||||||
|
"0BSD", # Keep this list in sync with those in `/about.toml`
|
||||||
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
|
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
|
||||||
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
|
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`
|
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ lzma-rust2 = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
rand = { workspace = true, features = ["thread_rng"] }
|
rand = { workspace = true, features = ["thread_rng"] }
|
||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
interprocess = "2.4.2"
|
||||||
fd-lock = "4.0.4"
|
fd-lock = "4.0.4"
|
||||||
ctrlc = "3.5.1"
|
ctrlc = "3.5.1"
|
||||||
window_clipboard = "0.5"
|
window_clipboard = "0.5"
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ pub(crate) struct App {
|
||||||
start_render_sender: SyncSender<()>,
|
start_render_sender: SyncSender<()>,
|
||||||
web_communication_initialized: bool,
|
web_communication_initialized: bool,
|
||||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||||
#[cfg_attr(not(target_os = "macos"), expect(unused))]
|
|
||||||
preferences: Preferences,
|
preferences: Preferences,
|
||||||
launch_documents: Option<Vec<PathBuf>>,
|
launch_documents: Option<Vec<PathBuf>>,
|
||||||
startup_time: Option<Instant>,
|
startup_time: Option<Instant>,
|
||||||
|
|
@ -320,7 +319,7 @@ impl App {
|
||||||
tracing::error!("OpenLaunchDocuments should only be sent once");
|
tracing::error!("OpenLaunchDocuments should only be sent once");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.open_files(launch_documents);
|
self.app_event_scheduler.schedule(AppEvent::OpenFiles(launch_documents));
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::UpdateMenu { entries } => {
|
DesktopFrontendMessage::UpdateMenu { entries } => {
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
|
|
@ -478,13 +477,28 @@ impl App {
|
||||||
tracing::info!("Exiting main event loop");
|
tracing::info!("Exiting main event loop");
|
||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
AppEvent::OpenFiles(paths) => {
|
||||||
AppEvent::AddLaunchDocuments(paths) => {
|
// Accumulate launch documents until OpenLaunchDocuments message is received
|
||||||
if let Some(launch_documents) = &mut self.launch_documents {
|
if let Some(launch_documents) = &mut self.launch_documents {
|
||||||
launch_documents.extend(paths);
|
launch_documents.extend(paths);
|
||||||
} else {
|
return;
|
||||||
self.open_files(paths);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
AppEvent::MenuEvent { id } => {
|
AppEvent::MenuEvent { id } => {
|
||||||
|
|
@ -492,24 +506,6 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
impl ApplicationHandler for App {
|
||||||
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
|
pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
|
||||||
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
|
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
|
||||||
|
pub(crate) const APP_SOCKET_FILE_NAME: &str = "instance.sock";
|
||||||
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
||||||
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
|
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
|
||||||
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
|
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ pub(crate) enum AppEvent {
|
||||||
DesktopWrapperMessage(DesktopWrapperMessage),
|
DesktopWrapperMessage(DesktopWrapperMessage),
|
||||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||||
Exit,
|
Exit,
|
||||||
#[cfg(target_os = "macos")]
|
OpenFiles(Vec<std::path::PathBuf>),
|
||||||
AddLaunchDocuments(Vec<std::path::PathBuf>),
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
MenuEvent {
|
MenuEvent {
|
||||||
id: String,
|
id: String,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ mod gpu_context;
|
||||||
mod persist;
|
mod persist;
|
||||||
mod preferences;
|
mod preferences;
|
||||||
mod render;
|
mod render;
|
||||||
|
mod socket;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
pub(crate) mod consts;
|
pub(crate) mod consts;
|
||||||
|
|
@ -58,7 +59,13 @@ pub fn start() {
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
tracing::error!("Another instance is already running, Exiting.");
|
tracing::error!("Another instance is already running, Exiting.");
|
||||||
std::process::exit(1);
|
if !cli.files.is_empty()
|
||||||
|
&& let Err(error) = socket::send(socket::Message::OpenFiles(cli.files))
|
||||||
|
{
|
||||||
|
tracing::error!("Failed to send socket message to running instance: {}", error);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -78,6 +85,8 @@ pub fn start() {
|
||||||
let (app_event_sender, app_event_receiver) = std::sync::mpsc::channel();
|
let (app_event_sender, app_event_receiver) = std::sync::mpsc::channel();
|
||||||
let app_event_scheduler = event_loop.create_app_event_scheduler(app_event_sender);
|
let app_event_scheduler = event_loop.create_app_event_scheduler(app_event_sender);
|
||||||
|
|
||||||
|
let _socket_handle = socket::start(app_event_scheduler.clone());
|
||||||
|
|
||||||
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
|
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
if cli.disable_ui_acceleration {
|
if cli.disable_ui_acceleration {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
use interprocess::local_socket::{GenericFilePath, GenericNamespaced, ListenerNonblockingMode, ListenerOptions, Name, prelude::*};
|
||||||
|
use std::io::{ErrorKind, Read, Write};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::consts::APP_SOCKET_FILE_NAME;
|
||||||
|
use crate::event::{AppEvent, AppEventScheduler};
|
||||||
|
|
||||||
|
// TODO: Needs to be integrated/replaced with the action system.
|
||||||
|
// TODO: At that point this should just wrap the action, meaning all actions bindable by the user can also be accessed via the socket.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub(crate) enum Message {
|
||||||
|
OpenFiles(Vec<std::path::PathBuf>),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message(message: Message, app_event_scheduler: &AppEventScheduler) {
|
||||||
|
match message {
|
||||||
|
Message::OpenFiles(paths) => {
|
||||||
|
app_event_scheduler.schedule(AppEvent::OpenFiles(paths));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send(message: Message) -> std::io::Result<()> {
|
||||||
|
let data = ron::ser::to_string(&message).map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
|
||||||
|
let mut connection = interprocess::local_socket::Stream::connect(socket_name())?;
|
||||||
|
connection.write_all(data.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SocketHandle {
|
||||||
|
thread: Option<thread::JoinHandle<()>>,
|
||||||
|
shutdown_sender: mpsc::Sender<()>,
|
||||||
|
}
|
||||||
|
impl Drop for SocketHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.shutdown_sender.send(());
|
||||||
|
let _ = self.thread.take().expect("SocketHandle can only be dropped once").join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start(app_event_scheduler: AppEventScheduler) -> SocketHandle {
|
||||||
|
let (shutdown_sender, shutdown_receiver) = mpsc::channel();
|
||||||
|
|
||||||
|
let thread = thread::Builder::new()
|
||||||
|
.name("socket".to_string())
|
||||||
|
.spawn(move || run(app_event_scheduler, shutdown_receiver))
|
||||||
|
.expect("Failed to spawn socket thread");
|
||||||
|
|
||||||
|
SocketHandle {
|
||||||
|
shutdown_sender,
|
||||||
|
thread: Some(thread),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(app_event_scheduler: AppEventScheduler, shutdown_receiver: mpsc::Receiver<()>) {
|
||||||
|
let listener = match ListenerOptions::new()
|
||||||
|
.name(socket_name())
|
||||||
|
.nonblocking(ListenerNonblockingMode::Accept)
|
||||||
|
.try_overwrite(true)
|
||||||
|
.max_spin_time(Duration::from_millis(100))
|
||||||
|
.create_sync()
|
||||||
|
{
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!("Failed to bind socket: {}", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_backoff = Duration::from_millis(100);
|
||||||
|
let mut backoff = Duration::ZERO;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if backoff.is_zero() {
|
||||||
|
match shutdown_receiver.try_recv() {
|
||||||
|
Ok(()) | Err(mpsc::TryRecvError::Disconnected) => break,
|
||||||
|
Err(mpsc::TryRecvError::Empty) => {}
|
||||||
|
}
|
||||||
|
backoff = Duration::from_nanos(1);
|
||||||
|
} else {
|
||||||
|
match shutdown_receiver.recv_timeout(backoff) {
|
||||||
|
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||||
|
}
|
||||||
|
backoff = (backoff * 2).min(max_backoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
match listener.accept() {
|
||||||
|
Ok(mut connection) => {
|
||||||
|
backoff = Duration::ZERO;
|
||||||
|
|
||||||
|
let app_event_scheduler = app_event_scheduler.clone();
|
||||||
|
let spawn_result = thread::Builder::new().name("socket-connection".to_string()).spawn(move || {
|
||||||
|
let mut data = String::new();
|
||||||
|
if let Err(error) = connection.read_to_string(&mut data) {
|
||||||
|
tracing::error!("Failed to read socket message: {}", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match ron::de::from_str(&data) {
|
||||||
|
Ok(message) => handle_message(message, &app_event_scheduler),
|
||||||
|
Err(error) => tracing::error!("Failed to deserialize socket message: {}", error),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Err(error) = spawn_result {
|
||||||
|
tracing::error!("Failed to spawn socket connection thread: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!("Failed to accept socket connection: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_name() -> Name<'static> {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
let user = std::env::var("USERNAME").unwrap_or_default();
|
||||||
|
let name = format!("{user}-{app}-{APP_SOCKET_FILE_NAME}", app = crate::consts::APP_NAME);
|
||||||
|
name.to_ns_name::<GenericNamespaced>().expect("valid named pipe name")
|
||||||
|
} else {
|
||||||
|
crate::dirs::app_data_dir().join(APP_SOCKET_FILE_NAME).to_fs_name::<GenericFilePath>().expect("valid socket path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::ops::DerefMut;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Mutex, Once};
|
use std::sync::{Mutex, Once};
|
||||||
|
|
@ -14,9 +15,61 @@ use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSURL};
|
||||||
use crate::event::{AppEvent, AppEventScheduler};
|
use crate::event::{AppEvent, AppEventScheduler};
|
||||||
|
|
||||||
static APP_EVENT_SCHEDULER: Mutex<Option<AppEventScheduler>> = Mutex::new(None);
|
static APP_EVENT_SCHEDULER: Mutex<Option<AppEventScheduler>> = Mutex::new(None);
|
||||||
|
static PENDING_EVENTS: Mutex<Option<Vec<AppEvent>>> = Mutex::new(Some(Vec::new()));
|
||||||
|
|
||||||
|
fn dispatch_event(event: AppEvent) {
|
||||||
|
let app_event_scheduler_guard = APP_EVENT_SCHEDULER.lock().unwrap();
|
||||||
|
if let Some(app_event_scheduler) = app_event_scheduler_guard.deref() {
|
||||||
|
app_event_scheduler.schedule(event);
|
||||||
|
} else if let Some(pending_events) = PENDING_EVENTS.lock().unwrap().deref_mut() {
|
||||||
|
pending_events.push(event);
|
||||||
|
} else {
|
||||||
|
tracing::error!("Failed to dispatch event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance() -> objc2::rc::Retained<NSApplication> {
|
||||||
|
unsafe { msg_send![GraphiteApplication::class(), sharedApplication] }
|
||||||
|
}
|
||||||
|
|
||||||
static INSTALL_DELEGATE: Once = Once::new();
|
static INSTALL_DELEGATE: Once = Once::new();
|
||||||
|
|
||||||
static LAUNCH_DOCUMENTS: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
|
pub(super) fn init() {
|
||||||
|
let _ = instance();
|
||||||
|
|
||||||
|
INSTALL_DELEGATE.call_once(|| {
|
||||||
|
let mtm = MainThreadMarker::new().expect("should only ever be 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();
|
||||||
|
|
||||||
|
if let Some(mut pending_events) = PENDING_EVENTS.lock().unwrap().take() {
|
||||||
|
pending_events.drain(..).for_each(|event| {
|
||||||
|
app_event_scheduler.schedule(event);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tracing::error!("Failed to take PENDING_EVENTS and schedule them. This a bug.");
|
||||||
|
}
|
||||||
|
|
||||||
|
*app_event_scheduler_guard = Some(app_event_scheduler);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn hide() {
|
||||||
|
instance().hide(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn hide_others() {
|
||||||
|
instance().hideOtherApplications(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn show_all() {
|
||||||
|
instance().unhideAllApplications(None);
|
||||||
|
}
|
||||||
|
|
||||||
define_class!(
|
define_class!(
|
||||||
#[unsafe(super(NSApplication, NSResponder, NSObject))]
|
#[unsafe(super(NSApplication, NSResponder, NSObject))]
|
||||||
|
|
@ -47,62 +100,20 @@ define_class!(
|
||||||
unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate {
|
unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate {
|
||||||
#[unsafe(method(application:openURLs:))]
|
#[unsafe(method(application:openURLs:))]
|
||||||
fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
|
fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
|
||||||
let app_event_scheduler = APP_EVENT_SCHEDULER.lock().unwrap();
|
let paths = (0..urls.count())
|
||||||
|
.filter_map(|index| {
|
||||||
|
let url = urls.objectAtIndex(index);
|
||||||
|
if !url.isFileURL() {
|
||||||
|
tracing::error!("Ignoring open URL event for non-file URL: {:?}", url);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let cstr = unsafe { CStr::from_ptr(url.fileSystemRepresentation().as_ptr()) };
|
||||||
|
let path = PathBuf::from(OsStr::from_bytes(cstr.to_bytes()));
|
||||||
|
Some(path)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap();
|
dispatch_event(AppEvent::OpenFiles(paths));
|
||||||
|
|
||||||
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() {
|
|
||||||
instance().hide(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn hide_others() {
|
|
||||||
instance().hideOtherApplications(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn show_all() {
|
|
||||||
instance().unhideAllApplications(None);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue