main.rs

   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    collections::{BTreeMap, BTreeSet},
  16    env,
  17    ffi::OsStr,
  18    fs, io,
  19    path::{Path, PathBuf},
  20    process::ExitStatus,
  21    sync::Arc,
  22    thread::{self, JoinHandle},
  23};
  24use tempfile::{NamedTempFile, TempDir};
  25use util::paths::PathWithPosition;
  26use walkdir::WalkDir;
  27
  28#[cfg(any(target_os = "linux", target_os = "freebsd"))]
  29use std::io::IsTerminal;
  30
  31const URL_PREFIX: [&'static str; 5] = ["zed://", "http://", "https://", "file://", "ssh://"];
  32
  33struct Detect;
  34
  35trait InstalledApp {
  36    fn zed_version_string(&self) -> String;
  37    fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()>;
  38    fn run_foreground(
  39        &self,
  40        ipc_url: String,
  41        user_data_dir: Option<&str>,
  42    ) -> io::Result<ExitStatus>;
  43    fn path(&self) -> PathBuf;
  44}
  45
  46#[derive(Parser, Debug)]
  47#[command(
  48    name = "zed",
  49    disable_version_flag = true,
  50    before_help = "The Zed CLI binary.
  51This CLI is a separate binary that invokes Zed.
  52
  53Examples:
  54    `zed`
  55          Simply opens Zed
  56    `zed --foreground`
  57          Runs in foreground (shows all logs)
  58    `zed path-to-your-project`
  59          Open your project in Zed
  60    `zed -n path-to-file `
  61          Open file/folder in a new window",
  62    after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'"
  63)]
  64struct Args {
  65    /// Wait for all of the given paths to be opened/closed before exiting.
  66    ///
  67    /// When opening a directory, waits until the created window is closed.
  68    #[arg(short, long)]
  69    wait: bool,
  70    /// Add files to the currently open workspace
  71    #[arg(short, long, overrides_with_all = ["new", "reuse"])]
  72    add: bool,
  73    /// Create a new workspace
  74    #[arg(short, long, overrides_with_all = ["add", "reuse"])]
  75    new: bool,
  76    /// Reuse an existing window, replacing its workspace
  77    #[arg(short, long, overrides_with_all = ["add", "new"])]
  78    reuse: bool,
  79    /// Sets a custom directory for all user data (e.g., database, extensions, logs).
  80    /// This overrides the default platform-specific data directory location:
  81    #[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
  82    #[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")]
  83    #[cfg_attr(
  84        not(any(target_os = "windows", target_os = "macos")),
  85        doc = "`$XDG_DATA_HOME/zed`."
  86    )]
  87    #[arg(long, value_name = "DIR")]
  88    user_data_dir: Option<String>,
  89    /// The paths to open in Zed (space-separated).
  90    ///
  91    /// Use `path:line:column` syntax to open a file at the given line and column.
  92    paths_with_position: Vec<String>,
  93    /// Print Zed's version and the app path.
  94    #[arg(short, long)]
  95    version: bool,
  96    /// Run zed in the foreground (useful for debugging)
  97    #[arg(long)]
  98    foreground: bool,
  99    /// Custom path to Zed.app or the zed binary
 100    #[arg(long)]
 101    zed: Option<PathBuf>,
 102    /// Run zed in dev-server mode
 103    #[arg(long)]
 104    dev_server_token: Option<String>,
 105    /// The username and WSL distribution to use when opening paths. If not specified,
 106    /// Zed will attempt to open the paths directly.
 107    ///
 108    /// The username is optional, and if not specified, the default user for the distribution
 109    /// will be used.
 110    ///
 111    /// Example: `me@Ubuntu` or `Ubuntu`.
 112    ///
 113    /// WARN: You should not fill in this field by hand.
 114    #[cfg(target_os = "windows")]
 115    #[arg(long, value_name = "USER@DISTRO")]
 116    wsl: Option<String>,
 117    /// Not supported in Zed CLI, only supported on Zed binary
 118    /// Will attempt to give the correct command to run
 119    #[arg(long)]
 120    system_specs: bool,
 121    /// Pairs of file paths to diff. Can be specified multiple times.
 122    /// When directories are provided, recurses into them and shows all changed files in a single multi-diff view.
 123    #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
 124    diff: Vec<String>,
 125    /// Uninstall Zed from user system
 126    #[cfg(all(
 127        any(target_os = "linux", target_os = "macos"),
 128        not(feature = "no-bundled-uninstall")
 129    ))]
 130    #[arg(long)]
 131    uninstall: bool,
 132
 133    /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
 134    /// by having Zed act like netcat communicating over a Unix socket.
 135    #[arg(long, hide = true)]
 136    askpass: Option<String>,
 137}
 138
 139/// Parses a path containing a position (e.g. `path:line:column`)
 140/// and returns its canonicalized string representation.
 141///
 142/// If a part of path doesn't exist, it will canonicalize the
 143/// existing part and append the non-existing part.
 144///
 145/// This method must return an absolute path, as many zed
 146/// crates assume absolute paths.
 147fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
 148    match Path::new(argument_str).canonicalize() {
 149        Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)),
 150        Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| {
 151            let curdir = env::current_dir().context("retrieving current directory")?;
 152            let mut children = Vec::new();
 153            let root;
 154            loop {
 155                // canonicalize handles './', and '/'.
 156                if let Ok(canonicalized) = fs::canonicalize(&path) {
 157                    root = canonicalized;
 158                    break;
 159                }
 160                // The comparison to `curdir` is just a shortcut
 161                // since we know it is canonical. The other one
 162                // is if `argument_str` is a string that starts
 163                // with a name (e.g. "foo/bar").
 164                if path == curdir || path == Path::new("") {
 165                    root = curdir;
 166                    break;
 167                }
 168                children.push(
 169                    path.file_name()
 170                        .with_context(|| format!("parsing as path with position {argument_str}"))?
 171                        .to_owned(),
 172                );
 173                if !path.pop() {
 174                    unreachable!("parsing as path with position {argument_str}");
 175                }
 176            }
 177            Ok(children.iter().rev().fold(root, |mut path, child| {
 178                path.push(child);
 179                path
 180            }))
 181        }),
 182    }
 183    .map(|path_with_pos| path_with_pos.to_string(&|path| path.to_string_lossy().into_owned()))
 184}
 185
 186fn expand_directory_diff_pairs(
 187    diff_pairs: Vec<[String; 2]>,
 188) -> anyhow::Result<(Vec<[String; 2]>, Vec<TempDir>)> {
 189    let mut expanded = Vec::new();
 190    let mut temp_dirs = Vec::new();
 191
 192    for pair in diff_pairs {
 193        let left = PathBuf::from(&pair[0]);
 194        let right = PathBuf::from(&pair[1]);
 195
 196        if left.is_dir() && right.is_dir() {
 197            let (mut pairs, temp_dir) = expand_directory_pair(&left, &right)?;
 198            expanded.append(&mut pairs);
 199            if let Some(temp_dir) = temp_dir {
 200                temp_dirs.push(temp_dir);
 201            }
 202        } else {
 203            expanded.push(pair);
 204        }
 205    }
 206
 207    Ok((expanded, temp_dirs))
 208}
 209
 210fn expand_directory_pair(
 211    left: &Path,
 212    right: &Path,
 213) -> anyhow::Result<(Vec<[String; 2]>, Option<TempDir>)> {
 214    let left_files = collect_files(left)?;
 215    let right_files = collect_files(right)?;
 216
 217    let mut rel_paths = BTreeSet::new();
 218    rel_paths.extend(left_files.keys().cloned());
 219    rel_paths.extend(right_files.keys().cloned());
 220
 221    let mut temp_dir = TempDir::new()?;
 222    let mut temp_dir_used = false;
 223    let mut pairs = Vec::new();
 224
 225    for rel in rel_paths {
 226        match (left_files.get(&rel), right_files.get(&rel)) {
 227            (Some(left_path), Some(right_path)) => {
 228                pairs.push([
 229                    left_path.to_string_lossy().into_owned(),
 230                    right_path.to_string_lossy().into_owned(),
 231                ]);
 232            }
 233            (Some(left_path), None) => {
 234                let stub = create_empty_stub(&mut temp_dir, &rel)?;
 235                temp_dir_used = true;
 236                pairs.push([
 237                    left_path.to_string_lossy().into_owned(),
 238                    stub.to_string_lossy().into_owned(),
 239                ]);
 240            }
 241            (None, Some(right_path)) => {
 242                let stub = create_empty_stub(&mut temp_dir, &rel)?;
 243                temp_dir_used = true;
 244                pairs.push([
 245                    stub.to_string_lossy().into_owned(),
 246                    right_path.to_string_lossy().into_owned(),
 247                ]);
 248            }
 249            (None, None) => {}
 250        }
 251    }
 252
 253    let temp_dir = if temp_dir_used { Some(temp_dir) } else { None };
 254    Ok((pairs, temp_dir))
 255}
 256
 257fn collect_files(root: &Path) -> anyhow::Result<BTreeMap<PathBuf, PathBuf>> {
 258    let mut files = BTreeMap::new();
 259
 260    for entry in WalkDir::new(root) {
 261        let entry = entry?;
 262        if entry.file_type().is_file() {
 263            let rel = entry
 264                .path()
 265                .strip_prefix(root)
 266                .context("stripping directory prefix")?
 267                .to_path_buf();
 268            files.insert(rel, entry.into_path());
 269        }
 270    }
 271
 272    Ok(files)
 273}
 274
 275fn create_empty_stub(temp_dir: &mut TempDir, rel: &Path) -> anyhow::Result<PathBuf> {
 276    let stub_path = temp_dir.path().join(rel);
 277    if let Some(parent) = stub_path.parent() {
 278        fs::create_dir_all(parent)?;
 279    }
 280    fs::File::create(&stub_path)?;
 281    Ok(stub_path)
 282}
 283
 284#[cfg(test)]
 285mod tests {
 286    use super::*;
 287    use serde_json::json;
 288    use util::path;
 289    use util::paths::SanitizedPath;
 290    use util::test::TempTree;
 291
 292    macro_rules! assert_path_eq {
 293        ($left:expr, $right:expr) => {
 294            assert_eq!(
 295                SanitizedPath::new(Path::new(&$left)),
 296                SanitizedPath::new(Path::new(&$right))
 297            )
 298        };
 299    }
 300
 301    fn cwd() -> PathBuf {
 302        env::current_dir().unwrap()
 303    }
 304
 305    static CWD_LOCK: Mutex<()> = Mutex::new(());
 306
 307    fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> {
 308        let _lock = CWD_LOCK.lock();
 309        let old_cwd = cwd();
 310        env::set_current_dir(path)?;
 311        let result = f();
 312        env::set_current_dir(old_cwd)?;
 313        result
 314    }
 315
 316    #[test]
 317    fn test_parse_non_existing_path() {
 318        // Absolute path
 319        let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap();
 320        assert_path_eq!(result, path!("/non/existing/path.txt"));
 321
 322        // Absolute path in cwd
 323        let path = cwd().join(path!("non/existing/path.txt"));
 324        let expected = path.to_string_lossy().to_string();
 325        let result = parse_path_with_position(&expected).unwrap();
 326        assert_path_eq!(result, expected);
 327
 328        // Relative path
 329        let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap();
 330        assert_path_eq!(result, expected)
 331    }
 332
 333    #[test]
 334    fn test_parse_existing_path() {
 335        let temp_tree = TempTree::new(json!({
 336            "file.txt": "",
 337        }));
 338        let file_path = temp_tree.path().join("file.txt");
 339        let expected = file_path.to_string_lossy().to_string();
 340
 341        // Absolute path
 342        let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap();
 343        assert_path_eq!(result, expected);
 344
 345        // Relative path
 346        let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap();
 347        assert_path_eq!(result, expected);
 348    }
 349
 350    // NOTE:
 351    // While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
 352    // we assume that they are not supported out of the box.
 353    #[cfg(not(windows))]
 354    #[test]
 355    fn test_parse_symlink_file() {
 356        let temp_tree = TempTree::new(json!({
 357            "target.txt": "",
 358        }));
 359        let target_path = temp_tree.path().join("target.txt");
 360        let symlink_path = temp_tree.path().join("symlink.txt");
 361        std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
 362
 363        // Absolute path
 364        let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap();
 365        assert_eq!(result, target_path.to_string_lossy());
 366
 367        // Relative path
 368        let result =
 369            with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap();
 370        assert_eq!(result, target_path.to_string_lossy());
 371    }
 372
 373    #[cfg(not(windows))]
 374    #[test]
 375    fn test_parse_symlink_dir() {
 376        let temp_tree = TempTree::new(json!({
 377            "some": {
 378                "dir": { // symlink target
 379                    "ec": {
 380                        "tory": {
 381                            "file.txt": "",
 382        }}}}}));
 383
 384        let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt");
 385        let expected = target_file_path.to_string_lossy();
 386
 387        let dir_path = temp_tree.path().join("some/dir");
 388        let symlink_path = temp_tree.path().join("symlink");
 389        std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap();
 390
 391        // Absolute path
 392        let result =
 393            parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap())
 394                .unwrap();
 395        assert_eq!(result, expected);
 396
 397        // Relative path
 398        let result = with_cwd(temp_tree.path(), || {
 399            parse_path_with_position("symlink/ec/tory/file.txt")
 400        })
 401        .unwrap();
 402        assert_eq!(result, expected);
 403    }
 404}
 405
 406fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
 407    let mut source = PathWithPosition::parse_str(source);
 408
 409    let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
 410        if user.is_empty() {
 411            anyhow::bail!("user is empty in wsl argument");
 412        }
 413        (Some(user), distro)
 414    } else {
 415        (None, wsl)
 416    };
 417
 418    let mut args = vec!["--distribution", distro_name];
 419    if let Some(user) = user {
 420        args.push("--user");
 421        args.push(user);
 422    }
 423
 424    let command = [
 425        OsStr::new("realpath"),
 426        OsStr::new("-s"),
 427        source.path.as_ref(),
 428    ];
 429
 430    let output = util::command::new_std_command("wsl.exe")
 431        .args(&args)
 432        .arg("--exec")
 433        .args(&command)
 434        .output()?;
 435    let result = if output.status.success() {
 436        String::from_utf8_lossy(&output.stdout).to_string()
 437    } else {
 438        let fallback = util::command::new_std_command("wsl.exe")
 439            .args(&args)
 440            .arg("--")
 441            .args(&command)
 442            .output()?;
 443        String::from_utf8_lossy(&fallback.stdout).to_string()
 444    };
 445
 446    source.path = Path::new(result.trim()).to_owned();
 447
 448    Ok(source.to_string(&|path| path.to_string_lossy().into_owned()))
 449}
 450
 451fn main() -> Result<()> {
 452    #[cfg(unix)]
 453    util::prevent_root_execution();
 454
 455    // Exit flatpak sandbox if needed
 456    #[cfg(target_os = "linux")]
 457    {
 458        flatpak::try_restart_to_host();
 459        flatpak::ld_extra_libs();
 460    }
 461
 462    // Intercept version designators
 463    #[cfg(target_os = "macos")]
 464    if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
 465        // 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.
 466        use std::str::FromStr as _;
 467
 468        if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
 469            return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
 470        }
 471    }
 472    let args = Args::parse();
 473
 474    // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
 475    if let Some(socket) = &args.askpass {
 476        askpass::main(socket);
 477        return Ok(());
 478    }
 479
 480    // Set custom data directory before any path operations
 481    let user_data_dir = args.user_data_dir.clone();
 482    if let Some(dir) = &user_data_dir {
 483        paths::set_custom_data_dir(dir);
 484    }
 485
 486    #[cfg(target_os = "linux")]
 487    let args = flatpak::set_bin_if_no_escape(args);
 488
 489    let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
 490
 491    if args.version {
 492        println!("{}", app.zed_version_string());
 493        return Ok(());
 494    }
 495
 496    if args.system_specs {
 497        let path = app.path();
 498        let msg = [
 499            "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.",
 500            "To retrieve the system specs on the command line, run the following command:",
 501            &format!("{} --system-specs", path.display()),
 502        ];
 503        anyhow::bail!(msg.join("\n"));
 504    }
 505
 506    #[cfg(all(
 507        any(target_os = "linux", target_os = "macos"),
 508        not(feature = "no-bundled-uninstall")
 509    ))]
 510    if args.uninstall {
 511        static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");
 512
 513        let tmp_dir = tempfile::tempdir()?;
 514        let script_path = tmp_dir.path().join("uninstall.sh");
 515        fs::write(&script_path, UNINSTALL_SCRIPT)?;
 516
 517        use std::os::unix::fs::PermissionsExt as _;
 518        fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
 519
 520        let status = std::process::Command::new("sh")
 521            .arg(&script_path)
 522            .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME)
 523            .status()
 524            .context("Failed to execute uninstall script")?;
 525
 526        std::process::exit(status.code().unwrap_or(1));
 527    }
 528
 529    let (server, server_name) =
 530        IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
 531    let url = format!("zed-cli://{server_name}");
 532
 533    let open_new_workspace = if args.new {
 534        Some(true)
 535    } else if args.add {
 536        Some(false)
 537    } else {
 538        None
 539    };
 540
 541    let env = {
 542        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 543        {
 544            use collections::HashMap;
 545
 546            // On Linux, the desktop entry uses `cli` to spawn `zed`.
 547            // We need to handle env vars correctly since std::env::vars() may not contain
 548            // project-specific vars (e.g. those set by direnv).
 549            // By setting env to None here, the LSP will use worktree env vars instead,
 550            // which is what we want.
 551            if !std::io::stdout().is_terminal() {
 552                None
 553            } else {
 554                Some(std::env::vars().collect::<HashMap<_, _>>())
 555            }
 556        }
 557
 558        #[cfg(target_os = "windows")]
 559        {
 560            // On Windows, by default, a child process inherits a copy of the environment block of the parent process.
 561            // So we don't need to pass env vars explicitly.
 562            None
 563        }
 564
 565        #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
 566        {
 567            use collections::HashMap;
 568
 569            Some(std::env::vars().collect::<HashMap<_, _>>())
 570        }
 571    };
 572
 573    let exit_status = Arc::new(Mutex::new(None));
 574    let mut paths = vec![];
 575    let mut urls = vec![];
 576    let mut diff_paths = vec![];
 577    let mut stdin_tmp_file: Option<fs::File> = None;
 578    let mut anonymous_fd_tmp_files = vec![];
 579
 580    // Check if any diff paths are directories to determine diff_all mode
 581    let diff_all_mode = args
 582        .diff
 583        .chunks(2)
 584        .any(|pair| Path::new(&pair[0]).is_dir() || Path::new(&pair[1]).is_dir());
 585
 586    for path in args.diff.chunks(2) {
 587        diff_paths.push([
 588            parse_path_with_position(&path[0])?,
 589            parse_path_with_position(&path[1])?,
 590        ]);
 591    }
 592
 593    let (expanded_diff_paths, temp_dirs) = expand_directory_diff_pairs(diff_paths)?;
 594    diff_paths = expanded_diff_paths;
 595    // Prevent automatic cleanup of temp directories containing empty stub files
 596    // for directory diffs. The CLI process may exit before Zed has read these
 597    // files (e.g., when RPC-ing into an already-running instance). The files
 598    // live in the OS temp directory and will be cleaned up on reboot.
 599    for temp_dir in temp_dirs {
 600        let _ = temp_dir.keep();
 601    }
 602
 603    #[cfg(target_os = "windows")]
 604    let wsl = args.wsl.as_ref();
 605    #[cfg(not(target_os = "windows"))]
 606    let wsl = None;
 607
 608    for path in args.paths_with_position.iter() {
 609        if URL_PREFIX.iter().any(|&prefix| path.starts_with(prefix)) {
 610            urls.push(path.to_string());
 611        } else if path == "-" && args.paths_with_position.len() == 1 {
 612            let file = NamedTempFile::new()?;
 613            paths.push(file.path().to_string_lossy().into_owned());
 614            let (file, _) = file.keep()?;
 615            stdin_tmp_file = Some(file);
 616        } else if let Some(file) = anonymous_fd(path) {
 617            let tmp_file = NamedTempFile::new()?;
 618            paths.push(tmp_file.path().to_string_lossy().into_owned());
 619            let (tmp_file, _) = tmp_file.keep()?;
 620            anonymous_fd_tmp_files.push((file, tmp_file));
 621        } else if let Some(wsl) = wsl {
 622            urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
 623        } else {
 624            paths.push(parse_path_with_position(path)?);
 625        }
 626    }
 627
 628    // When only diff paths are provided (no regular paths), add the current
 629    // working directory so the workspace opens with the right context.
 630    if paths.is_empty() && urls.is_empty() && !diff_paths.is_empty() {
 631        if let Ok(cwd) = env::current_dir() {
 632            paths.push(cwd.to_string_lossy().into_owned());
 633        }
 634    }
 635
 636    anyhow::ensure!(
 637        args.dev_server_token.is_none(),
 638        "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
 639    );
 640
 641    rayon::ThreadPoolBuilder::new()
 642        .num_threads(4)
 643        .stack_size(10 * 1024 * 1024)
 644        .thread_name(|ix| format!("RayonWorker{}", ix))
 645        .build_global()
 646        .unwrap();
 647
 648    let sender: JoinHandle<anyhow::Result<()>> = thread::Builder::new()
 649        .name("CliReceiver".to_string())
 650        .spawn({
 651            let exit_status = exit_status.clone();
 652            let user_data_dir_for_thread = user_data_dir.clone();
 653            move || {
 654                let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
 655                let (tx, rx) = (handshake.requests, handshake.responses);
 656
 657                #[cfg(target_os = "windows")]
 658                let wsl = args.wsl;
 659                #[cfg(not(target_os = "windows"))]
 660                let wsl = None;
 661
 662                tx.send(CliRequest::Open {
 663                    paths,
 664                    urls,
 665                    diff_paths,
 666                    diff_all: diff_all_mode,
 667                    wsl,
 668                    wait: args.wait,
 669                    open_new_workspace,
 670                    reuse: args.reuse,
 671                    env,
 672                    user_data_dir: user_data_dir_for_thread,
 673                })?;
 674
 675                while let Ok(response) = rx.recv() {
 676                    match response {
 677                        CliResponse::Ping => {}
 678                        CliResponse::Stdout { message } => println!("{message}"),
 679                        CliResponse::Stderr { message } => eprintln!("{message}"),
 680                        CliResponse::Exit { status } => {
 681                            exit_status.lock().replace(status);
 682                            return Ok(());
 683                        }
 684                    }
 685                }
 686
 687                Ok(())
 688            }
 689        })
 690        .unwrap();
 691
 692    let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =
 693        stdin_tmp_file.map(|mut tmp_file| {
 694            thread::Builder::new()
 695                .name("CliStdin".to_string())
 696                .spawn(move || {
 697                    let mut stdin = std::io::stdin().lock();
 698                    if !io::IsTerminal::is_terminal(&stdin) {
 699                        io::copy(&mut stdin, &mut tmp_file)?;
 700                    }
 701                    Ok(())
 702                })
 703                .unwrap()
 704        });
 705
 706    let anonymous_fd_pipe_handles: Vec<_> = anonymous_fd_tmp_files
 707        .into_iter()
 708        .map(|(mut file, mut tmp_file)| {
 709            thread::Builder::new()
 710                .name("CliAnonymousFd".to_string())
 711                .spawn(move || io::copy(&mut file, &mut tmp_file))
 712                .unwrap()
 713        })
 714        .collect();
 715
 716    if args.foreground {
 717        app.run_foreground(url, user_data_dir.as_deref())?;
 718    } else {
 719        app.launch(url, user_data_dir.as_deref())?;
 720        sender.join().unwrap()?;
 721        if let Some(handle) = stdin_pipe_handle {
 722            handle.join().unwrap()?;
 723        }
 724        for handle in anonymous_fd_pipe_handles {
 725            handle.join().unwrap()?;
 726        }
 727    }
 728
 729    if let Some(exit_status) = exit_status.lock().take() {
 730        std::process::exit(exit_status);
 731    }
 732    Ok(())
 733}
 734
 735fn anonymous_fd(path: &str) -> Option<fs::File> {
 736    #[cfg(target_os = "linux")]
 737    {
 738        use std::os::fd::{self, FromRawFd};
 739
 740        let fd_str = path.strip_prefix("/proc/self/fd/")?;
 741
 742        let link = fs::read_link(path).ok()?;
 743        if !link.starts_with("memfd:") {
 744            return None;
 745        }
 746
 747        let fd: fd::RawFd = fd_str.parse().ok()?;
 748        let file = unsafe { fs::File::from_raw_fd(fd) };
 749        Some(file)
 750    }
 751    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
 752    {
 753        use std::os::{
 754            fd::{self, FromRawFd},
 755            unix::fs::FileTypeExt,
 756        };
 757
 758        let fd_str = path.strip_prefix("/dev/fd/")?;
 759
 760        let metadata = fs::metadata(path).ok()?;
 761        let file_type = metadata.file_type();
 762        if !file_type.is_fifo() && !file_type.is_socket() {
 763            return None;
 764        }
 765        let fd: fd::RawFd = fd_str.parse().ok()?;
 766        let file = unsafe { fs::File::from_raw_fd(fd) };
 767        Some(file)
 768    }
 769    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
 770    {
 771        _ = path;
 772        // not implemented for bsd, windows. Could be, but isn't yet
 773        None
 774    }
 775}
 776
 777#[cfg(any(target_os = "linux", target_os = "freebsd"))]
 778mod linux {
 779    use std::{
 780        env,
 781        ffi::OsString,
 782        io,
 783        os::unix::net::{SocketAddr, UnixDatagram},
 784        path::{Path, PathBuf},
 785        process::{self, ExitStatus},
 786        thread,
 787        time::Duration,
 788    };
 789
 790    use anyhow::{Context as _, anyhow};
 791    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
 792    use fork::Fork;
 793
 794    use crate::{Detect, InstalledApp};
 795
 796    struct App(PathBuf);
 797
 798    impl Detect {
 799        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
 800            let path = if let Some(path) = path {
 801                path.to_path_buf().canonicalize()?
 802            } else {
 803                let cli = env::current_exe()?;
 804                let dir = cli.parent().context("no parent path for cli")?;
 805
 806                // libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
 807                // ./zed is for the target directory in development builds.
 808                let possible_locations =
 809                    ["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
 810                possible_locations
 811                    .iter()
 812                    .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
 813                    .with_context(|| {
 814                        format!("could not find any of: {}", possible_locations.join(", "))
 815                    })?
 816            };
 817
 818            Ok(App(path))
 819        }
 820    }
 821
 822    impl InstalledApp for App {
 823        fn zed_version_string(&self) -> String {
 824            format!(
 825                "Zed {}{}{}{}",
 826                if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
 827                    "".to_string()
 828                } else {
 829                    format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
 830                },
 831                option_env!("RELEASE_VERSION").unwrap_or_default(),
 832                match option_env!("ZED_COMMIT_SHA") {
 833                    Some(commit_sha) => format!(" {commit_sha} "),
 834                    None => "".to_string(),
 835                },
 836                self.0.display(),
 837            )
 838        }
 839
 840        fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
 841            let data_dir = user_data_dir
 842                .map(PathBuf::from)
 843                .unwrap_or_else(|| paths::data_dir().clone());
 844
 845            let sock_path = data_dir.join(format!(
 846                "zed-{}.sock",
 847                *release_channel::RELEASE_CHANNEL_NAME
 848            ));
 849            let sock = UnixDatagram::unbound()?;
 850            if sock.connect(&sock_path).is_err() {
 851                self.boot_background(ipc_url, user_data_dir)?;
 852            } else {
 853                sock.send(ipc_url.as_bytes())?;
 854            }
 855            Ok(())
 856        }
 857
 858        fn run_foreground(
 859            &self,
 860            ipc_url: String,
 861            user_data_dir: Option<&str>,
 862        ) -> io::Result<ExitStatus> {
 863            let mut cmd = std::process::Command::new(self.0.clone());
 864            cmd.arg(ipc_url);
 865            if let Some(dir) = user_data_dir {
 866                cmd.arg("--user-data-dir").arg(dir);
 867            }
 868            cmd.status()
 869        }
 870
 871        fn path(&self) -> PathBuf {
 872            self.0.clone()
 873        }
 874    }
 875
 876    impl App {
 877        fn boot_background(
 878            &self,
 879            ipc_url: String,
 880            user_data_dir: Option<&str>,
 881        ) -> anyhow::Result<()> {
 882            let path = &self.0;
 883
 884            match fork::fork() {
 885                Ok(Fork::Parent(_)) => Ok(()),
 886                Ok(Fork::Child) => {
 887                    unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") };
 888                    if fork::setsid().is_err() {
 889                        eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
 890                        process::exit(1);
 891                    }
 892                    if fork::close_fd().is_err() {
 893                        eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
 894                    }
 895                    let mut args: Vec<OsString> =
 896                        vec![path.as_os_str().to_owned(), OsString::from(ipc_url)];
 897                    if let Some(dir) = user_data_dir {
 898                        args.push(OsString::from("--user-data-dir"));
 899                        args.push(OsString::from(dir));
 900                    }
 901                    let error = exec::execvp(path.clone(), &args);
 902                    // if exec succeeded, we never get here.
 903                    eprintln!("failed to exec {:?}: {}", path, error);
 904                    process::exit(1)
 905                }
 906                Err(_) => Err(anyhow!(io::Error::last_os_error())),
 907            }
 908        }
 909
 910        fn wait_for_socket(
 911            &self,
 912            sock_addr: &SocketAddr,
 913            sock: &mut UnixDatagram,
 914        ) -> Result<(), std::io::Error> {
 915            for _ in 0..100 {
 916                thread::sleep(Duration::from_millis(10));
 917                if sock.connect_addr(sock_addr).is_ok() {
 918                    return Ok(());
 919                }
 920            }
 921            sock.connect_addr(sock_addr)
 922        }
 923    }
 924}
 925
 926#[cfg(target_os = "linux")]
 927mod flatpak {
 928    use std::ffi::OsString;
 929    use std::path::PathBuf;
 930    use std::process::Command;
 931    use std::{env, process};
 932
 933    const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH";
 934    const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE";
 935
 936    /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
 937    pub fn ld_extra_libs() {
 938        let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
 939            env::split_paths(&paths).collect()
 940        } else {
 941            Vec::new()
 942        };
 943
 944        if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
 945            paths.push(extra_path.into());
 946        }
 947
 948        unsafe { env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap()) };
 949    }
 950
 951    /// Restarts outside of the sandbox if currently running within it
 952    pub fn try_restart_to_host() {
 953        if let Some(flatpak_dir) = get_flatpak_dir() {
 954            let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
 955            args.append(&mut get_xdg_env_args());
 956            args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
 957            args.push(
 958                format!(
 959                    "--env={EXTRA_LIB_ENV_NAME}={}",
 960                    flatpak_dir.join("lib").to_str().unwrap()
 961                )
 962                .into(),
 963            );
 964            args.push(flatpak_dir.join("bin").join("zed").into());
 965
 966            let mut is_app_location_set = false;
 967            for arg in &env::args_os().collect::<Vec<_>>()[1..] {
 968                args.push(arg.clone());
 969                is_app_location_set |= arg == "--zed";
 970            }
 971
 972            if !is_app_location_set {
 973                args.push("--zed".into());
 974                args.push(flatpak_dir.join("libexec").join("zed-editor").into());
 975            }
 976
 977            let error = exec::execvp("/usr/bin/flatpak-spawn", args);
 978            eprintln!("failed restart cli on host: {:?}", error);
 979            process::exit(1);
 980        }
 981    }
 982
 983    pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
 984        if env::var(NO_ESCAPE_ENV_NAME).is_ok()
 985            && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("dev.zed.Zed"))
 986            && args.zed.is_none()
 987        {
 988            args.zed = Some("/app/libexec/zed-editor".into());
 989            unsafe { env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") };
 990        }
 991        args
 992    }
 993
 994    fn get_flatpak_dir() -> Option<PathBuf> {
 995        if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
 996            return None;
 997        }
 998
 999        if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
