1#![cfg_attr(
2 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
3 allow(dead_code)
4)]
5
6use anyhow::{Context, Result};
7use clap::Parser;
8use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
9use collections::HashMap;
10use parking_lot::Mutex;
11use std::{
12 env, fs, io,
13 path::{Path, PathBuf},
14 process::ExitStatus,
15 sync::Arc,
16 thread::{self, JoinHandle},
17};
18use tempfile::NamedTempFile;
19use util::paths::PathWithPosition;
20
21struct Detect;
22
23trait InstalledApp {
24 fn zed_version_string(&self) -> String;
25 fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
26 fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
27}
28
29#[derive(Parser, Debug)]
30#[command(
31 name = "zed",
32 disable_version_flag = true,
33 after_help = "To read from stdin, append '-' (e.g. 'ps axf | zed -')"
34)]
35struct Args {
36 /// Wait for all of the given paths to be opened/closed before exiting.
37 #[arg(short, long)]
38 wait: bool,
39 /// Add files to the currently open workspace
40 #[arg(short, long, overrides_with = "new")]
41 add: bool,
42 /// Create a new workspace
43 #[arg(short, long, overrides_with = "add")]
44 new: bool,
45 /// A sequence of space-separated paths that you want to open.
46 ///
47 /// Use `path:line:row` syntax to open a file at a specific location.
48 /// Non-existing paths and directories will ignore `:line:row` suffix.
49 paths_with_position: Vec<String>,
50 /// Print Zed's version and the app path.
51 #[arg(short, long)]
52 version: bool,
53 /// Run zed in the foreground (useful for debugging)
54 #[arg(long)]
55 foreground: bool,
56 /// Custom path to Zed.app or the zed binary
57 #[arg(long)]
58 zed: Option<PathBuf>,
59 /// Run zed in dev-server mode
60 #[arg(long)]
61 dev_server_token: Option<String>,
62}
63
64fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
65 let canonicalized = match Path::new(argument_str).canonicalize() {
66 Ok(existing_path) => PathWithPosition::from_path(existing_path),
67 Err(_) => {
68 let path = PathWithPosition::parse_str(argument_str);
69 let curdir = env::current_dir().context("reteiving current directory")?;
70 path.map_path(|path| match fs::canonicalize(&path) {
71 Ok(path) => Ok(path),
72 Err(e) => {
73 if let Some(mut parent) = path.parent() {
74 if parent == Path::new("") {
75 parent = &curdir
76 }
77 match fs::canonicalize(parent) {
78 Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
79 Err(_) => Err(e),
80 }
81 } else {
82 Err(e)
83 }
84 }
85 })
86 }
87 .with_context(|| format!("parsing as path with position {argument_str}"))?,
88 };
89 Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
90}
91
92fn main() -> Result<()> {
93 // Exit flatpak sandbox if needed
94 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
95 {
96 flatpak::try_restart_to_host();
97 flatpak::ld_extra_libs();
98 }
99
100 // Intercept version designators
101 #[cfg(target_os = "macos")]
102 if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
103 // 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.
104 use std::str::FromStr as _;
105
106 if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
107 return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
108 }
109 }
110 let args = Args::parse();
111
112 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
113 let args = flatpak::set_bin_if_no_escape(args);
114
115 let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
116
117 if args.version {
118 println!("{}", app.zed_version_string());
119 return Ok(());
120 }
121
122 let (server, server_name) =
123 IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
124 let url = format!("zed-cli://{server_name}");
125
126 let open_new_workspace = if args.new {
127 Some(true)
128 } else if args.add {
129 Some(false)
130 } else {
131 None
132 };
133
134 let env = Some(std::env::vars().collect::<HashMap<_, _>>());
135 let exit_status = Arc::new(Mutex::new(None));
136 let mut paths = vec![];
137 let mut urls = vec![];
138 let mut stdin_tmp_file: Option<fs::File> = None;
139 for path in args.paths_with_position.iter() {
140 if path.starts_with("zed://")
141 || path.starts_with("http://")
142 || path.starts_with("https://")
143 || path.starts_with("file://")
144 || path.starts_with("ssh://")
145 {
146 urls.push(path.to_string());
147 } else if path == "-" && args.paths_with_position.len() == 1 {
148 let file = NamedTempFile::new()?;
149 paths.push(file.path().to_string_lossy().to_string());
150 let (file, _) = file.keep()?;
151 stdin_tmp_file = Some(file);
152 } else {
153 paths.push(parse_path_with_position(path)?)
154 }
155 }
156
157 if let Some(_) = args.dev_server_token {
158 return Err(anyhow::anyhow!(
159 "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
160 ))?;
161 }
162
163 let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
164 let exit_status = exit_status.clone();
165 move || {
166 let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
167 let (tx, rx) = (handshake.requests, handshake.responses);
168
169 tx.send(CliRequest::Open {
170 paths,
171 urls,
172 wait: args.wait,
173 open_new_workspace,
174 env,
175 })?;
176
177 while let Ok(response) = rx.recv() {
178 match response {
179 CliResponse::Ping => {}
180 CliResponse::Stdout { message } => println!("{message}"),
181 CliResponse::Stderr { message } => eprintln!("{message}"),
182 CliResponse::Exit { status } => {
183 exit_status.lock().replace(status);
184 return Ok(());
185 }
186 }
187 }
188
189 Ok(())
190 }
191 });
192
193 let pipe_handle: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
194 if let Some(mut tmp_file) = stdin_tmp_file {
195 let mut stdin = std::io::stdin().lock();
196 if io::IsTerminal::is_terminal(&stdin) {
197 return Ok(());
198 }
199 let mut buffer = [0; 8 * 1024];
200 loop {
201 let bytes_read = io::Read::read(&mut stdin, &mut buffer)?;
202 if bytes_read == 0 {
203 break;
204 }
205 io::Write::write(&mut tmp_file, &buffer[..bytes_read])?;
206 }
207 io::Write::flush(&mut tmp_file)?;
208 }
209 Ok(())
210 });
211
212 if args.foreground {
213 app.run_foreground(url)?;
214 } else {
215 app.launch(url)?;
216 sender.join().unwrap()?;
217 pipe_handle.join().unwrap()?;
218 }
219
220 if let Some(exit_status) = exit_status.lock().take() {
221 std::process::exit(exit_status);
222 }
223 Ok(())
224}
225
226#[cfg(any(target_os = "linux", target_os = "freebsd"))]
227mod linux {
228 use std::{
229 env,
230 ffi::OsString,
231 io,
232 os::unix::net::{SocketAddr, UnixDatagram},
233 path::{Path, PathBuf},
234 process::{self, ExitStatus},
235 thread,
236 time::Duration,
237 };
238
239 use anyhow::anyhow;
240 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
241 use fork::Fork;
242 use once_cell::sync::Lazy;
243
244 use crate::{Detect, InstalledApp};
245
246 static RELEASE_CHANNEL: Lazy<String> =
247 Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
248
249 struct App(PathBuf);
250
251 impl Detect {
252 pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
253 let path = if let Some(path) = path {
254 path.to_path_buf().canonicalize()?
255 } else {
256 let cli = env::current_exe()?;
257 let dir = cli
258 .parent()
259 .ok_or_else(|| anyhow!("no parent path for cli"))?;
260
261 // libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
262 // ./zed is for the target directory in development builds.
263 let possible_locations =
264 ["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
265 possible_locations
266 .iter()
267 .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
268 .ok_or_else(|| {
269 anyhow!("could not find any of: {}", possible_locations.join(", "))
270 })?
271 };
272
273 Ok(App(path))
274 }
275 }
276
277 impl InstalledApp for App {
278 fn zed_version_string(&self) -> String {
279 format!(
280 "Zed {}{} – {}",
281 if *RELEASE_CHANNEL == "stable" {
282 "".to_string()
283 } else {
284 format!(" {} ", *RELEASE_CHANNEL)
285 },
286 option_env!("RELEASE_VERSION").unwrap_or_default(),
287 self.0.display(),
288 )
289 }
290
291 fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
292 let sock_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL));
293 let sock = UnixDatagram::unbound()?;
294 if sock.connect(&sock_path).is_err() {
295 self.boot_background(ipc_url)?;
296 } else {
297 sock.send(ipc_url.as_bytes())?;
298 }
299 Ok(())
300 }
301
302 fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
303 std::process::Command::new(self.0.clone())
304 .arg(ipc_url)
305 .status()
306 }
307 }
308
309 impl App {
310 fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
311 let path = &self.0;
312
313 match fork::fork() {
314 Ok(Fork::Parent(_)) => Ok(()),
315 Ok(Fork::Child) => {
316 std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
317 if let Err(_) = fork::setsid() {
318 eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
319 process::exit(1);
320 }
321 if let Err(_) = fork::close_fd() {
322 eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
323 }
324 let error =
325 exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
326 // if exec succeeded, we never get here.
327 eprintln!("failed to exec {:?}: {}", path, error);
328 process::exit(1)
329 }
330 Err(_) => Err(anyhow!(io::Error::last_os_error())),
331 }
332 }
333
334 fn wait_for_socket(
335 &self,
336 sock_addr: &SocketAddr,
337 sock: &mut UnixDatagram,
338 ) -> Result<(), std::io::Error> {
339 for _ in 0..100 {
340 thread::sleep(Duration::from_millis(10));
341 if sock.connect_addr(&sock_addr).is_ok() {
342 return Ok(());
343 }
344 }
345 sock.connect_addr(&sock_addr)
346 }
347 }
348}
349
350#[cfg(any(target_os = "linux", target_os = "freebsd"))]
351mod flatpak {
352 use std::ffi::OsString;
353 use std::path::PathBuf;
354 use std::process::Command;
355 use std::{env, process};
356
357 const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH";
358 const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE";
359
360 /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
361 pub fn ld_extra_libs() {
362 let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
363 env::split_paths(&paths).collect()
364 } else {
365 Vec::new()
366 };
367
368 if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
369 paths.push(extra_path.into());
370 }
371
372 env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap());
373 }
374
375 /// Restarts outside of the sandbox if currently running within it
376 pub fn try_restart_to_host() {
377 if let Some(flatpak_dir) = get_flatpak_dir() {
378 let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
379 args.append(&mut get_xdg_env_args());
380 args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
381 args.push(
382 format!(
383 "--env={EXTRA_LIB_ENV_NAME}={}",
384 flatpak_dir.join("lib").to_str().unwrap()
385 )
386 .into(),
387 );
388 args.push(flatpak_dir.join("bin").join("zed").into());
389
390 let mut is_app_location_set = false;
391 for arg in &env::args_os().collect::<Vec<_>>()[1..] {
392 args.push(arg.clone());
393 is_app_location_set |= arg == "--zed";
394 }
395
396 if !is_app_location_set {
397 args.push("--zed".into());
398 args.push(flatpak_dir.join("libexec").join("zed-editor").into());
399 }
400
401 let error = exec::execvp("/usr/bin/flatpak-spawn", args);
402 eprintln!("failed restart cli on host: {:?}", error);
403 process::exit(1);
404 }
405 }
406
407 pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
408 if env::var(NO_ESCAPE_ENV_NAME).is_ok()
409 && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
410 {
411 if args.zed.is_none() {
412 args.zed = Some("/app/libexec/zed-editor".into());
413 env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
414 }
415 }
416 args
417 }
418
419 fn get_flatpak_dir() -> Option<PathBuf> {
420 if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
421 return None;
422 }
423
424 if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
425 if !flatpak_id.starts_with("dev.zed.Zed") {
426 return None;
427 }
428
429 let install_dir = Command::new("/usr/bin/flatpak-spawn")
430 .arg("--host")
431 .arg("flatpak")
432 .arg("info")
433 .arg("--show-location")
434 .arg(flatpak_id)
435 .output()
436 .unwrap();
437 let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
438 Some(install_dir.join("files"))
439 } else {
440 None
441 }
442 }
443
444 fn get_xdg_env_args() -> Vec<OsString> {
445 let xdg_keys = [
446 "XDG_DATA_HOME",
447 "XDG_CONFIG_HOME",
448 "XDG_CACHE_HOME",
449 "XDG_STATE_HOME",
450 ];
451 env::vars()
452 .filter(|(key, _)| xdg_keys.contains(&key.as_str()))
453 .map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
454 .collect()
455 }
456}
457
458// todo("windows")
459#[cfg(target_os = "windows")]
460mod windows {
461 use crate::{Detect, InstalledApp};
462 use std::io;
463 use std::path::Path;
464 use std::process::ExitStatus;
465
466 struct App;
467 impl InstalledApp for App {
468 fn zed_version_string(&self) -> String {
469 unimplemented!()
470 }
471 fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
472 unimplemented!()
473 }
474 fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
475 unimplemented!()
476 }
477 }
478
479 impl Detect {
480 pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
481 Ok(App)
482 }
483 }
484}
485
486#[cfg(target_os = "macos")]
487mod mac_os {
488 use anyhow::{anyhow, Context, Result};
489 use core_foundation::{
490 array::{CFArray, CFIndex},
491 string::kCFStringEncodingUTF8,
492 url::{CFURLCreateWithBytes, CFURL},
493 };
494 use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
495 use serde::Deserialize;
496 use std::{
497 ffi::OsStr,
498 fs, io,
499 path::{Path, PathBuf},
500 process::{Command, ExitStatus},
501 ptr,
502 };
503
504 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
505
506 use crate::{Detect, InstalledApp};
507
508 #[derive(Debug, Deserialize)]
509 struct InfoPlist {
510 #[serde(rename = "CFBundleShortVersionString")]
511 bundle_short_version_string: String,
512 }
513
514 enum Bundle {
515 App {
516 app_bundle: PathBuf,
517 plist: InfoPlist,
518 },
519 LocalPath {
520 executable: PathBuf,
521 plist: InfoPlist,
522 },
523 }
524
525 fn locate_bundle() -> Result<PathBuf> {
526 let cli_path = std::env::current_exe()?.canonicalize()?;
527 let mut app_path = cli_path.clone();
528 while app_path.extension() != Some(OsStr::new("app")) {
529 if !app_path.pop() {
530 return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
531 }
532 }
533 Ok(app_path)
534 }
535
536 impl Detect {
537 pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
538 let bundle_path = if let Some(bundle_path) = path {
539 bundle_path
540 .canonicalize()
541 .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
542 } else {
543 locate_bundle().context("bundle autodiscovery")?
544 };
545
546 match bundle_path.extension().and_then(|ext| ext.to_str()) {
547 Some("app") => {
548 let plist_path = bundle_path.join("Contents/Info.plist");
549 let plist =
550 plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
551 format!("Reading *.app bundle plist file at {plist_path:?}")
552 })?;
553 Ok(Bundle::App {
554 app_bundle: bundle_path,
555 plist,
556 })
557 }
558 _ => {
559 println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
560 let plist_path = bundle_path
561 .parent()
562 .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
563 .join("WebRTC.framework/Resources/Info.plist");
564 let plist =
565 plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
566 format!("Reading dev bundle plist file at {plist_path:?}")
567 })?;
568 Ok(Bundle::LocalPath {
569 executable: bundle_path,
570 plist,
571 })
572 }
573 }
574 }
575 }
576
577 impl InstalledApp for Bundle {
578 fn zed_version_string(&self) -> String {
579 let is_dev = matches!(self, Self::LocalPath { .. });
580 format!(
581 "Zed {}{} – {}",
582 self.plist().bundle_short_version_string,
583 if is_dev { " (dev)" } else { "" },
584 self.path().display(),
585 )
586 }
587
588 fn launch(&self, url: String) -> anyhow::Result<()> {
589 match self {
590 Self::App { app_bundle, .. } => {
591 let app_path = app_bundle;
592
593 let status = unsafe {
594 let app_url = CFURL::from_path(app_path, true)
595 .with_context(|| format!("invalid app path {app_path:?}"))?;
596 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
597 ptr::null(),
598 url.as_ptr(),
599 url.len() as CFIndex,
600 kCFStringEncodingUTF8,
601 ptr::null(),
602 ));
603 // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
604 let urls_to_open =
605 CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
606 LSOpenFromURLSpec(
607 &LSLaunchURLSpec {
608 appURL: app_url.as_concrete_TypeRef(),
609 itemURLs: urls_to_open.as_concrete_TypeRef(),
610 passThruParams: ptr::null(),
611 launchFlags: kLSLaunchDefaults,
612 asyncRefCon: ptr::null_mut(),
613 },
614 ptr::null_mut(),
615 )
616 };
617
618 anyhow::ensure!(
619 status == 0,
620 "cannot start app bundle {}",
621 self.zed_version_string()
622 );
623 }
624
625 Self::LocalPath { executable, .. } => {
626 let executable_parent = executable
627 .parent()
628 .with_context(|| format!("Executable {executable:?} path has no parent"))?;
629 let subprocess_stdout_file = fs::File::create(
630 executable_parent.join("zed_dev.log"),
631 )
632 .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
633 let subprocess_stdin_file =
634 subprocess_stdout_file.try_clone().with_context(|| {
635 format!("Cloning descriptor for file {subprocess_stdout_file:?}")
636 })?;
637 let mut command = std::process::Command::new(executable);
638 let command = command
639 .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
640 .stderr(subprocess_stdout_file)
641 .stdout(subprocess_stdin_file)
642 .arg(url);
643
644 command
645 .spawn()
646 .with_context(|| format!("Spawning {command:?}"))?;
647 }
648 }
649
650 Ok(())
651 }
652
653 fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
654 let path = match self {
655 Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
656 Bundle::LocalPath { executable, .. } => executable.clone(),
657 };
658
659 std::process::Command::new(path).arg(ipc_url).status()
660 }
661 }
662
663 impl Bundle {
664 fn plist(&self) -> &InfoPlist {
665 match self {
666 Self::App { plist, .. } => plist,
667 Self::LocalPath { plist, .. } => plist,
668 }
669 }
670
671 fn path(&self) -> &Path {
672 match self {
673 Self::App { app_bundle, .. } => app_bundle,
674 Self::LocalPath { executable, .. } => executable,
675 }
676 }
677 }
678
679 pub(super) fn spawn_channel_cli(
680 channel: release_channel::ReleaseChannel,
681 leftover_args: Vec<String>,
682 ) -> Result<()> {
683 use anyhow::bail;
684
685 let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
686 let app_id_output = Command::new("osascript")
687 .arg("-e")
688 .arg(&app_id_prompt)
689 .output()?;
690 if !app_id_output.status.success() {
691 bail!("Could not determine app id for {}", channel.display_name());
692 }
693 let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
694 let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
695 let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
696 if !app_path_output.status.success() {
697 bail!(
698 "Could not determine app path for {}",
699 channel.display_name()
700 );
701 }
702 let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
703 let cli_path = format!("{app_path}/Contents/MacOS/cli");
704 Command::new(cli_path).args(leftover_args).spawn()?;
705 Ok(())
706 }
707}