diff --git a/src/commands/tui.rs b/src/commands/tui.rs index 4a2058f..043f77c 100644 --- a/src/commands/tui.rs +++ b/src/commands/tui.rs @@ -1,6 +1,7 @@ //! `tui` subcommand mod ls; mod progress; +mod restore; mod snapshots; mod widgets; diff --git a/src/commands/tui/ls.rs b/src/commands/tui/ls.rs index 5d91a5f..b1e37d7 100644 --- a/src/commands/tui/ls.rs +++ b/src/commands/tui/ls.rs @@ -11,17 +11,21 @@ use style::palette::tailwind; use crate::commands::{ ls::{NodeLs, Summary}, - tui::widgets::{popup_text, Draw, PopUpText, ProcessEvent, SelectTable, WithBlock}, + tui::{ + restore::Restore, + widgets::{popup_text, Draw, PopUpText, ProcessEvent, SelectTable, WithBlock}, + }, }; // the states this screen can be in -enum CurrentScreen { +enum CurrentScreen<'a, P, S> { Snapshot, ShowHelp(PopUpText), + Restore(Restore<'a, P, S>), } const INFO_TEXT: &str = - "(Esc) quit | (Enter) enter dir | (Backspace) return to parent | (?) show all commands"; + "(Esc) quit | (Enter) enter dir | (Backspace) return to parent | (r) restore | (?) show all commands"; const HELP_TEXT: &str = r#" General Commands: @@ -29,19 +33,21 @@ General Commands: q,Esc : exit Enter : enter dir Backspace : return to parent dir + r : restore selected item n : toggle numeric IDs ? : show this help page "#; pub(crate) struct Snapshot<'a, P, S> { - current_screen: CurrentScreen, + current_screen: CurrentScreen<'a, P, S>, numeric: bool, table: WithBlock, repo: &'a Repository, snapshot: SnapshotFile, path: PathBuf, trees: Vec, + tree: Tree, } impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { @@ -59,7 +65,8 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { repo, snapshot, path: PathBuf::new(), - trees: vec![tree], + trees: Vec::new(), + tree, }; app.update_table(); Ok(app) @@ -94,16 +101,19 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { .collect() } + pub fn selected_node(&self) -> Option<&Node> { + self.table.widget.selected().map(|i| &self.tree.nodes[i]) + } + pub fn update_table(&mut self) { - let tree = self.trees.last().unwrap(); - let old_selection = if tree.nodes.is_empty() { + let old_selection = if self.tree.nodes.is_empty() { None } else { Some(self.table.widget.selected().unwrap_or_default()) }; let mut rows = Vec::new(); let mut summary = Summary::default(); - for node in &tree.nodes { + for node in &self.tree.nodes { summary.update(node); let row = self.ls_row(node); rows.push(row); @@ -116,7 +126,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { .title(format!("{}:{}", self.snapshot.id, self.path.display())) .title_bottom(format!( "total: {}, files: {}, dirs: {}, size: {} - {}", - tree.nodes.len(), + self.tree.nodes.len(), summary.files, summary.dirs, summary.size, @@ -132,10 +142,12 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { pub fn enter(&mut self) -> Result<()> { if let Some(idx) = self.table.widget.selected() { - let node = &self.trees.last().unwrap().nodes[idx]; + let node = &self.tree.nodes[idx]; if node.is_dir() { self.path.push(node.name()); - self.trees.push(self.repo.get_tree(&node.subtree.unwrap())?); + let tree = self.tree.clone(); + self.tree = self.repo.get_tree(&node.subtree.unwrap())?; + self.trees.push(tree); } } self.table.widget.set_to(0); @@ -145,12 +157,14 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { pub fn goback(&mut self) -> bool { _ = self.path.pop(); - _ = self.trees.pop(); - if !self.trees.is_empty() { + if let Some(tree) = self.trees.pop() { + self.tree = tree; self.table.widget.set_to(0); self.update_table(); + false + } else { + true } - self.trees.is_empty() } pub fn toggle_numeric(&mut self) { @@ -174,6 +188,17 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { CurrentScreen::ShowHelp(popup_text("help", HELP_TEXT.into())); } Char('n') => self.toggle_numeric(), + Char('r') => { + if let Some(node) = self.selected_node() { + let path = self.path.join(node.name()); + let restore = Restore::new( + self.repo, + node.clone(), + format!("{}:{}", self.snapshot.id, path.display()), + ); + self.current_screen = CurrentScreen::Restore(restore); + } + } _ => self.table.input(event), }, _ => {} @@ -186,6 +211,11 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { } _ => {} }, + CurrentScreen::Restore(restore) => { + if restore.input(event)? { + self.current_screen = CurrentScreen::Snapshot; + } + } } Ok(false) } @@ -193,20 +223,24 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { pub fn draw(&mut self, area: Rect, f: &mut Frame<'_>) { let rects = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).split(area); - // draw the table - self.table.draw(rects[0], f); + if let CurrentScreen::Restore(restore) = &mut self.current_screen { + restore.draw(area, f); + } else { + // draw the table + self.table.draw(rects[0], f); - // draw the footer - 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]); + // draw the footer + 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]); + } // draw popups match &mut self.current_screen { - CurrentScreen::Snapshot => {} + CurrentScreen::Snapshot | CurrentScreen::Restore(_) => {} CurrentScreen::ShowHelp(popup) => popup.draw(area, f), } } diff --git a/src/commands/tui/restore.rs b/src/commands/tui/restore.rs new file mode 100644 index 0000000..a30ab1e --- /dev/null +++ b/src/commands/tui/restore.rs @@ -0,0 +1,157 @@ +use anyhow::Result; +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use ratatui::prelude::*; +use rustic_core::{ + repofile::Node, IndexedFull, LocalDestination, LsOptions, ProgressBars, Repository, + RestoreOptions, RestorePlan, +}; + +use crate::{ + commands::tui::widgets::{ + popup_input, popup_prompt, Draw, PopUpInput, PopUpPrompt, PopUpText, ProcessEvent, + PromptResult, TextInputResult, + }, + helpers::bytes_size_to_string, +}; + +use super::widgets::popup_text; + +// the states this screen can be in +enum CurrentScreen { + GetDestination(PopUpInput), + PromptRestore(PopUpPrompt, Option), + RestoreDone(PopUpText), +} + +pub(crate) struct Restore<'a, P, S> { + current_screen: CurrentScreen, + repo: &'a Repository, + opts: RestoreOptions, + node: Node, + source: String, + dest: String, +} + +impl<'a, P: ProgressBars, S: IndexedFull> Restore<'a, P, S> { + pub fn new(repo: &'a Repository, node: Node, source: String) -> Self { + let opts = RestoreOptions::default(); + let title = format!("restore {} to:", source); + let popup = popup_input(title, "enter restore destination", ""); + Self { + current_screen: CurrentScreen::GetDestination(popup), + node, + repo, + opts, + source, + dest: String::new(), + } + } + + pub fn compute_plan(&mut self, dest: String, dry_run: bool) -> Result { + self.dest = dest; + let dest = LocalDestination::new(&self.dest, true, !self.node.is_dir())?; + // for restore, always recurse into tree + let ls_opts = LsOptions { + recursive: true, + ..Default::default() + }; + let ls = self.repo.ls(&self.node, &ls_opts)?; + + let plan = self.repo.prepare_restore(&self.opts, ls, &dest, dry_run)?; + + Ok(plan) + } + + // restore using the plan + // + // Note: This currently runs `prepare_restore` again and doesn't use `plan` + // TODO: Fix when restore is changed such that `prepare_restore` is always dry_run and all modification is don in `restore` + fn restore(&self, _plan: RestorePlan) -> Result<()> { + let dest = LocalDestination::new(&self.dest, true, !self.node.is_dir())?; + // for restore, always recurse into tree + let ls_opts = LsOptions { + recursive: true, + ..Default::default() + }; + let ls = self.repo.ls(&self.node, &ls_opts)?; + let plan = self + .repo + .prepare_restore(&self.opts, ls.clone(), &dest, false)?; + + // the actual restore + self.repo.restore(plan, &self.opts, ls, &dest)?; + Ok(()) + } + + pub fn input(&mut self, event: Event) -> Result { + use KeyCode::*; + match &mut self.current_screen { + CurrentScreen::GetDestination(prompt) => match prompt.input(event) { + TextInputResult::Cancel => return Ok(true), + TextInputResult::Input(input) => { + let plan = self.compute_plan(input, true)?; + let fs = plan.stats.files; + let ds = plan.stats.dirs; + let popup = popup_prompt( + "restore information", + Text::from(format!( + r#" +restoring from: {} +restoring to: {} + +Files: {} to restore, {} unchanged, {} verified, {} to modify, {} additional +Dirs: {} to restore, {} to modify, {} additional +Total restore size: {} + +Do you want to proceed (y/n)? + "#, + self.source, + self.dest, + fs.restore, + fs.unchanged, + fs.verified, + fs.modify, + fs.additional, + ds.restore, + ds.modify, + ds.additional, + bytes_size_to_string(plan.restore_size) + )), + ); + self.current_screen = CurrentScreen::PromptRestore(popup, Some(plan)); + } + TextInputResult::None => {} + }, + CurrentScreen::PromptRestore(prompt, plan) => match prompt.input(event) { + PromptResult::Ok => { + let plan = plan.take().unwrap(); + self.restore(plan)?; + self.current_screen = CurrentScreen::RestoreDone(popup_text( + "restore done", + format!("restored {} successfully to {}", self.source, self.dest).into(), + )); + } + PromptResult::Cancel => return Ok(true), + PromptResult::None => {} + }, + CurrentScreen::RestoreDone(_) => match event { + Event::Key(key) if key.kind == KeyEventKind::Press => { + if matches!(key.code, Char('q') | Esc | Enter | Char(' ')) { + return Ok(true); + } + } + _ => {} + }, + } + Ok(false) + } + + pub fn draw(&mut self, area: Rect, f: &mut Frame<'_>) { + // draw popups + match &mut self.current_screen { + CurrentScreen::GetDestination(popup) => popup.draw(area, f), + CurrentScreen::PromptRestore(popup, _) => popup.draw(area, f), + CurrentScreen::RestoreDone(popup) => popup.draw(area, f), + } + } +} diff --git a/src/commands/tui/widgets.rs b/src/commands/tui/widgets.rs index 9712800..afd1ad3 100644 --- a/src/commands/tui/widgets.rs +++ b/src/commands/tui/widgets.rs @@ -40,7 +40,7 @@ pub trait Draw { // the widgets we are using and convenience builders pub type PopUpInput = PopUp>; -pub fn popup_input(title: &'static str, text: &str, initial: &str) -> PopUpInput { +pub fn popup_input(title: impl Into>, text: &str, initial: &str) -> PopUpInput { PopUp(WithBlock::new( TextInput::new(text, initial), Block::bordered().title(title), @@ -56,7 +56,10 @@ pub fn popup_text(title: impl Into>, text: Text<'static>) -> PopU } pub type PopUpTable = PopUp>; -pub fn popup_table(title: &'static str, content: Vec>>) -> PopUpTable { +pub fn popup_table( + title: impl Into>, + content: Vec>>, +) -> PopUpTable { PopUp(WithBlock::new( SizedTable::new(content), Block::bordered().title(title),