use std::process::{Command, Stdio};
#[derive(Debug, Clone)]
pub struct FzfOpts {
pub prompt: String,
pub with_nth: String,
pub delimiter: String,
pub height: String,
pub extra_args: Vec<String>,
}
impl Default for FzfOpts {
fn default() -> Self {
Self {
prompt: "select > ".to_string(),
with_nth: "2..".to_string(),
delimiter: "\t".to_string(),
height: "40%".to_string(),
extra_args: vec!["--reverse".to_string(), "--no-multi".to_string()],
}
}
}
pub fn fzf_available() -> bool {
Command::new("fzf")
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
const FZF_EXIT_NO_MATCH: i32 = 1;
const FZF_EXIT_INTERRUPTED: i32 = 130;
pub fn run_fzf(rows: &[String], opts: &FzfOpts) -> Option<String> {
use std::io::Write;
if rows.is_empty() {
return None;
}
let mut cmd = Command::new("fzf");
cmd.arg("--prompt")
.arg(&opts.prompt)
.arg("--with-nth")
.arg(&opts.with_nth)
.arg("--delimiter")
.arg(&opts.delimiter)
.arg("--height")
.arg(&opts.height);
for extra in &opts.extra_args {
cmd.arg(extra);
}
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd.spawn().ok()?;
if let Some(mut stdin) = child.stdin.take() {
let payload = rows.join("\n");
let _ = stdin.write_all(payload.as_bytes());
let _ = stdin.write_all(b"\n");
}
let output = child.wait_with_output().ok()?;
if output.status.code() != Some(0) {
debug_assert!(matches!(
output.status.code(),
None | Some(FZF_EXIT_NO_MATCH) | Some(FZF_EXIT_INTERRUPTED) | Some(_)
));
return None;
}
let selected = String::from_utf8_lossy(&output.stdout);
let line = selected.lines().next()?; if line.is_empty() {
return None;
}
let delim = opts.delimiter.chars().next().unwrap_or('\t');
Some(line.split(delim).next().unwrap_or(line).to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fzf_available_is_stable() {
let a = fzf_available();
let b = fzf_available();
assert_eq!(
a, b,
"fzf_available() must be deterministic within one process"
);
}
#[test]
fn fzf_available_returns_bool() {
let result: bool = fzf_available();
let _ = result;
}
#[test]
fn fzf_opts_default_values() {
let opts = FzfOpts::default();
assert_eq!(opts.delimiter, "\t");
assert!(!opts.prompt.is_empty());
assert!(opts.extra_args.contains(&"--reverse".to_string()));
assert!(opts.extra_args.contains(&"--no-multi".to_string()));
}
}