diff --git a/Cargo.lock b/Cargo.lock index b0ca3b628c..69c75fef67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6344,6 +6344,7 @@ dependencies = [ "async-trait", "breadcrumbs", "chat_panel", + "cli", "client", "clock", "collections", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index ee4b019418..011ed9caa1 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -3,6 +3,14 @@ name = "cli" version = "0.1.0" edition = "2021" +[lib] +path = "src/cli.rs" +doctest = false + +[[bin]] +name = "cli" +path = "src/main.rs" + [dependencies] anyhow = "1.0" core-foundation = "0.9" diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs new file mode 100644 index 0000000000..af7e78ea31 --- /dev/null +++ b/crates/cli/src/cli.rs @@ -0,0 +1,21 @@ +pub use ipc_channel::ipc; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize)] +pub struct IpcHandshake { + pub requests: ipc::IpcSender, + pub responses: ipc::IpcReceiver, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum CliRequest { + Open { paths: Vec, wait: bool }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum CliResponse { + Stdout { message: String }, + Stderr { message: String }, + Exit { status: i32 }, +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a8531125c2..2977f97ad3 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,14 +1,14 @@ use anyhow::{anyhow, Result}; use clap::Parser; +use cli::{CliRequest, CliResponse, IpcHandshake}; use core_foundation::{ array::{CFArray, CFIndex}, string::kCFStringEncodingUTF8, url::{CFURLCreateWithBytes, CFURL}, }; use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType}; -use ipc_channel::ipc::IpcOneShotServer; -use serde::{Deserialize, Serialize}; -use std::{path::PathBuf, process, ptr}; +use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender}; +use std::{fs, path::PathBuf, ptr}; #[derive(Parser)] #[clap(name = "zed")] @@ -21,61 +21,55 @@ struct Args { paths: Vec, } -#[derive(Serialize, Deserialize)] -struct OpenResult { - exit_status: i32, - stdout_message: Option, - stderr_message: Option, -} - fn main() -> Result<()> { let args = Args::parse(); - let (server, server_name) = IpcOneShotServer::::new()?; let app_path = locate_app()?; - launch_app(app_path, args.paths, server_name)?; + let (tx, rx) = launch_app(app_path)?; - let (_, result) = server.accept()?; - if let Some(message) = result.stdout_message { - println!("{}", message); - } - if let Some(message) = result.stderr_message { - eprintln!("{}", message); + tx.send(CliRequest::Open { + paths: args + .paths + .into_iter() + .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error))) + .collect::>>()?, + wait: false, + })?; + + while let Ok(response) = rx.recv() { + match response { + CliResponse::Stdout { message } => println!("{message}"), + CliResponse::Stderr { message } => eprintln!("{message}"), + CliResponse::Exit { status } => std::process::exit(status), + } } - process::exit(result.exit_status) + Ok(()) } fn locate_app() -> Result { - Ok("/Applications/Zed.app".into()) + Ok("/Users/nathan/src/zed/target/debug/bundle/osx/Zed.app".into()) + // Ok("/Applications/Zed.app".into()) } -fn launch_app(app_path: PathBuf, paths_to_open: Vec, server_name: String) -> Result<()> { +fn launch_app(app_path: PathBuf) -> Result<(IpcSender, IpcReceiver)> { + let (server, server_name) = IpcOneShotServer::::new()?; + let status = unsafe { let app_url = CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?; - let mut urls_to_open = paths_to_open - .into_iter() - .map(|path| { - CFURL::from_path(&path, true).ok_or_else(|| anyhow!("{:?} is invalid", path)) - }) - .collect::>>()?; - let server_url = format!("zed_cli_response://{server_name}"); - urls_to_open.push(CFURL::wrap_under_create_rule(CFURLCreateWithBytes( + let url = format!("zed-cli://{server_name}"); + let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes( ptr::null(), - server_url.as_ptr(), - server_url.len() as CFIndex, + url.as_ptr(), + url.len() as CFIndex, kCFStringEncodingUTF8, ptr::null(), - ))); + )); + + let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); - let urls_to_open = CFArray::from_copyable( - &urls_to_open - .iter() - .map(|url| url.as_concrete_TypeRef()) - .collect::>(), - ); LSOpenFromURLSpec( &LSLaunchURLSpec { appURL: app_url.as_concrete_TypeRef(), @@ -87,8 +81,10 @@ fn launch_app(app_path: PathBuf, paths_to_open: Vec, server_name: Strin ptr::null_mut(), ) }; + if status == 0 { - Ok(()) + let (_, handshake) = server.accept()?; + Ok((handshake.requests, handshake.responses)) } else { Err(anyhow!("cannot start {:?}", app_path)) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 683ea46999..57df8879f7 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -248,7 +248,7 @@ impl App { self } - pub fn on_quit(self, mut callback: F) -> Self + pub fn on_quit(&mut self, mut callback: F) -> &mut Self where F: 'static + FnMut(&mut MutableAppContext), { @@ -260,7 +260,7 @@ impl App { self } - pub fn on_event(self, mut callback: F) -> Self + pub fn on_event(&mut self, mut callback: F) -> &mut Self where F: 'static + FnMut(Event, &mut MutableAppContext) -> bool, { @@ -274,7 +274,7 @@ impl App { self } - pub fn on_open_files(self, mut callback: F) -> Self + pub fn on_open_files(&mut self, mut callback: F) -> &mut Self where F: 'static + FnMut(Vec, &mut MutableAppContext), { @@ -288,6 +288,20 @@ impl App { self } + pub fn on_open_urls(&mut self, mut callback: F) -> &mut Self + where + F: 'static + FnMut(Vec, &mut MutableAppContext), + { + let cx = self.0.clone(); + self.0 + .borrow_mut() + .foreground_platform + .on_open_urls(Box::new(move |paths| { + callback(paths, &mut *cx.borrow_mut()) + })); + self + } + pub fn run(self, on_finish_launching: F) where F: 'static + FnOnce(&mut MutableAppContext), diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 9abff5b0f7..6d4215468a 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -64,6 +64,7 @@ pub(crate) trait ForegroundPlatform { fn on_quit(&self, callback: Box); fn on_event(&self, callback: Box bool>); fn on_open_files(&self, callback: Box)>); + fn on_open_urls(&self, callback: Box)>); fn run(&self, on_finish_launching: Box ()>); fn on_menu_command(&self, callback: Box); diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index eeadd0c476..813cf550d0 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -113,6 +113,7 @@ pub struct MacForegroundPlatformState { event: Option bool>>, menu_command: Option>, open_files: Option)>>, + open_urls: Option)>>, finish_launching: Option ()>>, menu_actions: Vec>, } @@ -218,6 +219,10 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { self.0.borrow_mut().open_files = Some(callback); } + fn on_open_urls(&self, callback: Box)>) { + self.0.borrow_mut().open_urls = Some(callback); + } + fn run(&self, on_finish_launching: Box ()>) { self.0.borrow_mut().finish_launching = Some(on_finish_launching); @@ -706,16 +711,16 @@ extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) { } } -extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, paths: id) { - let paths = unsafe { - (0..paths.count()) +extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { + let urls = unsafe { + (0..urls.count()) .into_iter() .filter_map(|i| { - let path = paths.objectAtIndex(i); + let path = urls.objectAtIndex(i); match dbg!( CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str() ) { - Ok(string) => Some(PathBuf::from(string)), + Ok(string) => Some(string.to_string()), Err(err) => { log::error!("error converting path to string: {}", err); None @@ -724,10 +729,10 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, paths: id) { }) .collect::>() }; - // let platform = unsafe { get_foreground_platform(this) }; - // if let Some(callback) = platform.0.borrow_mut().open_files.as_mut() { - // callback(paths); - // } + let platform = unsafe { get_foreground_platform(this) }; + if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() { + callback(urls); + } } extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index a18f52a4f6..e0dd3059fc 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -68,6 +68,8 @@ impl super::ForegroundPlatform for ForegroundPlatform { fn on_open_files(&self, _: Box)>) {} + fn on_open_urls(&self, _: Box)>) {} + fn run(&self, _on_finish_launching: Box ()>) { unimplemented!() } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index b4ae84a4f3..d327acb173 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -32,6 +32,7 @@ test-support = [ assets = { path = "../assets" } breadcrumbs = { path = "../breadcrumbs" } chat_panel = { path = "../chat_panel" } +cli = { path = "../cli" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } client = { path = "../client" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cead3ac390..a70473087e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,6 +3,7 @@ use anyhow::{anyhow, Context, Result}; use assets::Assets; +use cli::{ipc, CliRequest, CliResponse, IpcHandshake}; use client::{self, http, ChannelList, UserStore}; use fs::OpenOptions; use futures::{channel::oneshot, StreamExt}; @@ -26,7 +27,7 @@ use zed::{ fn main() { init_logger(); - let app = gpui::App::new(Assets).unwrap(); + let mut app = gpui::App::new(Assets).unwrap(); load_embedded_fonts(&app); let fs = Arc::new(RealFs); @@ -87,6 +88,12 @@ fn main() { }) }; + app.on_open_urls(|urls, _| { + if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { + connect_to_cli(server_name).log_err(); + } + }); + app.run(move |cx| { let http = http::client(); let client = client::Client::new(http.clone()); @@ -292,3 +299,29 @@ fn load_config_files( .detach(); rx } + +fn connect_to_cli(server_name: &str) -> Result<()> { + let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) + .context("error connecting to cli")?; + let (request_tx, request_rx) = ipc::channel::()?; + let (response_tx, response_rx) = ipc::channel::()?; + + handshake_tx + .send(IpcHandshake { + requests: request_tx, + responses: response_rx, + }) + .context("error sending ipc handshake")?; + + std::thread::spawn(move || { + while let Ok(cli_request) = request_rx.recv() { + log::info!("{cli_request:?}"); + response_tx.send(CliResponse::Stdout { + message: "Hi, CLI!".into(), + })?; + response_tx.send(CliResponse::Exit { status: 0 })?; + } + Ok::<_, anyhow::Error>(()) + }); + Ok(()) +}