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