@@ -1,6 +1,6 @@
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
use clap::Parser;
-use cli::{CliRequest, CliResponse, IpcHandshake};
+use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
@@ -43,20 +43,10 @@ struct InfoPlist {
fn main() -> Result<()> {
let args = Args::parse();
- let bundle_path = if let Some(bundle_path) = args.bundle_path {
- bundle_path.canonicalize()?
- } else {
- locate_bundle()?
- };
+ let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
if args.version {
- let plist_path = bundle_path.join("Contents/Info.plist");
- let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
- println!(
- "Zed {} – {}",
- plist.bundle_short_version_string,
- bundle_path.to_string_lossy()
- );
+ println!("{}", bundle.zed_version_string());
return Ok(());
}
@@ -66,7 +56,7 @@ fn main() -> Result<()> {
}
}
- let (tx, rx) = launch_app(bundle_path)?;
+ let (tx, rx) = bundle.launch()?;
tx.send(CliRequest::Open {
paths: args
@@ -89,6 +79,148 @@ fn main() -> Result<()> {
Ok(())
}
+enum Bundle {
+ App {
+ app_bundle: PathBuf,
+ plist: InfoPlist,
+ },
+ LocalPath {
+ executable: PathBuf,
+ plist: InfoPlist,
+ },
+}
+
+impl Bundle {
+ fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
+ 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: excutable,
+ ..
+ } => excutable,
+ }
+ }
+
+ fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
+ let (server, server_name) =
+ IpcOneShotServer::<IpcHandshake>::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(),
+ ));
+ 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))
+ }
+
+ 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(),
+ )
+ }
+}
+
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
@@ -106,38 +238,3 @@ fn locate_bundle() -> Result<PathBuf> {
}
Ok(app_path)
}
-
-fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
- let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
- let url = format!("zed-cli://{server_name}");
-
- let status = unsafe {
- let app_url =
- CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
- let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
- ptr::null(),
- url.as_ptr(),
- url.len() as CFIndex,
- kCFStringEncodingUTF8,
- ptr::null(),
- ));
- 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(),
- )
- };
-
- if status == 0 {
- let (_, handshake) = server.accept()?;
- Ok((handshake.requests, handshake.responses))
- } else {
- Err(anyhow!("cannot start {:?}", app_path))
- }
-}
@@ -6,7 +6,7 @@ use assets::Assets;
use backtrace::Backtrace;
use cli::{
ipc::{self, IpcSender},
- CliRequest, CliResponse, IpcHandshake,
+ CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
};
use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE;
@@ -37,7 +37,10 @@ use std::{
os::unix::prelude::OsStrExt,
panic,
path::PathBuf,
- sync::{Arc, Weak},
+ sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc, Weak,
+ },
thread,
time::Duration,
};
@@ -89,29 +92,17 @@ fn main() {
};
let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded();
+ let cli_connections_tx = Arc::new(cli_connections_tx);
let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded();
+ let open_paths_tx = Arc::new(open_paths_tx);
+ let urls_callback_triggered = Arc::new(AtomicBool::new(false));
+
+ let callback_cli_connections_tx = Arc::clone(&cli_connections_tx);
+ let callback_open_paths_tx = Arc::clone(&open_paths_tx);
+ let callback_urls_callback_triggered = Arc::clone(&urls_callback_triggered);
app.on_open_urls(move |urls, _| {
- if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
- if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
- cli_connections_tx
- .unbounded_send(cli_connection)
- .map_err(|_| anyhow!("no listener for cli connections"))
- .log_err();
- };
- } else {
- let paths: Vec<_> = urls
- .iter()
- .flat_map(|url| url.strip_prefix("file://"))
- .map(|url| {
- let decoded = urlencoding::decode_binary(url.as_bytes());
- PathBuf::from(OsStr::from_bytes(decoded.as_ref()))
- })
- .collect();
- open_paths_tx
- .unbounded_send(paths)
- .map_err(|_| anyhow!("no listener for open urls requests"))
- .log_err();
- }
+ callback_urls_callback_triggered.store(true, Ordering::Release);
+ open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx);
})
.on_reopen(move |cx| {
if cx.has_global::<Weak<AppState>>() {
@@ -234,6 +225,14 @@ fn main() {
workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx);
}
} else {
+ // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead
+ // of an *app, hence gets no specific callbacks run. Emulate them here, if needed.
+ if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
+ && !urls_callback_triggered.load(Ordering::Acquire)
+ {
+ open_urls(collect_url_args(), &cli_connections_tx, &open_paths_tx)
+ }
+
if let Ok(Some(connection)) = cli_connections_rx.try_next() {
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
.detach();
@@ -284,6 +283,37 @@ fn main() {
});
}
+fn open_urls(
+ urls: Vec<String>,
+ cli_connections_tx: &mpsc::UnboundedSender<(
+ mpsc::Receiver<CliRequest>,
+ IpcSender<CliResponse>,
+ )>,
+ open_paths_tx: &mpsc::UnboundedSender<Vec<PathBuf>>,
+) {
+ if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
+ if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
+ cli_connections_tx
+ .unbounded_send(cli_connection)
+ .map_err(|_| anyhow!("no listener for cli connections"))
+ .log_err();
+ };
+ } else {
+ let paths: Vec<_> = urls
+ .iter()
+ .flat_map(|url| url.strip_prefix("file://"))
+ .map(|url| {
+ let decoded = urlencoding::decode_binary(url.as_bytes());
+ PathBuf::from(OsStr::from_bytes(decoded.as_ref()))
+ })
+ .collect();
+ open_paths_tx
+ .unbounded_send(paths)
+ .map_err(|_| anyhow!("no listener for open urls requests"))
+ .log_err();
+ }
+}
+
async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
if let Some(location) = workspace::last_opened_workspace_paths().await {
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))
@@ -514,7 +544,8 @@ async fn load_login_shell_environment() -> Result<()> {
}
fn stdout_is_a_pty() -> bool {
- unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 }
+ std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none()
+ && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 }
}
fn collect_path_args() -> Vec<PathBuf> {
@@ -527,7 +558,11 @@ fn collect_path_args() -> Vec<PathBuf> {
None
}
})
- .collect::<Vec<_>>()
+ .collect()
+}
+
+fn collect_url_args() -> Vec<String> {
+ env::args().skip(1).collect()
}
fn load_embedded_fonts(app: &App) {