axl_lib/
fzf.rs

1use anyhow::Result;
2use colored::Colorize;
3use std::{
4    ffi::OsStr,
5    fmt::{Debug, Display},
6    io::Write,
7    process::{Command, Stdio},
8};
9use thiserror::Error;
10use tracing::instrument;
11
12#[derive(Error, Debug)]
13pub enum FzfError {
14    #[error("could not find any items to choose from")]
15    NoItemsFound,
16}
17
18#[derive(Debug)]
19pub struct FzfCmd {
20    command: Command,
21}
22
23impl Default for FzfCmd {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl FzfCmd {
30    pub fn new() -> Self {
31        Self {
32            command: Command::new("fzf"),
33        }
34    }
35
36    #[instrument(skip(self))]
37    pub fn arg<S>(&mut self, arg: S) -> &mut Self
38    where
39        S: AsRef<OsStr> + Debug,
40    {
41        self.command.arg(arg);
42        self
43    }
44
45    #[instrument(skip(self))]
46    pub fn args<I, S>(&mut self, args: I) -> &mut Self
47    where
48        I: IntoIterator<Item = S> + Debug,
49        S: AsRef<OsStr> + Debug,
50    {
51        self.command.args(args);
52        self
53    }
54
55    #[instrument(skip(self))]
56    pub fn find_vec<T>(&mut self, input: Vec<T>) -> Result<String>
57    where
58        T: Debug + Display,
59    {
60        let projects_string: String = input.iter().fold(String::new(), |acc, project_name| {
61            format!("{acc}\n{project_name}")
62        });
63        self.find_string(projects_string.trim_start())
64    }
65
66    #[instrument(skip(self))]
67    pub fn find_string(&mut self, input: &str) -> Result<String> {
68        let mut fzf_child = self
69            .command
70            .stdin(Stdio::piped())
71            .stdout(Stdio::piped())
72            .spawn()
73            .expect("fzf command should spawn");
74
75        // Get the stdin handle of the child process
76        fzf_child.stdin.as_mut().map_or_else(
77            || {
78                eprintln!("Failed to get stdin handle for the child process");
79            },
80            |stdin| {
81                // Write your input string to the command's stdin
82                stdin
83                    .write_all(input.as_bytes())
84                    .expect("should be able to pass project names to fzf stdin");
85            },
86        );
87
88        // Ensure the child process has finished
89        let output = fzf_child.wait_with_output()?;
90
91        if output.status.success() {
92            return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
93        }
94
95        Ok("".to_string())
96    }
97
98    #[instrument(err)]
99    pub fn pick_many(items: Vec<String>) -> Result<Vec<String>> {
100        if items.is_empty() {
101            eprintln!("\n{}\n", "No items found to choose from.".blue().bold());
102            Err(FzfError::NoItemsFound)?
103        }
104
105        Ok(Self::new()
106            .args(vec!["--phony", "--multi"])
107            .find_vec(items)?
108            .trim_end()
109            .split('\n')
110            .map(|s| s.to_string())
111            .collect())
112    }
113
114    #[instrument(err)]
115    pub fn pick_many_filtered(items: Vec<String>) -> Result<Vec<String>> {
116        if items.is_empty() {
117            eprintln!("\n{}\n", "No items found to choose from.".blue().bold());
118            Err(FzfError::NoItemsFound)?
119        }
120
121        Ok(Self::new()
122            .arg("--multi")
123            .find_vec(items)?
124            .trim_end()
125            .split('\n')
126            .map(|s| s.to_string())
127            .collect())
128    }
129
130    #[instrument(err)]
131    pub fn pick_one_filtered(items: Vec<String>) -> Result<String> {
132        if items.is_empty() {
133            eprintln!("\n{}\n", "No items found to choose from.".blue().bold());
134            Err(FzfError::NoItemsFound)?
135        }
136
137        let picked: Vec<_> = Self::new()
138            .find_vec(items)?
139            .trim_end()
140            .split('\n')
141            .map(|s| s.to_string())
142            .collect();
143
144        Ok(picked.first().expect("you must choose one item").clone())
145    }
146}