1#![cfg_attr(any(target_os = "linux", target_os = "windows"), allow(dead_code))]
2
3use anyhow::{Context, Result};
4use clap::Parser;
5use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
6use std::{
7 env, fs,
8 path::{Path, PathBuf},
9};
10use util::paths::PathLikeWithPosition;
11
12struct Detect;
13
14trait InstalledApp {
15 fn zed_version_string(&self) -> String;
16 fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
17}
18
19#[derive(Parser, Debug)]
20#[command(name = "zed", disable_version_flag = true)]
21struct Args {
22 /// Wait for all of the given paths to be opened/closed before exiting.
23 #[arg(short, long)]
24 wait: bool,
25 /// Add files to the currently open workspace
26 #[arg(short, long, overrides_with = "new")]
27 add: bool,
28 /// Create a new workspace
29 #[arg(short, long, overrides_with = "add")]
30 new: bool,
31 /// A sequence of space-separated paths that you want to open.
32 ///
33 /// Use `path:line:row` syntax to open a file at a specific location.
34 /// Non-existing paths and directories will ignore `:line:row` suffix.
35 #[arg(value_parser = parse_path_with_position)]
36 paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
37 /// Print Zed's version and the app path.
38 #[arg(short, long)]
39 version: bool,
40 /// Custom path to Zed.app or the zed binary
41 #[arg(long)]
42 zed: Option<PathBuf>,
43 /// Run zed in dev-server mode
44 #[arg(long)]
45 dev_server_token: Option<String>,
46}
47
48fn parse_path_with_position(
49 argument_str: &str,
50) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
51 PathLikeWithPosition::parse_str(argument_str, |path_str| {
52 Ok(Path::new(path_str).to_path_buf())
53 })
54}
55
56fn main() -> Result<()> {
57 // Intercept version designators
58 #[cfg(target_os = "macos")]
59 if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
60 // When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
61 use std::str::FromStr as _;
62
63 if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
64 return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
65 }
66 }
67 let args = Args::parse();
68
69 let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
70
71 if args.version {
72 println!("{}", app.zed_version_string());
73 return Ok(());
74 }
75
76 let curdir = env::current_dir()?;
77 let mut paths = vec![];
78 for path in args.paths_with_position {
79 let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
80 Ok(path) => Ok(path),
81 Err(e) => {
82 if let Some(mut parent) = path.parent() {
83 if parent == Path::new("") {
84 parent = &curdir;
85 }
86 match fs::canonicalize(parent) {
87 Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
88 Err(_) => Err(e),
89 }
90 } else {
91 Err(e)
92 }
93 }
94 })?;
95 paths.push(canonicalized.to_string(|path| path.display().to_string()))
96 }
97
98 let (server, server_name) =
99 IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
100 let url = format!("zed-cli://{server_name}");
101
102 app.launch(url)?;
103 let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
104 let (tx, rx) = (handshake.requests, handshake.responses);
105
106 let open_new_workspace = if args.new {
107 Some(true)
108 } else if args.add {
109 Some(false)
110 } else {
111 None
112 };
113
114 tx.send(CliRequest::Open {
115 paths,
116 wait: args.wait,
117 open_new_workspace,
118 dev_server_token: args.dev_server_token,
119 })?;
120
121 while let Ok(response) = rx.recv() {
122 match response {
123 CliResponse::Ping => {}
124 CliResponse::Stdout { message } => println!("{message}"),
125 CliResponse::Stderr { message } => eprintln!("{message}"),
126 CliResponse::Exit { status } => std::process::exit(status),
127 }
128 }
129
130 Ok(())
131}
132
133#[cfg(target_os = "linux")]
134mod linux {
135 use std::{
136 env,
137 ffi::OsString,
138 io,
139 os::{
140 linux::net::SocketAddrExt,
141 unix::net::{SocketAddr, UnixDatagram},
142 },
143 path::{Path, PathBuf},
144 process, thread,
145 time::Duration,
146 };
147
148 use anyhow::anyhow;
149 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
150 use fork::Fork;
151 use once_cell::sync::Lazy;
152
153 use crate::{Detect, InstalledApp};
154
155 static RELEASE_CHANNEL: Lazy<String> =
156 Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
157
158 struct App(PathBuf);
159
160 impl Detect {
161 pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
162 let path = if let Some(path) = path {
163 path.to_path_buf().canonicalize()
164 } else {
165 let cli = env::current_exe()?;
166 let dir = cli
167 .parent()
168 .ok_or_else(|| anyhow!("no parent path for cli"))?;
169
170 match dir.join("zed").canonicalize() {
171 Ok(path) => Ok(path),
172 // development builds have Zed capitalized
173 Err(e) => match dir.join("Zed").canonicalize() {
174 Ok(path) => Ok(path),
175 Err(_) => Err(e),
176 },
177 }
178 }?;
179
180 Ok(App(path))
181 }
182 }
183
184 impl InstalledApp for App {
185 fn zed_version_string(&self) -> String {
186 format!(
187 "Zed {}{} – {}",
188 if *RELEASE_CHANNEL == "stable" {
189 "".to_string()
190 } else {
191 format!(" {} ", *RELEASE_CHANNEL)
192 },
193 option_env!("RELEASE_VERSION").unwrap_or_default(),
194 self.0.display(),
195 )
196 }
197
198 fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
199 let uid: u32 = unsafe { libc::getuid() };
200 let sock_addr =
201 SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
202
203 let sock = UnixDatagram::unbound()?;
204 if sock.connect_addr(&sock_addr).is_err() {
205 self.boot_background(ipc_url)?;
206 } else {
207 sock.send(ipc_url.as_bytes())?;
208 }
209 Ok(())
210 }
211 }
212
213 impl App {
214 fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
215 let path = &self.0;
216
217 match fork::fork() {
218 Ok(Fork::Parent(_)) => Ok(()),
219 Ok(Fork::Child) => {
220 std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
221 if let Err(_) = fork::setsid() {
222 eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
223 process::exit(1);
224 }
225 if std::env::var("ZED_KEEP_FD").is_err() {
226 if let Err(_) = fork::close_fd() {
227 eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
228 }
229 }
230 let error =
231 exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
232 // if exec succeeded, we never get here.
233 eprintln!("failed to exec {:?}: {}", path, error);
234 process::exit(1)
235 }
236 Err(_) => Err(anyhow!(io::Error::last_os_error())),
237 }
238 }
239
240 fn wait_for_socket(
241 &self,
242 sock_addr: &SocketAddr,
243 sock: &mut UnixDatagram,
244 ) -> Result<(), std::io::Error> {
245 for _ in 0..100 {
246 thread::sleep(Duration::from_millis(10));
247 if sock.connect_addr(&sock_addr).is_ok() {
248 return Ok(());
249 }
250 }
251 sock.connect_addr(&sock_addr)
252 }
253 }
254}
255
256// todo("windows")
257#[cfg(target_os = "windows")]
258mod windows {
259 use crate::{Detect, InstalledApp};
260 use std::path::Path;
261
262 struct App;
263 impl InstalledApp for App {
264 fn zed_version_string(&self) -> String {
265 unimplemented!()
266 }
267 fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
268 unimplemented!()
269 }
270 }
271
272 impl Detect {
273 pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
274 Ok(App)
275 }
276 }
277}
278
279#[cfg(target_os = "macos")]
280mod mac_os {
281 use anyhow::{anyhow, Context, Result};
282 use core_foundation::{
283 array::{CFArray, CFIndex},
284 string::kCFStringEncodingUTF8,
285 url::{CFURLCreateWithBytes, CFURL},
286 };
287 use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
288 use serde::Deserialize;
289 use std::{
290 ffi::OsStr,
291 fs,
292 path::{Path, PathBuf},
293 process::Command,
294 ptr,
295 };
296
297 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
298
299 use crate::{Detect, InstalledApp};
300
301 #[derive(Debug, Deserialize)]
302 struct InfoPlist {
303 #[serde(rename = "CFBundleShortVersionString")]
304 bundle_short_version_string: String,
305 }
306
307 enum Bundle {
308 App {
309 app_bundle: PathBuf,
310 plist: InfoPlist,
311 },
312 LocalPath {
313 executable: PathBuf,
314 plist: InfoPlist,
315 },
316 }
317
318 fn locate_bundle() -> Result<PathBuf> {
319 let cli_path = std::env::current_exe()?.canonicalize()?;
320 let mut app_path = cli_path.clone();
321 while app_path.extension() != Some(OsStr::new("app")) {
322 if !app_path.pop() {
323 return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
324 }
325 }
326 Ok(app_path)
327 }
328
329 impl Detect {
330 pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
331 let bundle_path = if let Some(bundle_path) = path {
332 bundle_path
333 .canonicalize()
334 .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
335 } else {
336 locate_bundle().context("bundle autodiscovery")?
337 };
338
339 match bundle_path.extension().and_then(|ext| ext.to_str()) {
340 Some("app") => {
341 let plist_path = bundle_path.join("Contents/Info.plist");
342 let plist =
343 plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
344 format!("Reading *.app bundle plist file at {plist_path:?}")
345 })?;
346 Ok(Bundle::App {
347 app_bundle: bundle_path,
348 plist,
349 })
350 }
351 _ => {
352 println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
353 let plist_path = bundle_path
354 .parent()
355 .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
356 .join("WebRTC.framework/Resources/Info.plist");
357 let plist =
358 plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
359 format!("Reading dev bundle plist file at {plist_path:?}")
360 })?;
361 Ok(Bundle::LocalPath {
362 executable: bundle_path,
363 plist,
364 })
365 }
366 }
367 }
368 }
369
370 impl InstalledApp for Bundle {
371 fn zed_version_string(&self) -> String {
372 let is_dev = matches!(self, Self::LocalPath { .. });
373 format!(
374 "Zed {}{} – {}",
375 self.plist().bundle_short_version_string,
376 if is_dev { " (dev)" } else { "" },
377 self.path().display(),
378 )
379 }
380
381 fn launch(&self, url: String) -> anyhow::Result<()> {
382 match self {
383 Self::App { app_bundle, .. } => {
384 let app_path = app_bundle;
385
386 let status = unsafe {
387 let app_url = CFURL::from_path(app_path, true)
388 .with_context(|| format!("invalid app path {app_path:?}"))?;
389 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
390 ptr::null(),
391 url.as_ptr(),
392 url.len() as CFIndex,
393 kCFStringEncodingUTF8,
394 ptr::null(),
395 ));
396 // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
397 let urls_to_open =
398 CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
399 LSOpenFromURLSpec(
400 &LSLaunchURLSpec {
401 appURL: app_url.as_concrete_TypeRef(),
402 itemURLs: urls_to_open.as_concrete_TypeRef(),
403 passThruParams: ptr::null(),
404 launchFlags: kLSLaunchDefaults,
405 asyncRefCon: ptr::null_mut(),
406 },
407 ptr::null_mut(),
408 )
409 };
410
411 anyhow::ensure!(
412 status == 0,
413 "cannot start app bundle {}",
414 self.zed_version_string()
415 );
416 }
417
418 Self::LocalPath { executable, .. } => {
419 let executable_parent = executable
420 .parent()
421 .with_context(|| format!("Executable {executable:?} path has no parent"))?;
422 let subprocess_stdout_file = fs::File::create(
423 executable_parent.join("zed_dev.log"),
424 )
425 .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
426 let subprocess_stdin_file =
427 subprocess_stdout_file.try_clone().with_context(|| {
428 format!("Cloning descriptor for file {subprocess_stdout_file:?}")
429 })?;
430 let mut command = std::process::Command::new(executable);
431 let command = command
432 .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
433 .stderr(subprocess_stdout_file)
434 .stdout(subprocess_stdin_file)
435 .arg(url);
436
437 command
438 .spawn()
439 .with_context(|| format!("Spawning {command:?}"))?;
440 }
441 }
442
443 Ok(())
444 }
445 }
446
447 impl Bundle {
448 fn plist(&self) -> &InfoPlist {
449 match self {
450 Self::App { plist, .. } => plist,
451 Self::LocalPath { plist, .. } => plist,
452 }
453 }
454
455 fn path(&self) -> &Path {
456 match self {
457 Self::App { app_bundle, .. } => app_bundle,
458 Self::LocalPath { executable, .. } => executable,
459 }
460 }
461 }
462
463 pub(super) fn spawn_channel_cli(
464 channel: release_channel::ReleaseChannel,
465 leftover_args: Vec<String>,
466 ) -> Result<()> {
467 use anyhow::bail;
468
469 let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
470 let app_id_output = Command::new("osascript")
471 .arg("-e")
472 .arg(&app_id_prompt)
473 .output()?;
474 if !app_id_output.status.success() {
475 bail!("Could not determine app id for {}", channel.display_name());
476 }
477 let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
478 let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
479 let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
480 if !app_path_output.status.success() {
481 bail!(
482 "Could not determine app path for {}",
483 channel.display_name()
484 );
485 }
486 let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
487 let cli_path = format!("{app_path}/Contents/MacOS/cli");
488 Command::new(cli_path).args(leftover_args).spawn()?;
489 Ok(())
490 }
491}