Desktop: Add simple cli interface (#3059)

* Open document from cli and add some files to .gitignore

* Revert .gitignore

* Revert .gitignore

* Implement cli properly

* Add ui-accelerated-painting option

* Format code

* Revert changes in .vscode folder

* Don't use global variables for cli

* Apply suggested changes

* Make ready for merge

* Improve names

* remove comment

---------

Co-authored-by: Timon <me@timon.zip>
This commit is contained in:
Mateo 2025-10-13 18:09:40 -03:00 committed by GitHub
parent e366e4d64e
commit 497758c273
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 71 additions and 18 deletions

1
Cargo.lock generated
View File

@ -2252,6 +2252,7 @@ dependencies = [
"bytemuck",
"cef",
"cef-dll-sys",
"clap",
"core-foundation",
"derivative",
"dirs",

View File

@ -43,6 +43,7 @@ rfd = { workspace = true }
open = { workspace = true }
rand = { workspace = true, features = ["thread_rng"] }
serde = { workspace = true }
clap = { workspace = true, features = ["derive"] }
# Hardware acceleration dependencies
ash = { version = "0.38", optional = true }

View File

@ -1,4 +1,6 @@
use rfd::AsyncFileDialog;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::Sender;
@ -39,6 +41,7 @@ pub(crate) struct App {
web_communication_initialized: bool,
web_communication_startup_buffer: Vec<Vec<u8>>,
persistent_data: PersistentData,
launch_documents: Vec<PathBuf>,
}
impl App {
@ -48,6 +51,7 @@ impl App {
wgpu_context: WgpuContext,
app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler,
launch_documents: Vec<PathBuf>,
) -> Self {
let rendering_app_event_scheduler = app_event_scheduler.clone();
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
@ -79,6 +83,7 @@ impl App {
web_communication_startup_buffer: Vec::new(),
persistent_data,
native_window: Default::default(),
launch_documents,
}
}
@ -102,7 +107,7 @@ impl App {
let show_dialog = async move { dialog.pick_file().await.map(|f| f.path().to_path_buf()) };
if let Some(path) = futures::executor::block_on(show_dialog)
&& let Ok(content) = std::fs::read(&path)
&& let Ok(content) = fs::read(&path)
{
let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
@ -135,7 +140,7 @@ impl App {
});
}
DesktopFrontendMessage::WriteFile { path, content } => {
if let Err(e) = std::fs::write(&path, content) {
if let Err(e) = fs::write(&path, content) {
tracing::error!("Failed to write file {}: {}", path.display(), e);
}
}
@ -197,6 +202,14 @@ impl App {
DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => {
self.persistent_data.set_document_order(ids);
}
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
self.persistent_data.write_preferences(preferences);
}
DesktopFrontendMessage::PersistenceLoadPreferences => {
let preferences = self.persistent_data.load_preferences();
let message = DesktopWrapperMessage::LoadPreferences { preferences };
responses.push(message);
}
DesktopFrontendMessage::PersistenceLoadCurrentDocument => {
if let Some((id, document)) = self.persistent_data.current_document() {
let message = DesktopWrapperMessage::LoadDocument {
@ -232,13 +245,23 @@ impl App {
responses.push(message);
}
}
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
self.persistent_data.write_preferences(preferences);
}
DesktopFrontendMessage::PersistenceLoadPreferences => {
let preferences = self.persistent_data.load_preferences();
let message = DesktopWrapperMessage::LoadPreferences { preferences };
responses.push(message);
DesktopFrontendMessage::OpenLaunchDocuments => {
if self.launch_documents.is_empty() {
return;
}
let app_event_scheduler = self.app_event_scheduler.clone();
let launch_documents = std::mem::take(&mut self.launch_documents);
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());
}
}
});
}
}
}
@ -389,7 +412,7 @@ impl ApplicationHandler for App {
}
WindowEvent::DragDropped { paths, .. } => {
for path in paths {
match std::fs::read(&path) {
match fs::read(&path) {
Ok(content) => {
let message = DesktopWrapperMessage::OpenFile { path, content };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));

View File

@ -63,7 +63,7 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
}
#[cfg(target_os = "macos")]
pub(crate) fn initialize(self, event_handler: H) -> Result<impl CefContext, InitError> {
pub(crate) fn initialize(self, event_handler: H, disable_gpu_acceleration: bool) -> Result<impl CefContext, InitError> {
let instance_dir = create_instance_dir();
let settings = Settings {
@ -77,11 +77,11 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
self.initialize_inner(&event_handler, settings)?;
create_browser(event_handler, instance_dir)
create_browser(event_handler, instance_dir, disable_gpu_acceleration)
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn initialize(self, event_handler: H) -> Result<impl CefContext, InitError> {
pub(crate) fn initialize(self, event_handler: H, disable_gpu_acceleration: bool) -> Result<impl CefContext, InitError> {
let instance_dir = create_instance_dir();
let settings = Settings {
@ -94,7 +94,7 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
self.initialize_inner(&event_handler, settings)?;
super::multithreaded::run_on_ui_thread(move || match create_browser(event_handler, instance_dir) {
super::multithreaded::run_on_ui_thread(move || match create_browser(event_handler, instance_dir, disable_gpu_acceleration) {
Ok(context) => {
super::multithreaded::CONTEXT.with(|b| {
*b.borrow_mut() = Some(context);
@ -125,14 +125,21 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
}
}
fn create_browser<H: CefEventHandler>(event_handler: H, instance_dir: PathBuf) -> Result<SingleThreadedCefContext, InitError> {
fn create_browser<H: CefEventHandler>(event_handler: H, instance_dir: PathBuf, disable_gpu_acceleration: bool) -> Result<SingleThreadedCefContext, InitError> {
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));
#[cfg(feature = "accelerated_paint")]
let use_accelerated_paint = if disable_gpu_acceleration {
false
} else {
crate::cef::platform::should_enable_hardware_acceleration()
};
let window_info = WindowInfo {
windowless_rendering_enabled: 1,
#[cfg(feature = "accelerated_paint")]
shared_texture_enabled: if crate::cef::platform::should_enable_hardware_acceleration() { 1 } else { 0 },
shared_texture_enabled: use_accelerated_paint as i32,
..Default::default()
};

9
desktop/src/cli.rs Normal file
View File

@ -0,0 +1,9 @@
#[derive(clap::Parser)]
#[clap(name = "graphite-editor", version)]
pub struct Cli {
#[arg(help = "Files to open on startup")]
pub files: Vec<std::path::PathBuf>,
#[arg(long, action = clap::ArgAction::SetTrue, help = "Disable hardware accelerated UI rendering")]
pub disable_ui_acceleration: bool,
}

View File

@ -1,3 +1,4 @@
use clap::Parser;
use std::process::exit;
use tracing_subscriber::EnvFilter;
use winit::event_loop::EventLoop;
@ -6,6 +7,7 @@ pub(crate) mod consts;
mod app;
mod cef;
mod cli;
mod dirs;
mod event;
mod native_window;
@ -16,6 +18,7 @@ mod gpu_context;
use app::App;
use cef::CefHandler;
use cli::Cli;
use event::CreateAppEventSchedulerEventLoopExt;
fn main() {
@ -31,6 +34,8 @@ fn main() {
return;
}
let cli = Cli::parse();
let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context());
let event_loop = EventLoop::new().unwrap();
@ -40,7 +45,7 @@ fn main() {
let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel();
let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), window_size_receiver);
let cef_context = match cef_context_builder.initialize(cef_handler) {
let cef_context = match cef_context_builder.initialize(cef_handler, cli.disable_ui_acceleration) {
Ok(c) => {
tracing::info!("CEF initialized successfully");
c
@ -63,7 +68,7 @@ fn main() {
}
};
let mut app = App::new(Box::new(cef_context), window_size_sender, wgpu_context, app_event_receiver, app_event_scheduler);
let mut app = App::new(Box::new(cef_context), window_size_sender, wgpu_context, app_event_receiver, app_event_scheduler, cli.files);
event_loop.run_app(&mut app).unwrap();
}

View File

@ -110,6 +110,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
FrontendMessage::TriggerLoadRestAutoSaveDocuments => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadRemainingDocuments);
}
FrontendMessage::TriggerOpenLaunchDocuments => {
dispatcher.respond(DesktopFrontendMessage::OpenLaunchDocuments);
}
FrontendMessage::TriggerSavePreferences { preferences } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceWritePreferences { preferences });
}

View File

@ -8,6 +8,7 @@ pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Prefere
pub enum DesktopFrontendMessage {
ToWeb(Vec<FrontendMessage>),
OpenLaunchDocuments,
OpenFileDialog {
title: String,
filters: Vec<FileFilter>,

View File

@ -102,6 +102,7 @@ pub enum FrontendMessage {
},
TriggerLoadFirstAutoSaveDocument,
TriggerLoadRestAutoSaveDocuments,
TriggerOpenLaunchDocuments,
TriggerLoadPreferences,
TriggerOpenDocument,
TriggerPaste,

View File

@ -153,6 +153,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Tell frontend to finish loading persistent documents
responses.add(FrontendMessage::TriggerLoadRestAutoSaveDocuments);
responses.add(FrontendMessage::TriggerOpenLaunchDocuments);
}
PortfolioMessage::DocumentPassMessage { document_id, message } => {
if let Some(document) = self.documents.get_mut(&document_id) {