feat(interactive): Allow to view text files (#1216)

Adds the possibility to view text files up to 1MiB in the interactive
snapshot tree view. Note this is a quick-and-dirty implementation which
could be improved better non-text-files and which could be optimized to
read files of all sizes
This commit is contained in:
aawsome 2024-09-08 19:32:44 +02:00 committed by GitHub
parent 12660103a9
commit 740cb4699d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 21 deletions

View File

@ -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),
}
}
}

View File

@ -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<Title<'static>>,
text: &str,
lines: u16,
) -> PopUpInput {
PopUp(WithBlock::new(
TextInput::new(None, text, lines, false),
Block::bordered().title(title),
))
}

View File

@ -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);
}
_ => {}
}
}
}