use std::path::Path;
use std::process::{Command, Stdio};
use anyhow::{Result, bail};
use rcmd_tui::app::Exec;
const TERMINALS: &[(&str, &str)] = &[
("x-terminal-emulator", "-e"),
("alacritty", "-e"),
("kitty", "-e"),
("wezterm", "-e"),
("foot", "-e"),
("ghostty", "-e"),
("konsole", "-e"),
("gnome-terminal", "--"),
("xfce4-terminal", "-x"),
("urxvt", "-e"),
("xterm", "-e"),
];
pub fn run(exec: &Exec, cwd: &Path) -> Result<Option<String>> {
match exec {
Exec::Quiet(cmd) => {
spawn_detached(cmd, cwd)?;
Ok(None)
}
Exec::Command(cmd) => match terminal_for(cmd) {
Some((program, args)) => {
spawn_in(&program, &args, cwd)?;
Ok(None)
}
None => {
spawn_detached(cmd, cwd)?;
Ok(Some(
"no terminal emulator found - ran it detached, with no output".into(),
))
}
},
Exec::Shell => match terminal_for("") {
Some((program, args)) => {
spawn_in(&program, &args, cwd)?;
Ok(None)
}
None => bail!("no terminal emulator found (set $TERMINAL)"),
},
}
}
fn terminal_for(cmd: &str) -> Option<(String, Vec<String>)> {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into());
let script = match cmd.is_empty() {
true => None,
false => Some(format!(
"{cmd}; printf '\\n[rcmd-egui] press Enter to close '; read _",
)),
};
let mut candidates: Vec<(String, String)> = Vec::new();
if let Ok(preferred) = std::env::var("TERMINAL")
&& !preferred.is_empty()
{
candidates.push((preferred, "-e".into()));
}
candidates.extend(
TERMINALS
.iter()
.map(|(name, flag)| ((*name).to_string(), (*flag).to_string())),
);
let (program, flag) = candidates.into_iter().find(|(name, _)| which(name))?;
let args = match &script {
Some(script) => vec![flag, shell, "-c".into(), script.clone()],
None => vec![flag, shell],
};
Some((program, args))
}
fn which(name: &str) -> bool {
let path = Path::new(name);
if path.is_absolute() {
return path.is_file();
}
let Ok(var) = std::env::var("PATH") else {
return false;
};
std::env::split_paths(&var).any(|dir| dir.join(name).is_file())
}
fn spawn_in(program: &str, args: &[String], cwd: &Path) -> Result<()> {
Command::new(program)
.args(args)
.current_dir(cwd)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
Ok(())
}
fn spawn_detached(cmd: &str, cwd: &Path) -> Result<()> {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into());
spawn_in(&shell, &["-c".to_string(), cmd.to_string()], cwd)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_command_gets_a_pause_and_a_shell_does_not() {
if let Some((_, args)) = terminal_for("ls") {
let script = args.last().expect("a script");
assert!(script.starts_with("ls;"));
assert!(script.contains("press Enter"));
}
if let Some((_, args)) = terminal_for("") {
assert!(!args.iter().any(|a| a == "-c"));
}
}
#[test]
fn which_finds_what_is_there_and_not_what_is_not() {
assert!(which("sh"));
assert!(!which("a-program-nobody-has-installed-here"));
assert!(which("/bin/sh") || which("/usr/bin/sh"));
assert!(!which("/nonexistent/binary"));
}
}