дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(app): install the openidea command on macOS, Windows and Linux (#2009)
Adds Install/Uninstall 'openidea' Command actions. macOS uses the bundle launcher, Windows a user PATH wrapper and named pipe, and Linux the executable/AppImage path with a PATH hint. Files, folders, positions and `diff` go to the running app or start it. Closes #1286 Co-authored-by: jaw52 <[email protected]>
Цей коміт міститься в:
1 джерело
25424465c0
коміт
0bfd4fa3cc
28 files changed
+1601
-42
No files matched your search
@@ -0,0 +1,169 @@
|
||||
//! The `openidea` command: this executable in CLI mode (started by the bundle's
|
||||
//! launcher script with [`CLI_FLAG`]), the socket the app answers it on, and
|
||||
//! installing the command on `PATH`.
|
||||
|
||||
use ide_cli::args::{self, Command};
|
||||
use ide_cli::{Request, Response, socket};
|
||||
use ide_core::{IdeError, IdeResult, IpcPath};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// First argument from `Contents/Resources/bin/openidea`.
|
||||
const CLI_FLAG: &str = "--cli";
|
||||
/// How long a starting app has to open its socket.
|
||||
const START_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Runs as the `openidea` command when started with [`CLI_FLAG`]; returns the exit code.
|
||||
pub fn run(identifier: &str) -> Option<i32> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
if args.next().as_deref() != Some(CLI_FLAG) {
|
||||
return None;
|
||||
}
|
||||
let args: Vec<String> = args.collect();
|
||||
#[cfg(windows)]
|
||||
ide_cli::attach_console();
|
||||
Some(match client(identifier, &args) {
|
||||
Ok(()) => 0,
|
||||
Err(message) => {
|
||||
eprintln!("openidea: {message}");
|
||||
1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn client(identifier: &str, args: &[String]) -> Result<(), String> {
|
||||
let cwd = std::env::current_dir().map_err(|e| format!("no working directory: {e}"))?;
|
||||
let command = args::parse(args, &cwd)?;
|
||||
let diff = matches!(command, Command::Diff(_));
|
||||
let targets = match command {
|
||||
Command::Help => {
|
||||
print!("{}", args::USAGE);
|
||||
return Ok(());
|
||||
}
|
||||
Command::Version => {
|
||||
println!("OpenIDEA {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
Command::Open(targets) | Command::Diff(targets) => targets,
|
||||
};
|
||||
let socket = socket::socket_path(identifier).ok_or("no cache directory for the OpenIDEA socket")?;
|
||||
let request = Request { diff, ..Request::open(targets) };
|
||||
let response = match socket::send(&socket, &request) {
|
||||
Err(e) if matches!(e.kind(), ErrorKind::NotFound | ErrorKind::ConnectionRefused) => {
|
||||
start_app()?;
|
||||
send_when_started(&socket, &request)?
|
||||
}
|
||||
sent => sent.map_err(|e| format!("cannot reach OpenIDEA: {e}"))?,
|
||||
};
|
||||
match response {
|
||||
Response::Ok => Ok(()),
|
||||
Response::Error { message } => Err(message),
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the app detached from this terminal: through Launch Services for a
|
||||
/// bundle, else (a development build) this executable without [`CLI_FLAG`].
|
||||
fn start_app() -> Result<(), String> {
|
||||
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
|
||||
let image = if cfg!(target_os = "linux") { std::env::var_os("APPIMAGE").map(PathBuf::from) } else { None };
|
||||
let exe = ide_cli::install::stable_executable(&exe, image.as_deref()).map_err(|e| e.to_string())?;
|
||||
let mut command = match app_bundle(&exe) {
|
||||
Some(bundle) => {
|
||||
let mut open = std::process::Command::new("/usr/bin/open");
|
||||
open.arg("-a").arg(bundle);
|
||||
open
|
||||
}
|
||||
None => std::process::Command::new(&exe),
|
||||
};
|
||||
command.stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());
|
||||
ide_core::process::host_env(&mut command);
|
||||
#[cfg(unix)]
|
||||
std::os::unix::process::CommandExt::process_group(&mut command, 0);
|
||||
#[cfg(windows)]
|
||||
std::os::windows::process::CommandExt::creation_flags(&mut command, 0x0000_0008 | 0x0000_0200);
|
||||
command.spawn().map(drop).map_err(|e| format!("cannot start OpenIDEA: {e}"))
|
||||
}
|
||||
|
||||
fn send_when_started(socket: &Path, request: &Request) -> Result<Response, String> {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
match socket::send(socket, request) {
|
||||
Err(e) if matches!(e.kind(), ErrorKind::NotFound | ErrorKind::ConnectionRefused) => {
|
||||
if start.elapsed() > START_TIMEOUT {
|
||||
return Err("OpenIDEA did not start in time".into());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
sent => return sent.map_err(|e| format!("cannot reach OpenIDEA: {e}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `….app` of the bundle executable `exe` (`….app/Contents/MacOS/openidea`).
|
||||
fn app_bundle(exe: &Path) -> Option<PathBuf> {
|
||||
let bundle = exe.parent()?.parent()?.parent()?;
|
||||
(cfg!(target_os = "macos") && bundle.extension().is_some_and(|e| e == "app")).then(|| bundle.to_path_buf())
|
||||
}
|
||||
|
||||
/// Answers the `openidea` command for this app: its targets open like paths from Finder.
|
||||
pub fn serve(app: &AppHandle, identifier: &str) {
|
||||
let Some(socket) = socket::socket_path(identifier) else { return };
|
||||
let app = app.clone();
|
||||
let served = socket::serve(&socket, move |request| {
|
||||
let handle = app.clone();
|
||||
match app.run_on_main_thread(move || {
|
||||
if request.diff {
|
||||
let mut targets = request.targets.into_iter();
|
||||
if let (Some(left), Some(right)) = (targets.next(), targets.next()) {
|
||||
crate::windows::open_diff_files(
|
||||
&handle,
|
||||
crate::windows::DiffFilesLaunch { left: left.path.into(), right: right.path.into() },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::windows::open_targets(&handle, request.targets);
|
||||
}
|
||||
}) {
|
||||
Ok(()) => Response::Ok,
|
||||
Err(e) => Response::Error { message: format!("OpenIDEA cannot open windows: {e}") },
|
||||
}
|
||||
});
|
||||
if let Err(e) = served {
|
||||
tracing::warn!("cannot listen for the openidea command on {}: {e}", socket.display());
|
||||
}
|
||||
}
|
||||
|
||||
fn install_error(e: std::io::Error) -> IdeError {
|
||||
if e.kind() == ErrorKind::Interrupted { IdeError::cancelled() } else { IdeError::internal(e.to_string()) }
|
||||
}
|
||||
|
||||
/// Links the `openidea` command onto `PATH`; returns the link. Blocks while the
|
||||
/// administrator password prompt is up.
|
||||
pub fn install() -> IdeResult<crate::commands::window::CliInstallation> {
|
||||
let result = ide_cli::install::install_current().map_err(install_error)?;
|
||||
Ok(crate::commands::window::CliInstallation {
|
||||
path: result.path.into(),
|
||||
path_directory: result.path_directory.map(Into::into),
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes every installed `openidea` command link; returns them.
|
||||
pub fn uninstall() -> IdeResult<Vec<IpcPath>> {
|
||||
let links = ide_cli::install::uninstall_current().map_err(install_error)?;
|
||||
Ok(links.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bundle_of_the_app_executable() {
|
||||
let exe = Path::new("/Applications/OpenIDEA.app/Contents/MacOS/openidea");
|
||||
let expected = cfg!(target_os = "macos").then(|| PathBuf::from("/Applications/OpenIDEA.app"));
|
||||
assert_eq!(app_bundle(exe), expected);
|
||||
assert_eq!(app_bundle(Path::new("/repo/target/debug/openidea")), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача