use std::{collections::BTreeSet, iter::once, mem, str::FromStr};
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use itertools::Itertools;
use jiff::Zoned;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph},
};
use rustic_core::{
SnapshotGroup, SnapshotGroupCriterion, StringList,
repofile::{DeleteOption, SnapshotFile},
};
use style::palette::tailwind;
use crate::{
commands::{
snapshots::{fill_table, snap_to_table},
tui::{
diff::{Diff, DiffResult},
ls::{Ls, LsResult},
summary::StatisticsBuilder,
tree::{Tree, TreeIterItem, TreeNode},
widgets::{
Draw, PopUpInput, PopUpPrompt, PopUpTable, PopUpText, ProcessEvent, PromptResult,
SelectTable, TextInputResult, WithBlock, popup_input, popup_prompt, popup_table,
popup_text,
},
},
},
filtering::SnapshotFilter,
repository::IndexedRepo,
};
use super::summary::SummaryMap;
enum CurrentScreen<'a> {
Snapshots,
ShowHelp(PopUpText),
Table(PopUpTable),
EnterProperty((PopUpInput, SnapshotProperty)),
EnterFilter(PopUpInput),
PromptWrite(PopUpPrompt),
PromptExit(PopUpPrompt),
Dir(Box<Ls<'a>>),
Diff(Box<Diff<'a>>),
}
#[derive(Clone, Copy)]
pub enum SnapshotProperty {
Label,
Description,
AddTags,
SetTags,
RemoveTags,
Hostname,
}
impl SnapshotProperty {
fn process(self, snap: &mut SnapshotFile, value: &String) -> bool {
match self {
Self::Label => {
if &snap.label == value {
return false;
}
snap.label.clone_from(value);
true
}
Self::Description => {
if snap.description.is_none() && value.is_empty()
|| snap.description.as_ref() == Some(value)
{
return false;
}
if value.is_empty() {
snap.description = None;
} else {
snap.description = Some(value.clone());
}
true
}
Self::AddTags => snap.add_tags(vec![StringList::from_str(value).unwrap()]),
Self::SetTags => snap.set_tags(vec![StringList::from_str(value).unwrap()]),
Self::RemoveTags => snap.remove_tags(&[StringList::from_str(value).unwrap()]),
Self::Hostname => {
if &snap.hostname == value {
return false;
}
snap.hostname.clone_from(value);
true
}
}
}
}
#[derive(Clone, Copy, Default)]
struct SnapStatus {
marked: bool,
modified: bool,
to_forget: bool,
}
impl SnapStatus {
fn toggle_mark(&mut self) {
self.marked = !self.marked;
}
}
#[derive(Debug)]
enum View {
Filter,
All,
Marked,
Modified,
}
#[derive(PartialEq, Eq)]
enum SnapshotNode {
Group(SnapshotGroup),
Snap(usize),
}
const INFO_TEXT: &str = "(Esc) quit | (F5) reload snapshots | (Enter) show contents | (v) toggle view | (i) show snapshot | (?) show all commands";
const HELP_TEXT: &str = r"General Commands:
q, Esc : exit
F5 : re-read all snapshots from repository
Enter : show snapshot contents
v : toggle snapshot view [Filtered -> All -> Marked -> Modified]
V : modify filter to use
Ctrl-v : reset filter
i : show detailed snapshot information for selected snapshot
w : write modified snapshots and delete snapshots to-forget
? : show this help page
Commands for marking snapshot(s):
x,Space : toggle marking for selected snapshot
X : toggle markings for all snapshots
Ctrl-x : clear all markings
Commands applied to marked snapshot(s) (selected if none marked):
f : toggle to-forget for snapshot(s)
Ctrl-f : clear to-forget for snapshot(s)
l : set label for snapshot(s)
Ctrl-l : remove label for snapshot(s)
d : set description for snapshot(s)
Ctrl-d : remove description for snapshot(s)
D : diff snapshots if 2 snapshots are selected or with parent
S : compute information for snapshot(s) and show summary
t : add tag(s) for snapshot(s)
Ctrl-t : remove all tags for snapshot(s)
s : set tag(s) for snapshot(s)
r : remove tag(s) for snapshot(s)
H : set hostname for snapshot(s)
p : set delete protection for snapshot(s)
Ctrl-p : remove delete protection for snapshot(s)
";
pub struct Snapshots<'a> {
current_screen: CurrentScreen<'a>,
current_view: View,
table: WithBlock<SelectTable>,
repo: &'a IndexedRepo,
snaps_status: Vec<SnapStatus>,
snapshots: Vec<SnapshotFile>,
original_snapshots: Vec<SnapshotFile>,
filtered_snapshots: Vec<usize>,
tree: Tree<SnapshotNode, usize>,
filter: SnapshotFilter,
default_filter: SnapshotFilter,
group_by: SnapshotGroupCriterion,
summary_map: SummaryMap,
}
impl<'a> Snapshots<'a> {
pub fn new(
repo: &'a IndexedRepo,
filter: SnapshotFilter,
group_by: SnapshotGroupCriterion,
) -> Result<Self> {
let header = [
"", " ID", "Time", "Host", "Label", "Tags", "Paths", "Files", "Dirs", "Size",
]
.into_iter()
.map(Text::from)
.collect();
let mut app = Self {
current_screen: CurrentScreen::Snapshots,
current_view: View::Filter,
table: WithBlock::new(SelectTable::new(header), Block::new()),
repo,
snaps_status: Vec::new(),
original_snapshots: Vec::new(),
snapshots: Vec::new(),
filtered_snapshots: Vec::new(),
tree: Tree::Leaf(0),
default_filter: filter.clone(),
filter,
group_by,
summary_map: SummaryMap::default(),
};
app.reread()?;
Ok(app)
}
fn selected_tree(&self) -> Option<TreeIterItem<'_, SnapshotNode, usize>> {
self.table
.widget
.selected()
.and_then(|selected| self.tree.iter_open().nth(selected))
}
fn selected_tree_mut(&mut self) -> Option<&mut Tree<SnapshotNode, usize>> {
self.table
.widget
.selected()
.and_then(|selected| self.tree.nth_mut(selected))
}
fn snap_idx(&self) -> Vec<usize> {
self.selected_tree()
.iter()
.flat_map(|item| item.tree.iter().map(|item| item.tree))
.filter_map(|tree| tree.leaf_data().copied())
.collect()
}
fn selected_snapshot(&self) -> Option<&SnapshotFile> {
self.selected_tree().map(|tree_info| match tree_info.tree {
Tree::Leaf(index)
| Tree::Node(TreeNode {
data: SnapshotNode::Snap(index),
..
}) => Some(&self.snapshots[*index]),
_ => None,
})?
}
pub fn has_mark(&self) -> bool {
self.snaps_status.iter().any(|s| s.marked)
}
pub fn has_modified(&self) -> bool {
self.snaps_status.iter().any(|s| s.modified)
}
pub fn toggle_view_mark(&mut self) {
match self.current_view {
View::Filter => self.current_view = View::All,
View::All => {
self.current_view = View::Marked;
if !self.has_mark() {
self.toggle_view_mark();
}
}
View::Marked => {
self.current_view = View::Modified;
if !self.has_modified() {
self.toggle_view_mark();
}
}
View::Modified => self.current_view = View::Filter,
}
}
pub fn toggle_view(&mut self) {
self.toggle_view_mark();
self.apply_view();
}
pub fn apply_view(&mut self) {
self.filtered_snapshots = self
.snapshots
.iter()
.enumerate()
.zip(self.snaps_status.iter())
.filter_map(|((i, sn), status)| {
match self.current_view {
View::All => true,
View::Filter => self.filter.matches(sn),
View::Marked => status.marked,
View::Modified => status.modified,
}
.then_some(i)
})
.collect();
self.create_tree();
}
pub fn create_tree(&mut self) {
let old_tree = self.selected_tree().map(|t| t.tree);
let mut result = Vec::new();
for (group, snaps) in &self
.filtered_snapshots
.iter()
.chunk_by(|i| SnapshotGroup::from_snapshot(&self.snapshots[**i], self.group_by))
{
let mut same_id_group = Vec::new();
for (_, s) in &snaps.into_iter().chunk_by(|i| self.snapshots[**i].tree) {
let leaves: Vec<_> = s.map(|i| Tree::leaf(*i)).collect();
let first = leaves[0].leaf_data().unwrap(); if leaves.len() == 1 {
same_id_group.push(Tree::leaf(*first));
} else {
same_id_group.push(Tree::node(SnapshotNode::Snap(*first), false, leaves));
}
}
result.push(Tree::node(SnapshotNode::Group(group), false, same_id_group));
}
let tree = Tree::node(SnapshotNode::Snap(0), true, result);
let len = tree.iter_open().count();
let selected = if len == 0 {
None
} else {
Some(
tree.iter()
.position(|info| Some(info.tree) == old_tree)
.unwrap_or(len - 1),
)
};
self.tree = tree;
self.update_table();
self.table.widget.select(selected);
}
fn table_row(&self, info: TreeIterItem<'_, SnapshotNode, usize>) -> Vec<Text<'static>> {
let (has_mark, has_not_mark, has_modified, has_to_forget) = info
.tree
.iter()
.filter_map(|item| item.leaf_data().copied())
.fold(
(false, false, false, false),
|(mut a, mut b, mut c, mut d), i| {
if self.snaps_status[i].marked {
a = true;
} else {
b = true;
}
if self.snaps_status[i].modified {
c = true;
}
if self.snaps_status[i].to_forget {
d = true;
}
(a, b, c, d)
},
);
let mark = match (has_mark, has_not_mark) {
(false, _) => " ",
(true, true) => "*",
(true, false) => "X",
};
let modified = if has_modified { "*" } else { " " };
let del = if has_to_forget { "🗑" } else { "" };
let mut collapse = " ".repeat(info.depth);
collapse.push_str(match info.tree {
Tree::Leaf(_) => "",
Tree::Node(TreeNode { open: false, .. }) => "\u{25b6} ", Tree::Node(TreeNode { open: true, .. }) => "\u{25bc} ", });
match info.tree {
Tree::Leaf(index)
| Tree::Node(TreeNode {
data: SnapshotNode::Snap(index),
..
}) => {
let snap = &self.snapshots[*index];
let symbols = match (
snap.delete == DeleteOption::NotSet,
snap.description.is_none(),
) {
(true, true) => "",
(true, false) => "🗎",
(false, true) => "🛡",
(false, false) => "🛡 🗎",
};
let count = info.tree.child_count();
once(&mark.to_string())
.chain(snap_to_table(snap, count).iter())
.cloned()
.enumerate()
.map(|(i, mut content)| {
if i == 1 {
content = format!("{collapse}{modified}{del}{content}{symbols}");
}
Text::from(content)
})
.collect()
}
Tree::Node(TreeNode {
data: SnapshotNode::Group(group),
..
}) => {
let host = group
.hostname
.as_ref()
.map(String::from)
.unwrap_or_default();
let label = group.label.as_ref().map(String::from).unwrap_or_default();
let paths = group
.paths
.as_ref()
.map_or_else(String::default, StringList::formatln);
let tags = group
.tags
.as_ref()
.map_or_else(String::default, StringList::formatln);
[
mark.to_string(),
format!("{collapse}{modified}{del}group"),
String::default(),
host,
label,
tags,
paths,
String::default(),
String::default(),
String::default(),
]
.into_iter()
.map(Text::from)
.collect()
}
}
}
pub fn update_table(&mut self) {
let max_tags = self
.filtered_snapshots
.iter()
.map(|&i| self.snapshots[i].tags.iter().count())
.max()
.unwrap_or(1);
let max_paths = self
.filtered_snapshots
.iter()
.map(|&i| self.snapshots[i].paths.iter().count())
.max()
.unwrap_or(1);
let height = max_tags.max(max_paths).max(1) + 1;
let rows = self
.tree
.iter_open()
.map(|tree| self.table_row(tree))
.collect();
self.table.widget.set_content(rows, height);
self.table.block = Block::new()
.borders(Borders::BOTTOM)
.title_bottom(format!(
"{:?} view: {}, total: {}, marked: {}, modified: {}, to forget: {}",
self.current_view,
self.filtered_snapshots.len(),
self.snapshots.len(),
self.count_marked_snaps(),
self.count_modified_snaps(),
self.count_forget_snaps()
))
.title_alignment(Alignment::Center);
}
pub fn toggle_mark(&mut self) {
for snap_idx in self.snap_idx() {
self.snaps_status[snap_idx].toggle_mark();
}
self.update_table();
}
pub fn toggle_mark_all(&mut self) {
for snap_idx in &self.filtered_snapshots {
self.snaps_status[*snap_idx].toggle_mark();
}
self.update_table();
}
pub fn clear_marks(&mut self) {
for status in &mut self.snaps_status {
status.marked = false;
}
self.update_table();
}
pub fn reset_filter(&mut self) {
self.filter = self.default_filter.clone();
self.apply_view();
}
pub fn collapse(&mut self) {
if let Some(tree) = self.selected_tree_mut() {
tree.close();
self.update_table();
}
}
pub fn extendable(&self) -> bool {
matches!(self.selected_tree(), Some(tree_info) if tree_info.tree.openable())
}
pub fn extend(&mut self) {
if let Some(tree) = self.selected_tree_mut() {
tree.open();
self.update_table();
}
}
pub fn snapshot_details(&self) -> PopUpTable {
let mut rows = Vec::new();
if let Some(snap) = self.selected_snapshot() {
fill_table(snap, |title, value| {
rows.push(vec![Text::from(title.to_string()), Text::from(value)]);
});
}
popup_table("snapshot details", rows)
}
pub fn snapshots_summary(&mut self) -> Result<PopUpTable> {
let p = self
.repo
.progress_counter("computing (sub)-dir information");
let mut summary_map = mem::take(&mut self.summary_map);
self.process_marked_snaps(|snap| {
_ = summary_map.compute(self.repo, snap.tree, &p);
false
});
let mut stats_builder = StatisticsBuilder::default();
let mut count = 0;
self.process_marked_snaps(|snap| {
stats_builder.append_from_tree(snap.tree, &summary_map);
count += 1;
false
});
p.finish();
let stats = stats_builder.build(self.repo)?;
self.summary_map = summary_map;
Ok(popup_table(
"snapshots summary",
stats.table(format!("{count} snapshot(s)")),
))
}
pub fn dir(&mut self) -> Result<Option<Ls<'a>>> {
self.selected_snapshot().cloned().map_or(Ok(None), |snap| {
Some(Ls::new(
self.repo,
snap,
"",
mem::take(&mut self.summary_map),
))
.transpose()
})
}
pub fn diff(&mut self) -> Result<Option<Diff<'a>>> {
let snaps: Vec<_> = self
.snapshots
.iter()
.zip(self.snaps_status.iter())
.filter_map(|(snap, status)| status.marked.then_some(snap))
.collect();
let from_parent = |sn: &SnapshotFile| {
sn.parent.and_then(|parent| {
self.repo
.get_snapshot_from_str(&parent.to_string(), |_| true)
.ok()
.map(|p| (p, sn.clone()))
})
};
let snaps = match snaps.len() {
2 => Some((snaps[0].clone(), snaps[1].clone())),
1 => from_parent(snaps[0]),
0 => self.selected_snapshot().and_then(from_parent),
_ => None,
};
if let Some((left, right)) = snaps {
Some(Diff::new(
self.repo,
left,
right,
"",
"",
mem::take(&mut self.summary_map),
))
.transpose()
} else {
Ok(None)
}
}
pub fn count_marked_snaps(&self) -> usize {
self.snaps_status.iter().filter(|s| s.marked).count()
}
pub fn count_modified_snaps(&self) -> usize {
self.snaps_status.iter().filter(|s| s.modified).count()
}
pub fn count_forget_snaps(&self) -> usize {
self.snaps_status.iter().filter(|s| s.to_forget).count()
}
pub fn process_marked_snaps(&mut self, mut process: impl FnMut(&mut SnapshotFile) -> bool) {
let has_mark = self.has_mark();
if !has_mark {
self.toggle_mark();
}
for ((snap, status), original_snap) in self
.snapshots
.iter_mut()
.zip(self.snaps_status.iter_mut())
.zip(self.original_snapshots.iter())
{
if status.marked && process(snap) {
status.modified =
serde_json::to_string(snap).ok() != serde_json::to_string(original_snap).ok();
}
}
if !has_mark {
self.toggle_mark();
}
self.update_table();
}
pub fn get_snap_entity(&mut self, f: impl Fn(&SnapshotFile) -> String) -> String {
let has_mark = self.has_mark();
if !has_mark {
self.toggle_mark();
}
let entity = self
.snapshots
.iter()
.zip(self.snaps_status.iter())
.filter_map(|(snap, status)| status.marked.then_some(f(snap)))
.reduce(|entity, e| if entity == e { e } else { String::new() })
.unwrap_or_default();
if !has_mark {
self.toggle_mark();
}
entity
}
pub fn get_label(&mut self) -> String {
self.get_snap_entity(|snap| snap.label.clone())
}
pub fn get_tags(&mut self) -> String {
self.get_snap_entity(|snap| snap.tags.formatln())
}
pub fn get_hostname(&mut self) -> String {
self.get_snap_entity(|snap| snap.hostname.clone())
}
pub fn get_description(&mut self) -> String {
self.get_snap_entity(|snap| snap.description.clone().unwrap_or_default())
}
pub fn get_filter(&self) -> Result<String> {
Ok(toml::to_string_pretty(&self.filter)?)
}
pub fn set_filter(&mut self, filter: String) {
if let Ok(filter) = toml::from_str::<SnapshotFilter>(&filter) {
self.filter = filter;
self.apply_view();
}
}
pub fn set_property(&mut self, property: SnapshotProperty, value: String) {
self.process_marked_snaps(|snap| property.process(snap, &value));
}
pub fn clear_label(&mut self) {
self.set_property(SnapshotProperty::Label, String::new());
}
pub fn clear_description(&mut self) {
self.set_property(SnapshotProperty::Description, String::new());
}
pub fn clear_tags(&mut self) {
self.set_property(SnapshotProperty::SetTags, String::new());
}
pub fn set_delete_protection_to(&mut self, delete: DeleteOption) {
self.process_marked_snaps(|snap| {
if snap.delete == delete {
return false;
}
snap.delete = delete.clone();
true
});
}
pub fn toggle_to_forget(&mut self) {
let has_mark = self.has_mark();
if !has_mark {
self.toggle_mark();
}
let now = Zoned::now();
for (snap, status) in self.snapshots.iter_mut().zip(self.snaps_status.iter_mut()) {
if status.marked {
if status.to_forget {
status.to_forget = false;
} else if !snap.must_keep(&now) {
status.to_forget = true;
}
}
}
if !has_mark {
self.toggle_mark();
}
self.update_table();
}
pub fn clear_to_forget(&mut self) {
for status in &mut self.snaps_status {
status.to_forget = false;
}
self.update_table();
}
pub fn apply_input(&mut self, input: String) {
match self.current_screen {
CurrentScreen::EnterProperty((_, prop)) => self.set_property(prop, input),
CurrentScreen::EnterFilter(_) => self.set_filter(input),
_ => {}
}
}
pub fn set_delete_protection(&mut self) {
self.set_delete_protection_to(DeleteOption::Never);
}
pub fn clear_delete_protection(&mut self) {
self.set_delete_protection_to(DeleteOption::NotSet);
}
pub fn write(&mut self) -> Result<()> {
if !self.has_modified() && self.count_forget_snaps() == 0 {
return Ok(());
};
let save_snaps: Vec<_> = self
.snapshots
.iter()
.zip(self.snaps_status.iter())
.filter_map(|(snap, status)| (status.modified && !status.to_forget).then_some(snap))
.cloned()
.collect();
let old_snap_ids = save_snaps.iter().map(|sn| sn.id);
let snap_ids_to_forget = self
.snapshots
.iter()
.zip(self.snaps_status.iter())
.filter_map(|(snap, status)| status.to_forget.then_some(snap.id));
let delete_ids: Vec<_> = old_snap_ids.chain(snap_ids_to_forget).collect();
self.repo.save_snapshots(save_snaps)?;
self.repo.delete_snapshots(&delete_ids)?;
let ids: BTreeSet<_> = delete_ids.into_iter().collect();
self.snapshots.retain(|snap| !ids.contains(&snap.id));
self.reread()
}
pub fn reread(&mut self) -> Result<()> {
let snapshots = mem::take(&mut self.snapshots);
self.snapshots = self.repo.update_all_snapshots(snapshots)?;
self.snapshots.sort_unstable_by(|sn1, sn2| {
SnapshotGroup::from_snapshot(sn1, self.group_by)
.cmp(&SnapshotGroup::from_snapshot(sn2, self.group_by))
.then(sn1.time.cmp(&sn2.time))
});
self.snaps_status = vec![SnapStatus::default(); self.snapshots.len()];
self.original_snapshots.clone_from(&self.snapshots);
self.table.widget.select(None);
self.apply_view();
Ok(())
}
}
impl<'a> ProcessEvent for Snapshots<'a> {
type Result = Result<bool>;
fn input(&mut self, event: Event) -> Result<bool> {
use KeyCode::{Char, Enter, Esc, F, Left, Right};
match &mut self.current_screen {
CurrentScreen::Snapshots => {
match event {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if key.modifiers == KeyModifiers::CONTROL {
match key.code {
Char('f') => self.clear_to_forget(),
Char('x') | Char(' ') => self.clear_marks(),
Char('l') => self.clear_label(),
Char('d') => self.clear_description(),
Char('t') => self.clear_tags(),
Char('p') => self.clear_delete_protection(),
Char('v') => self.reset_filter(),
_ => {}
}
} else {
match key.code {
Esc | Char('q') => {
self.current_screen = CurrentScreen::PromptExit(popup_prompt(
"exit rustic",
"do you want to exit? (y/n)".into(),
));
}
Char('f') => self.toggle_to_forget(),
F(5) => self.reread()?,
Enter => {
if let Some(dir) = self.dir()? {
self.current_screen = CurrentScreen::Dir(Box::new(dir));
}
}
Right => {
if self.extendable() {
self.extend();
} else if let Some(dir) = self.dir()? {
self.current_screen = CurrentScreen::Dir(Box::new(dir));
}
}
Char('+') => {
if self.extendable() {
self.extend();
}
}
Left | Char('-') => self.collapse(),
Char('?') => {
self.current_screen = CurrentScreen::ShowHelp(popup_text(
"help",
HELP_TEXT.into(),
));
}
Char('x') | Char(' ') => {
self.toggle_mark();
self.table.widget.next();
}
Char('X') => self.toggle_mark_all(),
Char('v') => self.toggle_view(),
Char('V') => {
self.current_screen = CurrentScreen::EnterFilter(popup_input(
"set filter (Ctrl-s to confirm)",
"enter filter in TOML format",
&self.get_filter()?,
15,
));
}
Char('i') => {
self.current_screen =
CurrentScreen::Table(self.snapshot_details());
}
Char('S') => {
self.current_screen =
CurrentScreen::Table(self.snapshots_summary()?);
}
Char('l') => {
self.current_screen = CurrentScreen::EnterProperty((
popup_input(
"set label",
"enter label",
&self.get_label(),
1,
),
SnapshotProperty::Label,
));
}
Char('d') => {
self.current_screen = CurrentScreen::EnterProperty((
popup_input(
"set description (Ctrl-s to confirm)",
"enter description",
&self.get_description(),
5,
),
SnapshotProperty::Description,
));
}
Char('D') => {
if let Some(diff) = self.diff()? {
self.current_screen = CurrentScreen::Diff(Box::new(diff));
}
}
Char('t') => {
self.current_screen = CurrentScreen::EnterProperty((
popup_input("add tags", "enter tags", "", 1),
SnapshotProperty::AddTags,
));
}
Char('s') => {
self.current_screen = CurrentScreen::EnterProperty((
popup_input("set tags", "enter tags", &self.get_tags(), 1),
SnapshotProperty::SetTags,
));
}
Char('r') => {
self.current_screen = CurrentScreen::EnterProperty((
popup_input("remove tags", "enter tags", "", 1),
SnapshotProperty::RemoveTags,
));
}
Char('H') => {
self.current_screen = CurrentScreen::EnterProperty((
popup_input(
"set hostname",
"enter hostname",
&self.get_hostname(),
1,
),
SnapshotProperty::Hostname,
));
}
Char('p') => self.set_delete_protection(),
Char('w') => {
let msg = format!(
"Do you want to write {} modified and remove {} snapshots? (y/n)",
self.count_modified_snaps(),
self.count_forget_snaps()
);
self.current_screen = CurrentScreen::PromptWrite(popup_prompt(
"write snapshots",
msg.into(),
));
}
_ => self.table.input(event),
}
}
}
_ => {}
}
}
CurrentScreen::Table(_) | CurrentScreen::ShowHelp(_) => match event {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if matches!(key.code, Char('q' | ' ' | 'i' | '?') | Esc | Enter) {
self.current_screen = CurrentScreen::Snapshots;
}
}
_ => {}
},
CurrentScreen::EnterProperty((prompt, _)) | CurrentScreen::EnterFilter(prompt) => {
match prompt.input(event) {
TextInputResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
TextInputResult::Input(input) => {
self.apply_input(input);
self.current_screen = CurrentScreen::Snapshots;
}
TextInputResult::None => {}
}
}
CurrentScreen::PromptWrite(prompt) => match prompt.input(event) {
PromptResult::Ok => {
self.write()?;
self.current_screen = CurrentScreen::Snapshots;
}
PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
PromptResult::None => {}
},
CurrentScreen::PromptExit(prompt) => match prompt.input(event) {
PromptResult::Ok => return Ok(true),
PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
PromptResult::None => {}
},
CurrentScreen::Dir(dir) => match dir.input(event)? {
LsResult::Exit => return Ok(true),
LsResult::Return(summary_map) => {
self.current_screen = CurrentScreen::Snapshots;
self.summary_map = summary_map;
}
LsResult::None => {}
},
CurrentScreen::Diff(diff) => match diff.input(event)? {
DiffResult::Exit => return Ok(true),
DiffResult::Return(summary_map) => {
self.current_screen = CurrentScreen::Snapshots;
self.summary_map = summary_map;
}
DiffResult::None => {}
},
}
Ok(false)
}
}
impl<'a> Draw for Snapshots<'a> {
fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
if let CurrentScreen::Dir(dir) = &mut self.current_screen {
dir.draw(area, f);
return;
}
if let CurrentScreen::Diff(diff) = &mut self.current_screen {
diff.draw(area, f);
return;
}
let rects = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).split(area);
self.table.draw(rects[0], f);
let buffer_bg = tailwind::SLATE.c950;
let row_fg = tailwind::SLATE.c200;
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
.style(Style::new().fg(row_fg).bg(buffer_bg))
.centered();
f.render_widget(info_footer, rects[1]);
match &mut self.current_screen {
CurrentScreen::Table(popup) => popup.draw(area, f),
CurrentScreen::ShowHelp(popup) => popup.draw(area, f),
CurrentScreen::EnterProperty((popup, _)) | CurrentScreen::EnterFilter(popup) => {
popup.draw(area, f);
}
CurrentScreen::PromptWrite(popup) | CurrentScreen::PromptExit(popup) => {
popup.draw(area, f);
}
_ => {}
}
}
}