diff --git a/src/commands/tui.rs b/src/commands/tui.rs index 219a43157..bcc6e397d 100644 --- a/src/commands/tui.rs +++ b/src/commands/tui.rs @@ -6,6 +6,7 @@ mod snapshots; mod tree; mod widgets; +use crossterm::event::{KeyEvent, KeyModifiers}; use progress::TuiProgressBars; use snapshots::Snapshots; @@ -74,14 +75,18 @@ fn run_app( let event = event::read()?; use KeyCode::*; - match event { - Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { - Char('q') | Esc => return Ok(()), - _ => {} - }, - _ => {} + if let Event::Key(KeyEvent { + code: Char('c'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + .. + }) = event + { + return Ok(()); + } + if app.snapshots.input(event)? { + return Ok(()); } - app.snapshots.input(event)?; } } diff --git a/src/commands/tui/ls.rs b/src/commands/tui/ls.rs index 6f000eddc..40c8a1283 100644 --- a/src/commands/tui/ls.rs +++ b/src/commands/tui/ls.rs @@ -50,6 +50,12 @@ pub(crate) struct Snapshot<'a, P, S> { tree: Tree, } +pub enum SnapshotResult { + Exit, + Return, + None, +} + impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { pub fn new(repo: &'a Repository, snapshot: SnapshotFile) -> Result { let header = ["Name", "Size", "Mode", "User", "Group", "Time"] @@ -172,7 +178,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { self.update_table(); } - pub fn input(&mut self, event: Event) -> Result { + pub fn input(&mut self, event: Event) -> Result { use KeyCode::*; match &mut self.current_screen { CurrentScreen::Snapshot => match event { @@ -180,9 +186,12 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { Enter | Right => self.enter()?, Backspace | Left => { if self.goback() { - return Ok(true); + return Ok(SnapshotResult::Return); } } + Esc | Char('q') => { + return Ok(SnapshotResult::Exit); + } Char('?') => { self.current_screen = CurrentScreen::ShowHelp(popup_text("help", HELP_TEXT.into())); @@ -217,7 +226,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> { } } } - Ok(false) + Ok(SnapshotResult::None) } pub fn draw(&mut self, area: Rect, f: &mut Frame<'_>) { diff --git a/src/commands/tui/restore.rs b/src/commands/tui/restore.rs index a30ab1e20..5c18cb35b 100644 --- a/src/commands/tui/restore.rs +++ b/src/commands/tui/restore.rs @@ -36,7 +36,7 @@ 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", ""); + let popup = popup_input(title, "enter restore destination", "", 1); Self { current_screen: CurrentScreen::GetDestination(popup), node, diff --git a/src/commands/tui/snapshots.rs b/src/commands/tui/snapshots.rs index c4d14f0fa..643de1721 100644 --- a/src/commands/tui/snapshots.rs +++ b/src/commands/tui/snapshots.rs @@ -14,7 +14,7 @@ use crate::{ commands::{ snapshots::{fill_table, snap_to_table}, tui::{ - ls::Snapshot, + ls::{Snapshot, SnapshotResult}, tree::{Tree, TreeIterItem, TreeNode}, widgets::{ popup_input, popup_prompt, popup_table, popup_text, Draw, PopUpInput, PopUpPrompt, @@ -32,6 +32,7 @@ enum CurrentScreen<'a, P, S> { ShowHelp(PopUpText), SnapshotDetails(PopUpTable), EnterLabel(PopUpInput), + EnterDescription(PopUpInput), EnterAddTags(PopUpInput), EnterSetTags(PopUpInput), EnterRemoveTags(PopUpInput), @@ -90,6 +91,8 @@ Commands applied to marked snapshot(s) (selected if none marked): l : set label for snapshot(s) Ctrl-l : remove label for snapshot(s) + d : set description for snapshot(s) + Ctrl-d : remove description for snapshot(s) t : add tag(s) for snapshot(s) Ctrl-t : remove all tags for snapshot(s) s : set tag(s) for snapshot(s) @@ -504,46 +507,37 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { self.update_table(); } - pub fn get_label(&mut self) -> String { + pub fn get_snap_entity(&mut self, f: impl Fn(&SnapshotFile) -> String) -> String { let has_mark = self.has_mark(); if !has_mark { self.toggle_mark(); } - let label = self + let entity = self .snapshots .iter() .zip(self.snaps_status.iter()) - .filter_map(|(snap, status)| status.marked.then_some(snap.label.clone())) - .reduce(|label, l| if label == l { l } else { String::new() }) + .filter_map(|(snap, status)| status.marked.then_some(f(snap))) + .reduce(|entity, e| if entity == e { e } else { String::new() }) .unwrap_or_default(); if !has_mark { self.toggle_mark(); } - label + entity } - pub fn get_tags(&mut self) -> String { - let has_mark = self.has_mark(); - - if !has_mark { - self.toggle_mark(); - } + pub fn get_label(&mut self) -> String { + self.get_snap_entity(|snap| snap.label.clone()) + } - let label = self - .snapshots - .iter() - .zip(self.snaps_status.iter()) - .filter_map(|(snap, status)| status.marked.then_some(snap.tags.formatln())) - .reduce(|tags, t| if tags == t { t } else { String::new() }) - .unwrap_or_default(); + pub fn get_tags(&mut self) -> String { + self.get_snap_entity(|snap| snap.tags.formatln()) + } - if !has_mark { - self.toggle_mark(); - } - label + pub fn get_description(&mut self) -> String { + self.get_snap_entity(|snap| snap.description.clone().unwrap_or_default()) } pub fn set_label(&mut self, label: String) { @@ -560,6 +554,21 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { self.set_label(String::new()); } + pub fn set_description(&mut self, desc: String) { + let desc = if desc.is_empty() { None } else { Some(desc) }; + self.process_marked_snaps(|snap| { + if snap.description == desc { + return false; + } + snap.description = desc.clone(); + true + }); + } + + pub fn clear_description(&mut self) { + self.set_description(String::new()); + } + pub fn add_tags(&mut self, tags: String) { let tags = vec![StringList::from_str(&tags).unwrap()]; self.process_marked_snaps(|snap| snap.add_tags(tags.clone())); @@ -593,6 +602,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { pub fn apply_input(&mut self, input: String) { match self.current_screen { CurrentScreen::EnterLabel(_) => self.set_label(input), + CurrentScreen::EnterDescription(_) => self.set_description(input), CurrentScreen::EnterAddTags(_) => self.add_tags(input), CurrentScreen::EnterSetTags(_) => self.set_tags(input), CurrentScreen::EnterRemoveTags(_) => self.remove_tags(input), @@ -639,7 +649,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { Ok(()) } - pub fn input(&mut self, event: Event) -> Result<()> { + pub fn input(&mut self, event: Event) -> Result { use KeyCode::*; match &mut self.current_screen { CurrentScreen::Snapshots => { @@ -650,12 +660,14 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { Char('x') => self.clear_marks(), Char('f') => self.clear_filter(), Char('l') => self.clear_label(), + Char('d') => self.clear_description(), Char('t') => self.clear_tags(), Char('p') => self.clear_delete_protection(), _ => {} } } else { match key.code { + Esc | Char('q') => return Ok(true), F(5) => self.reread()?, Enter => { if let Some(dir) = self.dir()? { @@ -692,13 +704,24 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { "set label", "enter label", &self.get_label(), + 1, )); } + Char('d') => { + self.current_screen = + CurrentScreen::EnterDescription(popup_input( + "set description (Ctrl-s to confirm)", + "enter description", + &self.get_description(), + 5, + )); + } Char('t') => { self.current_screen = CurrentScreen::EnterAddTags(popup_input( "add tags", "enter tags", "", + 1, )); } Char('s') => { @@ -706,11 +729,12 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { "set tags", "enter tags", &self.get_tags(), + 1, )); } Char('r') => { self.current_screen = CurrentScreen::EnterRemoveTags( - popup_input("remove tags", "enter tags", ""), + popup_input("remove tags", "enter tags", "", 1), ); } // TODO: Allow to enter delete protection option @@ -744,6 +768,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { _ => {} }, CurrentScreen::EnterLabel(prompt) + | CurrentScreen::EnterDescription(prompt) | CurrentScreen::EnterAddTags(prompt) | CurrentScreen::EnterSetTags(prompt) | CurrentScreen::EnterRemoveTags(prompt) => match prompt.input(event) { @@ -762,13 +787,13 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots, PromptResult::None => {} }, - CurrentScreen::Dir(dir) => { - if dir.input(event)? { - self.current_screen = CurrentScreen::Snapshots; - } - } + CurrentScreen::Dir(dir) => match dir.input(event)? { + SnapshotResult::Exit => return Ok(true), + SnapshotResult::Return => self.current_screen = CurrentScreen::Snapshots, + SnapshotResult::None => {} + }, } - Ok(()) + Ok(false) } pub fn draw(&mut self, area: Rect, f: &mut Frame<'_>) { @@ -795,6 +820,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { CurrentScreen::SnapshotDetails(popup) => popup.draw(area, f), CurrentScreen::ShowHelp(popup) => popup.draw(area, f), CurrentScreen::EnterLabel(popup) + | CurrentScreen::EnterDescription(popup) | CurrentScreen::EnterAddTags(popup) | CurrentScreen::EnterSetTags(popup) | CurrentScreen::EnterRemoveTags(popup) => popup.draw(area, f), diff --git a/src/commands/tui/widgets.rs b/src/commands/tui/widgets.rs index afd1ad36b..a6081f904 100644 --- a/src/commands/tui/widgets.rs +++ b/src/commands/tui/widgets.rs @@ -40,9 +40,14 @@ pub trait Draw { // the widgets we are using and convenience builders pub type PopUpInput = PopUp>; -pub fn popup_input(title: impl Into>, text: &str, initial: &str) -> PopUpInput { +pub fn popup_input( + title: impl Into>, + text: &str, + initial: &str, + lines: u16, +) -> PopUpInput { PopUp(WithBlock::new( - TextInput::new(text, initial), + TextInput::new(text, initial, lines), Block::bordered().title(title), )) } diff --git a/src/commands/tui/widgets/text_input.rs b/src/commands/tui/widgets/text_input.rs index be7950127..f5fa35da6 100644 --- a/src/commands/tui/widgets/text_input.rs +++ b/src/commands/tui/widgets/text_input.rs @@ -1,9 +1,11 @@ use super::*; +use crossterm::event::KeyModifiers; use tui_textarea::TextArea; pub struct TextInput { textarea: TextArea<'static>, + lines: u16, } pub enum TextInputResult { @@ -13,18 +15,18 @@ pub enum TextInputResult { } impl TextInput { - pub fn new(text: &str, initial: &str) -> Self { + pub fn new(text: &str, initial: &str, lines: u16) -> Self { let mut textarea = TextArea::default(); textarea.set_style(Style::default()); textarea.set_placeholder_text(text); _ = textarea.insert_str(initial); - Self { textarea } + Self { textarea, lines } } } impl SizedWidget for TextInput { fn height(&self) -> Option { - Some(1) + Some(self.lines) } } @@ -38,14 +40,18 @@ impl ProcessEvent for TextInput { type Result = TextInputResult; fn input(&mut self, event: Event) -> TextInputResult { if let Event::Key(key) = event { - if key.kind != KeyEventKind::Press { - return TextInputResult::None; - } use KeyCode::*; match key { KeyEvent { code: Esc, .. } => return TextInputResult::Cancel, - KeyEvent { code: Enter, .. } => { - return TextInputResult::Input(self.textarea.lines()[0].clone()); + KeyEvent { code: Enter, .. } if self.lines == 1 => { + return TextInputResult::Input(self.textarea.lines().join("\n")); + } + KeyEvent { + code: Char('s'), + modifiers: KeyModifiers::CONTROL, + .. + } => { + return TextInputResult::Input(self.textarea.lines().join("\n")); } key => { _ = self.textarea.input(key);