1use anyhow::{anyhow, Result};
2use clap::Parser;
3use cli::{CliRequest, CliResponse, IpcHandshake};
4use core_foundation::{
5 array::{CFArray, CFIndex},
6 string::kCFStringEncodingUTF8,
7 url::{CFURLCreateWithBytes, CFURL},
8};
9use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
10use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
11use serde::Deserialize;
12use std::{ffi::OsStr, fs, path::PathBuf, ptr};
13
14#[derive(Parser)]
15#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
16struct Args {
17 /// Wait for all of the given paths to be closed before exiting.
18 #[clap(short, long)]
19 wait: bool,
20 /// A sequence of space-separated paths that you want to open.
21 #[clap()]
22 paths: Vec<PathBuf>,
23 /// Print Zed's version and the app path.
24 #[clap(short, long)]
25 version: bool,
26 /// Custom Zed.app path
27 #[clap(short, long)]
28 bundle_path: Option<PathBuf>,
29}
30
31#[derive(Debug, Deserialize)]
32struct InfoPlist {
33 #[serde(rename = "CFBundleShortVersionString")]
34 bundle_short_version_string: String,
35}
36
37fn main() -> Result<()> {
38 let args = Args::parse();
39
40 let bundle_path = if let Some(bundle_path) = args.bundle_path {
41 bundle_path.canonicalize()?
42 } else {
43 locate_bundle()?
44 };
45
46 if args.version {
47 let plist_path = bundle_path.join("Contents/Info.plist");
48 let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
49 println!(
50 "Zed {} – {}",
51 plist.bundle_short_version_string,
52 bundle_path.to_string_lossy()
53 );
54 return Ok(());
55 }
56
57 let (tx, rx) = launch_app(bundle_path)?;
58
59 tx.send(CliRequest::Open {
60 paths: args
61 .paths
62 .into_iter()
63 .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
64 .collect::<Result<Vec<PathBuf>>>()?,
65 wait: args.wait,
66 })?;
67
68 while let Ok(response) = rx.recv() {
69 match response {
70 CliResponse::Ping => {}
71 CliResponse::Stdout { message } => println!("{message}"),
72 CliResponse::Stderr { message } => eprintln!("{message}"),
73 CliResponse::Exit { status } => std::process::exit(status),
74 }
75 }
76
77 Ok(())
78}
79
80fn locate_bundle() -> Result<PathBuf> {
81 let cli_path = std::env::current_exe()?.canonicalize()?;
82 let mut app_path = cli_path.clone();
83 while app_path.extension() != Some(OsStr::new("app")) {
84 if !app_path.pop() {
85 return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
86 }
87 }
88 Ok(app_path)
89}
90
91fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
92 let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
93 let url = format!("zed-cli://{server_name}");
94
95 let status = unsafe {
96 let app_url =
97 CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
98 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
99 ptr::null(),
100 url.as_ptr(),
101 url.len() as CFIndex,
102 kCFStringEncodingUTF8,
103 ptr::null(),
104 ));
105 let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
106 LSOpenFromURLSpec(
107 &LSLaunchURLSpec {
108 appURL: app_url.as_concrete_TypeRef(),
109 itemURLs: urls_to_open.as_concrete_TypeRef(),
110 passThruParams: ptr::null(),
111 launchFlags: kLSLaunchDefaults,
112 asyncRefCon: ptr::null_mut(),
113 },
114 ptr::null_mut(),
115 )
116 };
117
118 if status == 0 {
119 let (_, handshake) = server.accept()?;
120 Ok((handshake.requests, handshake.responses))
121 } else {
122 Err(anyhow!("cannot start {:?}", app_path))
123 }
124}