Allow CLI to start Zed from local sources
Zed now is able to behave as if it's being started from CLI (`ZED_FORCE_CLI_MODE` env var) Zed CLI accepts regular binary file path into `-b` parameter (only *.app before), and tries to start it as Zed editor with `ZED_FORCE_CLI_MODE` env var and other params needed.
This commit is contained in:
parent
421db9225a
commit
903eed964a
3 changed files with 211 additions and 75 deletions
|
@ -20,3 +20,7 @@ pub enum CliResponse {
|
||||||
Stderr { message: String },
|
Stderr { message: String },
|
||||||
Exit { status: i32 },
|
Exit { status: i32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When Zed started not as an *.app but as a binary (e.g. local development),
|
||||||
|
/// there's a possibility to tell it to behave "regularly".
|
||||||
|
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{CliRequest, CliResponse, IpcHandshake};
|
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
|
||||||
use core_foundation::{
|
use core_foundation::{
|
||||||
array::{CFArray, CFIndex},
|
array::{CFArray, CFIndex},
|
||||||
string::kCFStringEncodingUTF8,
|
string::kCFStringEncodingUTF8,
|
||||||
|
@ -43,20 +43,10 @@ struct InfoPlist {
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let bundle_path = if let Some(bundle_path) = args.bundle_path {
|
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
|
||||||
bundle_path.canonicalize()?
|
|
||||||
} else {
|
|
||||||
locate_bundle()?
|
|
||||||
};
|
|
||||||
|
|
||||||
if args.version {
|
if args.version {
|
||||||
let plist_path = bundle_path.join("Contents/Info.plist");
|
println!("{}", bundle.zed_version_string());
|
||||||
let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
|
|
||||||
println!(
|
|
||||||
"Zed {} – {}",
|
|
||||||
plist.bundle_short_version_string,
|
|
||||||
bundle_path.to_string_lossy()
|
|
||||||
);
|
|
||||||
return Ok(());
|
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 {
|
tx.send(CliRequest::Open {
|
||||||
paths: args
|
paths: args
|
||||||
|
@ -89,31 +79,83 @@ fn main() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn touch(path: &Path) -> io::Result<()> {
|
enum Bundle {
|
||||||
match OpenOptions::new().create(true).write(true).open(path) {
|
App {
|
||||||
Ok(_) => Ok(()),
|
app_bundle: PathBuf,
|
||||||
Err(e) => Err(e),
|
plist: InfoPlist,
|
||||||
}
|
},
|
||||||
|
LocalPath {
|
||||||
|
executable: PathBuf,
|
||||||
|
plist: InfoPlist,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn locate_bundle() -> Result<PathBuf> {
|
impl Bundle {
|
||||||
let cli_path = std::env::current_exe()?.canonicalize()?;
|
fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||||
let mut app_path = cli_path.clone();
|
let bundle_path = if let Some(bundle_path) = args_bundle_path {
|
||||||
while app_path.extension() != Some(OsStr::new("app")) {
|
bundle_path
|
||||||
if !app_path.pop() {
|
.canonicalize()
|
||||||
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
|
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
|
||||||
}
|
} else {
|
||||||
}
|
locate_bundle().context("bundle autodiscovery")?
|
||||||
Ok(app_path)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
match bundle_path.extension().and_then(|ext| ext.to_str()) {
|
||||||
let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
|
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}");
|
let url = format!("zed-cli://{server_name}");
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::App { app_bundle, .. } => {
|
||||||
|
let app_path = app_bundle;
|
||||||
|
|
||||||
let status = unsafe {
|
let status = unsafe {
|
||||||
let app_url =
|
let app_url = CFURL::from_path(app_path, true)
|
||||||
CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
|
.with_context(|| format!("invalid app path {app_path:?}"))?;
|
||||||
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
|
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
|
||||||
ptr::null(),
|
ptr::null(),
|
||||||
url.as_ptr(),
|
url.as_ptr(),
|
||||||
|
@ -134,10 +176,65 @@ fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<C
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
if status == 0 {
|
anyhow::ensure!(
|
||||||
let (_, handshake) = server.accept()?;
|
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))
|
Ok((handshake.requests, handshake.responses))
|
||||||
} else {
|
}
|
||||||
Err(anyhow!("cannot start {:?}", app_path))
|
|
||||||
|
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(()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn locate_bundle() -> Result<PathBuf> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use assets::Assets;
|
||||||
use backtrace::Backtrace;
|
use backtrace::Backtrace;
|
||||||
use cli::{
|
use cli::{
|
||||||
ipc::{self, IpcSender},
|
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 client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
|
@ -37,7 +37,10 @@ use std::{
|
||||||
os::unix::prelude::OsStrExt,
|
os::unix::prelude::OsStrExt,
|
||||||
panic,
|
panic,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, Weak},
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc, Weak,
|
||||||
|
},
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
@ -89,29 +92,17 @@ fn main() {
|
||||||
};
|
};
|
||||||
|
|
||||||
let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded();
|
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, 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, _| {
|
app.on_open_urls(move |urls, _| {
|
||||||
if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
|
callback_urls_callback_triggered.store(true, Ordering::Release);
|
||||||
if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
|
open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx);
|
||||||
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();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.on_reopen(move |cx| {
|
.on_reopen(move |cx| {
|
||||||
if cx.has_global::<Weak<AppState>>() {
|
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);
|
workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
} else {
|
} 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() {
|
if let Ok(Some(connection)) = cli_connections_rx.try_next() {
|
||||||
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
||||||
.detach();
|
.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) {
|
async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
|
||||||
if let Some(location) = workspace::last_opened_workspace_paths().await {
|
if let Some(location) = workspace::last_opened_workspace_paths().await {
|
||||||
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))
|
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 {
|
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> {
|
fn collect_path_args() -> Vec<PathBuf> {
|
||||||
|
@ -527,7 +558,11 @@ fn collect_path_args() -> Vec<PathBuf> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_url_args() -> Vec<String> {
|
||||||
|
env::args().skip(1).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_embedded_fonts(app: &App) {
|
fn load_embedded_fonts(app: &App) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue