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