Skip to main content

fpr_cli/
util.rs

1use std::{fs::ReadDir, path::Path};
2
3use chrono::{DateTime, FixedOffset, TimeZone};
4use regex::Regex;
5
6use crate::com::*;
7
8pub fn to_lines<const S: usize, I: AsRef<str>>(a: &[[I; S]]) -> Vec<String> {
9    fn width(s: &str) -> usize {
10        cfg_if::cfg_if! {
11            if #[cfg(feature = "unicode")] {
12                use unicode_width::*;
13                s.width()
14            } else {
15                s.len()
16            }
17        }
18    }
19    let w = match (0..S)
20        .map(|i| a.iter().map(|l| width(l[i].as_ref())).max().ok_or(()))
21        .collect::<Result<Vec<_>, _>>()
22    {
23        Ok(e) => e,
24        Err(_) => {
25            return vec![];
26        }
27    };
28    a.iter()
29        .map(|v| {
30            v.iter()
31                .enumerate()
32                .map(|(i, s)| format!("{}{: <2$}", s.as_ref(), "", w[i] - width(s.as_ref())))
33                .join(" ")
34        })
35        .collect()
36}
37pub fn to_table<const S: usize, I: AsRef<str>>(a: &[[I; S]]) -> String {
38    to_lines(a).join("\n")
39}
40
41fn to_option_lines<const S: usize, I: AsRef<str>, T>(
42    t: &[T],
43    f: fn(&T) -> [I; S],
44) -> Vec<ListOption<String>> {
45    to_lines(&t.iter().map(f).collect::<Vec<_>>())
46        .into_iter()
47        .enumerate()
48        .map(|(i, e)| ListOption::new(i, e))
49        .collect()
50}
51
52pub fn select_line<'a, const S: usize, I: AsRef<str>, T>(
53    prompt: &'a str,
54    t: &[T],
55    f: fn(&T) -> [I; S],
56) -> Select<'a, ListOption<String>> {
57    Select::new(prompt, to_option_lines(t, f))
58}
59pub fn select_multiple_line<'a, const S: usize, I: AsRef<str>, T>(
60    prompt: &'a str,
61    t: &[T],
62    f: fn(&T) -> [I; S],
63) -> MultiSelect<'a, ListOption<String>> {
64    MultiSelect::new(prompt, to_option_lines(t, f))
65}
66
67pub fn input_path<'_a>(prompt: &'_ str) -> Text<'_, '_> {
68    Text::new(prompt).with_autocomplete(filepath::Comp::default())
69}
70
71mod filepath {
72    use crate::com::*;
73
74    #[derive(Clone, Default)]
75    pub struct Comp {
76        input: String,
77        paths: Vec<String>,
78    }
79
80    impl Comp {
81        fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> {
82            if input == self.input {
83                return Ok(());
84            }
85
86            self.input = input.to_owned();
87            self.paths.clear();
88
89            let input_path = PathBuf::from(input);
90
91            let fb = input_path
92                .parent()
93                .map(|p| {
94                    if p.to_string_lossy() == "" {
95                        PathBuf::from(".")
96                    } else {
97                        p.to_owned()
98                    }
99                })
100                .unwrap_or_else(|| PathBuf::from("."));
101
102            let scan_dir = if input.ends_with('/') {
103                input_path
104            } else {
105                fb.clone()
106            };
107
108            let entries = match std::fs::read_dir(scan_dir) {
109                Ok(r) => r.filter_map(|e| e.ok()).collect(),
110                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
111                    match std::fs::read_dir(fb) {
112                        Ok(r) => r.filter_map(|e| e.ok()).collect(),
113                        Err(_) => vec![],
114                    }
115                }
116                Err(_) => vec![],
117            };
118
119            for entry in entries {
120                let path = entry.path();
121                let path_str = if path.is_dir() {
122                    format!("{}/", path.to_string_lossy())
123                } else {
124                    path.to_string_lossy().to_string()
125                };
126
127                self.paths.push(path_str);
128            }
129
130            Ok(())
131        }
132
133        fn fuzzy_sort(&self, input: &str) -> Vec<(String, i64)> {
134            let mut matches: Vec<(String, i64)> = self
135                .paths
136                .iter()
137                .filter_map(|path| {
138                    SkimMatcherV2::default()
139                        .smart_case()
140                        .fuzzy_match(path, input)
141                        .map(|score| (path.clone(), score))
142                })
143                .collect();
144
145            matches.sort_by(|a, b| b.1.cmp(&a.1));
146            matches
147        }
148    }
149
150    fn expand(s: &str) -> String {
151        match shellexpand::full(s) {
152            Ok(e) => e.to_string(),
153            Err(_) => s.to_owned(),
154        }
155    }
156
157    impl Autocomplete for Comp {
158        fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, CustomUserError> {
159            let input = &expand(input);
160            self.update_input(input)?;
161
162            let matches = self.fuzzy_sort(input);
163            Ok(matches.into_iter().take(15).map(|(path, _)| path).collect())
164        }
165
166        fn get_completion(
167            &mut self,
168            input: &str,
169            highlighted_suggestion: Option<String>,
170        ) -> Result<Replacement, CustomUserError> {
171            let input = &expand(input);
172            self.update_input(input)?;
173
174            Ok(match highlighted_suggestion {
175                Some(e) => Replacement::Some(e),
176                None => {
177                    let matches = self.fuzzy_sort(input);
178                    matches
179                        .first()
180                        .map(|(path, _)| Replacement::Some(path.clone()))
181                        .unwrap_or(Replacement::None)
182                }
183            })
184        }
185    }
186}
187
188#[derive(Clone)]
189pub struct MyDateTime<C: TimeZone> {
190    v: DateTime<C>,
191}
192impl<C: TimeZone> Into<DateTime<C>> for MyDateTime<C>
193where
194    DateTime<C>: From<DateTime<FixedOffset>>,
195{
196    fn into(self) -> DateTime<C> {
197        self.v.into()
198    }
199}
200
201impl<C: TimeZone> FromStr for MyDateTime<C>
202where
203    DateTime<C>: From<DateTime<FixedOffset>>,
204{
205    type Err = String;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        Ok(Self {
209            v: DateTime::parse_from_rfc3339(s)
210                .map_err(|e| format!("{e}"))?
211                .into(),
212        })
213    }
214}
215impl<C: TimeZone> Display for MyDateTime<C> {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        write!(
218            f,
219            "{}",
220            self.v.to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
221        )
222    }
223}
224
225impl<C: TimeZone> CustomTypeValidator<String> for MyDateTime<C> {
226    fn validate(
227        &self,
228        i: &String,
229    ) -> Result<inquire::validator::Validation, inquire::CustomUserError> {
230        use inquire::validator::Validation::*;
231        match DateTime::parse_from_rfc3339(i) {
232            Ok(_) => Ok(Valid),
233            Err(e) => Ok(Invalid(ErrorMessage::Custom(format!("{e}")))),
234        }
235    }
236}
237
238pub fn input_date<'_a, C: TimeZone>(prompt: &str) -> CustomType<'_, MyDateTime<C>>
239where
240    DateTime<C>: From<DateTime<FixedOffset>>,
241{
242    CustomType::<MyDateTime<C>>::new(prompt)
243}
244
245#[derive(Debug)]
246pub enum MyErr {
247    Inquire(inquire::InquireError),
248    Generic(String),
249}
250impl From<String> for MyErr {
251    fn from(v: String) -> Self {
252        Self::Generic(v)
253    }
254}
255impl From<inquire::InquireError> for MyErr {
256    fn from(v: inquire::InquireError) -> Self {
257        Self::Inquire(v)
258    }
259}
260impl From<MyErr> for String {
261    fn from(v: MyErr) -> Self {
262        use MyErr::*;
263        match v {
264            Inquire(e) => format!("{e}"),
265            Generic(e) => format!("{e}"),
266        }
267    }
268}
269
270pub trait Actions: Sized + Clone {
271    fn get(prompt: &str, starting_input: Option<&str>) -> Result<Self, MyErr>;
272    fn list() -> &'static [&'static str];
273    fn parse(s: &str) -> Option<Self>;
274}
275
276#[derive(Clone)]
277pub struct TextWithAutocomplete<I: Clone, const S: usize> {
278    i: Vec<I>,
279
280    input: String,
281    matches: Vec<String>,
282    print: fn(&I) -> [String; S],
283}
284impl<I: Clone, const S: usize> TextWithAutocomplete<I, S> {
285    fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> {
286        if input == self.input {
287            return Ok(());
288        }
289
290        self.input = input.to_owned();
291        let mut m: Vec<_> = self
292            .i
293            .iter()
294            .map(|c| {
295                let s = (self.print)(c);
296                let v = SkimMatcherV2::default()
297                    .smart_case()
298                    .fuzzy_match(&s.join(" "), input);
299                (s, v)
300            })
301            .collect();
302
303        m.sort_by(|a, b| b.1.cmp(&a.1));
304        self.matches = to_lines(&m.into_iter().map(|e| e.0).collect_vec());
305        Ok(())
306    }
307
308    pub fn new(i: Vec<I>, print: fn(&I) -> [String; S]) -> Self {
309        Self {
310            i,
311            print,
312            input: String::new(),
313            matches: Vec::new(),
314        }
315    }
316}
317
318impl<I: Clone, const S: usize> Autocomplete for TextWithAutocomplete<I, S> {
319    fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
320        self.update_input(input)?;
321        Ok(self.matches.to_owned())
322    }
323
324    fn get_completion(
325        &mut self,
326        input: &str,
327        highlighted_suggestion: Option<String>,
328    ) -> Result<inquire::autocompletion::Replacement, inquire::CustomUserError> {
329        self.update_input(input)?;
330
331        Ok(match highlighted_suggestion {
332            Some(e) => Replacement::Some(e),
333            None => self
334                .matches
335                .first()
336                .map(|e| Replacement::Some(e.to_owned()))
337                .unwrap_or(Replacement::None),
338        })
339    }
340}
341
342type Res<T> = Result<T, MyErr>;
343pub fn env_var(s: &str) -> Res<String> {
344    Ok(std::env::var(s).map_err(|e| format!("Failed to get env '{s}' because '{e}'"))?)
345}
346pub fn reg(s: &str) -> Res<Regex> {
347    Ok(Regex::new(s).map_err(|e| format!("Failed to compile regex '{s}' because '{e}'"))?)
348}
349pub fn fs_write<P: AsRef<Path>, C: AsRef<[u8]>>(p: P, c: C) -> Res<()> {
350    Ok(std::fs::write(p.as_ref(), c).map_err(|e| {
351        format!(
352            "Failed to write to '{}' because '{e}'",
353            p.as_ref().to_string_lossy()
354        )
355    })?)
356}
357pub fn fs_read<P: AsRef<Path>>(p: P) -> Res<Vec<u8>> {
358    Ok(std::fs::read(p.as_ref()).map_err(|e| {
359        format!(
360            "Failed to read '{}' because '{e}'",
361            p.as_ref().to_string_lossy()
362        )
363    })?)
364}
365pub fn fs_read_dir<P: AsRef<Path>>(p: P) -> Res<ReadDir> {
366    Ok(std::fs::read_dir(p.as_ref()).map_err(|e| {
367        format!(
368            "Failed to read '{}' because '{e}'",
369            p.as_ref().to_string_lossy()
370        )
371    })?)
372}