hyper_scripter/list/
list_impl.rs

1use super::{
2    exec_time_str, extract_help, get_screen_width, style, style_name,
3    table_lib::{Cell, Collumn, Table},
4    time_fmt, tree, DisplayStyle, Grid, Grouping, ListOptions, LONG_LATEST_TXT, SHORT_LATEST_TXT,
5};
6use crate::error::Result;
7use crate::query::{do_list_query, ListQuery};
8use crate::script::ScriptInfo;
9use crate::script_repo::{ScriptRepo, Visibility};
10use crate::tag::Tag;
11use crate::util::{get_display_type, DisplayType};
12use fxhash::FxHashMap as HashMap;
13use handlebars::Handlebars;
14use serde::Serialize;
15use std::borrow::Cow;
16use std::cmp::Reverse;
17use std::hash::Hash;
18use std::io::Write;
19
20type ListOptionWithOutput = ListOptions<Table, Grid>;
21
22pub fn ident_string(
23    format: &str,
24    name: &str,
25    ty: &DisplayType,
26    script: &ScriptInfo,
27) -> Result<String> {
28    #[derive(Serialize)]
29    pub struct TmplVal<'a> {
30        id: i64,
31        name: &'a str,
32        file: Cow<'a, str>,
33        ty: Cow<'a, str>,
34    }
35    let file = script.file_path_fallback();
36    let tmpl_val = TmplVal {
37        id: script.id,
38        file: file.to_string_lossy(),
39        ty: ty.display(),
40        name,
41    };
42
43    let reg = Handlebars::new();
44    Ok(reg.render_template(format, &tmpl_val)?)
45}
46
47#[derive(PartialEq, Eq, Hash)]
48struct TagsKey(Vec<Tag>);
49impl std::fmt::Display for TagsKey {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        if self.is_empty() {
52            write!(f, "(no tag)")?;
53            return Ok(());
54        }
55        write!(f, "[")?;
56        let mut first = true;
57        for tag in &self.0 {
58            if !first {
59                write!(f, " ")?;
60            }
61            first = false;
62            write!(f, "#{}", AsRef::<str>::as_ref(tag))?;
63        }
64        write!(f, "]")?;
65        Ok(())
66    }
67}
68impl TagsKey {
69    fn new(tags: impl Iterator<Item = Tag>) -> Self {
70        let mut tags: Vec<_> = tags.collect();
71        tags.sort();
72        TagsKey(tags)
73    }
74    fn is_empty(&self) -> bool {
75        self.0.is_empty()
76    }
77}
78
79fn sort_scripts(v: &mut Vec<&ScriptInfo>) {
80    v.sort_by_key(|s| Reverse(s.last_time()));
81}
82
83fn convert_opt<T>(opt: ListOptions, t: T) -> ListOptions<Table, T> {
84    ListOptions {
85        display_style: match opt.display_style {
86            DisplayStyle::Short(format, _) => DisplayStyle::Short(format, t),
87            DisplayStyle::Long(_) => {
88                time_fmt::init();
89                let mut table = Table::new(gen_title());
90                table.set_width(get_screen_width());
91                table.set_plain(opt.plain);
92                DisplayStyle::Long(table)
93            }
94        },
95        grouping: opt.grouping,
96        plain: opt.plain,
97        limit: opt.limit,
98    }
99}
100fn extract_table<U>(opt: ListOptions<Table, U>) -> Option<Table> {
101    match opt.display_style {
102        DisplayStyle::Short(..) => None,
103        DisplayStyle::Long(table) => Some(table),
104    }
105}
106pub fn fmt_meta(
107    script: &ScriptInfo,
108    is_latest: bool,
109    opt: &mut ListOptionWithOutput,
110) -> Result<()> {
111    let ty = get_display_type(&script.ty);
112    let color = ty.color();
113    match &mut opt.display_style {
114        DisplayStyle::Long(table) => {
115            let (name_txt, name_width) = style_name(
116                opt.plain,
117                is_latest,
118                LONG_LATEST_TXT,
119                color,
120                &script.name.key(),
121            )?;
122            let ty = ty.display();
123            let ty_width = ty.len();
124            let ty_txt = style(opt.plain, ty, |s| s.color(color).bold().done());
125
126            let help_msg = extract_help(script);
127
128            let row = vec![
129                Cell::new_with_len(name_txt, name_width),
130                Cell::new_with_len(ty_txt.to_string(), ty_width),
131                Cell::new(time_fmt::fmt(&script.write_time).to_string()),
132                Cell::new(exec_time_str(script).to_string()),
133                Cell::new(help_msg),
134            ];
135            table.add_row(row);
136        }
137        DisplayStyle::Short(format, grid) => {
138            let ident = ident_string(format, &script.name.to_string(), &ty, script)?;
139            let (ident, width) = style_name(opt.plain, is_latest, SHORT_LATEST_TXT, color, &ident)?;
140            grid.add(ident, width);
141        }
142    }
143    Ok(())
144}
145
146enum ScriptsEither<'a, I> {
147    Iter(I),
148    V(Vec<&'a ScriptInfo>),
149}
150impl<'a, I: ExactSizeIterator<Item = &'a ScriptInfo>> ScriptsEither<'a, I> {
151    fn new(iter: I, limit: Option<usize>) -> Self {
152        if let Some(limit) = limit {
153            let mut v: Vec<_> = iter.collect();
154            sort_scripts(&mut v);
155            v.truncate(limit);
156            Self::V(v)
157        } else {
158            Self::Iter(iter)
159        }
160    }
161    fn collect(self) -> Vec<&'a ScriptInfo> {
162        match self {
163            Self::Iter(iter) => iter.collect(),
164            Self::V(v) => v,
165        }
166    }
167    fn len(&self) -> usize {
168        match self {
169            Self::Iter(iter) => iter.len(),
170            Self::V(v) => v.len(),
171        }
172    }
173    fn sorted(&self) -> bool {
174        matches!(self, Self::V(..))
175    }
176    fn for_each<F: FnMut(&'a ScriptInfo)>(self, mut f: F) {
177        match self {
178            Self::Iter(iter) => {
179                for s in iter {
180                    f(s);
181                }
182            }
183            Self::V(v) => {
184                for s in v.into_iter() {
185                    f(s);
186                }
187            }
188        }
189    }
190}
191
192fn gen_title() -> Vec<Collumn> {
193    vec![
194        Collumn::new_fixed("Script"),
195        Collumn::new_fixed("Type"),
196        Collumn::new("Write"),
197        Collumn::new("Execute"),
198        Collumn::new("Help Message"),
199    ]
200}
201
202pub async fn fmt_list<W: Write>(
203    w: &mut W,
204    script_repo: &mut ScriptRepo,
205    opt: ListOptions,
206    queries: Vec<ListQuery>,
207) -> Result<()> {
208    if !opt.plain {
209        write!(
210            w,
211            "{} scripts ignored due to time filter\n",
212            script_repo.time_hidden_count
213        )?;
214    }
215
216    let latest_script_id = script_repo
217        .latest_mut(1, Visibility::Normal)
218        .map_or(-1, |s| s.id);
219
220    let scripts_iter = do_list_query(script_repo, queries)
221        .await?
222        .into_iter()
223        .map(|e| &*e.into_inner());
224    let scripts_either = ScriptsEither::new(scripts_iter, opt.limit.map(|l| l.get()));
225    let sorted = scripts_either.sorted();
226
227    let final_table: Option<Table>;
228    match opt.grouping {
229        Grouping::None => {
230            let mut opt = convert_opt(opt, Grid::new(scripts_either.len()));
231            let scripts = scripts_either.collect();
232            fmt_group(w, scripts, sorted, latest_script_id, &mut opt)?;
233            final_table = extract_table(opt);
234        }
235        Grouping::Tree => {
236            let mut opt = convert_opt(opt, &mut *w);
237            let scripts = scripts_either.collect();
238            tree::fmt(scripts, latest_script_id, &mut opt)?;
239            final_table = extract_table(opt);
240        }
241        Grouping::Tag => {
242            let mut opt = convert_opt(opt, Grid::new(scripts_either.len()));
243            let mut script_map: HashMap<TagsKey, Vec<&ScriptInfo>> = HashMap::default();
244            scripts_either.for_each(|script| {
245                let key = TagsKey::new(script.tags.iter().cloned());
246                let v = script_map.entry(key).or_default();
247                v.push(script);
248            });
249
250            let mut scripts: Vec<_> = script_map
251                .into_iter()
252                .map(|(k, v)| {
253                    // NOTE: 以群組中執行次數的最大值排序, 無標籤永遠在上
254                    let sort_key = if k.is_empty() {
255                        None
256                    } else {
257                        v.iter()
258                            .map(|s| {
259                                if s.exec_time.is_none() {
260                                    0
261                                } else {
262                                    s.exec_count
263                                }
264                            })
265                            .max()
266                    };
267                    (sort_key, k, v)
268                })
269                .collect();
270
271            scripts.sort_by_key(|(sort_key, _, _)| *sort_key);
272
273            for (_, tags, scripts) in scripts.into_iter() {
274                if !opt.grouping.is_none() {
275                    let tags_txt = style(opt.plain, tags, |s| s.dimmed().italic().done());
276                    match &mut opt.display_style {
277                        DisplayStyle::Long(table) => {
278                            table.add_row(vec![Cell::new_with_len(tags_txt.to_string(), 0)]);
279                        }
280                        DisplayStyle::Short(_, _) => {
281                            writeln!(w, "{}", tags_txt)?;
282                        }
283                    }
284                }
285                fmt_group(w, scripts, sorted, latest_script_id, &mut opt)?;
286            }
287            final_table = extract_table(opt);
288        }
289    }
290    if let Some(mut table) = final_table {
291        write!(w, "{}", table.display())?;
292        log::debug!("tree table: {:?}", table);
293    }
294    Ok(())
295}
296
297fn fmt_group<W: Write>(
298    w: &mut W,
299    mut scripts: Vec<&ScriptInfo>,
300    sorted: bool,
301    latest_script_id: i64,
302    opt: &mut ListOptionWithOutput,
303) -> Result<()> {
304    if !sorted {
305        sort_scripts(&mut scripts);
306    }
307    for script in scripts.into_iter() {
308        let is_latest = script.id == latest_script_id;
309        fmt_meta(script, is_latest, opt)?;
310    }
311    match &mut opt.display_style {
312        DisplayStyle::Short(_, grid) => {
313            let grid_display = grid.fit_into_screen();
314            write!(w, "{}", grid_display)?;
315            drop(grid_display);
316            grid.clear();
317        }
318        _ => (),
319    }
320    Ok(())
321}