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