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