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