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 fzf_child.stdin.as_mut().map_or_else(
77 || {
78 eprintln!("Failed to get stdin handle for the child process");
79 },
80 |stdin| {
81 stdin
83 .write_all(input.as_bytes())
84 .expect("should be able to pass project names to fzf stdin");
85 },
86 );
87
88 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}