1use anyhow::{anyhow, Context, Result};
2use clap::Parser;
3use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
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 = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
47
48 if args.version {
49 println!("{}", bundle.zed_version_string());
50 return Ok(());
51 }
52
53 for path in args.paths.iter() {
54 if !path.exists() {
55 touch(path.as_path())?;
56 }
57 }
58
59 let (tx, rx) = bundle.launch()?;
60
61 tx.send(CliRequest::Open {
62 paths: args
63 .paths
64 .into_iter()
65 .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
66 .collect::<Result<Vec<PathBuf>>>()?,
67 wait: args.wait,
68 })?;
69
70 while let Ok(response) = rx.recv() {
71 match response {
72 CliResponse::Ping => {}
73 CliResponse::Stdout { message } => println!("{message}"),
74 CliResponse::Stderr { message } => eprintln!("{message}"),
75 CliResponse::Exit { status } => std::process::exit(status),
76 }
77 }
78
79 Ok(())
80}
81
82enum Bundle {
83 App {
84 app_bundle: PathBuf,
85 plist: InfoPlist,
86 },
87 LocalPath {
88 executable: PathBuf,
89 plist: InfoPlist,
90 },
91}
92
93impl Bundle {
94 fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
95 let bundle_path = if let Some(bundle_path) = args_bundle_path {
96 bundle_path
97 .canonicalize()
98 .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
99 } else {
100 locate_bundle().context("bundle autodiscovery")?
101 };
102
103 match bundle_path.extension().and_then(|ext| ext.to_str()) {
104 Some("app") => {
105 let plist_path = bundle_path.join("Contents/Info.plist");
106 let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
107 format!("Reading *.app bundle plist file at {plist_path:?}")
108 })?;
109 Ok(Self::App {
110 app_bundle: bundle_path,
111 plist,
112 })
113 }
114 _ => {
115 println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
116 let plist_path = bundle_path
117 .parent()
118 .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
119 .join("WebRTC.framework/Resources/Info.plist");
120 let plist = plist::from_file::<_, InfoPlist>(&plist_path)
121 .with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
122 Ok(Self::LocalPath {
123 executable: bundle_path,
124 plist,
125 })
126 }
127 }
128 }
129
130 fn plist(&self) -> &InfoPlist {
131 match self {
132 Self::App { plist, .. } => plist,
133 Self::LocalPath { plist, .. } => plist,
134 }
135 }
136
137 fn path(&self) -> &Path {
138 match self {
139 Self::App { app_bundle, .. } => app_bundle,
140 Self::LocalPath {
141 executable: excutable,
142 ..
143 } => excutable,
144 }
145 }
146
147 fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
148 let (server, server_name) =
149 IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
150 let url = format!("zed-cli://{server_name}");
151
152 match self {
153 Self::App { app_bundle, .. } => {
154 let app_path = app_bundle;
155
156 let status = unsafe {
157 let app_url = CFURL::from_path(app_path, true)
158 .with_context(|| format!("invalid app path {app_path:?}"))?;
159 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
160 ptr::null(),
161 url.as_ptr(),
162 url.len() as CFIndex,
163 kCFStringEncodingUTF8,
164 ptr::null(),
165 ));
166 let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
167 LSOpenFromURLSpec(
168 &LSLaunchURLSpec {
169 appURL: app_url.as_concrete_TypeRef(),
170 itemURLs: urls_to_open.as_concrete_TypeRef(),
171 passThruParams: ptr::null(),
172 launchFlags: kLSLaunchDefaults,
173 asyncRefCon: ptr::null_mut(),
174 },
175 ptr::null_mut(),
176 )
177 };
178
179 anyhow::ensure!(
180 status == 0,
181 "cannot start app bundle {}",
182 self.zed_version_string()
183 );
184 }
185 Self::LocalPath { executable, .. } => {
186 let executable_parent = executable
187 .parent()
188 .with_context(|| format!("Executable {executable:?} path has no parent"))?;
189 let subprocess_stdout_file =
190 fs::File::create(executable_parent.join("zed_dev.log"))
191 .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
192 let subprocess_stdin_file =
193 subprocess_stdout_file.try_clone().with_context(|| {
194 format!("Cloning descriptor for file {subprocess_stdout_file:?}")
195 })?;
196 let mut command = std::process::Command::new(executable);
197 let command = command
198 .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
199 .stderr(subprocess_stdout_file)
200 .stdout(subprocess_stdin_file)
201 .arg(url);
202
203 command
204 .spawn()
205 .with_context(|| format!("Spawning {command:?}"))?;
206 }
207 }
208
209 let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
210 Ok((handshake.requests, handshake.responses))
211 }
212
213 fn zed_version_string(&self) -> String {
214 let is_dev = matches!(self, Self::LocalPath { .. });
215 format!(
216 "Zed {}{} – {}",
217 self.plist().bundle_short_version_string,
218 if is_dev { " (dev)" } else { "" },
219 self.path().display(),
220 )
221 }
222}
223
224fn touch(path: &Path) -> io::Result<()> {
225 match OpenOptions::new().create(true).write(true).open(path) {
226 Ok(_) => Ok(()),
227 Err(e) => Err(e),
228 }
229}
230
231fn locate_bundle() -> Result<PathBuf> {
232 let cli_path = std::env::current_exe()?.canonicalize()?;
233 let mut app_path = cli_path.clone();
234 while app_path.extension() != Some(OsStr::new("app")) {
235 if !app_path.pop() {
236 return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
237 }
238 }
239 Ok(app_path)
240}