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}
265
266#[derive(Clone)]
267pub struct TextWithAutocomplete<I: Clone, const S: usize> {
268 i: Vec<I>,
269
270 input: String,
271 matches: Vec<String>,
272 print: fn(&I) -> [String; S],
273}
274impl<I: Clone, const S: usize> TextWithAutocomplete<I, S> {
275 fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> {
276 if input == self.input {
277 return Ok(());
278 }
279
280 self.input = input.to_owned();
281 let mut m: Vec<_> = self
282 .i
283 .iter()
284 .map(|c| {
285 let s = (self.print)(c);
286 let v = SkimMatcherV2::default()
287 .smart_case()
288 .fuzzy_match(&s.join(" "), input);
289 (s, v)
290 })
291 .collect();
292
293 m.sort_by(|a, b| b.1.cmp(&a.1));
294 self.matches = to_lines(&m.into_iter().map(|e| e.0).collect_vec());
295 Ok(())
296 }
297
298 pub fn new(i: Vec<I>, print: fn(&I) -> [String; S]) -> Self {
299 Self {
300 i,
301 print,
302 input: String::new(),
303 matches: Vec::new(),
304 }
305 }
306}
307
308impl<I: Clone, const S: usize> Autocomplete for TextWithAutocomplete<I, S> {
309 fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
310 self.update_input(input)?;
311 Ok(self.matches.to_owned())
312 }
313
314 fn get_completion(
315 &mut self,
316 input: &str,
317 highlighted_suggestion: Option<String>,
318 ) -> Result<inquire::autocompletion::Replacement, inquire::CustomUserError> {
319 self.update_input(input)?;
320
321 Ok(match highlighted_suggestion {
322 Some(e) => Replacement::Some(e),
323 None => self
324 .matches
325 .first()
326 .map(|e| Replacement::Some(e.to_owned()))
327 .unwrap_or(Replacement::None),
328 })
329 }
330}
331
332type Res<T> = Result<T, MyErr>;
333pub fn env_var(s: &str) -> Res<String> {
334 Ok(std::env::var(s).map_err(|e| format!("Failed to get env '{s}' because '{e}'"))?)
335}
336pub fn reg(s: &str) -> Res<Regex> {
337 Ok(Regex::new(s).map_err(|e| format!("Failed to compile regex '{s}' because '{e}'"))?)
338}
339pub fn fs_write<P: AsRef<Path>, C: AsRef<[u8]>>(p: P, c: C) -> Res<()> {
340 Ok(std::fs::write(p.as_ref(), c).map_err(|e| {
341 format!(
342 "Failed to write to '{}' because '{e}'",
343 p.as_ref().to_string_lossy()
344 )
345 })?)
346}
347pub fn fs_read<P: AsRef<Path>>(p: P) -> Res<Vec<u8>> {
348 Ok(std::fs::read(p.as_ref()).map_err(|e| {
349 format!(
350 "Failed to read '{}' because '{e}'",
351 p.as_ref().to_string_lossy()
352 )
353 })?)
354}
355pub fn fs_read_dir<P: AsRef<Path>>(p: P) -> Res<ReadDir> {
356 Ok(std::fs::read_dir(p.as_ref()).map_err(|e| {
357 format!(
358 "Failed to read '{}' because '{e}'",
359 p.as_ref().to_string_lossy()
360 )
361 })?)
362}