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
38enum 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#[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 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 self.create_tree();
296 }
297
298 pub fn create_tree(&mut self) {
299 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(); 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} ", Tree::Node(TreeNode { open: true, .. }) => "\u{25bc} ", });
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 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 let mut summary_map = mem::take(&mut self.summary_map);
542
543 self.process_marked_snaps(|snap| {
544 _ = 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 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 .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 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 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 let ids: BTreeSet<_> = delete_ids.into_iter().collect();
804 self.snapshots.retain(|snap| !ids.contains(&snap.id));
805 self.reread()
807 }
808
809 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 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 self.table.draw(rects[0], f);
1045
1046 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 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}