1000            if !flatpak_id.starts_with("dev.zed.Zed") {
1001                return None;
1002            }
1003
1004            let install_dir = Command::new("/usr/bin/flatpak-spawn")
1005                .arg("--host")
1006                .arg("flatpak")
1007                .arg("info")
1008                .arg("--show-location")
1009                .arg(flatpak_id)
1010                .output()
1011                .unwrap();
1012            let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
1013            Some(install_dir.join("files"))
1014        } else {
1015            None
1016        }
1017    }
1018
1019    fn get_xdg_env_args() -> Vec<OsString> {
1020        let xdg_keys = [
1021            "XDG_DATA_HOME",
1022            "XDG_CONFIG_HOME",
1023            "XDG_CACHE_HOME",
1024            "XDG_STATE_HOME",
1025        ];
1026        env::vars()
1027            .filter(|(key, _)| xdg_keys.contains(&key.as_str()))
1028            .map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
1029            .collect()
1030    }
1031}
1032
1033#[cfg(target_os = "windows")]
1034mod windows {
1035    use anyhow::Context;
1036    use release_channel::app_identifier;
1037    use windows::{
1038        Win32::{
1039            Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_WRITE, GetLastError},
1040            Storage::FileSystem::{
1041                CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
1042            },
1043            System::Threading::CreateMutexW,
1044        },
1045        core::HSTRING,
1046    };
1047
1048    use crate::{Detect, InstalledApp};
1049    use std::io;
1050    use std::path::{Path, PathBuf};
1051    use std::process::ExitStatus;
1052
1053    fn check_single_instance() -> bool {
1054        let mutex = unsafe {
1055            CreateMutexW(
1056                None,
1057                false,
1058                &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())),
1059            )
1060            .expect("Unable to create instance sync event")
1061        };
1062        let last_err = unsafe { GetLastError() };
1063        let _ = unsafe { CloseHandle(mutex) };
1064        last_err != ERROR_ALREADY_EXISTS
1065    }
1066
1067    struct App(PathBuf);
1068
1069    impl InstalledApp for App {
1070        fn zed_version_string(&self) -> String {
1071            format!(
1072                "Zed {}{}{}{}",
1073                if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
1074                    "".to_string()
1075                } else {
1076                    format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
1077                },
1078                option_env!("RELEASE_VERSION").unwrap_or_default(),
1079                match option_env!("ZED_COMMIT_SHA") {
1080                    Some(commit_sha) => format!(" {commit_sha} "),
1081                    None => "".to_string(),
1082                },
1083                self.0.display(),
1084            )
1085        }
1086
1087        fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
1088            if check_single_instance() {
1089                let mut cmd = std::process::Command::new(self.0.clone());
1090                cmd.arg(ipc_url);
1091                if let Some(dir) = user_data_dir {
1092                    cmd.arg("--user-data-dir").arg(dir);
1093                }
1094                cmd.spawn()?;
1095            } else {
1096                unsafe {
1097                    let pipe = CreateFileW(
1098                        &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
1099                        GENERIC_WRITE.0,
1100                        FILE_SHARE_MODE::default(),
1101                        None,
1102                        OPEN_EXISTING,
1103                        FILE_FLAGS_AND_ATTRIBUTES::default(),
1104                        None,
1105                    )?;
1106                    let message = ipc_url.as_bytes();
1107                    let mut bytes_written = 0;
1108                    WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
1109                    CloseHandle(pipe)?;
1110                }
1111            }
1112            Ok(())
1113        }
1114
1115        fn run_foreground(
1116            &self,
1117            ipc_url: String,
1118            user_data_dir: Option<&str>,
1119        ) -> io::Result<ExitStatus> {
1120            let mut cmd = std::process::Command::new(self.0.clone());
1121            cmd.arg(ipc_url).arg("--foreground");
1122            if let Some(dir) = user_data_dir {
1123                cmd.arg("--user-data-dir").arg(dir);
1124            }
1125            cmd.spawn()?.wait()
1126        }
1127
1128        fn path(&self) -> PathBuf {
1129            self.0.clone()
1130        }
1131    }
1132
1133    impl Detect {
1134        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
1135            let path = if let Some(path) = path {
1136                path.to_path_buf().canonicalize()?
1137            } else {
1138                let cli = std::env::current_exe()?;
1139                let dir = cli.parent().context("no parent path for cli")?;
1140
1141                // ../Zed.exe is the standard, lib/zed is for MSYS2, ./zed.exe is for the target
1142                // directory in development builds.
1143                let possible_locations = ["../Zed.exe", "../lib/zed/zed-editor.exe", "./zed.exe"];
1144                possible_locations
1145                    .iter()
1146                    .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
1147                    .context(format!(
1148                        "could not find any of: {}",
1149                        possible_locations.join(", ")
1150                    ))?
1151            };
1152
1153            Ok(App(path))
1154        }
1155    }
1156}
1157
1158#[cfg(target_os = "macos")]
1159mod mac_os {
1160    use anyhow::{Context as _, Result};
1161    use core_foundation::{
1162        array::{CFArray, CFIndex},
1163        base::TCFType as _,
1164        string::kCFStringEncodingUTF8,
1165        url::{CFURL, CFURLCreateWithBytes},
1166    };
1167    use core_services::{LSLaunchURLSpec, LSOpenFromURLSpec, kLSLaunchDefaults};
1168    use serde::Deserialize;
1169    use std::{
1170        ffi::OsStr,
1171        fs, io,
1172        path::{Path, PathBuf},
1173        process::{Command, ExitStatus},
1174        ptr,
1175    };
1176
1177    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
1178
1179    use crate::{Detect, InstalledApp};
1180
1181    #[derive(Debug, Deserialize)]
1182    struct InfoPlist {
1183        #[serde(rename = "CFBundleShortVersionString")]
1184        bundle_short_version_string: String,
1185    }
1186
1187    enum Bundle {
1188        App {
1189            app_bundle: PathBuf,
1190            plist: InfoPlist,
1191        },
1192        LocalPath {
1193            executable: PathBuf,
1194        },
1195    }
1196
1197    fn locate_bundle() -> Result<PathBuf> {
1198        let cli_path = std::env::current_exe()?.canonicalize()?;
1199        let mut app_path = cli_path.clone();
1200        while app_path.extension() != Some(OsStr::new("app")) {
1201            anyhow::ensure!(
1202                app_path.pop(),
1203                "cannot find app bundle containing {cli_path:?}"
1204            );
1205        }
1206        Ok(app_path)
1207    }
1208
1209    impl Detect {
1210        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
1211            let bundle_path = if let Some(bundle_path) = path {
1212                bundle_path
1213                    .canonicalize()
1214                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
1215            } else {
1216                locate_bundle().context("bundle autodiscovery")?
1217            };
1218
1219            match bundle_path.extension().and_then(|ext| ext.to_str()) {
1220                Some("app") => {
1221                    let plist_path = bundle_path.join("Contents/Info.plist");
1222                    let plist =
1223                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
1224                            format!("Reading *.app bundle plist file at {plist_path:?}")
1225                        })?;
1226                    Ok(Bundle::App {
1227                        app_bundle: bundle_path,
1228                        plist,
1229                    })
1230                }
1231                _ => Ok(Bundle::LocalPath {
1232                    executable: bundle_path,
1233                }),
1234            }
1235        }
1236    }
1237
1238    impl InstalledApp for Bundle {
1239        fn zed_version_string(&self) -> String {
1240            format!("Zed {}{}", self.version(), self.path().display(),)
1241        }
1242
1243        fn launch(&self, url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
1244            match self {
1245                Self::App { app_bundle, .. } => {
1246                    let app_path = app_bundle;
1247
1248                    let status = unsafe {
1249                        let app_url = CFURL::from_path(app_path, true)
1250                            .with_context(|| format!("invalid app path {app_path:?}"))?;
1251                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
1252                            ptr::null(),
1253                            url.as_ptr(),
1254                            url.len() as CFIndex,
1255                            kCFStringEncodingUTF8,
1256                            ptr::null(),
1257                        ));
1258                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
1259                        let urls_to_open =
1260                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
1261                        LSOpenFromURLSpec(
1262                            &LSLaunchURLSpec {
1263                                appURL: app_url.as_concrete_TypeRef(),
1264                                itemURLs: urls_to_open.as_concrete_TypeRef(),
1265                                passThruParams: ptr::null(),
1266                                launchFlags: kLSLaunchDefaults,
1267                                asyncRefCon: ptr::null_mut(),
1268                            },
1269                            ptr::null_mut(),
1270                        )
1271                    };
1272
1273                    anyhow::ensure!(
1274                        status == 0,
1275                        "cannot start app bundle {}",
1276                        self.zed_version_string()
1277                    );
1278                }
1279
1280                Self::LocalPath { executable, .. } => {
1281                    let executable_parent = executable
1282                        .parent()
1283                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
1284                    let subprocess_stdout_file = fs::File::create(
1285                        executable_parent.join("zed_dev.log"),
1286                    )
1287                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
1288                    let subprocess_stdin_file =
1289                        subprocess_stdout_file.try_clone().with_context(|| {
1290                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
1291                        })?;
1292                    let mut command = std::process::Command::new(executable);
1293                    command.env(FORCE_CLI_MODE_ENV_VAR_NAME, "");
1294                    if let Some(dir) = user_data_dir {
1295                        command.arg("--user-data-dir").arg(dir);
1296                    }
1297                    command
1298                        .stderr(subprocess_stdout_file)
1299                        .stdout(subprocess_stdin_file)
1300                        .arg(url);
1301
1302                    command
1303                        .spawn()
1304                        .with_context(|| format!("Spawning {command:?}"))?;
1305                }
1306            }
1307
1308            Ok(())
1309        }
1310
1311        fn run_foreground(
1312            &self,
1313            ipc_url: String,
1314            user_data_dir: Option<&str>,
1315        ) -> io::Result<ExitStatus> {
1316            let path = match self {
1317                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
1318                Bundle::LocalPath { executable, .. } => executable.clone(),
1319            };
1320
1321            let mut cmd = std::process::Command::new(path);
1322            cmd.arg(ipc_url);
1323            if let Some(dir) = user_data_dir {
1324                cmd.arg("--user-data-dir").arg(dir);
1325            }
1326            cmd.status()
1327        }
1328
1329        fn path(&self) -> PathBuf {
1330            match self {
1331                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
1332                Bundle::LocalPath { executable, .. } => executable.clone(),
1333            }
1334        }
1335    }
1336
1337    impl Bundle {
1338        fn version(&self) -> String {
1339            match self {
1340                Self::App { plist, .. } => plist.bundle_short_version_string.clone(),
1341                Self::LocalPath { .. } => "<development>".to_string(),
1342            }
1343        }
1344
1345        fn path(&self) -> &Path {
1346            match self {
1347                Self::App { app_bundle, .. } => app_bundle,
1348                Self::LocalPath { executable, .. } => executable,
1349            }
1350        }
1351    }
1352
1353    pub(super) fn spawn_channel_cli(
1354        channel: release_channel::ReleaseChannel,
1355        leftover_args: Vec<String>,
1356    ) -> Result<()> {
1357        use anyhow::bail;
1358
1359        let app_path_prompt = format!(
1360            "POSIX path of (path to application \"{}\")",
1361            channel.display_name()
1362        );
1363        let app_path_output = Command::new("osascript")
1364            .arg("-e")
1365            .arg(&app_path_prompt)
1366            .output()?;
1367        if !app_path_output.status.success() {
1368            bail!(
1369                "Could not determine app path for {}",
1370                channel.display_name()
1371            );
1372        }
1373        let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
1374        let cli_path = format!("{app_path}/Contents/MacOS/cli");
1375        Command::new(cli_path).args(leftover_args).spawn()?;
1376        Ok(())
1377    }
1378}