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