Skip to main content

rustic_rs/commands/tui/
snapshots.rs

1use std::{collections::BTreeSet, iter::once, mem, str::FromStr};
2
3use anyhow::Result;
4use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
5use itertools::Itertools;
6use jiff::Zoned;
7use ratatui::{
8    prelude::*,
9    widgets::{Block, Borders, Paragraph},
10};
11use rustic_core::{
12    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            summary::StatisticsBuilder,
24            tree::{Tree, TreeIterItem, TreeNode},
25            widgets::{
26                Draw, PopUpInput, PopUpPrompt, PopUpTable, PopUpText, ProcessEvent, PromptResult,
27                SelectTable, TextInputResult, WithBlock, popup_input, popup_prompt, popup_table,
28                popup_text,
29            },
30        },
31    },
32    filtering::SnapshotFilter,
33    repository::IndexedRepo,
34};
35
36use super::summary::SummaryMap;
37
38// the states this screen can be in
39enum CurrentScreen<'a> {
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>>),
48    Diff(Box<Diff<'a>>),
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> {
165    current_screen: CurrentScreen<'a>,
166    current_view: View,
167    table: WithBlock<SelectTable>,
168    repo: &'a IndexedRepo,
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> Snapshots<'a> {
181    pub fn new(
182        repo: &'a IndexedRepo,
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 p = self
538            .repo
539            .progress_counter("computing (sub)-dir information");
540        // Take out summary_map to fix lifetime issues
541        let mut summary_map = mem::take(&mut self.summary_map);
542
543        self.process_marked_snaps(|snap| {
544            // TODO: error handling
545            _ = summary_map.compute(self.repo, snap.tree, &p);
546            false
547        });
548
549        let mut stats_builder = StatisticsBuilder::default();
550        let mut count = 0;
551        self.process_marked_snaps(|snap| {
552            stats_builder.append_from_tree(snap.tree, &summary_map);
553            count += 1;
554            false
555        });
556        p.finish();
557        let stats = stats_builder.build(self.repo)?;
558
559        // put summary_map back into place
560        self.summary_map = summary_map;
561
562        Ok(popup_table(
563            "snapshots summary",
564            stats.table(format!("{count} snapshot(s)")),
565        ))
566    }
567
568    pub fn dir(&mut self) -> Result<Option<Ls<'a>>> {
569        self.selected_snapshot().cloned().map_or(Ok(None), |snap| {
570            Some(Ls::new(
571                self.repo,
572                snap,
573                "",
574                mem::take(&mut self.summary_map),
575            ))
576            .transpose()
577        })
578    }
579
580    pub fn diff(&mut self) -> Result<Option<Diff<'a>>> {
581        let snaps: Vec<_> = self
582            .snapshots
583            .iter()
584            .zip(self.snaps_status.iter())
585            .filter_map(|(snap, status)| status.marked.then_some(snap))
586            .collect();
587
588        let from_parent = |sn: &SnapshotFile| {
589            sn.parent.and_then(|parent| {
590                self.repo
591                    // TODO: get snapshot directly from ID, once implemented in Repository
592                    .get_snapshot_from_str(&parent.to_string(), |_| true)
593                    .ok()
594                    .map(|p| (p, sn.clone()))
595            })
596        };
597
598        let snaps = match snaps.len() {
599            2 => Some((snaps[0].clone(), snaps[1].clone())),
600            1 => from_parent(snaps[0]),
601            0 => self.selected_snapshot().and_then(from_parent),
602            _ => None,
603        };
604
605        if let Some((left, right)) = snaps {
606            Some(Diff::new(
607                self.repo,
608                left,
609                right,
610                "",
611                "",
612                mem::take(&mut self.summary_map),
613            ))
614            .transpose()
615        } else {
616            Ok(None)
617        }
618    }
619
620    pub fn count_marked_snaps(&self) -> usize {
621        self.snaps_status.iter().filter(|s| s.marked).count()
622    }
623
624    pub fn count_modified_snaps(&self) -> usize {
625        self.snaps_status.iter().filter(|s| s.modified).count()
626    }
627
628    pub fn count_forget_snaps(&self) -> usize {
629        self.snaps_status.iter().filter(|s| s.to_forget).count()
630    }
631
632    // process marked snapshots (or the current one if none is marked)
633    // the process function must return true if it modified the snapshot, else false
634    pub fn process_marked_snaps(&mut self, mut process: impl FnMut(&mut SnapshotFile) -> bool) {
635        let has_mark = self.has_mark();
636
637        if !has_mark {
638            self.toggle_mark();
639        }
640
641        for ((snap, status), original_snap) in self
642            .snapshots
643            .iter_mut()
644            .zip(self.snaps_status.iter_mut())
645            .zip(self.original_snapshots.iter())
646        {
647            if status.marked && process(snap) {
648                // Note that snap impls Eq, but only by comparing the time!
649                status.modified =
650                    serde_json::to_string(snap).ok() != serde_json::to_string(original_snap).ok();
651            }
652        }
653
654        if !has_mark {
655            self.toggle_mark();
656        }
657        self.update_table();
658    }
659
660    pub fn get_snap_entity(&mut self, f: impl Fn(&SnapshotFile) -> String) -> String {
661        let has_mark = self.has_mark();
662
663        if !has_mark {
664            self.toggle_mark();
665        }
666
667        let entity = self
668            .snapshots
669            .iter()
670            .zip(self.snaps_status.iter())
671            .filter_map(|(snap, status)| status.marked.then_some(f(snap)))
672            .reduce(|entity, e| if entity == e { e } else { String::new() })
673            .unwrap_or_default();
674
675        if !has_mark {
676            self.toggle_mark();
677        }
678        entity
679    }
680
681    pub fn get_label(&mut self) -> String {
682        self.get_snap_entity(|snap| snap.label.clone())
683    }
684
685    pub fn get_tags(&mut self) -> String {
686        self.get_snap_entity(|snap| snap.tags.formatln())
687    }
688
689    pub fn get_hostname(&mut self) -> String {
690        self.get_snap_entity(|snap| snap.hostname.clone())
691    }
692
693    pub fn get_description(&mut self) -> String {
694        self.get_snap_entity(|snap| snap.description.clone().unwrap_or_default())
695    }
696
697    pub fn get_filter(&self) -> Result<String> {
698        Ok(toml::to_string_pretty(&self.filter)?)
699    }
700
701    pub fn set_filter(&mut self, filter: String) {
702        if let Ok(filter) = toml::from_str::<SnapshotFilter>(&filter) {
703            self.filter = filter;
704            self.apply_view();
705        }
706    }
707
708    pub fn set_property(&mut self, property: SnapshotProperty, value: String) {
709        self.process_marked_snaps(|snap| property.process(snap, &value));
710    }
711
712    pub fn clear_label(&mut self) {
713        self.set_property(SnapshotProperty::Label, String::new());
714    }
715
716    pub fn clear_description(&mut self) {
717        self.set_property(SnapshotProperty::Description, String::new());
718    }
719
720    pub fn clear_tags(&mut self) {
721        self.set_property(SnapshotProperty::SetTags, String::new());
722    }
723
724    pub fn set_delete_protection_to(&mut self, delete: DeleteOption) {
725        self.process_marked_snaps(|snap| {
726            if snap.delete == delete {
727                return false;
728            }
729            snap.delete = delete.clone();
730            true
731        });
732    }
733
734    pub fn toggle_to_forget(&mut self) {
735        let has_mark = self.has_mark();
736
737        if !has_mark {
738            self.toggle_mark();
739        }
740
741        let now = Zoned::now();
742        for (snap, status) in self.snapshots.iter_mut().zip(self.snaps_status.iter_mut()) {
743            if status.marked {
744                if status.to_forget {
745                    status.to_forget = false;
746                } else if !snap.must_keep(&now) {
747                    status.to_forget = true;
748                }
749            }
750        }
751
752        if !has_mark {
753            self.toggle_mark();
754        }
755        self.update_table();
756    }
757
758    pub fn clear_to_forget(&mut self) {
759        for status in &mut self.snaps_status {
760            status.to_forget = false;
761        }
762        self.update_table();
763    }
764
765    pub fn apply_input(&mut self, input: String) {
766        match self.current_screen {
767            CurrentScreen::EnterProperty((_, prop)) => self.set_property(prop, input),
768            CurrentScreen::EnterFilter(_) => self.set_filter(input),
769            _ => {}
770        }
771    }
772
773    pub fn set_delete_protection(&mut self) {
774        self.set_delete_protection_to(DeleteOption::Never);
775    }
776
777    pub fn clear_delete_protection(&mut self) {
778        self.set_delete_protection_to(DeleteOption::NotSet);
779    }
780
781    pub fn write(&mut self) -> Result<()> {
782        if !self.has_modified() && self.count_forget_snaps() == 0 {
783            return Ok(());
784        };
785
786        let save_snaps: Vec<_> = self
787            .snapshots
788            .iter()
789            .zip(self.snaps_status.iter())
790            .filter_map(|(snap, status)| (status.modified && !status.to_forget).then_some(snap))
791            .cloned()
792            .collect();
793        let old_snap_ids = save_snaps.iter().map(|sn| sn.id);
794        let snap_ids_to_forget = self
795            .snapshots
796            .iter()
797            .zip(self.snaps_status.iter())
798            .filter_map(|(snap, status)| status.to_forget.then_some(snap.id));
799        let delete_ids: Vec<_> = old_snap_ids.chain(snap_ids_to_forget).collect();
800        self.repo.save_snapshots(save_snaps)?;
801        self.repo.delete_snapshots(&delete_ids)?;
802        // remove snapshots-to-reread
803        let ids: BTreeSet<_> = delete_ids.into_iter().collect();
804        self.snapshots.retain(|snap| !ids.contains(&snap.id));
805        // re-read snapshots
806        self.reread()
807    }
808
809    // re-read all snapshots
810    pub fn reread(&mut self) -> Result<()> {
811        let snapshots = mem::take(&mut self.snapshots);
812        self.snapshots = self.repo.update_all_snapshots(snapshots)?;
813        self.snapshots.sort_unstable_by(|sn1, sn2| {
814            SnapshotGroup::from_snapshot(sn1, self.group_by)
815                .cmp(&SnapshotGroup::from_snapshot(sn1, self.group_by))
816                .then(sn1.time.cmp(&sn2.time))
817        });
818        self.snaps_status = vec![SnapStatus::default(); self.snapshots.len()];
819        self.original_snapshots.clone_from(&self.snapshots);
820        self.table.widget.select(None);
821        self.apply_view();
822        Ok(())
823    }
824}
825
826impl<'a> ProcessEvent for Snapshots<'a> {
827    type Result = Result<bool>;
828    fn input(&mut self, event: Event) -> Result<bool> {
829        use KeyCode::{Char, Enter, Esc, F, Left, Right};
830        match &mut self.current_screen {
831            CurrentScreen::Snapshots => {
832                match event {
833                    Event::Key(key) if key.kind == KeyEventKind::Press => {
834                        if key.modifiers == KeyModifiers::CONTROL {
835                            match key.code {
836                                Char('f') => self.clear_to_forget(),
837                                Char('x') | Char(' ') => self.clear_marks(),
838                                Char('l') => self.clear_label(),
839                                Char('d') => self.clear_description(),
840                                Char('t') => self.clear_tags(),
841                                Char('p') => self.clear_delete_protection(),
842                                Char('v') => self.reset_filter(),
843                                _ => {}
844                            }
845                        } else {
846                            match key.code {
847                                Esc | Char('q') => {
848                                    self.current_screen = CurrentScreen::PromptExit(popup_prompt(
849                                        "exit rustic",
850                                        "do you want to exit? (y/n)".into(),
851                                    ));
852                                }
853                                Char('f') => self.toggle_to_forget(),
854                                F(5) => self.reread()?,
855                                Enter => {
856                                    if let Some(dir) = self.dir()? {
857                                        self.current_screen = CurrentScreen::Dir(Box::new(dir));
858                                    }
859                                }
860                                Right => {
861                                    if self.extendable() {
862                                        self.extend();
863                                    } else if let Some(dir) = self.dir()? {
864                                        self.current_screen = CurrentScreen::Dir(Box::new(dir));
865                                    }
866                                }
867                                Char('+') => {
868                                    if self.extendable() {
869                                        self.extend();
870                                    }
871                                }
872                                Left | Char('-') => self.collapse(),
873                                Char('?') => {
874                                    self.current_screen = CurrentScreen::ShowHelp(popup_text(
875                                        "help",
876                                        HELP_TEXT.into(),
877                                    ));
878                                }
879                                Char('x') | Char(' ') => {
880                                    self.toggle_mark();
881                                    self.table.widget.next();
882                                }
883                                Char('X') => self.toggle_mark_all(),
884                                Char('v') => self.toggle_view(),
885                                Char('V') => {
886                                    self.current_screen = CurrentScreen::EnterFilter(popup_input(
887                                        "set filter (Ctrl-s to confirm)",
888                                        "enter filter in TOML format",
889                                        &self.get_filter()?,
890                                        15,
891                                    ));
892                                }
893                                Char('i') => {
894                                    self.current_screen =
895                                        CurrentScreen::Table(self.snapshot_details());
896                                }
897                                Char('S') => {
898                                    self.current_screen =
899                                        CurrentScreen::Table(self.snapshots_summary()?);
900                                }
901                                Char('l') => {
902                                    self.current_screen = CurrentScreen::EnterProperty((
903                                        popup_input(
904                                            "set label",
905                                            "enter label",
906                                            &self.get_label(),
907                                            1,
908                                        ),
909                                        SnapshotProperty::Label,
910                                    ));
911                                }
912                                Char('d') => {
913                                    self.current_screen = CurrentScreen::EnterProperty((
914                                        popup_input(
915                                            "set description (Ctrl-s to confirm)",
916                                            "enter description",
917                                            &self.get_description(),
918                                            5,
919                                        ),
920                                        SnapshotProperty::Description,
921                                    ));
922                                }
923                                Char('D') => {
924                                    if let Some(diff) = self.diff()? {
925                                        self.current_screen = CurrentScreen::Diff(Box::new(diff));
926                                    }
927                                }
928                                Char('t') => {
929                                    self.current_screen = CurrentScreen::EnterProperty((
930                                        popup_input("add tags", "enter tags", "", 1),
931                                        SnapshotProperty::AddTags,
932                                    ));
933                                }
934                                Char('s') => {
935                                    self.current_screen = CurrentScreen::EnterProperty((
936                                        popup_input("set tags", "enter tags", &self.get_tags(), 1),
937                                        SnapshotProperty::SetTags,
938                                    ));
939                                }
940                                Char('r') => {
941                                    self.current_screen = CurrentScreen::EnterProperty((
942                                        popup_input("remove tags", "enter tags", "", 1),
943                                        SnapshotProperty::RemoveTags,
944                                    ));
945                                }
946                                Char('H') => {
947                                    self.current_screen = CurrentScreen::EnterProperty((
948                                        popup_input(
949                                            "set hostname",
950                                            "enter hostname",
951                                            &self.get_hostname(),
952                                            1,
953                                        ),
954                                        SnapshotProperty::Hostname,
955                                    ));
956                                }
957                                // TODO: Allow to enter delete protection option
958                                Char('p') => self.set_delete_protection(),
959                                Char('w') => {
960                                    let msg = format!(
961                                        "Do you want to write {} modified and remove {} snapshots? (y/n)",
962                                        self.count_modified_snaps(),
963                                        self.count_forget_snaps()
964                                    );
965                                    self.current_screen = CurrentScreen::PromptWrite(popup_prompt(
966                                        "write snapshots",
967                                        msg.into(),
968                                    ));
969                                }
970                                _ => self.table.input(event),
971                            }
972                        }
973                    }
974                    _ => {}
975                }
976            }
977            CurrentScreen::Table(_) | CurrentScreen::ShowHelp(_) => match event {
978                Event::Key(key) if key.kind == KeyEventKind::Press => {
979                    if matches!(key.code, Char('q' | ' ' | 'i' | '?') | Esc | Enter) {
980                        self.current_screen = CurrentScreen::Snapshots;
981                    }
982                }
983                _ => {}
984            },
985            CurrentScreen::EnterProperty((prompt, _)) | CurrentScreen::EnterFilter(prompt) => {
986                match prompt.input(event) {
987                    TextInputResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
988                    TextInputResult::Input(input) => {
989                        self.apply_input(input);
990                        self.current_screen = CurrentScreen::Snapshots;
991                    }
992                    TextInputResult::None => {}
993                }
994            }
995            CurrentScreen::PromptWrite(prompt) => match prompt.input(event) {
996                PromptResult::Ok => {
997                    self.write()?;
998                    self.current_screen = CurrentScreen::Snapshots;
999                }
1000                PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
1001                PromptResult::None => {}
1002            },
1003            CurrentScreen::PromptExit(prompt) => match prompt.input(event) {
1004                PromptResult::Ok => return Ok(true),
1005                PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
1006                PromptResult::None => {}
1007            },
1008            CurrentScreen::Dir(dir) => match dir.input(event)? {
1009                LsResult::Exit => return Ok(true),
1010                LsResult::Return(summary_map) => {
1011                    self.current_screen = CurrentScreen::Snapshots;
1012                    self.summary_map = summary_map;
1013                }
1014                LsResult::None => {}
1015            },
1016            CurrentScreen::Diff(diff) => match diff.input(event)? {
1017                DiffResult::Exit => return Ok(true),
1018                DiffResult::Return(summary_map) => {
1019                    self.current_screen = CurrentScreen::Snapshots;
1020                    self.summary_map = summary_map;
1021                }
1022                DiffResult::None => {}
1023            },
1024        }
1025        Ok(false)
1026    }
1027}
1028
1029impl<'a> Draw for Snapshots<'a> {
1030    fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
1031        if let CurrentScreen::Dir(dir) = &mut self.current_screen {
1032            dir.draw(area, f);
1033            return;
1034        }
1035
1036        if let CurrentScreen::Diff(diff) = &mut self.current_screen {
1037            diff.draw(area, f);
1038            return;
1039        }
1040
1041        let rects = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).split(area);
1042
1043        // draw the table
1044        self.table.draw(rects[0], f);
1045
1046        // draw the footer
1047        let buffer_bg = tailwind::SLATE.c950;
1048        let row_fg = tailwind::SLATE.c200;
1049        let info_footer = Paragraph::new(Line::from(INFO_TEXT))
1050            .style(Style::new().fg(row_fg).bg(buffer_bg))
1051            .centered();
1052        f.render_widget(info_footer, rects[1]);
1053
1054        // draw popups
1055        match &mut self.current_screen {
1056            CurrentScreen::Table(popup) => popup.draw(area, f),
1057            CurrentScreen::ShowHelp(popup) => popup.draw(area, f),
1058            CurrentScreen::EnterProperty((popup, _)) | CurrentScreen::EnterFilter(popup) => {
1059                popup.draw(area, f);
1060            }
1061            CurrentScreen::PromptWrite(popup) | CurrentScreen::PromptExit(popup) => {
1062                popup.draw(area, f);
1063            }
1064            _ => {}
1065        }
1066    }
1067}