use crate::executor::execute_command;
use crate::types::CommandDef;
use anyhow::{Context, Result, bail};
use regex::Regex;
use std::{
collections::HashMap,
io::{Read, Write},
path::Path,
process::{Command as ProcessCommand, Stdio},
};
fn strip_ansi_escapes(s: &str) -> String {
Regex::new(r"\x1b\[[0-9;]*m")
.unwrap()
.replace_all(s, "")
.to_string()
}
#[cfg(test)]
mod ansi_tests {
use super::strip_ansi_escapes;
#[test]
fn test_strip_ansi_escapes() {
let input = "\x1b[31mHello\x1b[0m World \x1b[1;32m!";
let expected = "Hello World !";
assert_eq!(strip_ansi_escapes(input), expected);
}
}
pub fn choose_command<'a>(
commands_vec: &'a [CommandDef],
config_dir: &Path,
filter_cmd: &str,
initial_query: Option<&str>,
) -> Result<&'a CommandDef> {
if commands_vec.is_empty() {
bail!(
"No command snippets defined. Looked in: {}",
config_dir.display()
);
}
let mut choice_map: HashMap<String, &CommandDef> = HashMap::new();
let prefix = "\x1b[33m";
let suffix = "\x1b[0m";
let mut colored_lines = Vec::new();
for cmd_def in commands_vec.iter() {
let tags_str = if cmd_def.tags.is_empty() {
String::new()
} else {
cmd_def
.tags
.iter()
.map(|t| format!("#{}", t))
.collect::<Vec<_>>()
.join(" ")
};
let raw_line = if tags_str.is_empty() {
cmd_def.description.clone()
} else {
format!("{} {}", cmd_def.description, tags_str)
};
let colored_line = if tags_str.is_empty() {
cmd_def.description.clone()
} else {
format!("{} {}{}{}", cmd_def.description, prefix, tags_str, suffix)
};
choice_map.insert(raw_line.clone(), cmd_def);
colored_lines.push(colored_line);
}
let mut parts = filter_cmd.split_whitespace();
let filter_prog = parts.next().unwrap();
let mut effective_args: Vec<String> = parts.map(|s| s.to_string()).collect();
if let Some(query) = initial_query {
match filter_prog {
"fzf" => {
effective_args.push("--query".to_string());
effective_args.push(query.to_string());
}
prog if prog == "gum"
&& effective_args
.first()
.map(|s| s == "filter")
.unwrap_or(false) =>
{
effective_args.push("--filter".to_string());
effective_args.push(query.to_string());
}
_ => {}
}
}
let mut filter_child = ProcessCommand::new(filter_prog)
.args(&effective_args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.with_context(|| format!("Failed to spawn filter command '{}'", filter_cmd))?;
{
let mut stdin = filter_child
.stdin
.take()
.context("Failed to open filter stdin")?;
for line in &colored_lines {
writeln!(stdin, "{}", line).context("Failed to write to filter stdin")?;
}
}
let mut selected = String::new();
{
let mut stdout = filter_child
.stdout
.take()
.context("Failed to open filter stdout")?;
stdout
.read_to_string(&mut selected)
.context("Failed to read filter output")?;
}
let status = filter_child
.wait()
.context("Failed to wait for filter process")?;
if !status.success() {
std::process::exit(1);
}
let key = strip_ansi_escapes(selected.trim());
choice_map
.get(&key)
.copied()
.with_context(|| format!("Selected command '{}' not found", key))
}
#[cfg(all(test, not(target_os = "windows")))]
mod smoke_tests {
use super::*;
use crate::types::CommandDef;
use std::path::{Path, PathBuf};
#[test]
fn smoke_select_and_execute() {
let cmd1 = CommandDef {
description: "First".to_string(),
command: "echo first".to_string(),
source_file: PathBuf::from("x.toml"),
tags: Vec::new(),
};
let cmd2 = CommandDef {
description: "Second".to_string(),
command: "false".to_string(),
source_file: PathBuf::from("y.toml"),
tags: Vec::new(),
};
let commands = vec![cmd1, cmd2];
let res = select_and_execute_command(&commands, Path::new("."), "head -n1", None);
assert!(res.is_ok(), "Expected Ok, got {:?}", res);
}
}
pub fn select_and_execute_command(
commands_vec: &[CommandDef],
config_dir: &Path,
filter_cmd: &str,
initial_query: Option<&str>,
) -> Result<()> {
let cmd_def = choose_command(commands_vec, config_dir, filter_cmd, initial_query)?;
execute_command(cmd_def).with_context(|| {
format!(
"Failed to execute command snippet '{}'",
cmd_def.description
)
})
}