#![cfg_attr( any(target_os = "linux", target_os = "freebsd", target_os = "windows"), allow(dead_code))] use anyhow::{Context as _, Result};use clap::Parser;use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};use collections::HashMap;use parking_lot::Mutex;use std::{ env, fs, io, path::{Path, PathBuf}, process::ExitStatus, sync::Arc, thread::{self, JoinHandle},};use tempfile::NamedTempFile;use util::paths::PathWithPosition; #[cfg(any(target_os = "linux", target_os = "freebsd"))]use std::io::IsTerminal; struct Detect; trait InstalledApp { fn zed_version_string(&self) -> String; fn launch(&self, ipc_url: String) -> anyhow::Result<()>; fn run_foreground( &self, ipc_url: String, user_data_dir: Option<&str>, ) -> io::Result<ExitStatus>; fn path(&self) -> PathBuf;} #[derive(Parser, Debug)]#[command( name = "zed", disable_version_flag = true, before_help = "The Zed CLI binary.This CLI is a separate binary that invokes Zed. Examples: `zed` Simply opens Zed `zed --foreground` Runs in foreground (shows all logs) `zed path-to-your-project` Open your project in Zed `zed -n path-to-file ` Open file/folder in a new window", after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'")]struct Args { /// Wait for all of the given paths to be opened/closed before exiting. #[arg(short, long)] wait: bool, /// Add files to the currently open workspace #[arg(short, long, overrides_with = "new")] add: bool, /// Create a new workspace #[arg(short, long, overrides_with = "add")] new: bool, /// Sets a custom directory for all user data (e.g., database, extensions, logs). /// This overrides the default platform-specific data directory location. /// On macOS, the default is `~/Library/Application Support/Zed`. /// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`. /// On Windows, the default is `%LOCALAPPDATA%\Zed`. #[arg(long, value_name = "DIR")] user_data_dir: Option<String>, /// The paths to open in Zed (space-separated). /// /// Use `path:line:column` syntax to open a file at the given line and column. paths_with_position: Vec<String>, /// Print Zed's version and the app path. #[arg(short, long)] version: bool, /// Run zed in the foreground (useful for debugging) #[arg(long)] foreground: bool, /// Custom path to Zed.app or the zed binary #[arg(long)] zed: Option<PathBuf>, /// Run zed in dev-server mode #[arg(long)] dev_server_token: Option<String>, /// Not supported in Zed CLI, only supported on Zed binary /// Will attempt to give the correct command to run #[arg(long)] system_specs: bool, /// Pairs of file paths to diff. Can be specified multiple times. #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] diff: Vec<String>, /// Uninstall Zed from user system #[cfg(all( any(target_os = "linux", target_os = "macos"), not(feature = "no-bundled-uninstall") ))] #[arg(long)] uninstall: bool,} fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> { let canonicalized = match Path::new(argument_str).canonicalize() { Ok(existing_path) => PathWithPosition::from_path(existing_path), Err(_) => { let path = PathWithPosition::parse_str(argument_str); let curdir = env::current_dir().context("retrieving current directory")?; path.map_path(|path| match fs::canonicalize(&path) { Ok(path) => Ok(path), Err(e) => { if let Some(mut parent) = path.parent() { if parent == Path::new("") { parent = &curdir } match fs::canonicalize(parent) { Ok(parent) => Ok(parent.join(path.file_name().unwrap())), Err(_) => Err(e), } } else { Err(e) } } }) } .with_context(|| format!("parsing as path with position {argument_str}"))?, }; Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))} fn main() -> Result<()> { #[cfg(unix)] util::prevent_root_execution(); // Exit flatpak sandbox if needed #[cfg(target_os = "linux")] { flatpak::try_restart_to_host(); flatpak::ld_extra_libs(); } // Intercept version designators #[cfg(target_os = "macos")] if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) { // 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. use std::str::FromStr as _; if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) { return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect()); } } let args = Args::parse(); // Set custom data directory before any path operations let user_data_dir = args.user_data_dir.clone(); if let Some(dir) = &user_data_dir { paths::set_custom_data_dir(dir); } #[cfg(target_os = "linux")] let args = flatpak::set_bin_if_no_escape(args); let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?; if args.version { println!("{}", app.zed_version_string()); return Ok(()); } if args.system_specs { let path = app.path(); let msg = [ "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.", "To retrieve the system specs on the command line, run the following command:", &format!("{} --system-specs", path.display()), ]; anyhow::bail!(msg.join("\n")); } #[cfg(all( any(target_os = "linux", target_os = "macos"), not(feature = "no-bundled-uninstall") ))] if args.uninstall { static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh"); let tmp_dir = tempfile::tempdir()?; let script_path = tmp_dir.path().join("uninstall.sh"); fs::write(&script_path, UNINSTALL_SCRIPT)?; use std::os::unix::fs::PermissionsExt as _; fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?; let status = std::process::Command::new("sh") .arg(&script_path) .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME) .status() .context("Failed to execute uninstall script")?; std::process::exit(status.code().unwrap_or(1)); } let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?; let url = format!("zed-cli://{server_name}"); let open_new_workspace = if args.new { Some(true) } else if args.add { Some(false) } else { None }; let env = { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { // On Linux, the desktop entry uses `cli` to spawn `zed`. // We need to handle env vars correctly since std::env::vars() may not contain // project-specific vars (e.g. those set by direnv). // By setting env to None here, the LSP will use worktree env vars instead, // which is what we want. if !std::io::stdout().is_terminal() { None } else { Some(std::env::vars().collect::<HashMap<_, _>>()) } } #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] Some(std::env::vars().collect::<HashMap<_, _>>()) }; let exit_status = Arc::new(Mutex::new(None)); let mut paths = vec![]; let mut urls = vec![]; let mut diff_paths = vec![]; let mut stdin_tmp_file: Option<fs::File> = None; let mut anonymous_fd_tmp_files = vec![]; for path in args.diff.chunks(2) { diff_paths.push([ parse_path_with_position(&path[0])?, parse_path_with_position(&path[1])?, ]); } for path in args.paths_with_position.iter() { if path.starts_with("zed://") || path.starts_with("http://") || path.starts_with("https://") || path.starts_with("file://") || path.starts_with("ssh://") { urls.push(path.to_string()); } else if path == "-" && args.paths_with_position.len() == 1 { let file = NamedTempFile::new()?; paths.push(file.path().to_string_lossy().to_string()); let (file, _) = file.keep()?; stdin_tmp_file = Some(file); } else if let Some(file) = anonymous_fd(path) { let tmp_file = NamedTempFile::new()?; paths.push(tmp_file.path().to_string_lossy().to_string()); let (tmp_file, _) = tmp_file.keep()?; anonymous_fd_tmp_files.push((file, tmp_file)); } else { paths.push(parse_path_with_position(path)?) } } anyhow::ensure!( args.dev_server_token.is_none(), "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development" ); let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({ let exit_status = exit_status.clone(); let user_data_dir_for_thread = user_data_dir.clone(); move || { let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; let (tx, rx) = (handshake.requests, handshake.responses); tx.send(CliRequest::Open { paths, urls, diff_paths, wait: args.wait, open_new_workspace, env, user_data_dir: user_data_dir_for_thread, })?; while let Ok(response) = rx.recv() { match response { CliResponse::Ping => {} CliResponse::Stdout { message } => println!("{message}"), CliResponse::Stderr { message } => eprintln!("{message}"), CliResponse::Exit { status } => { exit_status.lock().replace(status); return Ok(()); } } } Ok(()) } }); let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> = stdin_tmp_file.map(|tmp_file| { thread::spawn(move || { let stdin = std::io::stdin().lock(); if io::IsTerminal::is_terminal(&stdin) { return Ok(()); } return pipe_to_tmp(stdin, tmp_file); }) }); let anonymous_fd_pipe_handles: Vec<JoinHandle<anyhow::Result<()>>> = anonymous_fd_tmp_files .into_iter() .map(|(file, tmp_file)| thread::spawn(move || pipe_to_tmp(file, tmp_file))) .collect(); if args.foreground { app.run_foreground(url, user_data_dir.as_deref())?; } else { app.launch(url)?; sender.join().unwrap()?; if let Some(handle) = stdin_pipe_handle { handle.join().unwrap()?; } for handle in anonymous_fd_pipe_handles { handle.join().unwrap()?; } } if let Some(exit_status) = exit_status.lock().take() { std::process::exit(exit_status); } Ok(())} fn pipe_to_tmp(mut src: impl io::Read, mut dest: fs::File) -> Result<()> { let mut buffer = [0; 8 * 1024]; loop { let bytes_read = match src.read(&mut buffer) { Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, res => res?, }; if bytes_read == 0 { break; } io::Write::write_all(&mut dest, &buffer[..bytes_read])?; } io::Write::flush(&mut dest)?; Ok(())} fn anonymous_fd(path: &str) -> Option<fs::File> { #[cfg(target_os = "linux")] { use std::os::fd::{self, FromRawFd}; let fd_str = path.strip_prefix("/proc/self/fd/")?; let link = fs::read_link(path).ok()?; if !link.starts_with("memfd:") { return None; } let fd: fd::RawFd = fd_str.parse().ok()?; let file = unsafe { fs::File::from_raw_fd(fd) }; return Some(file); } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { use std::os::{ fd::{self, FromRawFd}, unix::fs::FileTypeExt, }; let fd_str = path.strip_prefix("/dev/fd/")?; let metadata = fs::metadata(path).ok()?; let file_type = metadata.file_type(); if !file_type.is_fifo() && !file_type.is_socket() { return None; } let fd: fd::RawFd = fd_str.parse().ok()?; let file = unsafe { fs::File::from_raw_fd(fd) }; return Some(file); } #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))] { _ = path; // not implemented for bsd, windows. Could be, but isn't yet return None; }} #[cfg(any(target_os = "linux", target_os = "freebsd"))]mod linux { use std::{ env, ffi::OsString, io, os::unix::net::{SocketAddr, UnixDatagram}, path::{Path, PathBuf}, process::{self, ExitStatus}, sync::LazyLock, thread, time::Duration, }; use anyhow::{Context as _, anyhow}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use fork::Fork; use crate::{Detect, InstalledApp}; static RELEASE_CHANNEL: LazyLock<String> = LazyLock::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()); struct App(PathBuf); impl Detect { pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> { let path = if let Some(path) = path { path.to_path_buf().canonicalize()? } else { let cli = env::current_exe()?; let dir = cli.parent().context("no parent path for cli")?; // libexec is the standard, lib/zed is for Arch (and other non-libexec distros), // ./zed is for the target directory in development builds. let possible_locations = ["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"]; possible_locations .iter() .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli)) .with_context(|| { format!("could not find any of: {}", possible_locations.join(", ")) })? }; Ok(App(path)) } } impl InstalledApp for App { fn zed_version_string(&self) -> String { format!( "Zed {}{}{} – {}", if *RELEASE_CHANNEL == "stable" { "".to_string() } else { format!("{} ", *RELEASE_CHANNEL) }, option_env!("RELEASE_VERSION").unwrap_or_default(), match option_env!("ZED_COMMIT_SHA") { Some(commit_sha) => format!(" {commit_sha} "), None => "".to_string(), }, self.0.display(), ) } fn launch(&self, ipc_url: String) -> anyhow::Result<()> { let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL)); let sock = UnixDatagram::unbound()?; if sock.connect(&sock_path).is_err() { self.boot_background(ipc_url)?; } else { sock.send(ipc_url.as_bytes())?; } Ok(()) } fn run_foreground( &self, ipc_url: String, user_data_dir: Option<&str>, ) -> io::Result<ExitStatus> { let mut cmd = std::process::Command::new(self.0.clone()); cmd.arg(ipc_url); if let Some(dir) = user_data_dir { cmd.arg("--user-data-dir").arg(dir); } cmd.status() } fn path(&self) -> PathBuf { self.0.clone() } } impl App { fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {Lines 1–500 of 992. Load more
#![cfg_attr( any(target_os = "linux", target_os = "freebsd", target_os = "windows"), allow(dead_code) )] use anyhow::{Context as _, Result}; use clap::Parser; use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer}; use collections::HashMap; use parking_lot::Mutex; use std::{ env, fs, io, path::{Path, PathBuf}, process::ExitStatus, sync::Arc, thread::{self, JoinHandle}, }; use tempfile::NamedTempFile; use util::paths::PathWithPosition; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use std::io::IsTerminal; struct Detect; trait InstalledApp { fn zed_version_string(&self) -> String; fn launch(&self, ipc_url: String) -> anyhow::Result<()>; fn run_foreground( &self, ipc_url: String, user_data_dir: Option<&str>, ) -> io::Result<ExitStatus>; fn path(&self) -> PathBuf; } #[derive(Parser, Debug)] #[command( name = "zed", disable_version_flag = true, before_help = "The Zed CLI binary. This CLI is a separate binary that invokes Zed. Examples: `zed` Simply opens Zed `zed --foreground` Runs in foreground (shows all logs) `zed path-to-your-project` Open your project in Zed `zed -n path-to-file ` Open file/folder in a new window", after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'" )] struct Args { /// Wait for all of the given paths to be opened/closed before exiting. #[arg(short, long)] wait: bool, /// Add files to the currently open workspace #[arg(short, long, overrides_with = "new")] add: bool, /// Create a new workspace #[arg(short, long, overrides_with = "add")] new: bool, /// Sets a custom directory for all user data (e.g., database, extensions, logs). /// This overrides the default platform-specific data directory location. /// On macOS, the default is `~/Library/Application Support/Zed`. /// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`. /// On Windows, the default is `%LOCALAPPDATA%\Zed`. #[arg(long, value_name = "DIR")] user_data_dir: Option<String>, /// The paths to open in Zed (space-separated). /// /// Use `path:line:column` syntax to open a file at the given line and column. paths_with_position: Vec<String>, /// Print Zed's version and the app path. #[arg(short, long)] version: bool, /// Run zed in the foreground (useful for debugging) #[arg(long)] foreground: bool, /// Custom path to Zed.app or the zed binary #[arg(long)] zed: Option<PathBuf>, /// Run zed in dev-server mode #[arg(long)] dev_server_token: Option<String>, /// Not supported in Zed CLI, only supported on Zed binary /// Will attempt to give the correct command to run #[arg(long)] system_specs: bool, /// Pairs of file paths to diff. Can be specified multiple times. #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] diff: Vec<String>, /// Uninstall Zed from user system #[cfg(all( any(target_os = "linux", target_os = "macos"), not(feature = "no-bundled-uninstall") ))] #[arg(long)] uninstall: bool, } fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> { let canonicalized = match Path::new(argument_str).canonicalize() { Ok(existing_path) => PathWithPosition::from_path(existing_path), Err(_) => { let path = PathWithPosition::parse_str(argument_str); let curdir = env::current_dir().context("retrieving current directory")?; path.map_path(|path| match fs::canonicalize(&path) { Ok(path) => Ok(path), Err(e) => { if let Some(mut parent) = path.parent() { if parent == Path::new("") { parent = &curdir } match fs::canonicalize(parent) { Ok(parent) => Ok(parent.join(path.file_name().unwrap())), Err(_) => Err(e), } } else { Err(e) } } }) } .with_context(|| format!("parsing as path with position {argument_str}"))?, }; Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string())) } fn main() -> Result<()> { #[cfg(unix)] util::prevent_root_execution(); // Exit flatpak sandbox if needed #[cfg(target_os = "linux")] { flatpak::try_restart_to_host(); flatpak::ld_extra_libs(); } // Intercept version designators #[cfg(target_os = "macos")] if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) { // 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. use std::str::FromStr as _; if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) { return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect()); } } let args = Args::parse(); // Set custom data directory before any path operations let user_data_dir = args.user_data_dir.clone(); if let Some(dir) = &user_data_dir { paths::set_custom_data_dir(dir); } #[cfg(target_os = "linux")] let args = flatpak::set_bin_if_no_escape(args); let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?; if args.version { println!("{}", app.zed_version_string()); return Ok(()); } if args.system_specs { let path = app.path(); let msg = [ "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.", "To retrieve the system specs on the command line, run the following command:", &format!("{} --system-specs", path.display()), ]; anyhow::bail!(msg.join("\n")); } #[cfg(all( any(target_os = "linux", target_os = "macos"), not(feature = "no-bundled-uninstall") ))] if args.uninstall { static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh"); let tmp_dir = tempfile::tempdir()?; let script_path = tmp_dir.path().join("uninstall.sh"); fs::write(&script_path, UNINSTALL_SCRIPT)?; use std::os::unix::fs::PermissionsExt as _; fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?; let status = std::process::Command::new("sh") .arg(&script_path) .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME) .status() .context("Failed to execute uninstall script")?; std::process::exit(status.code().unwrap_or(1)); } let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?; let url = format!("zed-cli://{server_name}"); let open_new_workspace = if args.new { Some(true) } else if args.add { Some(false) } else { None }; let env = { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { // On Linux, the desktop entry uses `cli` to spawn `zed`. // We need to handle env vars correctly since std::env::vars() may not contain // project-specific vars (e.g. those set by direnv). // By setting env to None here, the LSP will use worktree env vars instead, // which is what we want. if !std::io::stdout().is_terminal() { None } else { Some(std::env::vars().collect::<HashMap<_, _>>()) } } #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] Some(std::env::vars().collect::<HashMap<_, _>>()) }; let exit_status = Arc::new(Mutex::new(None)); let mut paths = vec![]; let mut urls = vec![]; let mut diff_paths = vec![]; let mut stdin_tmp_file: Option<fs::File> = None; let mut anonymous_fd_tmp_files = vec![]; for path in args.diff.chunks(2) { diff_paths.push([ parse_path_with_position(&path[0])?, parse_path_with_position(&path[1])?, ]); } for path in args.paths_with_position.iter() { if path.starts_with("zed://") || path.starts_with("http://") || path.starts_with("https://") || path.starts_with("file://") || path.starts_with("ssh://") { urls.push(path.to_string()); } else if path == "-" && args.paths_with_position.len() == 1 { let file = NamedTempFile::new()?; paths.push(file.path().to_string_lossy().to_string()); let (file, _) = file.keep()?; stdin_tmp_file = Some(file); } else if let Some(file) = anonymous_fd(path) { let tmp_file = NamedTempFile::new()?; paths.push(tmp_file.path().to_string_lossy().to_string()); let (tmp_file, _) = tmp_file.keep()?; anonymous_fd_tmp_files.push((file, tmp_file)); } else { paths.push(parse_path_with_position(path)?) } } anyhow::ensure!( args.dev_server_token.is_none(), "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development" ); let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({ let exit_status = exit_status.clone(); let user_data_dir_for_thread = user_data_dir.clone(); move || { let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; let (tx, rx) = (handshake.requests, handshake.responses); tx.send(CliRequest::Open { paths, urls, diff_paths, wait: args.wait, open_new_workspace, env, user_data_dir: user_data_dir_for_thread, })?; while let Ok(response) = rx.recv() { match response { CliResponse::Ping => {} CliResponse::Stdout { message } => println!("{message}"), CliResponse::Stderr { message } => eprintln!("{message}"), CliResponse::Exit { status } => { exit_status.lock().replace(status); return Ok(()); } } } Ok(()) } }); let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> = stdin_tmp_file.map(|tmp_file| { thread::spawn(move || { let stdin = std::io::stdin().lock(); if io::IsTerminal::is_terminal(&stdin) { return Ok(()); } return pipe_to_tmp(stdin, tmp_file); }) }); let anonymous_fd_pipe_handles: Vec<JoinHandle<anyhow::Result<()>>> = anonymous_fd_tmp_files .into_iter() .map(|(file, tmp_file)| thread::spawn(move || pipe_to_tmp(file, tmp_file))) .collect(); if args.foreground { app.run_foreground(url, user_data_dir.as_deref())?; } else { app.launch(url)?; sender.join().unwrap()?; if let Some(handle) = stdin_pipe_handle { handle.join().unwrap()?; } for handle in anonymous_fd_pipe_handles { handle.join().unwrap()?; } } if let Some(exit_status) = exit_status.lock().take() { std::process::exit(exit_status); } Ok(()) } fn pipe_to_tmp(mut src: impl io::Read, mut dest: fs::File) -> Result<()> { let mut buffer = [0; 8 * 1024]; loop { let bytes_read = match src.read(&mut buffer) { Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, res => res?, }; if bytes_read == 0 { break; } io::Write::write_all(&mut dest, &buffer[..bytes_read])?; } io::Write::flush(&mut dest)?; Ok(()) } fn anonymous_fd(path: &str) -> Option<fs::File> { #[cfg(target_os = "linux")] { use std::os::fd::{self, FromRawFd}; let fd_str = path.strip_prefix("/proc/self/fd/")?; let link = fs::read_link(path).ok()?; if !link.starts_with("memfd:") { return None; } let fd: fd::RawFd = fd_str.parse().ok()?; let file = unsafe { fs::File::from_raw_fd(fd) }; return Some(file); } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { use std::os::{ fd::{self, FromRawFd}, unix::fs::FileTypeExt, }; let fd_str = path.strip_prefix("/dev/fd/")?; let metadata = fs::metadata(path).ok()?; let file_type = metadata.file_type(); if !file_type.is_fifo() && !file_type.is_socket() { return None; } let fd: fd::RawFd = fd_str.parse().ok()?; let file = unsafe { fs::File::from_raw_fd(fd) }; return Some(file); } #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))] { _ = path; // not implemented for bsd, windows. Could be, but isn't yet return None; } } #[cfg(any(target_os = "linux", target_os = "freebsd"))] mod linux { use std::{ env, ffi::OsString, io, os::unix::net::{SocketAddr, UnixDatagram}, path::{Path, PathBuf}, process::{self, ExitStatus}, sync::LazyLock, thread, time::Duration, }; use anyhow::{Context as _, anyhow}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use fork::Fork; use crate::{Detect, InstalledApp}; static RELEASE_CHANNEL: LazyLock<String> = LazyLock::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()); struct App(PathBuf); impl Detect { pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> { let path = if let Some(path) = path { path.to_path_buf().canonicalize()? } else { let cli = env::current_exe()?; let dir = cli.parent().context("no parent path for cli")?; // libexec is the standard, lib/zed is for Arch (and other non-libexec distros), // ./zed is for the target directory in development builds. let possible_locations = ["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"]; possible_locations .iter() .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli)) .with_context(|| { format!("could not find any of: {}", possible_locations.join(", ")) })? }; Ok(App(path)) } } impl InstalledApp for App { fn zed_version_string(&self) -> String { format!( "Zed {}{}{} – {}", if *RELEASE_CHANNEL == "stable" { "".to_string() } else { format!("{} ", *RELEASE_CHANNEL) }, option_env!("RELEASE_VERSION").unwrap_or_default(), match option_env!("ZED_COMMIT_SHA") { Some(commit_sha) => format!(" {commit_sha} "), None => "".to_string(), }, self.0.display(), ) } fn launch(&self, ipc_url: String) -> anyhow::Result<()> { let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL)); let sock = UnixDatagram::unbound()?; if sock.connect(&sock_path).is_err() { self.boot_background(ipc_url)?; } else { sock.send(ipc_url.as_bytes())?; } Ok(()) } fn run_foreground( &self, ipc_url: String, user_data_dir: Option<&str>, ) -> io::Result<ExitStatus> { let mut cmd = std::process::Command::new(self.0.clone()); cmd.arg(ipc_url); if let Some(dir) = user_data_dir { cmd.arg("--user-data-dir").arg(dir); } cmd.status() } fn path(&self) -> PathBuf { self.0.clone() } } impl App { fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {