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