#![cfg_attr(target_os = "linux", allow(dead_code))] use anyhow::{anyhow, Context, Result}; use clap::Parser; use cli::{CliRequest, CliResponse}; use serde::Deserialize; use std::{ env, ffi::OsStr, fs::{self}, path::{Path, PathBuf}, }; use util::paths::PathLikeWithPosition; #[derive(Parser, Debug)] #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))] struct Args { /// Wait for all of the given paths to be opened/closed before exiting. #[clap(short, long)] wait: bool, /// Add files to the currently open workspace #[clap(short, long, overrides_with = "new")] add: bool, /// Create a new workspace #[clap(short, long, overrides_with = "add")] new: bool, /// A sequence of space-separated paths that you want to open. /// /// Use `path:line:row` syntax to open a file at a specific location. /// Non-existing paths and directories will ignore `:line:row` suffix. #[clap(value_parser = parse_path_with_position)] paths_with_position: Vec>, /// Print Zed's version and the app path. #[clap(short, long)] version: bool, /// Custom Zed.app path #[clap(short, long)] bundle_path: Option, } fn parse_path_with_position( argument_str: &str, ) -> Result, std::convert::Infallible> { PathLikeWithPosition::parse_str(argument_str, |path_str| { Ok(Path::new(path_str).to_path_buf()) }) } #[derive(Debug, Deserialize)] struct InfoPlist { #[serde(rename = "CFBundleShortVersionString")] bundle_short_version_string: String, } fn main() -> Result<()> { let args = Args::parse(); let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?; if args.version { println!("{}", bundle.zed_version_string()); return Ok(()); } let curdir = env::current_dir()?; let mut paths = vec![]; for path in args.paths_with_position { let canonicalized = path.map_path_like(|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) } } })?; paths.push(canonicalized.to_string(|path| path.display().to_string())) } let (tx, rx) = bundle.launch()?; let open_new_workspace = if args.new { Some(true) } else if args.add { Some(false) } else { None }; tx.send(CliRequest::Open { paths, wait: args.wait, open_new_workspace, })?; while let Ok(response) = rx.recv() { match response { CliResponse::Ping => {} CliResponse::Stdout { message } => println!("{message}"), CliResponse::Stderr { message } => eprintln!("{message}"), CliResponse::Exit { status } => std::process::exit(status), } } Ok(()) } enum Bundle { App { app_bundle: PathBuf, plist: InfoPlist, }, LocalPath { executable: PathBuf, plist: InfoPlist, }, } fn locate_bundle() -> Result { let cli_path = std::env::current_exe()?.canonicalize()?; let mut app_path = cli_path.clone(); while app_path.extension() != Some(OsStr::new("app")) { if !app_path.pop() { return Err(anyhow!("cannot find app bundle containing {:?}", cli_path)); } } Ok(app_path) } #[cfg(target_os = "linux")] mod linux { use std::path::Path; use cli::{CliRequest, CliResponse}; use ipc_channel::ipc::{IpcReceiver, IpcSender}; use crate::{Bundle, InfoPlist}; impl Bundle { pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result { unimplemented!() } pub fn plist(&self) -> &InfoPlist { unimplemented!() } pub fn path(&self) -> &Path { unimplemented!() } pub fn launch(&self) -> anyhow::Result<(IpcSender, IpcReceiver)> { unimplemented!() } pub fn zed_version_string(&self) -> String { unimplemented!() } } } // todo("windows") #[cfg(target_os = "windows")] mod windows { use std::path::Path; use cli::{CliRequest, CliResponse}; use ipc_channel::ipc::{IpcReceiver, IpcSender}; use crate::{Bundle, InfoPlist}; impl Bundle { pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result { unimplemented!() } pub fn plist(&self) -> &InfoPlist { unimplemented!() } pub fn path(&self) -> &Path { unimplemented!() } pub fn launch(&self) -> anyhow::Result<(IpcSender, IpcReceiver)> { unimplemented!() } pub fn zed_version_string(&self) -> String { unimplemented!() } } } #[cfg(target_os = "macos")] mod mac_os { use anyhow::Context; use core_foundation::{ array::{CFArray, CFIndex}, string::kCFStringEncodingUTF8, url::{CFURLCreateWithBytes, CFURL}, }; use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType}; use std::{fs, path::Path, ptr}; use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME}; use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender}; use crate::{locate_bundle, Bundle, InfoPlist}; impl Bundle { pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result { let bundle_path = if let Some(bundle_path) = args_bundle_path { bundle_path .canonicalize() .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))? } else { locate_bundle().context("bundle autodiscovery")? }; match bundle_path.extension().and_then(|ext| ext.to_str()) { Some("app") => { let plist_path = bundle_path.join("Contents/Info.plist"); let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| { format!("Reading *.app bundle plist file at {plist_path:?}") })?; Ok(Self::App { app_bundle: bundle_path, plist, }) } _ => { println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build"); let plist_path = bundle_path .parent() .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))? .join("WebRTC.framework/Resources/Info.plist"); let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| { format!("Reading dev bundle plist file at {plist_path:?}") })?; Ok(Self::LocalPath { executable: bundle_path, plist, }) } } } fn plist(&self) -> &InfoPlist { match self { Self::App { plist, .. } => plist, Self::LocalPath { plist, .. } => plist, } } fn path(&self) -> &Path { match self { Self::App { app_bundle, .. } => app_bundle, Self::LocalPath { executable, .. } => executable, } } pub fn launch(&self) -> anyhow::Result<(IpcSender, IpcReceiver)> { let (server, server_name) = IpcOneShotServer::::new().context("Handshake before Zed spawn")?; let url = format!("zed-cli://{server_name}"); match self { Self::App { app_bundle, .. } => { let app_path = app_bundle; let status = unsafe { let app_url = CFURL::from_path(app_path, true) .with_context(|| format!("invalid app path {app_path:?}"))?; let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes( ptr::null(), url.as_ptr(), url.len() as CFIndex, kCFStringEncodingUTF8, ptr::null(), )); // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); LSOpenFromURLSpec( &LSLaunchURLSpec { appURL: app_url.as_concrete_TypeRef(), itemURLs: urls_to_open.as_concrete_TypeRef(), passThruParams: ptr::null(), launchFlags: kLSLaunchDefaults, asyncRefCon: ptr::null_mut(), }, ptr::null_mut(), ) }; anyhow::ensure!( status == 0, "cannot start app bundle {}", self.zed_version_string() ); } Self::LocalPath { executable, .. } => { let executable_parent = executable .parent() .with_context(|| format!("Executable {executable:?} path has no parent"))?; let subprocess_stdout_file = fs::File::create( executable_parent.join("zed_dev.log"), ) .with_context(|| format!("Log file creation in {executable_parent:?}"))?; let subprocess_stdin_file = subprocess_stdout_file.try_clone().with_context(|| { format!("Cloning descriptor for file {subprocess_stdout_file:?}") })?; let mut command = std::process::Command::new(executable); let command = command .env(FORCE_CLI_MODE_ENV_VAR_NAME, "") .stderr(subprocess_stdout_file) .stdout(subprocess_stdin_file) .arg(url); command .spawn() .with_context(|| format!("Spawning {command:?}"))?; } } let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; Ok((handshake.requests, handshake.responses)) } pub fn zed_version_string(&self) -> String { let is_dev = matches!(self, Self::LocalPath { .. }); format!( "Zed {}{} – {}", self.plist().bundle_short_version_string, if is_dev { " (dev)" } else { "" }, self.path().display(), ) } } }