rustic_rs/commands/tui/
snapshots.rs

1use std::{collections::BTreeSet, iter::once, mem, str::FromStr};
2
3use anyhow::Result;
4use chrono::Local;
5use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
6use itertools::Itertools;
7use ratatui::{
8    prelude::*,
9    widgets::{Block, Borders, Paragraph},
10};
11use rustic_core::{
12    IndexedFull, ProgressBars, Repository, SnapshotGroup, SnapshotGroupCriterion, StringList,
13    repofile::{DeleteOption, SnapshotFile},
14};
15use style::palette::tailwind;
16
17use crate::{
18    commands::{
19        snapshots::{fill_table, snap_to_table},
20        tui::{
21            diff::{Diff, DiffResult},
22            ls::{Ls, LsResult},
23            tree::{Tree, TreeIterItem, TreeNode},
24            widgets::{
25                Draw, PopUpInput, PopUpPrompt, PopUpTable, PopUpText, ProcessEvent, PromptResult,
26                SelectTable, TextInputResult, WithBlock, popup_input, popup_prompt, popup_table,
27                popup_text,
28            },
29        },
30    },
31    filtering::SnapshotFilter,
32};
33
34use super::summary::SummaryMap;
35
36// the states this screen can be in
37enum CurrentScreen<'a, P, S> {
38    Snapshots,
39    ShowHelp(PopUpText),
40    SnapshotDetails(PopUpTable),
41    EnterProperty((PopUpInput, SnapshotProperty)),
42    EnterFilter(PopUpInput),
43    PromptWrite(PopUpPrompt),
44    PromptExit(PopUpPrompt),
45    Dir(Box<Ls<'a, P, S>>),
46    Diff(Box<Diff<'a, P, S>>),
47}
48
49#[derive(Clone, Copy)]
50pub enum SnapshotProperty {
51    Label,
52    Description,
53    AddTags,
54    SetTags,
55    RemoveTags,
56    Hostname,
57}
58
59impl SnapshotProperty {
60    fn process(self, snap: &mut SnapshotFile, value: &String) -> bool {
61        match self {
62            Self::Label => {
63                if &snap.label == value {
64                    return false;
65                }
66                snap.label.clone_from(value);
67                true
68            }
69            Self::Description => {
70                if snap.description.is_none() && value.is_empty()
71                    || snap.description.as_ref() == Some(value)
72                {
73                    return false;
74                }
75                if value.is_empty() {
76                    snap.description = None;
77                } else {
78                    snap.description = Some(value.clone());
79                }
80                true
81            }
82            Self::AddTags => snap.add_tags(vec![StringList::from_str(value).unwrap()]),
83            Self::SetTags => snap.set_tags(vec![StringList::from_str(value).unwrap()]),
84            Self::RemoveTags => snap.remove_tags(&[StringList::from_str(value).unwrap()]),
85            Self::Hostname => {
86                if &snap.hostname == value {
87                    return false;
88                }
89                snap.hostname.clone_from(value);
90                true
91            }
92        }
93    }
94}
95
96// status of each snapshot
97#[derive(Clone, Copy, Default)]
98struct SnapStatus {
99    marked: bool,
100    modified: bool,
101    to_forget: bool,
102}
103
104impl SnapStatus {
105    fn toggle_mark(&mut self) {
106        self.marked = !self.marked;
107    }
108}
109
110#[derive(Debug)]
111enum View {
112    Filter,
113    All,
114    Marked,
115    Modified,
116}
117
118#[derive(PartialEq, Eq)]
119enum SnapshotNode {
120    Group(SnapshotGroup),
121    Snap(usize),
122}
123
124const INFO_TEXT: &str = "(Esc) quit | (F5) reload snapshots | (Enter) show contents | (v) toggle view | (i) show snapshot | (?) show all commands";
125
126const HELP_TEXT: &str = r"General Commands:
127  q, Esc : exit
128      F5 : re-read all snapshots from repository
129   Enter : show snapshot contents
130       v : toggle snapshot view [Filtered -> All -> Marked -> Modified]
131       V : modify filter to use     
132  Ctrl-v : reset filter
133       i : show detailed snapshot information for selected snapshot
134       w : write modified snapshots and delete snapshots to-forget
135       ? : show this help page
136 
137 Commands for marking snapshot(s):
138 
139       x : toggle marking for selected snapshot
140       X : toggle markings for all snapshots
141  Ctrl-x : clear all markings
142 
143 Commands applied to marked snapshot(s) (selected if none marked):
144 
145       f : toggle to-forget for snapshot(s)
146  Ctrl-f : clear to-forget for snapshot(s)
147       l : set label for snapshot(s)
148  Ctrl-l : remove label for snapshot(s)
149       d : set description for snapshot(s)
150  Ctrl-d : remove description for snapshot(s)
151       D : diff snapshots if 2 snapshots are selected or with parent
152       t : add tag(s) for snapshot(s)
153  Ctrl-t : remove all tags for snapshot(s)
154       s : set tag(s) for snapshot(s)
155       r : remove tag(s) for snapshot(s)
156       H : set hostname for snapshot(s) 
157       p : set delete protection for snapshot(s)
158  Ctrl-p : remove delete protection for snapshot(s)
159";
160
161pub struct Snapshots<'a, P, S> {
162    current_screen: CurrentScreen<'a, P, S>,
163    current_view: View,
164    table: WithBlock<SelectTable>,
165    repo: &'a Repository<P, S>,
166    snaps_status: Vec<SnapStatus>,
167    snapshots: Vec<SnapshotFile>,
168    original_snapshots: Vec<SnapshotFile>,
169    filtered_snapshots: Vec<usize>,
170    tree: Tree<SnapshotNode, usize>,
171    filter: SnapshotFilter,
172    default_filter: SnapshotFilter,
173    group_by: SnapshotGroupCriterion,
174    summary_map: SummaryMap,
175}
176
177impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> {
178    pub fn new(
179        repo: &'a Repository<P, S>,
180        filter: SnapshotFilter,
181        group_by: SnapshotGroupCriterion,
182    ) -> Result<Self> {
183        let header = [
184            "", " ID", "Time", "Host", "Label", "Tags", "Paths", "Files", "Dirs", "Size",
185        ]
186        .into_iter()
187        .map(Text::from)
188        .collect();
189
190        let mut app = Self {
191            current_screen: CurrentScreen::Snapshots,
192            current_view: View::Filter,
193            table: WithBlock::new(SelectTable::new(header), Block::new()),
194            repo,
195            snaps_status: Vec::new(),
196            original_snapshots: Vec::new(),
197            snapshots: Vec::new(),
198            filtered_snapshots: Vec::new(),
199            tree: Tree::Leaf(0),
200            default_filter: filter.clone(),
201            filter,
202            group_by,
203            summary_map: SummaryMap::default(),
204        };
205        app.reread()?;
206        Ok(app)
207    }
208
209    fn selected_tree(&self) -> Option<TreeIterItem<'_, SnapshotNode, usize>> {
210        self.table
211            .widget
212            .selected()
213            .and_then(|selected| self.tree.iter_open().nth(selected))
214    }
215
216    fn selected_tree_mut(&mut self) -> Option<&mut Tree<SnapshotNode, usize>> {
217        self.table
218            .widget
219            .selected()
220            .and_then(|selected| self.tree.nth_mut(selected))
221    }
222
223    fn snap_idx(&self) -> Vec<usize> {
224        self.selected_tree()
225            .iter()
226            .flat_map(|item| item.tree.iter().map(|item| item.tree))
227            .filter_map(|tree| tree.leaf_data().copied())
228            .collect()
229    }
230
231    fn selected_snapshot(&self) -> Option<&SnapshotFile> {
232        self.selected_tree().map(|tree_info| match tree_info.tree {
233            Tree::Leaf(index)
234            | Tree::Node(TreeNode {
235                data: SnapshotNode::Snap(index),
236                ..
237            }) => Some(&self.snapshots[*index]),
238            _ => None,
239        })?
240    }
241
242    pub fn has_mark(&self) -> bool {
243        self.snaps_status.iter().any(|s| s.marked)
244    }
245
246    pub fn has_modified(&self) -> bool {
247        self.snaps_status.iter().any(|s| s.modified)
248    }
249
250    pub fn toggle_view_mark(&mut self) {
251        match self.current_view {
252            View::Filter => self.current_view = View::All,
253            View::All => {
254                self.current_view = View::Marked;
255                if !self.has_mark() {
256                    self.toggle_view_mark();
257                }
258            }
259            View::Marked => {
260                self.current_view = View::Modified;
261                if !self.has_modified() {
262                    self.toggle_view_mark();
263                }
264            }
265            View::Modified => self.current_view = View::Filter,
266        }
267    }
268
269    pub fn toggle_view(&mut self) {
270        self.toggle_view_mark();
271        self.apply_view();
272    }
273
274    pub fn apply_view(&mut self) {
275        // select snapshots to show
276        self.filtered_snapshots = self
277            .snapshots
278            .iter()
279            .enumerate()
280            .zip(self.snaps_status.iter())
281            .filter_map(|((i, sn), status)| {
282                match self.current_view {
283                    View::All => true,
284                    View::Filter => self.filter.matches(sn),
285                    View::Marked => status.marked,
286                    View::Modified => status.modified,
287                }
288                .then_some(i)
289            })
290            .collect();
291        // TODO: Handle filter post-processing in case of current_view == View::Filter
292        self.create_tree();
293    }
294
295    pub fn create_tree(&mut self) {
296        // remember current snapshot index
297        let old_tree = self.selected_tree().map(|t| t.tree);
298
299        let mut result = Vec::new();
300        for (group, snaps) in &self
301            .filtered_snapshots
302            .iter()
303            .chunk_by(|i| SnapshotGroup::from_snapshot(&self.snapshots[**i], self.group_by))
304        {
305            let mut same_id_group = Vec::new();
306            for (_, s) in &snaps.into_iter().chunk_by(|i| self.snapshots[**i].tree) {
307                let leaves: Vec<_> = s.map(|i| Tree::leaf(*i)).collect();
308                let first = leaves[0].leaf_data().unwrap(); // Cannot be None as leaves[0] is a leaf!
309                if leaves.len() == 1 {
310                    same_id_group.push(Tree::leaf(*first));
311                } else {
312                    same_id_group.push(Tree::node(SnapshotNode::Snap(*first), false, leaves));
313                }
314            }
315            result.push(Tree::node(SnapshotNode::Group(group), false, same_id_group));
316        }
317        let tree = Tree::node(SnapshotNode::Snap(0), true, result);
318
319        let len = tree.iter_open().count();
320        let selected = if len == 0 {
321            None
322        } else {
323            Some(
324                tree.iter()
325                    .position(|info| Some(info.tree) == old_tree)
326                    .unwrap_or(len - 1),
327            )
328        };
329
330        self.tree = tree;
331        self.update_table();
332        self.table.widget.select(selected);
333    }
334
335    fn table_row(&self, info: TreeIterItem<'_, SnapshotNode, usize>) -> Vec<Text<'static>> {
336        let (has_mark, has_not_mark, has_modified, has_to_forget) = info
337            .tree
338            .iter()
339            .filter_map(|item| item.leaf_data().copied())
340            .fold(
341                (false, false, false, false),
342                |(mut a, mut b, mut c, mut d), i| {
343                    if self.snaps_status[i].marked {
344                        a = true;
345                    } else {
346                        b = true;
347                    }
348
349                    if self.snaps_status[i].modified {
350                        c = true;
351                    }
352
353                    if self.snaps_status[i].to_forget {
354                        d = true;
355                    }
356
357                    (a, b, c, d)
358                },
359            );
360
361        let mark = match (has_mark, has_not_mark) {
362            (false, _) => " ",
363            (true, true) => "*",
364            (true, false) => "X",
365        };
366        let modified = if has_modified { "*" } else { " " };
367        let del = if has_to_forget { "🗑" } else { "" };
368        let mut collapse = "  ".repeat(info.depth);
369        collapse.push_str(match info.tree {
370            Tree::Leaf(_) => "",
371            Tree::Node(TreeNode { open: false, .. }) => "\u{25b6} ", // Arrow to right
372            Tree::Node(TreeNode { open: true, .. }) => "\u{25bc} ",  // Arrow down
373        });
374
375        match info.tree {
376            Tree::Leaf(index)
377            | Tree::Node(TreeNode {
378                data: SnapshotNode::Snap(index),
379                ..
380            }) => {
381                let snap = &self.snapshots[*index];
382                let symbols = match (
383                    snap.delete == DeleteOption::NotSet,
384                    snap.description.is_none(),
385                ) {
386                    (true, true) => "",
387                    (true, false) => "🗎",
388                    (false, true) => "🛡",
389                    (false, false) => "🛡 🗎",
390                };
391                let count = info.tree.child_count();
392                once(&mark.to_string())
393                    .chain(snap_to_table(snap, count).iter())
394                    .cloned()
395                    .enumerate()
396                    .map(|(i, mut content)| {
397                        if i == 1 {
398                            // ID gets modified and protected marks
399                            content = format!("{collapse}{modified}{del}{content}{symbols}");
400                        }
401                        Text::from(content)
402                    })
403                    .collect()
404            }
405            Tree::Node(TreeNode {
406                data: SnapshotNode::Group(group),
407                ..
408            }) => {
409                let host = group
410                    .hostname
411                    .as_ref()
412                    .map(String::from)
413                    .unwrap_or_default();
414                let label = group.label.as_ref().map(String::from).unwrap_or_default();
415
416                let paths = group
417                    .paths
418                    .as_ref()
419                    .map_or_else(String::default, StringList::formatln);
420                let tags = group
421                    .tags
422                    .as_ref()
423                    .map_or_else(String::default, StringList::formatln);
424                [
425                    mark.to_string(),
426                    format!("{collapse}{modified}{del}group"),
427                    String::default(),
428                    host,
429                    label,
430                    tags,
431                    paths,
432                    String::default(),
433                    String::default(),
434                    String::default(),
435                ]
436                .into_iter()
437                .map(Text::from)
438                .collect()
439            }
440        }
441    }
442
443    pub fn update_table(&mut self) {
444        let max_tags = self
445            .filtered_snapshots
446            .iter()
447            .map(|&i| self.snapshots[i].tags.iter().count())
448            .max()
449            .unwrap_or(1);
450        let max_paths = self
451            .filtered_snapshots
452            .iter()
453            .map(|&i| self.snapshots[i].paths.iter().count())
454            .max()
455            .unwrap_or(1);
456        let height = max_tags.max(max_paths).max(1) + 1;
457
458        let rows = self
459            .tree
460            .iter_open()
461            .map(|tree| self.table_row(tree))
462            .collect();
463
464        self.table.widget.set_content(rows, height);
465        self.table.block = Block::new()
466            .borders(Borders::BOTTOM)
467            .title_bottom(format!(
468                "{:?} view: {}, total: {}, marked: {}, modified: {}, to forget: {}",
469                self.current_view,
470                self.filtered_snapshots.len(),
471                self.snapshots.len(),
472                self.count_marked_snaps(),
473                self.count_modified_snaps(),
474                self.count_forget_snaps()
475            ))
476            .title_alignment(Alignment::Center);
477    }
478
479    pub fn toggle_mark(&mut self) {
480        for snap_idx in self.snap_idx() {
481            self.snaps_status[snap_idx].toggle_mark();
482        }
483        self.update_table();
484    }
485
486    pub fn toggle_mark_all(&mut self) {
487        for snap_idx in &self.filtered_snapshots {
488            self.snaps_status[*snap_idx].toggle_mark();
489        }
490        self.update_table();
491    }
492
493    pub fn clear_marks(&mut self) {
494        for status in &mut self.snaps_status {
495            status.marked = false;
496        }
497        self.update_table();
498    }
499
500    pub fn reset_filter(&mut self) {
501        self.filter = self.default_filter.clone();
502        self.apply_view();
503    }
504
505    pub fn collapse(&mut self) {
506        if let Some(tree) = self.selected_tree_mut() {
507            tree.close();
508            self.update_table();
509        }
510    }
511
512    pub fn extendable(&self) -> bool {
513        matches!(self.selected_tree(), Some(tree_info) if tree_info.tree.openable())
514    }
515
516    pub fn extend(&mut self) {
517        if let Some(tree) = self.selected_tree_mut() {
518            tree.open();
519            self.update_table();
520        }
521    }
522
523    pub fn snapshot_details(&self) -> PopUpTable {
524        let mut rows = Vec::new();
525        if let Some(snap) = self.selected_snapshot() {
526            fill_table(snap, |title, value| {
527                rows.push(vec![Text::from(title.to_string()), Text::from(value)]);
528            });
529        }
530        popup_table("snapshot details", rows)
531    }
532
533    pub fn dir(&mut self) -> Result<Option<Ls<'a, P, S>>> {
534        self.selected_snapshot().cloned().map_or(Ok(None), |snap| {
535            Some(Ls::new(
536                self.repo,
537                snap,
538                "",
539                mem::take(&mut self.summary_map),
540            ))
541            .transpose()
542        })
543    }
544
545    pub fn diff(&mut self) -> Result<Option<Diff<'a, P, S>>> {
546        let snaps: Vec<_> = self
547            .snapshots
548            .iter()
549            .zip(self.snaps_status.iter())
550            .filter_map(|(snap, status)| status.marked.then_some(snap))
551            .collect();
552
553        let from_parent = |sn: &SnapshotFile| {
554            sn.parent.and_then(|parent| {
555                self.repo
556                    // TODO: get snapshot directly from ID, once implemented in Repository
557                    .get_snapshot_from_str(&parent.to_string(), |_| true)
558                    .ok()
559                    .map(|p| (p, sn.clone()))
560            })
561        };
562
563        let snaps = match snaps.len() {
564            2 => Some((snaps[0].clone(), snaps[1].clone())),
565            1 => from_parent(snaps[0]),
566            0 => self.selected_snapshot().and_then(from_parent),
567            _ => None,
568        };
569
570        if let Some((left, right)) = snaps {
571            Some(Diff::new(
572                self.repo,
573                left,
574                right,
575                "",
576                "",
577                mem::take(&mut self.summary_map),
578            ))
579            .transpose()
580        } else {
581            Ok(None)
582        }
583    }
584
585    pub fn count_marked_snaps(&self) -> usize {
586        self.snaps_status.iter().filter(|s| s.marked).count()
587    }
588
589    pub fn count_modified_snaps(&self) -> usize {
590        self.snaps_status.iter().filter(|s| s.modified).count()
591    }
592
593    pub fn count_forget_snaps(&self) -> usize {
594        self.snaps_status.iter().filter(|s| s.to_forget).count()
595    }
596
597    // process marked snapshots (or the current one if none is marked)
598    // the process function must return true if it modified the snapshot, else false
599    pub fn process_marked_snaps(&mut self, mut process: impl FnMut(&mut SnapshotFile) -> bool) {
600        let has_mark = self.has_mark();
601
602        if !has_mark {
603            self.toggle_mark();
604        }
605
606        for ((snap, status), original_snap) in self
607            .snapshots
608            .iter_mut()
609            .zip(self.snaps_status.iter_mut())
610            .zip(self.original_snapshots.iter())
611        {
612            if status.marked && process(snap) {
613                // Note that snap impls Eq, but only by comparing the time!
614                status.modified =
615                    serde_json::to_string(snap).ok() != serde_json::to_string(original_snap).ok();
616            }
617        }
618
619        if !has_mark {
620            self.toggle_mark();
621        }
622        self.update_table();
623    }
624
625    pub fn get_snap_entity(&mut self, f: impl Fn(&SnapshotFile) -> String) -> String {
626        let has_mark = self.has_mark();
627
628        if !has_mark {
629            self.toggle_mark();
630        }
631
632        let entity = self
633            .snapshots
634            .iter()
635            .zip(self.snaps_status.iter())
636            .filter_map(|(snap, status)| status.marked.then_some(f(snap)))
637            .reduce(|entity, e| if entity == e { e } else { String::new() })
638            .unwrap_or_default();
639
640        if !has_mark {
641            self.toggle_mark();
642        }
643        entity
644    }
645
646    pub fn get_label(&mut self) -> String {
647        self.get_snap_entity(|snap| snap.label.clone())
648    }
649
650    pub fn get_tags(&mut self) -> String {
651        self.get_snap_entity(|snap| snap.tags.formatln())
652    }
653
654    pub fn get_hostname(&mut self) -> String {
655        self.get_snap_entity(|snap| snap.hostname.clone())
656    }
657
658    pub fn get_description(&mut self) -> String {
659        self.get_snap_entity(|snap| snap.description.clone().unwrap_or_default())
660    }
661
662    pub fn get_filter(&self) -> Result<String> {
663        Ok(toml::to_string_pretty(&self.filter)?)
664    }
665
666    pub fn set_filter(&mut self, filter: String) {
667        if let Ok(filter) = toml::from_str::<SnapshotFilter>(&filter) {
668            self.filter = filter;
669            self.apply_view();
670        }
671    }
672
673    pub fn set_property(&mut self, property: SnapshotProperty, value: String) {
674        self.process_marked_snaps(|snap| property.process(snap, &value));
675    }
676
677    pub fn clear_label(&mut self) {
678        self.set_property(SnapshotProperty::Label, String::new());
679    }
680
681    pub fn clear_description(&mut self) {
682        self.set_property(SnapshotProperty::Description, String::new());
683    }
684
685    pub fn clear_tags(&mut self) {
686        self.set_property(SnapshotProperty::SetTags, String::new());
687    }
688
689    pub fn set_delete_protection_to(&mut self, delete: DeleteOption) {
690        self.process_marked_snaps(|snap| {
691            if snap.delete == delete {
692                return false;
693            }
694            snap.delete = delete;
695            true
696        });
697    }
698
699    pub fn toggle_to_forget(&mut self) {
700        let has_mark = self.has_mark();
701
702        if !has_mark {
703            self.toggle_mark();
704        }
705
706        let now = Local::now();
707        for (snap, status) in self.snapshots.iter_mut().zip(self.snaps_status.iter_mut()) {
708            if status.marked {
709                if status.to_forget {
710                    status.to_forget = false;
711                } else if !snap.must_keep(now) {
712                    status.to_forget = true;
713                }
714            }
715        }
716
717        if !has_mark {
718            self.toggle_mark();
719        }
720        self.update_table();
721    }
722
723    pub fn clear_to_forget(&mut self) {
724        for status in &mut self.snaps_status {
725            status.to_forget = false;
726        }
727        self.update_table();
728    }
729
730    pub fn apply_input(&mut self, input: String) {
731        match self.current_screen {
732            CurrentScreen::EnterProperty((_, prop)) => self.set_property(prop, input),
733            CurrentScreen::EnterFilter(_) => self.set_filter(input),
734            _ => {}
735        }
736    }
737
738    pub fn set_delete_protection(&mut self) {
739        self.set_delete_protection_to(DeleteOption::Never);
740    }
741
742    pub fn clear_delete_protection(&mut self) {
743        self.set_delete_protection_to(DeleteOption::NotSet);
744    }
745
746    pub fn write(&mut self) -> Result<()> {
747        if !self.has_modified() && self.count_forget_snaps() == 0 {
748            return Ok(());
749        };
750
751        let save_snaps: Vec<_> = self
752            .snapshots
753            .iter()
754            .zip(self.snaps_status.iter())
755            .filter_map(|(snap, status)| (status.modified && !status.to_forget).then_some(snap))
756            .cloned()
757            .collect();
758        let old_snap_ids = save_snaps.iter().map(|sn| sn.id);
759        let snap_ids_to_forget = self
760            .snapshots
761            .iter()
762            .zip(self.snaps_status.iter())
763            .filter_map(|(snap, status)| status.to_forget.then_some(snap.id));
764        let delete_ids: Vec<_> = old_snap_ids.chain(snap_ids_to_forget).collect();
765        self.repo.save_snapshots(save_snaps)?;
766        self.repo.delete_snapshots(&delete_ids)?;
767        // remove snapshots-to-reread
768        let ids: BTreeSet<_> = delete_ids.into_iter().collect();
769        self.snapshots.retain(|snap| !ids.contains(&snap.id));
770        // re-read snapshots
771        self.reread()
772    }
773
774    // re-read all snapshots
775    pub fn reread(&mut self) -> Result<()> {
776        let snapshots = mem::take(&mut self.snapshots);
777        self.snapshots = self.repo.update_all_snapshots(snapshots)?;
778        self.snapshots
779            .sort_unstable_by(|sn1, sn2| sn1.cmp_group(self.group_by, sn2).then(sn1.cmp(sn2)));
780        self.snaps_status = vec![SnapStatus::default(); self.snapshots.len()];
781        self.original_snapshots.clone_from(&self.snapshots);
782        self.table.widget.select(None);
783        self.apply_view();
784        Ok(())
785    }
786}
787
788impl<'a, P: ProgressBars, S: IndexedFull> ProcessEvent for Snapshots<'a, P, S> {
789    type Result = Result<bool>;
790    fn input(&mut self, event: Event) -> Result<bool> {
791        use KeyCode::{Char, Enter, Esc, F, Left, Right};
792        match &mut self.current_screen {
793            CurrentScreen::Snapshots => {
794                match event {
795                    Event::Key(key) if key.kind == KeyEventKind::Press => {
796                        if key.modifiers == KeyModifiers::CONTROL {
797                            match key.code {
798                                Char('f') => self.clear_to_forget(),
799                                Char('x') => self.clear_marks(),
800                                Char('l') => self.clear_label(),
801                                Char('d') => self.clear_description(),
802                                Char('t') => self.clear_tags(),
803                                Char('p') => self.clear_delete_protection(),
804                                Char('v') => self.reset_filter(),
805                                _ => {}
806                            }
807                        } else {
808                            match key.code {
809                                Esc | Char('q') => {
810                                    self.current_screen = CurrentScreen::PromptExit(popup_prompt(
811                                        "exit rustic",
812                                        "do you want to exit? (y/n)".into(),
813                                    ));
814                                }
815                                Char('f') => self.toggle_to_forget(),
816                                F(5) => self.reread()?,
817                                Enter => {
818                                    if let Some(dir) = self.dir()? {
819                                        self.current_screen = CurrentScreen::Dir(Box::new(dir));
820                                    }
821                                }
822                                Right => {
823                                    if self.extendable() {
824                                        self.extend();
825                                    } else if let Some(dir) = self.dir()? {
826                                        self.current_screen = CurrentScreen::Dir(Box::new(dir));
827                                    }
828                                }
829                                Char('+') => {
830                                    if self.extendable() {
831                                        self.extend();
832                                    }
833                                }
834                                Left | Char('-') => self.collapse(),
835                                Char('?') => {
836                                    self.current_screen = CurrentScreen::ShowHelp(popup_text(
837                                        "help",
838                                        HELP_TEXT.into(),
839                                    ));
840                                }
841                                Char('x') => {
842                                    self.toggle_mark();
843                                    self.table.widget.next();
844                                }
845                                Char('X') => self.toggle_mark_all(),
846                                Char('v') => self.toggle_view(),
847                                Char('V') => {
848                                    self.current_screen = CurrentScreen::EnterFilter(popup_input(
849                                        "set filter (Ctrl-s to confirm)",
850                                        "enter filter in TOML format",
851                                        &self.get_filter()?,
852                                        15,
853                                    ));
854                                }
855                                Char('i') => {
856                                    self.current_screen =
857                                        CurrentScreen::SnapshotDetails(self.snapshot_details());
858                                }
859                                Char('l') => {
860                                    self.current_screen = CurrentScreen::EnterProperty((
861                                        popup_input(
862                                            "set label",
863                                            "enter label",
864                                            &self.get_label(),
865                                            1,
866                                        ),
867                                        SnapshotProperty::Label,
868                                    ));
869                                }
870                                Char('d') => {
871                                    self.current_screen = CurrentScreen::EnterProperty((
872                                        popup_input(
873                                            "set description (Ctrl-s to confirm)",
874                                            "enter description",
875                                            &self.get_description(),
876                                            5,
877                                        ),
878                                        SnapshotProperty::Description,
879                                    ));
880                                }
881                                Char('D') => {
882                                    if let Some(diff) = self.diff()? {
883                                        self.current_screen = CurrentScreen::Diff(Box::new(diff));
884                                    }
885                                }
886                                Char('t') => {
887                                    self.current_screen = CurrentScreen::EnterProperty((
888                                        popup_input("add tags", "enter tags", "", 1),
889                                        SnapshotProperty::AddTags,
890                                    ));
891                                }
892                                Char('s') => {
893                                    self.current_screen = CurrentScreen::EnterProperty((
894                                        popup_input("set tags", "enter tags", &self.get_tags(), 1),
895                                        SnapshotProperty::SetTags,
896                                    ));
897                                }
898                                Char('r') => {
899                                    self.current_screen = CurrentScreen::EnterProperty((
900                                        popup_input("remove tags", "enter tags", "", 1),
901                                        SnapshotProperty::RemoveTags,
902                                    ));
903                                }
904                                Char('H') => {
905                                    self.current_screen = CurrentScreen::EnterProperty((
906                                        popup_input(
907                                            "set hostname",
908                                            "enter hostname",
909                                            &self.get_hostname(),
910                                            1,
911                                        ),
912                                        SnapshotProperty::Hostname,
913                                    ));
914                                }
915                                // TODO: Allow to enter delete protection option
916                                Char('p') => self.set_delete_protection(),
917                                Char('w') => {
918                                    let msg = format!(
919                                        "Do you want to write {} modified and remove {} snapshots? (y/n)",
920                                        self.count_modified_snaps(),
921                                        self.count_forget_snaps()
922                                    );
923                                    self.current_screen = CurrentScreen::PromptWrite(popup_prompt(
924                                        "write snapshots",
925                                        msg.into(),
926                                    ));
927                                }
928                                _ => self.table.input(event),
929                            }
930                        }
931                    }
932                    _ => {}
933                }
934            }
935            CurrentScreen::SnapshotDetails(_) | CurrentScreen::ShowHelp(_) => match event {
936                Event::Key(key) if key.kind == KeyEventKind::Press => {
937                    if matches!(key.code, Char('q' | ' ' | 'i' | '?') | Esc | Enter) {
938                        self.current_screen = CurrentScreen::Snapshots;
939                    }
940                }
941                _ => {}
942            },
943            CurrentScreen::EnterProperty((prompt, _)) | CurrentScreen::EnterFilter(prompt) => {
944                match prompt.input(event) {
945                    TextInputResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
946                    TextInputResult::Input(input) => {
947                        self.apply_input(input);
948                        self.current_screen = CurrentScreen::Snapshots;
949                    }
950                    TextInputResult::None => {}
951                }
952            }
953            CurrentScreen::PromptWrite(prompt) => match prompt.input(event) {
954                PromptResult::Ok => {
955                    self.write()?;
956                    self.current_screen = CurrentScreen::Snapshots;
957                }
958                PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
959                PromptResult::None => {}
960            },
961            CurrentScreen::PromptExit(prompt) => match prompt.input(event) {
962                PromptResult::Ok => return Ok(true),
963                PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
964                PromptResult::None => {}
965            },
966            CurrentScreen::Dir(dir) => match dir.input(event)? {
967                LsResult::Exit => return Ok(true),
968                LsResult::Return(summary_map) => {
969                    self.current_screen = CurrentScreen::Snapshots;
970                    self.summary_map = summary_map;
971                }
972                LsResult::None => {}
973            },
974            CurrentScreen::Diff(diff) => match diff.input(event)? {
975                DiffResult::Exit => return Ok(true),
976                DiffResult::Return(summary_map) => {
977                    self.current_screen = CurrentScreen::Snapshots;
978                    self.summary_map = summary_map;
979                }
980                DiffResult::None => {}
981            },
982        }
983        Ok(false)
984    }
985}
986
987impl<'a, P: ProgressBars, S: IndexedFull> Draw for Snapshots<'a, P, S> {
988    fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
989        if let CurrentScreen::Dir(dir) = &mut self.current_screen {
990            dir.draw(area, f);
991            return;
992        }
993
994        if let CurrentScreen::Diff(diff) = &mut self.current_screen {
995            diff.draw(area, f);
996            return;
997        }
998
999        let rects = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).split(area);
1000
1001        // draw the table
1002        self.table.draw(rects[0], f);
1003
1004        // draw the footer
1005        let buffer_bg = tailwind::SLATE.c950;
1006        let row_fg = tailwind::SLATE.c200;
1007        let info_footer = Paragraph::new(Line::from(INFO_TEXT))
1008            .style(Style::new().fg(row_fg).bg(buffer_bg))
1009            .centered();
1010        f.render_widget(info_footer, rects[1]);
1011
1012        // draw popups
1013        match &mut self.current_screen {
1014            CurrentScreen::SnapshotDetails(popup) => popup.draw(area, f),
1015            CurrentScreen::ShowHelp(popup) => popup.draw(area, f),
1016            CurrentScreen::EnterProperty((popup, _)) | CurrentScreen::EnterFilter(popup) => {
1017                popup.draw(area, f);
1018            }
1019            CurrentScreen::PromptWrite(popup) | CurrentScreen::PromptExit(popup) => {
1020                popup.draw(area, f);
1021            }
1022            _ => {}
1023        }
1024    }
1025}