use std::{borrow::Cow, ffi::OsStr, path::PathBuf};
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyEventKind};
use itertools::{EitherOrBoth, Itertools};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph},
};
use rustic_core::repofile::{Node, SnapshotFile, Tree};
use style::palette::tailwind;
use crate::{
commands::{
diff::{DiffStatistics, NodeDiff},
snapshots::fill_table,
tui::widgets::{
Draw, PopUpPrompt, PopUpText, ProcessEvent, PromptResult, SelectTable, WithBlock,
popup_prompt, popup_text,
},
},
helpers::bytes_size_to_string,
repository::IndexedRepo,
};
use super::{
TuiResult,
summary::SummaryMap,
widgets::{PopUpTable, popup_table},
};
enum CurrentScreen {
Diff,
ShowHelp(PopUpText),
Table(PopUpTable),
PromptExit(PopUpPrompt),
PromptLeave(PopUpPrompt),
}
const INFO_TEXT: &str =
"(Esc) quit | (Enter) enter dir | (Backspace) return to parent | (?) show all commands";
const HELP_TEXT: &str = r"
Diff Commands:
m : toggle ignoring metadata
d : toggle show only different entries
s : compute information for (sub-)dirs and show summary
S : compute information for selected nodes and show summary
I : show information about snapshots
General Commands:
q,Esc : exit
Enter : enter dir
Backspace : return to parent dir
? : show this help page
";
#[derive(Clone)]
pub struct DiffNode(pub EitherOrBoth<Node>);
impl DiffNode {
fn only_subtrees(&self) -> Option<Self> {
let (left, right) = self
.0
.clone()
.map_any(
|n| n.subtree.is_some().then_some(n),
|n| n.subtree.is_some().then_some(n),
)
.left_and_right();
match (left.flatten(), right.flatten()) {
(Some(l), Some(r)) => Some(Self(EitherOrBoth::Both(l, r))),
(Some(l), None) => Some(Self(EitherOrBoth::Left(l))),
(None, Some(r)) => Some(Self(EitherOrBoth::Right(r))),
(None, None) => None,
}
}
fn name(&self) -> Cow<'_, OsStr> {
self.0.as_ref().reduce(|l, _| l).name()
}
pub fn map<'a, F, T>(&'a self, f: F) -> EitherOrBoth<T>
where
F: Fn(&'a Node) -> T,
{
self.0.as_ref().map_any(&f, &f)
}
}
#[derive(Default)]
struct DiffTree {
nodes: Vec<DiffNode>,
}
impl DiffTree {
fn from_node(repo: &IndexedRepo, node: &DiffNode) -> Result<Self> {
let tree_from_node = |node: Option<&Node>| {
node.map_or_else(
|| Ok(Tree::default()),
|node| {
node.subtree.map_or_else(
|| {
Ok(Tree {
nodes: vec![node.clone()],
})
},
|id| repo.get_tree(&id),
)
},
)
};
let left_tree = tree_from_node(node.0.as_ref().left())?;
let right_tree = tree_from_node(node.0.as_ref().right())?;
let nodes = left_tree
.nodes
.into_iter()
.merge_join_by(right_tree.nodes, |node_l, node_r| {
node_l.name().cmp(&node_r.name())
})
.map(DiffNode)
.collect();
Ok(Self { nodes })
}
}
pub struct Diff<'a> {
current_screen: CurrentScreen,
table: WithBlock<SelectTable>,
repo: &'a IndexedRepo,
snapshot_left: SnapshotFile,
snapshot_right: SnapshotFile,
path_left: PathBuf,
path_right: PathBuf,
trees: Vec<(DiffTree, DiffNode, usize)>, tree: DiffTree,
node: DiffNode,
summary_map: SummaryMap,
ignore_metadata: bool,
ignore_identical: bool,
}
pub enum DiffResult {
Exit,
Return(SummaryMap),
None,
}
impl TuiResult for DiffResult {
fn exit(&self) -> bool {
!matches!(self, Self::None)
}
}
impl<'a> Diff<'a> {
pub fn new(
repo: &'a IndexedRepo,
snap_left: SnapshotFile,
snap_right: SnapshotFile,
path_left: &str,
path_right: &str,
summary_map: SummaryMap,
) -> Result<Self> {
let header = [
"Name",
"Time",
"Size",
"- RepoSize",
"Time",
"Size",
"+ RepoSize",
]
.into_iter()
.map(Text::from)
.collect();
let left = repo.node_from_snapshot_and_path(&snap_left, path_left)?;
let right = repo.node_from_snapshot_and_path(&snap_right, path_right)?;
let node = DiffNode(EitherOrBoth::Both(left, right));
let mut tree = DiffTree::from_node(repo, &node)?;
let mut app = Self {
current_screen: CurrentScreen::Diff,
table: WithBlock::new(SelectTable::new(header), Block::new()),
repo,
snapshot_left: snap_left,
snapshot_right: snap_right,
path_left: path_left.parse()?,
path_right: path_right.parse()?,
trees: Vec::new(),
tree: DiffTree::default(),
node,
summary_map,
ignore_metadata: true,
ignore_identical: true,
};
tree.nodes.retain(|node| app.show_node(node));
app.tree = tree;
app.update_table();
Ok(app)
}
fn node_changed(&self, node: &DiffNode) -> NodeDiff {
let (left, right) = node.0.as_ref().left_and_right();
let mut changed = NodeDiff::from(left, right, |left, right| {
if left.content != right.content {
return false;
}
if self.ignore_metadata
&& let (Some(id_left), Some(id_right)) = (left.subtree, right.subtree)
&& let (Some(summary_left), Some(summary_right)) = (
self.summary_map.get(&id_left),
self.summary_map.get(&id_right),
)
{
return summary_left.id_without_meta == summary_right.id_without_meta;
}
left.subtree == right.subtree
});
if self.ignore_metadata {
changed = changed.ignore_metadata();
}
changed
}
fn show_node(&self, node: &DiffNode) -> bool {
!self.ignore_identical || !self.node_changed(node).is_identical()
}
fn ls_row(&self, node: &DiffNode, stat: &mut DiffStatistics) -> Vec<Text<'static>> {
let node_mtime = |node: &Node| {
node.meta.mtime.map_or_else(
|| "?".to_string(),
|t| t.strftime("%Y-%m-%d %H:%M:%S").to_string(),
)
};
let statistics = self
.summary_map
.compute_diff_statistics(node, self.repo)
.unwrap_or_default();
let (left, right) = statistics.stats.as_ref().left_and_right();
let left_size = left.map_or_else(String::new, |s| bytes_size_to_string(s.summary.size));
let right_size = right.map_or_else(String::new, |s| bytes_size_to_string(s.summary.size));
let left_only = left.map_or_else(String::new, |s| bytes_size_to_string(s.sizes.repo_size));
let right_only =
right.map_or_else(String::new, |s| bytes_size_to_string(s.sizes.repo_size));
let changed = self.node_changed(node);
stat.apply(changed);
let name = node.name();
let name = format!("{changed} {}", name.to_string_lossy());
let (left_mtime, right_mtime) = node.map(node_mtime).left_and_right();
[
name,
left_mtime.unwrap_or_default(),
left_size,
left_only,
right_mtime.unwrap_or_default(),
right_size,
right_only,
]
.into_iter()
.map(Text::from)
.collect()
}
pub fn update_table(&mut self) {
let mut stat = DiffStatistics::default();
let old_selection = if self.tree.nodes.is_empty() {
None
} else {
Some(self.table.widget.selected().unwrap_or_default())
};
let mut rows = Vec::new();
for node in &self.tree.nodes {
let row = self.ls_row(node, &mut stat);
rows.push(row);
}
self.table.widget.set_content(rows, 1);
self.table.block = Block::new()
.borders(Borders::BOTTOM | Borders::TOP)
.title_bottom(format!(
"total: {}, files: {}, dirs: {}; {} equal, {} metadata",
self.tree.nodes.len(),
stat.files,
stat.dirs,
if self.ignore_identical {
"hide"
} else {
"show"
},
if self.ignore_metadata {
"with"
} else {
"without"
}
))
.title(format!(
"{} | {}",
if self.node.0.has_left() {
format!("{}:{}", self.snapshot_left.id, self.path_left.display())
} else {
format!("({})", self.snapshot_left.id)
},
if self.node.0.has_right() {
format!("{}:{}", self.snapshot_right.id, self.path_right.display())
} else {
format!("({})", self.snapshot_right.id)
},
))
.title_alignment(Alignment::Center);
self.table.widget.select(old_selection);
}
pub fn selected_node(&self) -> Option<DiffNode> {
self.table
.widget
.selected()
.map(|idx| self.tree.nodes[idx].clone())
}
pub fn enter(&mut self) -> Result<()> {
if let Some(idx) = self.table.widget.selected() {
let node = &self.tree.nodes[idx];
if let Some(node) = node.only_subtrees() {
self.path_left.push(node.name());
self.path_right.push(node.name());
let tree = std::mem::take(&mut self.tree);
self.trees.push((tree, self.node.clone(), idx));
let mut tree = DiffTree::from_node(self.repo, &node)?;
tree.nodes.retain(|node| self.show_node(node));
self.tree = tree;
self.node = node;
self.table.widget.set_to(0);
self.update_table();
}
}
Ok(())
}
pub fn in_root(&self) -> bool {
self.trees.is_empty()
}
pub fn goback(&mut self) {
_ = self.path_left.pop();
_ = self.path_right.pop();
if let Some((tree, node, idx)) = self.trees.pop() {
self.tree = tree;
self.node = node;
self.table.widget.set_to(idx);
self.update_table();
}
}
pub fn toggle_ignore_metadata(&mut self) {
self.ignore_metadata = !self.ignore_metadata;
self.update_table();
}
pub fn toggle_ignore_identical(&mut self) -> Result<()> {
self.ignore_identical = !self.ignore_identical;
let mut tree = DiffTree::from_node(self.repo, &self.node)?;
tree.nodes.retain(|node| self.show_node(node));
self.tree = tree;
self.update_table();
Ok(())
}
pub fn compute_summary(&mut self, node: &DiffNode) -> Result<()> {
let p = self
.repo
.progress_counter("computing (sub)-dir information");
let (left, right) = node.0.as_ref().left_and_right();
if let Some(node) = left
&& let Some(id) = node.subtree
{
self.summary_map.compute(self.repo, id, &p)?;
}
if let Some(node) = right
&& let Some(id) = node.subtree
{
self.summary_map.compute(self.repo, id, &p)?;
}
p.finish();
self.update_table();
Ok(())
}
pub fn show_summary(&mut self, node: &DiffNode) -> Result<PopUpTable> {
self.compute_summary(node)?;
let stats = self
.summary_map
.compute_diff_statistics(node, self.repo)
.unwrap_or_default();
let title_left = if node.0.has_left() {
format!("{}:{}", self.snapshot_left.id, self.path_left.display())
} else {
format!("({})", self.snapshot_left.id)
};
let title_right = if node.0.has_right() {
format!("{}:{}", self.snapshot_right.id, self.path_right.display())
} else {
format!("({})", self.snapshot_right.id)
};
let rows = stats.table(title_left, title_right);
Ok(popup_table("diff summary", rows))
}
pub fn snapshot_details(&self) -> PopUpTable {
let mut rows = Vec::new();
let mut rows_right = Vec::new();
fill_table(&self.snapshot_left, |title, value| {
rows.push(vec![Text::from(title.to_string()), Text::from(value)]);
});
fill_table(&self.snapshot_right, |_, value| {
rows_right.push(Text::from(value));
});
for (row, right) in rows.iter_mut().zip(rows_right) {
row.push(right);
}
popup_table("snapshot details", rows)
}
}
impl<'a> ProcessEvent for Diff<'a> {
type Result = Result<DiffResult>;
fn input(&mut self, event: Event) -> Result<DiffResult> {
use KeyCode::{Backspace, Char, Enter, Esc, Left, Right};
match &mut self.current_screen {
CurrentScreen::Diff => match event {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
Enter | Right => self.enter()?,
Backspace | Left => {
if self.in_root() {
self.current_screen = CurrentScreen::PromptLeave(popup_prompt(
"leave diff",
"do you want to leave the diff view? (y/n)".into(),
));
} else {
self.goback();
}
}
Esc | Char('q') => {
self.current_screen = CurrentScreen::PromptExit(popup_prompt(
"exit rustic",
"do you want to exit? (y/n)".into(),
));
}
Char('?') => {
self.current_screen =
CurrentScreen::ShowHelp(popup_text("help", HELP_TEXT.into()));
}
Char('m') => self.toggle_ignore_metadata(),
Char('d') => self.toggle_ignore_identical()?,
Char('s') => {
self.current_screen =
CurrentScreen::Table(self.show_summary(&self.node.clone())?);
}
Char('S') => {
if let Some(node) = self.selected_node() {
self.current_screen = CurrentScreen::Table(self.show_summary(&node)?);
}
}
Char('I') => {
self.current_screen = CurrentScreen::Table(self.snapshot_details());
}
_ => 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::Diff;
}
}
_ => {}
},
CurrentScreen::PromptExit(prompt) => match prompt.input(event) {
PromptResult::Ok => return Ok(DiffResult::Exit),
PromptResult::Cancel => self.current_screen = CurrentScreen::Diff,
PromptResult::None => {}
},
CurrentScreen::PromptLeave(prompt) => match prompt.input(event) {
PromptResult::Ok => {
return Ok(DiffResult::Return(std::mem::take(&mut self.summary_map)));
}
PromptResult::Cancel => self.current_screen = CurrentScreen::Diff,
PromptResult::None => {}
},
}
Ok(DiffResult::None)
}
}
impl<'a> Draw for Diff<'a> {
fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
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::Diff => {}
CurrentScreen::Table(popup) => popup.draw(area, f),
CurrentScreen::ShowHelp(popup) => popup.draw(area, f),
CurrentScreen::PromptExit(popup) | CurrentScreen::PromptLeave(popup) => {
popup.draw(area, f);
}
}
}
}