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