diff --git a/src/commands/tui/ls.rs b/src/commands/tui/ls.rs index 6123461..2956fa1 100644 --- a/src/commands/tui/ls.rs +++ b/src/commands/tui/ls.rs @@ -5,6 +5,7 @@ use crossterm::event::{Event, KeyCode, KeyEventKind}; use ratatui::{prelude::*, widgets::*}; use rustic_core::{ repofile::{Node, SnapshotFile, Tree}, + vfs::OpenFile, IndexedFull, ProgressBars, Repository, }; use style::palette::tailwind; @@ -14,22 +15,25 @@ use crate::commands::{ tui::{ restore::Restore, widgets::{ - popup_prompt, popup_text, Draw, PopUpPrompt, PopUpText, ProcessEvent, PromptResult, - SelectTable, WithBlock, + popup_prompt, popup_scrollable_text, popup_text, Draw, PopUpPrompt, PopUpText, + ProcessEvent, PromptResult, SelectTable, TextInputResult, WithBlock, }, }, }; +use super::widgets::PopUpInput; + // the states this screen can be in enum CurrentScreen<'a, P, S> { Snapshot, ShowHelp(PopUpText), Restore(Restore<'a, P, S>), PromptExit(PopUpPrompt), + ShowFile(PopUpInput), } const INFO_TEXT: &str = - "(Esc) quit | (Enter) enter dir | (Backspace) return to parent | (r) restore | (?) show all commands"; + "(Esc) quit | (Enter) enter dir | (Backspace) return to parent | (v) view | (r) restore | (?) show all commands"; const HELP_TEXT: &str = r#" General Commands: @@ -37,6 +41,7 @@ General Commands: q,Esc : exit Enter : enter dir Backspace : return to parent dir + v : view file contents (text files only, up to 1MiB) r : restore selected item n : toggle numeric IDs ? : show this help page @@ -204,6 +209,33 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { CurrentScreen::ShowHelp(popup_text("help", HELP_TEXT.into())); } Char('n') => self.toggle_numeric(), + Char('v') => { + // viewing is not supported on cold repositories + if self.repo.config().is_hot != Some(true) { + if let Some(node) = self.selected_node() { + if node.is_file() { + if let Ok(data) = OpenFile::from_node(self.repo, node).read_at( + self.repo, + 0, + node.meta.size.min(1_000_000).try_into().unwrap(), + ) { + // viewing is only supported for text files + if let Ok(content) = String::from_utf8(data.to_vec()) { + let lines = content.lines().count(); + let path = self.path.join(node.name()); + let path = path.display(); + self.current_screen = + CurrentScreen::ShowFile(popup_scrollable_text( + format!("{}:/{path}", self.snapshot.id), + &content, + (lines + 1).min(40).try_into().unwrap(), + )); + } + } + } + } + } + } Char('r') => { if let Some(node) = self.selected_node() { let is_absolute = self @@ -231,6 +263,12 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { }, _ => {} }, + CurrentScreen::ShowFile(prompt) => match prompt.input(event) { + TextInputResult::Cancel | TextInputResult::Input(_) => { + self.current_screen = CurrentScreen::Snapshot; + } + TextInputResult::None => {} + }, CurrentScreen::ShowHelp(_) => match event { Event::Key(key) if key.kind == KeyEventKind::Press => { if matches!(key.code, Char('q') | Esc | Enter | Char(' ') | Char('?')) { @@ -276,6 +314,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { CurrentScreen::Snapshot | CurrentScreen::Restore(_) => {} CurrentScreen::ShowHelp(popup) => popup.draw(area, f), CurrentScreen::PromptExit(popup) => popup.draw(area, f), + CurrentScreen::ShowFile(popup) => popup.draw(area, f), } } } diff --git a/src/commands/tui/widgets.rs b/src/commands/tui/widgets.rs index f15e43b..e9fde21 100644 --- a/src/commands/tui/widgets.rs +++ b/src/commands/tui/widgets.rs @@ -49,7 +49,18 @@ pub fn popup_input( lines: u16, ) -> PopUpInput { PopUp(WithBlock::new( - TextInput::new(text, initial, lines), + TextInput::new(Some(text), initial, lines, true), + Block::bordered().title(title), + )) +} + +pub fn popup_scrollable_text( + title: impl Into>, + text: &str, + lines: u16, +) -> PopUpInput { + PopUp(WithBlock::new( + TextInput::new(None, text, lines, false), Block::bordered().title(title), )) } diff --git a/src/commands/tui/widgets/text_input.rs b/src/commands/tui/widgets/text_input.rs index 66d0980..9295d76 100644 --- a/src/commands/tui/widgets/text_input.rs +++ b/src/commands/tui/widgets/text_input.rs @@ -1,11 +1,12 @@ use super::*; use crossterm::event::KeyModifiers; -use tui_textarea::TextArea; +use tui_textarea::{CursorMove, TextArea}; pub struct TextInput { textarea: TextArea<'static>, lines: u16, + changeable: bool, } pub enum TextInputResult { @@ -15,12 +16,21 @@ pub enum TextInputResult { } impl TextInput { - pub fn new(text: &str, initial: &str, lines: u16) -> Self { + pub fn new(text: Option<&str>, initial: &str, lines: u16, changeable: bool) -> Self { let mut textarea = TextArea::default(); textarea.set_style(Style::default()); - textarea.set_placeholder_text(text); + if let Some(text) = text { + textarea.set_placeholder_text(text); + } _ = textarea.insert_str(initial); - Self { textarea, lines } + if !changeable { + textarea.move_cursor(CursorMove::Top); + } + Self { + textarea, + lines, + changeable, + } } } @@ -40,21 +50,36 @@ impl ProcessEvent for TextInput { type Result = TextInputResult; fn input(&mut self, event: Event) -> TextInputResult { if let Event::Key(key) = event { + let KeyEvent { + code, modifiers, .. + } = key; use KeyCode::*; - match key { - KeyEvent { code: Esc, .. } => return TextInputResult::Cancel, - KeyEvent { code: Enter, .. } if self.lines == 1 => { - return TextInputResult::Input(self.textarea.lines().join("\n")); + if self.changeable { + match (code, modifiers) { + (Esc, _) => return TextInputResult::Cancel, + (Enter, _) if self.lines == 1 => { + return TextInputResult::Input(self.textarea.lines().join("\n")); + } + (Char('s'), KeyModifiers::CONTROL) => { + return TextInputResult::Input(self.textarea.lines().join("\n")); + } + _ => { + _ = self.textarea.input(key); + } } - KeyEvent { - code: Char('s'), - modifiers: KeyModifiers::CONTROL, - .. - } => { - return TextInputResult::Input(self.textarea.lines().join("\n")); - } - key => { - _ = self.textarea.input(key); + } else { + match (code, modifiers) { + (Esc | Enter | Char('q') | Char('x'), _) => return TextInputResult::Cancel, + (Home, _) => { + self.textarea.move_cursor(CursorMove::Top); + } + (End, _) => { + self.textarea.move_cursor(CursorMove::Bottom); + } + (PageDown | PageUp | Up | Down, _) => { + _ = self.textarea.input(key); + } + _ => {} } } }