1use std::{collections::BTreeSet, iter::once, mem, str::FromStr};
2
3use anyhow::Result;
4use chrono::Local;
5use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
6use itertools::Itertools;
7use ratatui::{
8 prelude::*,
9 widgets::{Block, Borders, Paragraph},
10};
11use rustic_core::{
12 IndexedFull, Progress, ProgressBars, Repository, SnapshotGroup, SnapshotGroupCriterion,
13 StringList,
14 repofile::{DeleteOption, SnapshotFile},
15};
16use style::palette::tailwind;
17
18use crate::{
19 commands::{
20 snapshots::{fill_table, snap_to_table},
21 tui::{
22 diff::{Diff, DiffResult},
23 ls::{Ls, LsResult},
24 summary::StatisticsBuilder,
25 tree::{Tree, TreeIterItem, TreeNode},
26 widgets::{
27 Draw, PopUpInput, PopUpPrompt, PopUpTable, PopUpText, ProcessEvent, PromptResult,
28 SelectTable, TextInputResult, WithBlock, popup_input, popup_prompt, popup_table,
29 popup_text,
30 },
31 },
32 },
33 filtering::SnapshotFilter,
34};
35
36use super::summary::SummaryMap;
37
38enum CurrentScreen<'a, P, S> {
40 Snapshots,
41 ShowHelp(PopUpText),
42 Table(PopUpTable),
43 EnterProperty((PopUpInput, SnapshotProperty)),
44 EnterFilter(PopUpInput),
45 PromptWrite(PopUpPrompt),
46 PromptExit(PopUpPrompt),
47 Dir(Box<Ls<'a, P, S>>),
48 Diff(Box<Diff<'a, P, S>>),
49}
50
51#[derive(Clone, Copy)]
52pub enum SnapshotProperty {
53 Label,
54 Description,
55 AddTags,
56 SetTags,
57 RemoveTags,
58 Hostname,
59}
60
61impl SnapshotProperty {
62 fn process(self, snap: &mut SnapshotFile, value: &String) -> bool {
63 match self {
64 Self::Label => {
65 if &snap.label == value {
66 return false;
67 }
68 snap.label.clone_from(value);
69 true
70 }
71 Self::Description => {
72 if snap.description.is_none() && value.is_empty()
73 || snap.description.as_ref() == Some(value)
74 {
75 return false;
76 }
77 if value.is_empty() {
78 snap.description = None;
79 } else {
80 snap.description = Some(value.clone());
81 }
82 true
83 }
84 Self::AddTags => snap.add_tags(vec![StringList::from_str(value).unwrap()]),
85 Self::SetTags => snap.set_tags(vec![StringList::from_str(value).unwrap()]),
86 Self::RemoveTags => snap.remove_tags(&[StringList::from_str(value).unwrap()]),
87 Self::Hostname => {
88 if &snap.hostname == value {
89 return false;
90 }
91 snap.hostname.clone_from(value);
92 true
93 }
94 }
95 }
96}
97
98#[derive(Clone, Copy, Default)]
100struct SnapStatus {
101 marked: bool,
102 modified: bool,
103 to_forget: bool,
104}
105
106impl SnapStatus {
107 fn toggle_mark(&mut self) {
108 self.marked = !self.marked;
109 }
110}
111
112#[derive(Debug)]
113enum View {
114 Filter,
115 All,
116 Marked,
117 Modified,
118}
119
120#[derive(PartialEq, Eq)]
121enum SnapshotNode {
122 Group(SnapshotGroup),
123 Snap(usize),
124}
125
126const INFO_TEXT: &str = "(Esc) quit | (F5) reload snapshots | (Enter) show contents | (v) toggle view | (i) show snapshot | (?) show all commands";
127
128const HELP_TEXT: &str = r"General Commands:
129 q, Esc : exit
130 F5 : re-read all snapshots from repository
131 Enter : show snapshot contents
132 v : toggle snapshot view [Filtered -> All -> Marked -> Modified]
133 V : modify filter to use
134 Ctrl-v : reset filter
135 i : show detailed snapshot information for selected snapshot
136 w : write modified snapshots and delete snapshots to-forget
137 ? : show this help page
138
139Commands for marking snapshot(s):
140
141 x,Space : toggle marking for selected snapshot
142 X : toggle markings for all snapshots
143 Ctrl-x : clear all markings
144
145Commands applied to marked snapshot(s) (selected if none marked):
146
147 f : toggle to-forget for snapshot(s)
148 Ctrl-f : clear to-forget for snapshot(s)
149 l : set label for snapshot(s)
150 Ctrl-l : remove label for snapshot(s)
151 d : set description for snapshot(s)
152 Ctrl-d : remove description for snapshot(s)
153 D : diff snapshots if 2 snapshots are selected or with parent
154 S : compute information for snapshot(s) and show summary
155 t : add tag(s) for snapshot(s)
156 Ctrl-t : remove all tags for snapshot(s)
157 s : set tag(s) for snapshot(s)
158 r : remove tag(s) for snapshot(s)
159 H : set hostname for snapshot(s)
160 p : set delete protection for snapshot(s)
161 Ctrl-p : remove delete protection for snapshot(s)
162";
163
164pub struct Snapshots<'a, P, S> {
165 current_screen: CurrentScreen<'a, P, S>,
166 current_view: View,
167 table: WithBlock<SelectTable>,
168 repo: &'a Repository<P, S>,
169 snaps_status: Vec<SnapStatus>,
170 snapshots: Vec<SnapshotFile>,
171 original_snapshots: Vec<SnapshotFile>,
172 filtered_snapshots: Vec<usize>,
173 tree: Tree<SnapshotNode, usize>,
174 filter: SnapshotFilter,
175 default_filter: SnapshotFilter,
176 group_by: SnapshotGroupCriterion,
177 summary_map: SummaryMap,
178}
179
180impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> {
181 pub fn new(
182 repo: &'a Repository<P, S>,
183 filter: SnapshotFilter,
184 group_by: SnapshotGroupCriterion,
185 ) -> Result<Self> {
186 let header = [
187 "", " ID", "Time", "Host", "Label", "Tags", "Paths", "Files", "Dirs", "Size",
188 ]
189 .into_iter()
190 .map(Text::from)
191 .collect();
192
193 let mut app = Self {
194 current_screen: CurrentScreen::Snapshots,
195 current_view: View::Filter,
196 table: WithBlock::new(SelectTable::new(header), Block::new()),
197 repo,
198 snaps_status: Vec::new(),
199 original_snapshots: Vec::new(),
200 snapshots: Vec::new(),
201 filtered_snapshots: Vec::new(),
202 tree: Tree::Leaf(0),
203 default_filter: filter.clone(),
204 filter,
205 group_by,
206 summary_map: SummaryMap::default(),
207 };
208 app.reread()?;
209 Ok(app)
210 }
211
212 fn selected_tree(&self) -> Option<TreeIterItem<'_, SnapshotNode, usize>> {
213 self.table
214 .widget
215 .selected()
216 .and_then(|selected| self.tree.iter_open().nth(selected))
217 }
218
219 fn selected_tree_mut(&mut self) -> Option<&mut Tree<SnapshotNode, usize>> {
220 self.table
221 .widget
222 .selected()
223 .and_then(|selected| self.tree.nth_mut(selected))
224 }
225
226 fn snap_idx(&self) -> Vec<usize> {
227 self.selected_tree()
228 .iter()
229 .flat_map(|item| item.tree.iter().map(|item| item.tree))
230 .filter_map(|tree| tree.leaf_data().copied())
231 .collect()
232 }
233
234 fn selected_snapshot(&self) -> Option<&SnapshotFile> {
235 self.selected_tree().map(|tree_info| match tree_info.tree {
236 Tree::Leaf(index)
237 | Tree::Node(TreeNode {
238 data: SnapshotNode::Snap(index),
239 ..
240 }) => Some(&self.snapshots[*index]),
241 _ => None,
242 })?
243 }
244
245 pub fn has_mark(&self) -> bool {
246 self.snaps_status.iter().any(|s| s.marked)
247 }
248
249 pub fn has_modified(&self) -> bool {
250 self.snaps_status.iter().any(|s| s.modified)
251 }
252
253 pub fn toggle_view_mark(&mut self) {
254 match self.current_view {
255 View::Filter => self.current_view = View::All,
256 View::All => {
257 self.current_view = View::Marked;
258 if !self.has_mark() {
259 self.toggle_view_mark();
260 }
261 }
262 View::Marked => {
263 self.current_view = View::Modified;
264 if !self.has_modified() {
265 self.toggle_view_mark();
266 }
267 }
268 View::Modified => self.current_view = View::Filter,
269 }
270 }
271
272 pub fn toggle_view(&mut self) {
273 self.toggle_view_mark();
274 self.apply_view();
275 }
276
277 pub fn apply_view(&mut self) {
278 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 pb = self.repo.progress_bars();
538 let p = pb.progress_counter("computing (sub)-dir information");
539 let mut summary_map = mem::take(&mut self.summary_map);
541
542 self.process_marked_snaps(|snap| {
543 _ = summary_map.compute(self.repo, snap.tree, &p);
545 false
546 });
547
548 let mut stats_builder = StatisticsBuilder::default();
549 let mut count = 0;
550 self.process_marked_snaps(|snap| {
551 stats_builder.append_from_tree(snap.tree, &summary_map);
552 count += 1;
553 false
554 });
555 p.finish();
556 let stats = stats_builder.build(self.repo)?;
557
558 self.summary_map = summary_map;
560
561 Ok(popup_table(
562 "snapshots summary",
563 stats.table(format!("{count} snapshot(s)")),
564 ))
565 }
566
567 pub fn dir(&mut self) -> Result<Option<Ls<'a, P, S>>> {
568 self.selected_snapshot().cloned().map_or(Ok(None), |snap| {
569 Some(Ls::new(
570 self.repo,
571 snap,
572 "",
573 mem::take(&mut self.summary_map),
574 ))
575 .transpose()
576 })
577 }
578
579 pub fn diff(&mut self) -> Result<Option<Diff<'a, P, S>>> {
580 let snaps: Vec<_> = self
581 .snapshots
582 .iter()
583 .zip(self.snaps_status.iter())
584 .filter_map(|(snap, status)| status.marked.then_some(snap))
585 .collect();
586
587 let from_parent = |sn: &SnapshotFile| {
588 sn.parent.and_then(|parent| {
589 self.repo
590 .get_snapshot_from_str(&parent.to_string(), |_| true)
592 .ok()
593 .map(|p| (p, sn.clone()))
594 })
595 };
596
597 let snaps = match snaps.len() {
598 2 => Some((snaps[0].clone(), snaps[1].clone())),
599 1 => from_parent(snaps[0]),
600 0 => self.selected_snapshot().and_then(from_parent),
601 _ => None,
602 };
603
604 if let Some((left, right)) = snaps {
605 Some(Diff::new(
606 self.repo,
607 left,
608 right,
609 "",
610 "",
611 mem::take(&mut self.summary_map),
612 ))
613 .transpose()
614 } else {
615 Ok(None)
616 }
617 }
618
619 pub fn count_marked_snaps(&self) -> usize {
620 self.snaps_status.iter().filter(|s| s.marked).count()
621 }
622
623 pub fn count_modified_snaps(&self) -> usize {
624 self.snaps_status.iter().filter(|s| s.modified).count()
625 }
626
627 pub fn count_forget_snaps(&self) -> usize {
628 self.snaps_status.iter().filter(|s| s.to_forget).count()
629 }
630
631 pub fn process_marked_snaps(&mut self, mut process: impl FnMut(&mut SnapshotFile) -> bool) {
634 let has_mark = self.has_mark();
635
636 if !has_mark {
637 self.toggle_mark();
638 }
639
640 for ((snap, status), original_snap) in self
641 .snapshots
642 .iter_mut()
643 .zip(self.snaps_status.iter_mut())
644 .zip(self.original_snapshots.iter())
645 {
646 if status.marked && process(snap) {
647 status.modified =
649 serde_json::to_string(snap).ok() != serde_json::to_string(original_snap).ok();
650 }
651 }
652
653 if !has_mark {
654 self.toggle_mark();
655 }
656 self.update_table();
657 }
658
659 pub fn get_snap_entity(&mut self, f: impl Fn(&SnapshotFile) -> String) -> String {
660 let has_mark = self.has_mark();
661
662 if !has_mark {
663 self.toggle_mark();
664 }
665
666 let entity = self
667 .snapshots
668 .iter()
669 .zip(self.snaps_status.iter())
670 .filter_map(|(snap, status)| status.marked.then_some(f(snap)))
671 .reduce(|entity, e| if entity == e { e } else { String::new() })
672 .unwrap_or_default();
673
674 if !has_mark {
675 self.toggle_mark();
676 }
677 entity
678 }
679
680 pub fn get_label(&mut self) -> String {
681 self.get_snap_entity(|snap| snap.label.clone())
682 }
683
684 pub fn get_tags(&mut self) -> String {
685 self.get_snap_entity(|snap| snap.tags.formatln())
686 }
687
688 pub fn get_hostname(&mut self) -> String {
689 self.get_snap_entity(|snap| snap.hostname.clone())
690 }
691
692 pub fn get_description(&mut self) -> String {
693 self.get_snap_entity(|snap| snap.description.clone().unwrap_or_default())
694 }
695
696 pub fn get_filter(&self) -> Result<String> {
697 Ok(toml::to_string_pretty(&self.filter)?)
698 }
699
700 pub fn set_filter(&mut self, filter: String) {
701 if let Ok(filter) = toml::from_str::<SnapshotFilter>(&filter) {
702 self.filter = filter;
703 self.apply_view();
704 }
705 }
706
707 pub fn set_property(&mut self, property: SnapshotProperty, value: String) {
708 self.process_marked_snaps(|snap| property.process(snap, &value));
709 }
710
711 pub fn clear_label(&mut self) {
712 self.set_property(SnapshotProperty::Label, String::new());
713 }
714
715 pub fn clear_description(&mut self) {
716 self.set_property(SnapshotProperty::Description, String::new());
717 }
718
719 pub fn clear_tags(&mut self) {
720 self.set_property(SnapshotProperty::SetTags, String::new());
721 }
722
723 pub fn set_delete_protection_to(&mut self, delete: DeleteOption) {
724 self.process_marked_snaps(|snap| {
725 if snap.delete == delete {
726 return false;
727 }
728 snap.delete = delete;
729 true
730 });
731 }
732
733 pub fn toggle_to_forget(&mut self) {
734 let has_mark = self.has_mark();
735
736 if !has_mark {
737 self.toggle_mark();
738 }
739
740 let now = Local::now();
741 for (snap, status) in self.snapshots.iter_mut().zip(self.snaps_status.iter_mut()) {
742 if status.marked {
743 if status.to_forget {
744 status.to_forget = false;
745 } else if !snap.must_keep(now) {
746 status.to_forget = true;
747 }
748 }
749 }
750
751 if !has_mark {
752 self.toggle_mark();
753 }
754 self.update_table();
755 }
756
757 pub fn clear_to_forget(&mut self) {
758 for status in &mut self.snaps_status {
759 status.to_forget = false;
760 }
761 self.update_table();
762 }
763
764 pub fn apply_input(&mut self, input: String) {
765 match self.current_screen {
766 CurrentScreen::EnterProperty((_, prop)) => self.set_property(prop, input),
767 CurrentScreen::EnterFilter(_) => self.set_filter(input),
768 _ => {}
769 }
770 }
771
772 pub fn set_delete_protection(&mut self) {
773 self.set_delete_protection_to(DeleteOption::Never);
774 }
775
776 pub fn clear_delete_protection(&mut self) {
777 self.set_delete_protection_to(DeleteOption::NotSet);
778 }
779
780 pub fn write(&mut self) -> Result<()> {
781 if !self.has_modified() && self.count_forget_snaps() == 0 {
782 return Ok(());
783 };
784
785 let save_snaps: Vec<_> = self
786 .snapshots
787 .iter()
788 .zip(self.snaps_status.iter())
789 .filter_map(|(snap, status)| (status.modified && !status.to_forget).then_some(snap))
790 .cloned()
791 .collect();
792 let old_snap_ids = save_snaps.iter().map(|sn| sn.id);
793 let snap_ids_to_forget = self
794 .snapshots
795 .iter()
796 .zip(self.snaps_status.iter())
797 .filter_map(|(snap, status)| status.to_forget.then_some(snap.id));
798 let delete_ids: Vec<_> = old_snap_ids.chain(snap_ids_to_forget).collect();
799 self.repo.save_snapshots(save_snaps)?;
800 self.repo.delete_snapshots(&delete_ids)?;
801 let ids: BTreeSet<_> = delete_ids.into_iter().collect();
803 self.snapshots.retain(|snap| !ids.contains(&snap.id));
804 self.reread()
806 }
807
808 pub fn reread(&mut self) -> Result<()> {
810 let snapshots = mem::take(&mut self.snapshots);
811 self.snapshots = self.repo.update_all_snapshots(snapshots)?;
812 self.snapshots
813 .sort_unstable_by(|sn1, sn2| sn1.cmp_group(self.group_by, sn2).then(sn1.cmp(sn2)));
814 self.snaps_status = vec![SnapStatus::default(); self.snapshots.len()];
815 self.original_snapshots.clone_from(&self.snapshots);
816 self.table.widget.select(None);
817 self.apply_view();
818 Ok(())
819 }
820}
821
822impl<'a, P: ProgressBars, S: IndexedFull> ProcessEvent for Snapshots<'a, P, S> {
823 type Result = Result<bool>;
824 fn input(&mut self, event: Event) -> Result<bool> {
825 use KeyCode::{Char, Enter, Esc, F, Left, Right};
826 match &mut self.current_screen {
827 CurrentScreen::Snapshots => {
828 match event {
829 Event::Key(key) if key.kind == KeyEventKind::Press => {
830 if key.modifiers == KeyModifiers::CONTROL {
831 match key.code {
832 Char('f') => self.clear_to_forget(),
833 Char('x') | Char(' ') => self.clear_marks(),
834 Char('l') => self.clear_label(),
835 Char('d') => self.clear_description(),
836 Char('t') => self.clear_tags(),
837 Char('p') => self.clear_delete_protection(),
838 Char('v') => self.reset_filter(),
839 _ => {}
840 }
841 } else {
842 match key.code {
843 Esc | Char('q') => {
844 self.current_screen = CurrentScreen::PromptExit(popup_prompt(
845 "exit rustic",
846 "do you want to exit? (y/n)".into(),
847 ));
848 }
849 Char('f') => self.toggle_to_forget(),
850 F(5) => self.reread()?,
851 Enter => {
852 if let Some(dir) = self.dir()? {
853 self.current_screen = CurrentScreen::Dir(Box::new(dir));
854 }
855 }
856 Right => {
857 if self.extendable() {
858 self.extend();
859 } else if let Some(dir) = self.dir()? {
860 self.current_screen = CurrentScreen::Dir(Box::new(dir));
861 }
862 }
863 Char('+') => {
864 if self.extendable() {
865 self.extend();
866 }
867 }
868 Left | Char('-') => self.collapse(),
869 Char('?') => {
870 self.current_screen = CurrentScreen::ShowHelp(popup_text(
871 "help",
872 HELP_TEXT.into(),
873 ));
874 }
875 Char('x') | Char(' ') => {
876 self.toggle_mark();
877 self.table.widget.next();
878 }
879 Char('X') => self.toggle_mark_all(),
880 Char('v') => self.toggle_view(),
881 Char('V') => {
882 self.current_screen = CurrentScreen::EnterFilter(popup_input(
883 "set filter (Ctrl-s to confirm)",
884 "enter filter in TOML format",
885 &self.get_filter()?,
886 15,
887 ));
888 }
889 Char('i') => {
890 self.current_screen =
891 CurrentScreen::Table(self.snapshot_details());
892 }
893 Char('S') => {
894 self.current_screen =
895 CurrentScreen::Table(self.snapshots_summary()?);
896 }
897 Char('l') => {
898 self.current_screen = CurrentScreen::EnterProperty((
899 popup_input(
900 "set label",
901 "enter label",
902 &self.get_label(),
903 1,
904 ),
905 SnapshotProperty::Label,
906 ));
907 }
908 Char('d') => {
909 self.current_screen = CurrentScreen::EnterProperty((
910 popup_input(
911 "set description (Ctrl-s to confirm)",
912 "enter description",
913 &self.get_description(),
914 5,
915 ),
916 SnapshotProperty::Description,
917 ));
918 }
919 Char('D') => {
920 if let Some(diff) = self.diff()? {
921 self.current_screen = CurrentScreen::Diff(Box::new(diff));
922 }
923 }
924 Char('t') => {
925 self.current_screen = CurrentScreen::EnterProperty((
926 popup_input("add tags", "enter tags", "", 1),
927 SnapshotProperty::AddTags,
928 ));
929 }
930 Char('s') => {
931 self.current_screen = CurrentScreen::EnterProperty((
932 popup_input("set tags", "enter tags", &self.get_tags(), 1),
933 SnapshotProperty::SetTags,
934 ));
935 }
936 Char('r') => {
937 self.current_screen = CurrentScreen::EnterProperty((
938 popup_input("remove tags", "enter tags", "", 1),
939 SnapshotProperty::RemoveTags,
940 ));
941 }
942 Char('H') => {
943 self.current_screen = CurrentScreen::EnterProperty((
944 popup_input(
945 "set hostname",
946 "enter hostname",
947 &self.get_hostname(),
948 1,
949 ),
950 SnapshotProperty::Hostname,
951 ));
952 }
953 Char('p') => self.set_delete_protection(),
955 Char('w') => {
956 let msg = format!(
957 "Do you want to write {} modified and remove {} snapshots? (y/n)",
958 self.count_modified_snaps(),
959 self.count_forget_snaps()
960 );
961 self.current_screen = CurrentScreen::PromptWrite(popup_prompt(
962 "write snapshots",
963 msg.into(),
964 ));
965 }
966 _ => self.table.input(event),
967 }
968 }
969 }
970 _ => {}
971 }
972 }
973 CurrentScreen::Table(_) | CurrentScreen::ShowHelp(_) => match event {
974 Event::Key(key) if key.kind == KeyEventKind::Press => {
975 if matches!(key.code, Char('q' | ' ' | 'i' | '?') | Esc | Enter) {
976 self.current_screen = CurrentScreen::Snapshots;
977 }
978 }
979 _ => {}
980 },
981 CurrentScreen::EnterProperty((prompt, _)) | CurrentScreen::EnterFilter(prompt) => {
982 match prompt.input(event) {
983 TextInputResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
984 TextInputResult::Input(input) => {
985 self.apply_input(input);
986 self.current_screen = CurrentScreen::Snapshots;
987 }
988 TextInputResult::None => {}
989 }
990 }
991 CurrentScreen::PromptWrite(prompt) => match prompt.input(event) {
992 PromptResult::Ok => {
993 self.write()?;
994 self.current_screen = CurrentScreen::Snapshots;
995 }
996 PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
997 PromptResult::None => {}
998 },
999 CurrentScreen::PromptExit(prompt) => match prompt.input(event) {
1000 PromptResult::Ok => return Ok(true),
1001 PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
1002 PromptResult::None => {}
1003 },
1004 CurrentScreen::Dir(dir) => match dir.input(event)? {
1005 LsResult::Exit => return Ok(true),
1006 LsResult::Return(summary_map) => {
1007 self.current_screen = CurrentScreen::Snapshots;
1008 self.summary_map = summary_map;
1009 }
1010 LsResult::None => {}
1011 },
1012 CurrentScreen::Diff(diff) => match diff.input(event)? {
1013 DiffResult::Exit => return Ok(true),
1014 DiffResult::Return(summary_map) => {
1015 self.current_screen = CurrentScreen::Snapshots;
1016 self.summary_map = summary_map;
1017 }
1018 DiffResult::None => {}
1019 },
1020 }
1021 Ok(false)
1022 }
1023}
1024
1025impl<'a, P: ProgressBars, S: IndexedFull> Draw for Snapshots<'a, P, S> {
1026 fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
1027 if let CurrentScreen::Dir(dir) = &mut self.current_screen {
1028 dir.draw(area, f);
1029 return;
1030 }
1031
1032 if let CurrentScreen::Diff(diff) = &mut self.current_screen {
1033 diff.draw(area, f);
1034 return;
1035 }
1036
1037 let rects = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).split(area);
1038
1039 self.table.draw(rects[0], f);
1041
1042 let buffer_bg = tailwind::SLATE.c950;
1044 let row_fg = tailwind::SLATE.c200;
1045 let info_footer = Paragraph::new(Line::from(INFO_TEXT))
1046 .style(Style::new().fg(row_fg).bg(buffer_bg))
1047 .centered();
1048 f.render_widget(info_footer, rects[1]);
1049
1050 match &mut self.current_screen {
1052 CurrentScreen::Table(popup) => popup.draw(area, f),
1053 CurrentScreen::ShowHelp(popup) => popup.draw(area, f),
1054 CurrentScreen::EnterProperty((popup, _)) | CurrentScreen::EnterFilter(popup) => {
1055 popup.draw(area, f);
1056 }
1057 CurrentScreen::PromptWrite(popup) | CurrentScreen::PromptExit(popup) => {
1058 popup.draw(area, f);
1059 }
1060 _ => {}
1061 }
1062 }
1063}