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