mirror of
https://github.com/rustic-rs/rustic.git
synced 2025-10-26 11:18:51 +00:00
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:
parent
12660103a9
commit
740cb4699d
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
))
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user