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