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
36enum 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#[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 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 self.create_tree();
293 }
294
295 pub fn create_tree(&mut self) {
296 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(); 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} ", Tree::Node(TreeNode { open: true, .. }) => "\u{25bc} ", });
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 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 .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 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 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 let ids: BTreeSet<_> = delete_ids.into_iter().collect();
769 self.snapshots.retain(|snap| !ids.contains(&snap.id));
770 self.reread()
772 }
773
774 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 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 self.table.draw(rects[0], f);
1003
1004 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 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}