1#![allow(
2 clippy::disallowed_methods,
3 reason = "We are not in an async environment, so std::process::Command is fine"
4)]
5#![cfg_attr(
6 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
7 allow(dead_code)
8)]
9
10use anyhow::{Context as _, Result};
11use clap::Parser;
12use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
13use parking_lot::Mutex;
14use std::{
15 env, fs, io,
16 path::{Path, PathBuf},
17 process::ExitStatus,
18 sync::Arc,
19 thread::{self, JoinHandle},
20};
21use tempfile::NamedTempFile;
22use util::paths::PathWithPosition;
23
24#[cfg(any(target_os = "linux", target_os = "freebsd"))]
25use std::io::IsTerminal;
26
27const URL_PREFIX: [&'static str; 5] = ["zed://", "http://", "https://", "file://", "ssh://"];
28
29struct Detect;
30
31trait InstalledApp {
32 fn zed_version_string(&self) -> String;
33 fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
34 fn run_foreground(
35 &self,
36 ipc_url: String,
37 user_data_dir: Option<&str>,
38 ) -> io::Result<ExitStatus>;
39 fn path(&self) -> PathBuf;
40}
41
42#[derive(Parser, Debug)]
43#[command(
44 name = "zed",
45 disable_version_flag = true,
46 before_help = "The Zed CLI binary.
47This CLI is a separate binary that invokes Zed.
48
49Examples:
50 `zed`
51 Simply opens Zed
52 `zed --foreground`
53 Runs in foreground (shows all logs)
54 `zed path-to-your-project`
55 Open your project in Zed
56 `zed -n path-to-file `
57 Open file/folder in a new window",
58 after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'"
59)]
60struct Args {
61 /// Wait for all of the given paths to be opened/closed before exiting.
62 #[arg(short, long)]
63 wait: bool,
64 /// Add files to the currently open workspace
65 #[arg(short, long, overrides_with = "new")]
66 add: bool,
67 /// Create a new workspace
68 #[arg(short, long, overrides_with = "add")]
69 new: bool,
70 /// Sets a custom directory for all user data (e.g., database, extensions, logs).
71 /// This overrides the default platform-specific data directory location:
72 #[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
73 #[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")]
74 #[cfg_attr(
75 not(any(target_os = "windows", target_os = "macos")),
76 doc = "`$XDG_DATA_HOME/zed`."
77 )]
78 #[arg(long, value_name = "DIR")]
79 user_data_dir: Option<String>,
80 /// The paths to open in Zed (space-separated).
81 ///
82 /// Use `path:line:column` syntax to open a file at the given line and column.
83 paths_with_position: Vec<String>,
84 /// Print Zed's version and the app path.
85 #[arg(short, long)]
86 version: bool,
87 /// Run zed in the foreground (useful for debugging)
88 #[arg(long)]
89 foreground: bool,
90 /// Custom path to Zed.app or the zed binary
91 #[arg(long)]
92 zed: Option<PathBuf>,
93 /// Run zed in dev-server mode
94 #[arg(long)]
95 dev_server_token: Option<String>,
96 /// The username and WSL distribution to use when opening paths. If not specified,
97 /// Zed will attempt to open the paths directly.
98 ///
99 /// The username is optional, and if not specified, the default user for the distribution
100 /// will be used.
101 ///
102 /// Example: `me@Ubuntu` or `Ubuntu`.
103 ///
104 /// WARN: You should not fill in this field by hand.
105 #[cfg(target_os = "windows")]
106 #[arg(long, value_name = "USER@DISTRO")]
107 wsl: Option<String>,
108 /// Not supported in Zed CLI, only supported on Zed binary
109 /// Will attempt to give the correct command to run
110 #[arg(long)]
111 system_specs: bool,
112 /// Pairs of file paths to diff. Can be specified multiple times.
113 #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
114 diff: Vec<String>,
115 /// Uninstall Zed from user system
116 #[cfg(all(
117 any(target_os = "linux", target_os = "macos"),
118 not(feature = "no-bundled-uninstall")
119 ))]
120 #[arg(long)]
121 uninstall: bool,
122
123 /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
124 /// by having Zed act like netcat communicating over a Unix socket.
125 #[arg(long, hide = true)]
126 askpass: Option<String>,
127}
128
129fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
130 let canonicalized = match Path::new(argument_str).canonicalize() {
131 Ok(existing_path) => PathWithPosition::from_path(existing_path),
132 Err(_) => {
133 let path = PathWithPosition::parse_str(argument_str);
134 let curdir = env::current_dir().context("retrieving current directory")?;
135 path.map_path(|path| match fs::canonicalize(&path) {
136 Ok(path) => Ok(path),
137 Err(e) => {
138 if let Some(mut parent) = path.parent() {
139 if parent == Path::new("") {
140 parent = &curdir
141 }
142 match fs::canonicalize(parent) {
143 Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
144 Err(_) => Err(e),
145 }
146 } else {
147 Err(e)
148 }
149 }
150 })
151 }
152 .with_context(|| format!("parsing as path with position {argument_str}"))?,
153 };
154 Ok(canonicalized.to_string(|path| path.to_string_lossy().into_owned()))
155}
156
157fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
158 let mut source = PathWithPosition::parse_str(source);
159 let mut command = util::command::new_std_command("wsl.exe");
160
161 let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
162 if user.is_empty() {
163 anyhow::bail!("user is empty in wsl argument");
164 }
165 (Some(user), distro)
166 } else {
167 (None, wsl)
168 };
169
170 if let Some(user) = user {
171 command.arg("--user").arg(user);
172 }
173
174 let output = command
175 .arg("--distribution")
176 .arg(distro_name)
177 .arg("--exec")
178 .arg("wslpath")
179 .arg("-m")
180 .arg(&source.path)
181 .output()?;
182
183 let result = String::from_utf8_lossy(&output.stdout);
184 let prefix = format!("//wsl.localhost/{}", distro_name);
185 source.path = Path::new(result.trim().strip_prefix(&prefix).unwrap_or(&result)).to_owned();
186
187 Ok(source.to_string(|path| path.to_string_lossy().into_owned()))
188}
189
190fn main() -> Result<()> {
191 #[cfg(unix)]
192 util::prevent_root_execution();
193
194 // Exit flatpak sandbox if needed
195 #[cfg(target_os = "linux")]
196 {
197 flatpak::try_restart_to_host();
198 flatpak::ld_extra_libs();
199 }
200
201 // Intercept version designators
202 #[cfg(target_os = "macos")]
203 if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
204 // When the first argument is a name of a release channel, we're going to spawn off the CLI of that version, with trailing args passed along.
205 use std::str::FromStr as _;
206
207 if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
208 return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
209 }
210 }
211 let args = Args::parse();
212
213 // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
214 if let Some(socket) = &args.askpass {
215 askpass::main(socket);
216 return Ok(());
217 }
218
219 // Set custom data directory before any path operations
220 let user_data_dir = args.user_data_dir.clone();
221 if let Some(dir) = &user_data_dir {
222 paths::set_custom_data_dir(dir);
223 }
224
225 #[cfg(target_os = "linux")]
226 let args = flatpak::set_bin_if_no_escape(args);
227
228 let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
229
230 if args.version {
231 println!("{}", app.zed_version_string());
232 return Ok(());
233 }
234
235 if args.system_specs {
236 let path = app.path();
237 let msg = [
238 "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.",
239 "To retrieve the system specs on the command line, run the following command:",
240 &format!("{} --system-specs", path.display()),
241 ];
242 anyhow::bail!(msg.join("\n"));
243 }
244
245 #[cfg(all(
246 any(target_os = "linux", target_os = "macos"),
247 not(feature = "no-bundled-uninstall")
248 ))]
249 if args.uninstall {
250 static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");
251
252 let tmp_dir = tempfile::tempdir()?;
253 let script_path = tmp_dir.path().join("uninstall.sh");
254 fs::write(&script_path, UNINSTALL_SCRIPT)?;
255
256 use std::os::unix::fs::PermissionsExt as _;
257 fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
258
259 let status = std::process::Command::new("sh")
260 .arg(&script_path)
261 .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME)
262 .status()
263 .context("Failed to execute uninstall script")?;
264
265 std::process::exit(status.code().unwrap_or(1));
266 }
267
268 let (server, server_name) =
269 IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
270 let url = format!("zed-cli://{server_name}");
271
272 let open_new_workspace = if args.new {
273 Some(true)
274 } else if args.add {
275 Some(false)
276 } else {
277 None
278 };
279
280 let env = {
281 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
282 {
283 use collections::HashMap;
284
285 // On Linux, the desktop entry uses `cli` to spawn `zed`.
286 // We need to handle env vars correctly since std::env::vars() may not contain
287 // project-specific vars (e.g. those set by direnv).
288 // By setting env to None here, the LSP will use worktree env vars instead,
289 // which is what we want.
290 if !std::io::stdout().is_terminal() {
291 None
292 } else {
293 Some(std::env::vars().collect::<HashMap<_, _>>())
294 }
295 }
296
297 #[cfg(target_os = "windows")]
298 {
299 // On Windows, by default, a child process inherits a copy of the environment block of the parent process.
300 // So we don't need to pass env vars explicitly.
301 None
302 }
303
304 #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
305 {
306 use collections::HashMap;
307
308 Some(std::env::vars().collect::<HashMap<_, _>>())
309 }
310 };
311
312 let exit_status = Arc::new(Mutex::new(None));
313 let mut paths = vec![];
314 let mut urls = vec![];
315 let mut diff_paths = vec![];
316 let mut stdin_tmp_file: Option<fs::File> = None;
317 let mut anonymous_fd_tmp_files = vec![];
318
319 for path in args.diff.chunks(2) {
320 diff_paths.push([
321 parse_path_with_position(&path[0])?,
322 parse_path_with_position(&path[1])?,
323 ]);
324 }
325
326 #[cfg(target_os = "windows")]
327 let wsl = args.wsl.as_ref();
328 #[cfg(not(target_os = "windows"))]
329 let wsl = None;
330
331 for path in args.paths_with_position.iter() {
332 if URL_PREFIX.iter().any(|&prefix| path.starts_with(prefix)) {
333 urls.push(path.to_string());
334 } else if path == "-" && args.paths_with_position.len() == 1 {
335 let file = NamedTempFile::new()?;
336 paths.push(file.path().to_string_lossy().into_owned());
337 let (file, _) = file.keep()?;
338 stdin_tmp_file = Some(file);
339 } else if let Some(file) = anonymous_fd(path) {
340 let tmp_file = NamedTempFile::new()?;
341 paths.push(tmp_file.path().to_string_lossy().into_owned());
342 let (tmp_file, _) = tmp_file.keep()?;
343 anonymous_fd_tmp_files.push((file, tmp_file));
344 } else if let Some(wsl) = wsl {
345 urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
346 } else {
347 paths.push(parse_path_with_position(path)?);
348 }
349 }
350
351 anyhow::ensure!(
352 args.dev_server_token.is_none(),
353 "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
354 );
355
356 let sender: JoinHandle<anyhow::Result<()>> = thread::Builder::new()
357 .name("CliReceiver".to_string())
358 .spawn({
359 let exit_status = exit_status.clone();
360 let user_data_dir_for_thread = user_data_dir.clone();
361 move || {
362 let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
363 let (tx, rx) = (handshake.requests, handshake.responses);
364
365 #[cfg(target_os = "windows")]
366 let wsl = args.wsl;
367 #[cfg(not(target_os = "windows"))]
368 let wsl = None;
369
370 tx.send(CliRequest::Open {
371 paths,
372 urls,
373 diff_paths,
374 wsl,
375 wait: args.wait,
376 open_new_workspace,
377 env,
378 user_data_dir: user_data_dir_for_thread,
379 })?;
380
381 while let Ok(response) = rx.recv() {
382 match response {
383 CliResponse::Ping => {}
384 CliResponse::Stdout { message } => println!("{message}"),
385 CliResponse::Stderr { message } => eprintln!("{message}"),
386 CliResponse::Exit { status } => {
387 exit_status.lock().replace(status);
388 return Ok(());
389 }
390 }
391 }
392
393 Ok(())
394 }
395 })
396 .unwrap();
397
398 let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =
399 stdin_tmp_file.map(|mut tmp_file| {
400 thread::Builder::new()
401 .name("CliStdin".to_string())
402 .spawn(move || {
403 let mut stdin = std::io::stdin().lock();
404 if !io::IsTerminal::is_terminal(&stdin) {
405 io::copy(&mut stdin, &mut tmp_file)?;
406 }
407 Ok(())
408 })
409 .unwrap()
410 });
411
412 let anonymous_fd_pipe_handles: Vec<_> = anonymous_fd_tmp_files
413 .into_iter()
414 .map(|(mut file, mut tmp_file)| {
415 thread::Builder::new()
416 .name("CliAnonymousFd".to_string())
417 .spawn(move || io::copy(&mut file, &mut tmp_file))
418 .unwrap()
419 })
420 .collect();
421
422 if args.foreground {
423 app.run_foreground(url, user_data_dir.as_deref())?;
424 } else {
425 app.launch(url)?;
426 sender.join().unwrap()?;
427 if let Some(handle) = stdin_pipe_handle {
428 handle.join().unwrap()?;
429 }
430 for handle in anonymous_fd_pipe_handles {
431 handle.join().unwrap()?;
432 }
433 }
434
435 if let Some(exit_status) = exit_status.lock().take() {
436 std::process::exit(exit_status);
437 }
438 Ok(())
439}
440
441fn anonymous_fd(path: &str) -> Option<fs::File> {
442 #[cfg(target_os = "linux")]
443 {
444 use std::os::fd::{self, FromRawFd};
445
446 let fd_str = path.strip_prefix("/proc/self/fd/")?;
447
448 let link = fs::read_link(path).ok()?;
449 if !link.starts_with("memfd:") {
450 return None;
451 }
452
453 let fd: fd::RawFd = fd_str.parse().ok()?;
454 let file = unsafe { fs::File::from_raw_fd(fd) };
455 Some(file)
456 }
457 #[cfg(any(target_os = "macos", target_os = "freebsd"))]
458 {
459 use std::os::{
460 fd::{self, FromRawFd},
461 unix::fs::FileTypeExt,
462 };
463
464 let fd_str = path.strip_prefix("/dev/fd/")?;
465
466 let metadata = fs::metadata(path).ok()?;
467 let file_type = metadata.file_type();
468 if !file_type.is_fifo() && !file_type.is_socket() {
469 return None;
470 }
471 let fd: fd::RawFd = fd_str.parse().ok()?;
472 let file = unsafe { fs::File::from_raw_fd(fd) };
473 Some(file)
474 }
475 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
476 {
477 _ = path;
478 // not implemented for bsd, windows. Could be, but isn't yet
479 None
480 }
481}
482
483#[cfg(any(target_os = "linux", target_os = "freebsd"))]
484mod linux {
485 use std::{
486 env,
487 ffi::OsString,
488 io,
489 os::unix::net::{SocketAddr, UnixDatagram},
490 path::{Path, PathBuf},
491 process::{self, ExitStatus},
492 thread,
493 time::Duration,
494 };
495
496 use anyhow::{Context as _, anyhow};
497 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
498 use fork::Fork;
499
500 use crate::{Detect, InstalledApp};
501
502 struct App(PathBuf);
503
504 impl Detect {
505 pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
506 let path = if let Some(path) = path {
507 path.to_path_buf().canonicalize()?
508 } else {
509 let cli = env::current_exe()?;
510 let dir = cli.parent().context("no parent path for cli")?;
511
512 // libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
513 // ./zed is for the target directory in development builds.
514 let possible_locations =
515 ["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
516 possible_locations
517 .iter()
518 .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
519 .with_context(|| {
520 format!("could not find any of: {}", possible_locations.join(", "))
521 })?
522 };
523
524 Ok(App(path))
525 }
526 }
527
528 impl InstalledApp for App {
529 fn zed_version_string(&self) -> String {
530 format!(
531 "Zed {}{}{} – {}",
532 if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
533 "".to_string()
534 } else {
535 format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
536 },
537 option_env!("RELEASE_VERSION").unwrap_or_default(),
538 match option_env!("ZED_COMMIT_SHA") {
539 Some(commit_sha) => format!(" {commit_sha} "),
540 None => "".to_string(),
541 },
542 self.0.display(),
543 )
544 }
545
546 fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
547 let sock_path = paths::data_dir().join(format!(
548 "zed-{}.sock",
549 *release_channel::RELEASE_CHANNEL_NAME
550 ));
551 let sock = UnixDatagram::unbound()?;
552 if sock.connect(&sock_path).is_err() {
553 self.boot_background(ipc_url)?;
554 } else {
555 sock.send(ipc_url.as_bytes())?;
556 }
557 Ok(())
558 }
559
560 fn run_foreground(
561 &self,
562 ipc_url: String,
563 user_data_dir: Option<&str>,
564 ) -> io::Result<ExitStatus> {
565 let mut cmd = std::process::Command::new(self.0.clone());
566 cmd.arg(ipc_url);
567 if let Some(dir) = user_data_dir {
568 cmd.arg("--user-data-dir").arg(dir);
569 }
570 cmd.status()
571 }
572
573 fn path(&self) -> PathBuf {
574 self.0.clone()
575 }
576 }
577
578 impl App {
579 fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
580 let path = &self.0;
581
582 match fork::fork() {
583 Ok(Fork::Parent(_)) => Ok(()),
584 Ok(Fork::Child) => {
585 unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") };
586 if fork::setsid().is_err() {
587 eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
588 process::exit(1);
589 }
590 if fork::close_fd().is_err() {
591 eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
592 }
593 let error =
594 exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
595 // if exec succeeded, we never get here.
596 eprintln!("failed to exec {:?}: {}", path, error);
597 process::exit(1)
598 }
599 Err(_) => Err(anyhow!(io::Error::last_os_error())),
600 }
601 }
602
603 fn wait_for_socket(
604 &self,
605 sock_addr: &SocketAddr,
606 sock: &mut UnixDatagram,
607 ) -> Result<(), std::io::Error> {
608 for _ in 0..100 {
609 thread::sleep(Duration::from_millis(10));
610 if sock.connect_addr(sock_addr).is_ok() {
611 return Ok(());
612 }
613 }
614 sock.connect_addr(sock_addr)
615 }
616 }
617}
618
619#[cfg(target_os = "linux")]
620mod flatpak {
621 use std::ffi::OsString;
622 use std::path::PathBuf;
623 use std::process::Command;
624 use std::{env, process};
625
626 const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH";
627 const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE";
628
629 /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
630 pub fn ld_extra_libs() {
631 let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
632 env::split_paths(&paths).collect()
633 } else {
634 Vec::new()
635 };
636
637 if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
638 paths.push(extra_path.into());
639 }
640
641 unsafe { env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap()) };
642 }
643
644 /// Restarts outside of the sandbox if currently running within it
645 pub fn try_restart_to_host() {
646 if let Some(flatpak_dir) = get_flatpak_dir() {
647 let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
648 args.append(&mut get_xdg_env_args());
649 args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
650 args.push(
651 format!(
652 "--env={EXTRA_LIB_ENV_NAME}={}",
653 flatpak_dir.join("lib").to_str().unwrap()
654 )
655 .into(),
656 );
657 args.push(flatpak_dir.join("bin").join("zed").into());
658
659 let mut is_app_location_set = false;
660 for arg in &env::args_os().collect::<Vec<_>>()[1..] {
661 args.push(arg.clone());
662 is_app_location_set |= arg == "--zed";
663 }
664
665 if !is_app_location_set {
666 args.push("--zed".into());
667 args.push(flatpak_dir.join("libexec").join("zed-editor").into());
668 }
669
670 let error = exec::execvp("/usr/bin/flatpak-spawn", args);
671 eprintln!("failed restart cli on host: {:?}", error);
672 process::exit(1);
673 }
674 }
675
676 pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
677 if env::var(NO_ESCAPE_ENV_NAME).is_ok()
678 && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("dev.zed.Zed"))
679 && args.zed.is_none()
680 {
681 args.zed = Some("/app/libexec/zed-editor".into());
682 unsafe { env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") };
683 }
684 args
685 }
686
687 fn get_flatpak_dir() -> Option<PathBuf> {
688 if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
689 return None;
690 }
691
692 if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
693 if !flatpak_id.starts_with("dev.zed.Zed") {
694 return None;
695 }
696
697 let install_dir = Command::new("/usr/bin/flatpak-spawn")
698 .arg("--host")
699 .arg("flatpak")
700 .arg("info")
701 .arg("--show-location")
702 .arg(flatpak_id)
703 .output()
704 .unwrap();
705 let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
706 Some(install_dir.join("files"))
707 } else {
708 None
709 }
710 }
711
712 fn get_xdg_env_args() -> Vec<OsString> {
713 let xdg_keys = [
714 "XDG_DATA_HOME",
715 "XDG_CONFIG_HOME",
716 "XDG_CACHE_HOME",
717 "XDG_STATE_HOME",
718 ];
719 env::vars()
720 .filter(|(key, _)| xdg_keys.contains(&key.as_str()))
721 .map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
722 .collect()
723 }
724}
725
726#[cfg(target_os = "windows")]
727mod windows {
728 use anyhow::Context;
729 use release_channel::app_identifier;
730 use windows::{
731 Win32::{
732 Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_WRITE, GetLastError},
733 Storage::FileSystem::{
734 CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
735 },
736 System::Threading::CreateMutexW,
737 },
738 core::HSTRING,
739 };
740
741 use crate::{Detect, InstalledApp};
742 use std::io;
743 use std::path::{Path, PathBuf};
744 use std::process::ExitStatus;
745
746 fn check_single_instance() -> bool {
747 let mutex = unsafe {
748 CreateMutexW(
749 None,
750 false,
751 &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())),
752 )
753 .expect("Unable to create instance sync event")
754 };
755 let last_err = unsafe { GetLastError() };
756 let _ = unsafe { CloseHandle(mutex) };
757 last_err != ERROR_ALREADY_EXISTS
758 }
759
760 struct App(PathBuf);
761
762 impl InstalledApp for App {
763 fn zed_version_string(&self) -> String {
764 format!(
765 "Zed {}{}{} – {}",
766 if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
767 "".to_string()
768 } else {
769 format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
770 },
771 option_env!("RELEASE_VERSION").unwrap_or_default(),
772 match option_env!("ZED_COMMIT_SHA") {
773 Some(commit_sha) => format!(" {commit_sha} "),
774 None => "".to_string(),
775 },
776 self.0.display(),
777 )
778 }
779
780 fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
781 if check_single_instance() {
782 std::process::Command::new(self.0.clone())
783 .arg(ipc_url)
784 .spawn()?;
785 } else {
786 unsafe {
787 let pipe = CreateFileW(
788 &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
789 GENERIC_WRITE.0,
790 FILE_SHARE_MODE::default(),
791 None,
792 OPEN_EXISTING,
793 FILE_FLAGS_AND_ATTRIBUTES::default(),
794 None,
795 )?;
796 let message = ipc_url.as_bytes();
797 let mut bytes_written = 0;
798 WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
799 CloseHandle(pipe)?;
800 }
801 }
802 Ok(())
803 }
804
805 fn run_foreground(
806 &self,
807 ipc_url: String,
808 user_data_dir: Option<&str>,
809 ) -> io::Result<ExitStatus> {
810 let mut cmd = std::process::Command::new(self.0.clone());
811 cmd.arg(ipc_url).arg("--foreground");
812 if let Some(dir) = user_data_dir {
813 cmd.arg("--user-data-dir").arg(dir);
814 }
815 cmd.spawn()?.wait()
816 }
817
818 fn path(&self) -> PathBuf {
819 self.0.clone()
820 }
821 }
822
823 impl Detect {
824 pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
825 let path = if let Some(path) = path {
826 path.to_path_buf().canonicalize()?
827 } else {
828 let cli = std::env::current_exe()?;
829 let dir = cli.parent().context("no parent path for cli")?;
830
831 // ../Zed.exe is the standard, lib/zed is for MSYS2, ./zed.exe is for the target
832 // directory in development builds.
833 let possible_locations = ["../Zed.exe", "../lib/zed/zed-editor.exe", "./zed.exe"];
834 possible_locations
835 .iter()
836 .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
837 .context(format!(
838 "could not find any of: {}",
839 possible_locations.join(", ")
840 ))?
841 };
842
843 Ok(App(path))
844 }
845 }
846}
847
848#[cfg(target_os = "macos")]
849mod mac_os {
850 use anyhow::{Context as _, Result};
851 use core_foundation::{
852 array::{CFArray, CFIndex},
853 base::TCFType as _,
854 string::kCFStringEncodingUTF8,
855 url::{CFURL, CFURLCreateWithBytes},
856 };
857 use core_services::{LSLaunchURLSpec, LSOpenFromURLSpec, kLSLaunchDefaults};
858 use serde::Deserialize;
859 use std::{
860 ffi::OsStr,
861 fs, io,
862 path::{Path, PathBuf},
863 process::{Command, ExitStatus},
864 ptr,
865 };
866
867 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
868
869 use crate::{Detect, InstalledApp};
870
871 #[derive(Debug, Deserialize)]
872 struct InfoPlist {
873 #[serde(rename = "CFBundleShortVersionString")]
874 bundle_short_version_string: String,
875 }
876
877 enum Bundle {
878 App {
879 app_bundle: PathBuf,
880 plist: InfoPlist,
881 },
882 LocalPath {
883 executable: PathBuf,
884 },
885 }
886
887 fn locate_bundle() -> Result<PathBuf> {
888 let cli_path = std::env::current_exe()?.canonicalize()?;
889 let mut app_path = cli_path.clone();
890 while app_path.extension() != Some(OsStr::new("app")) {
891 anyhow::ensure!(
892 app_path.pop(),
893 "cannot find app bundle containing {cli_path:?}"
894 );
895 }
896 Ok(app_path)
897 }
898
899 impl Detect {
900 pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
901 let bundle_path = if let Some(bundle_path) = path {
902 bundle_path
903 .canonicalize()
904 .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
905 } else {
906 locate_bundle().context("bundle autodiscovery")?
907 };
908
909 match bundle_path.extension().and_then(|ext| ext.to_str()) {
910 Some("app") => {
911 let plist_path = bundle_path.join("Contents/Info.plist");
912 let plist =
913 plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
914 format!("Reading *.app bundle plist file at {plist_path:?}")
915 })?;
916 Ok(Bundle::App {
917 app_bundle: bundle_path,
918 plist,
919 })
920 }
921 _ => Ok(Bundle::LocalPath {
922 executable: bundle_path,
923 }),
924 }
925 }
926 }
927
928 impl InstalledApp for Bundle {
929 fn zed_version_string(&self) -> String {
930 format!("Zed {} – {}", self.version(), self.path().display(),)
931 }
932
933 fn launch(&self, url: String) -> anyhow::Result<()> {
934 match self {
935 Self::App { app_bundle, .. } => {
936 let app_path = app_bundle;
937
938 let status = unsafe {
939 let app_url = CFURL::from_path(app_path, true)
940 .with_context(|| format!("invalid app path {app_path:?}"))?;
941 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
942 ptr::null(),
943 url.as_ptr(),
944 url.len() as CFIndex,
945 kCFStringEncodingUTF8,
946 ptr::null(),
947 ));
948 // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
949 let urls_to_open =
950 CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
951 LSOpenFromURLSpec(
952 &LSLaunchURLSpec {
953 appURL: app_url.as_concrete_TypeRef(),
954 itemURLs: urls_to_open.as_concrete_TypeRef(),
955 passThruParams: ptr::null(),
956 launchFlags: kLSLaunchDefaults,
957 asyncRefCon: ptr::null_mut(),
958 },
959 ptr::null_mut(),
960 )
961 };
962
963 anyhow::ensure!(
964 status == 0,
965 "cannot start app bundle {}",
966 self.zed_version_string()
967 );
968 }
969
970 Self::LocalPath { executable, .. } => {
971 let executable_parent = executable
972 .parent()
973 .with_context(|| format!("Executable {executable:?} path has no parent"))?;
974 let subprocess_stdout_file = fs::File::create(
975 executable_parent.join("zed_dev.log"),
976 )
977 .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
978 let subprocess_stdin_file =
979 subprocess_stdout_file.try_clone().with_context(|| {
980 format!("Cloning descriptor for file {subprocess_stdout_file:?}")
981 })?;
982 let mut command = std::process::Command::new(executable);
983 let command = command
984 .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
985 .stderr(subprocess_stdout_file)
986 .stdout(subprocess_stdin_file)
987 .arg(url);
988
989 command
990 .spawn()
991 .with_context(|| format!("Spawning {command:?}"))?;
992 }
993 }
994
995 Ok(())
996 }
997
998 fn run_foreground(
999 &self,
1000 ipc_url: String,
1001 user_data_dir: Option<&str>,
1002 ) -> io::Result<ExitStatus> {
1003 let path = match self {
1004 Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
1005 Bundle::LocalPath { executable, .. } => executable.clone(),
1006 };
1007
1008 let mut cmd = std::process::Command::new(path);
1009 cmd.arg(ipc_url);
1010 if let Some(dir) = user_data_dir {
1011 cmd.arg("--user-data-dir").arg(dir);
1012 }
1013 cmd.status()
1014 }
1015
1016 fn path(&self) -> PathBuf {
1017 match self {
1018 Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
1019 Bundle::LocalPath { executable, .. } => executable.clone(),
1020 }
1021 }
1022 }
1023
1024 impl Bundle {
1025 fn version(&self) -> String {
1026 match self {
1027 Self::App { plist, .. } => plist.bundle_short_version_string.clone(),
1028 Self::LocalPath { .. } => "<development>".to_string(),
1029 }
1030 }
1031
1032 fn path(&self) -> &Path {
1033 match self {
1034 Self::App { app_bundle, .. } => app_bundle,
1035 Self::LocalPath { executable, .. } => executable,
1036 }
1037 }
1038 }
1039
1040 pub(super) fn spawn_channel_cli(
1041 channel: release_channel::ReleaseChannel,
1042 leftover_args: Vec<String>,
1043 ) -> Result<()> {
1044 use anyhow::bail;
1045
1046 let app_path_prompt = format!(
1047 "POSIX path of (path to application \"{}\")",
1048 channel.display_name()
1049 );
1050 let app_path_output = Command::new("osascript")
1051 .arg("-e")
1052 .arg(&app_path_prompt)
1053 .output()?;
1054 if !app_path_output.status.success() {
1055 bail!(
1056 "Could not determine app path for {}",
1057 channel.display_name()
1058 );
1059 }
1060 let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
1061 let cli_path = format!("{app_path}/Contents/MacOS/cli");
1062 Command::new(cli_path).args(leftover_args).spawn()?;
1063 Ok(())
1064 }
1065}