From 4ed7405edf6af7b2bb9dc8d560c2a0bb567d59b7 Mon Sep 17 00:00:00 2001 From: veeso Date: Fri, 26 Nov 2021 17:12:04 +0100 Subject: [PATCH] wip --- .../activities/auth/components/bookmarks.rs | 14 +- src/ui/activities/auth/view.rs | 2 +- src/ui/activities/filetransfer/actions/mod.rs | 20 +- .../activities/filetransfer/components/log.rs | 274 ++++++++++ .../filetransfer}/components/mod.rs | 41 +- .../filetransfer/components/transfer.rs | 468 ++++++++++++++++++ src/ui/activities/filetransfer/mod.rs | 199 ++++++-- src/ui/activities/filetransfer/update.rs | 23 +- src/ui/activities/setup/components/config.rs | 4 +- src/ui/activities/setup/components/theme.rs | 4 +- src/ui/activities/setup/view/mod.rs | 2 +- src/ui/components/logbox.rs | 433 ---------------- src/ui/mod.rs | 1 - 13 files changed, 963 insertions(+), 522 deletions(-) create mode 100644 src/ui/activities/filetransfer/components/log.rs rename src/ui/{ => activities/filetransfer}/components/mod.rs (51%) create mode 100644 src/ui/activities/filetransfer/components/transfer.rs delete mode 100644 src/ui/components/logbox.rs diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs index a7286bf..09052b4 100644 --- a/src/ui/activities/auth/components/bookmarks.rs +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -51,7 +51,12 @@ impl BookmarksList { .scroll(true) .step(4) .title("Bookmarks", Alignment::Left) - .rows(bookmarks.iter().map(|x| vec![TextSpan::from(x)]).collect()), + .rows( + bookmarks + .iter() + .map(|x| vec![TextSpan::from(x.as_str())]) + .collect(), + ), } } } @@ -125,7 +130,12 @@ impl RecentsList { .scroll(true) .step(4) .title("Recent connections", Alignment::Left) - .rows(bookmarks.iter().map(|x| vec![TextSpan::from(x)]).collect()), + .rows( + bookmarks + .iter() + .map(|x| vec![TextSpan::from(x.as_str())]) + .collect(), + ), } } } diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 254d1ce..a4918dd 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -886,7 +886,7 @@ impl AuthActivity { /// ### no_popup_mounted_clause /// /// Returns a sub clause which requires that no popup is mounted in order to be satisfied - fn no_popup_mounted_clause() -> SubClause { + fn no_popup_mounted_clause() -> SubClause { SubClause::And( Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::ErrorPopup, diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index e384951..2e4fad8 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -26,10 +26,10 @@ * SOFTWARE. */ pub(self) use super::{ - browser::FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, TransferOpts, + browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts, TransferPayload, }; -use tuirealm::{Payload, Value}; +use tuirealm::{State, StateValue}; // actions pub(crate) mod change_dir; @@ -79,7 +79,7 @@ impl FileTransferActivity { /// /// Get local file entry pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry { - match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) { + match self.get_selected_index(&Id::ExplorerLocal) { SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)), SelectedEntryIndex::Many(files) => { let files: Vec<&FsEntry> = files @@ -97,7 +97,7 @@ impl FileTransferActivity { /// /// Get remote file entry pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry { - match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) { + match self.get_selected_index(&Id::ExplorerRemote) { SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)), SelectedEntryIndex::Many(files) => { let files: Vec<&FsEntry> = files @@ -115,7 +115,7 @@ impl FileTransferActivity { /// /// Get remote file entry pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry { - match self.get_selected_index(super::COMPONENT_EXPLORER_FIND) { + match self.get_selected_index(&Id::ExplorerFind) { SelectedEntryIndex::One(idx) => { SelectedEntry::from(self.found().as_ref().unwrap().get(idx)) } @@ -133,14 +133,14 @@ impl FileTransferActivity { // -- private - fn get_selected_index(&self, component: &str) -> SelectedEntryIndex { - match self.view.get_state(component) { - Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx), - Some(Payload::Vec(files)) => { + fn get_selected_index(&self, id: &Id) -> SelectedEntryIndex { + match self.app.state(id) { + Ok(State::One(StateValue::Usize(idx))) => SelectedEntryIndex::One(idx), + Ok(State::Vec(files)) => { let list: Vec = files .iter() .map(|x| match x { - Value::Usize(v) => *v, + StateValue::Usize(v) => *v, _ => 0, }) .collect(); diff --git a/src/ui/activities/filetransfer/components/log.rs b/src/ui/activities/filetransfer/components/log.rs new file mode 100644 index 0000000..695bf48 --- /dev/null +++ b/src/ui/activities/filetransfer/components/log.rs @@ -0,0 +1,274 @@ +//! ## Log +//! +//! log tab component + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{Msg, UiMsg}; + +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table}; +use tuirealm::tui::layout::Corner; +use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue}; + +pub struct Log { + props: Props, + states: OwnStates, +} + +impl Log { + pub fn new(lines: Table, fg: Color, bg: Color) -> Self { + let mut props = Props::default(); + props.set(Attribute::Foreground, AttrValue::Color(fg)); + props.set(Attribute::Background, AttrValue::Color(bg)); + props.set(Attribute::Content, AttrValue::Table(lines)); + Self { + props, + states: OwnStates::default(), + } + } +} + +impl MockComponent for Log { + fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) { + let width: usize = area.width as usize - 4; + let focus = self + .props + .get_or(Attribute::Focus, AttrValue::Flag(false)) + .unwrap_flag(); + let fg = self + .props + .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset)) + .unwrap_color(); + let bg = self + .props + .get_or(Attribute::Background, AttrValue::Color(Color::Reset)) + .unwrap_color(); + // Make list + let list_items: Vec = self + .props + .get(Attribute::Content) + .unwrap() + .unwrap_table() + .iter() + .map(|row| ListItem::new(tui_realm_stdlib::utils::wrap_spans(row, width, &self.props))) + .collect(); + let w = TuiList::new(list_items) + .block(tui_realm_stdlib::utils::get_block( + Borders::default().color(fg), + Some(("Log".to_string(), Alignment::Left)), + focus, + None, + )) + .start_corner(Corner::BottomLeft) + .highlight_symbol(">> ") + .style(Style::default().bg(bg)) + .highlight_style(Style::default()); + let mut state: ListState = ListState::default(); + state.select(Some(self.states.list_index)); + frame.render_stateful_widget(w, area, &mut state); + } + + fn query(&self, attr: Attribute) -> Option { + self.props.query(attr) + } + + fn attr(&mut self, attr: Attribute, value: AttrValue) { + self.props.set(attr, value); + } + + fn state(&self) -> State { + State::One(StateValue::Usize(self.states.list_index)) + } + + fn perform(&mut self, cmd: Cmd) -> CmdResult { + match cmd { + Cmd::Move(Direction::Down) => { + let prev = self.states.list_index; + self.states.incr_list_index(); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Move(Direction::Up) => { + let prev = self.states.list_index; + self.states.decr_list_index(); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Scroll(Direction::Down) => { + let prev = self.states.list_index; + (0..8).for_each(|_| self.states.incr_list_index()); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Scroll(Direction::Up) => { + let prev = self.states.list_index; + (0..8).for_each(|_| self.states.decr_list_index()); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::GoTo(Position::Begin) => { + let prev = self.states.list_index; + self.states.list_index_at_first(); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::GoTo(Position::End) => { + let prev = self.states.list_index; + self.states.list_index_at_last(); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + _ => CmdResult::None, + } + } +} + +impl Component for Log { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + // -- comp msg + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::Ui(UiMsg::LogTabbed)), + _ => None, + } + } +} + +// -- states + +/// ## OwnStates +/// +/// OwnStates contains states for this component +#[derive(Clone)] +struct OwnStates { + list_index: usize, // Index of selected element in list + list_len: usize, // Length of file list + focus: bool, // Has focus? +} + +impl Default for OwnStates { + fn default() -> Self { + OwnStates { + list_index: 0, + list_len: 0, + focus: false, + } + } +} + +impl OwnStates { + /// ### set_list_len + /// + /// Set list length + pub fn set_list_len(&mut self, len: usize) { + self.list_len = len; + } + + /// ### get_list_index + /// + /// Return current value for list index + pub fn get_list_index(&self) -> usize { + self.list_index + } + + /// ### incr_list_index + /// + /// Incremenet list index + pub fn incr_list_index(&mut self) { + // Check if index is at last element + if self.list_index + 1 < self.list_len { + self.list_index += 1; + } + } + + /// ### decr_list_index + /// + /// Decrement list index + pub fn decr_list_index(&mut self) { + // Check if index is bigger than 0 + if self.list_index > 0 { + self.list_index -= 1; + } + } + + /// ### reset_list_index + /// + /// Reset list index to last element + pub fn reset_list_index(&mut self) { + self.list_index = 0; // Last element is always 0 + } +} diff --git a/src/ui/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs similarity index 51% rename from src/ui/components/mod.rs rename to src/ui/activities/filetransfer/components/mod.rs index a4b11df..6d2f357 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/activities/filetransfer/components/mod.rs @@ -1,6 +1,6 @@ //! ## Components //! -//! `Components` is the module which contains the definitions for all the GUI components for termscp +//! file transfer activity components /** * MIT License @@ -25,5 +25,40 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -// exports -pub mod logbox; +use super::{Msg, TransferMsg, UiMsg}; + +use tui_realm_stdlib::Phantom; +use tuirealm::{ + event::{Event, Key, KeyEvent, KeyModifiers}, + Component, MockComponent, NoUserEvent, +}; + +// -- export +mod log; +mod transfer; + +pub use self::log::Log; +pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote}; + +#[derive(Default, MockComponent)] +pub struct GlobalListener { + component: Phantom, +} + +impl Component for GlobalListener { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Char('q'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowQuitPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('h'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), + } + } +} diff --git a/src/ui/activities/filetransfer/components/transfer.rs b/src/ui/activities/filetransfer/components/transfer.rs new file mode 100644 index 0000000..1ca8ed2 --- /dev/null +++ b/src/ui/activities/filetransfer/components/transfer.rs @@ -0,0 +1,468 @@ +//! ## Transfer +//! +//! file transfer components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{Msg, TransferMsg, UiMsg}; + +use tui_realm_stdlib::List; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, Borders, Color, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +#[derive(MockComponent)] +pub struct ExplorerFind { + component: List, +} + +impl ExplorerFind { + pub fn new(title: &str, files: &str, bg: Color, fg: Color, hg: Color) -> Self { + Self { + component: List::default() + .background(bg) + .borders(Borders::default().color(fg)) + .foreground(fg) + .highlighted_color(hg) + .rewind(true) + .scroll(true) + .step(8) + .title(title, Alignment::Left) + .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), + } + } +} + +impl Component for ExplorerFind { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + // -- comp msg + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ExplorerTabbed)) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseFindExplorer)) + } + Event::Keyboard(KeyEvent { + code: Key::Left | Key::Right, + .. + }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Transfer(TransferMsg::EnterDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char(' '), + .. + }) => Some(Msg::Transfer(TransferMsg::TransferFile)), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)), + Event::Keyboard(KeyEvent { + code: Key::Char('b'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('e') | Key::Delete, + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('v'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('w'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)), + } + } +} + +#[derive(MockComponent)] +pub struct ExplorerLocal { + component: List, +} + +impl ExplorerLocal { + pub fn new(title: &str, files: &str, bg: Color, fg: Color, hg: Color) -> Self { + Self { + component: List::default() + .background(bg) + .borders(Borders::default().color(fg)) + .foreground(fg) + .highlighted_color(hg) + .rewind(true) + .scroll(true) + .step(8) + .title(title, Alignment::Left) + .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), + } + } +} + +impl Component for ExplorerLocal { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + // -- comp msg + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ExplorerTabbed)) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Left | Key::Right, + .. + }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Transfer(TransferMsg::EnterDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char(' '), + .. + }) => Some(Msg::Transfer(TransferMsg::TransferFile)), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)), + Event::Keyboard(KeyEvent { + code: Key::Char('b'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowCopyPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('d'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('e') | Key::Delete, + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('g'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowGotoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('i'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('l'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::ReloadDir)), + Event::Keyboard(KeyEvent { + code: Key::Char('m'), + modifiers: KeyModifiers::NONE, + }) => match self.state() { + State::One(StateValue::Usize(i)) => Some(Msg::Ui(UiMsg::SelectFile(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('o'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenTextFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('r'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowRenamePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('u'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::GoToParentDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('x'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowExecPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)), + Event::Keyboard(KeyEvent { + code: Key::Char('v'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('w'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)), + } + } +} + +#[derive(MockComponent)] +pub struct ExplorerRemote { + component: List, +} + +impl ExplorerRemote { + pub fn new(title: &str, files: &str, bg: Color, fg: Color, hg: Color) -> Self { + Self { + component: List::default() + .background(bg) + .borders(Borders::default().color(fg)) + .foreground(fg) + .highlighted_color(hg) + .rewind(true) + .scroll(true) + .step(8) + .title(title, Alignment::Left) + .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), + } + } +} + +impl Component for ExplorerRemote { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + // -- comp msg + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ExplorerTabbed)) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Left | Key::Right, + .. + }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Transfer(TransferMsg::EnterDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char(' '), + .. + }) => Some(Msg::Transfer(TransferMsg::TransferFile)), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)), + Event::Keyboard(KeyEvent { + code: Key::Char('b'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowCopyPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('d'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('e') | Key::Delete, + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('g'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowGotoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('i'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('l'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::ReloadDir)), + Event::Keyboard(KeyEvent { + code: Key::Char('m'), + modifiers: KeyModifiers::NONE, + }) => match self.state() { + State::One(StateValue::Usize(i)) => Some(Msg::Ui(UiMsg::SelectFile(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('o'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenTextFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('r'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowRenamePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('u'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::GoToParentDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('x'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowExecPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)), + Event::Keyboard(KeyEvent { + code: Key::Char('v'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('w'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)), + } + } +} diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 3a9f6ab..5f3dd41 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -26,19 +26,20 @@ * SOFTWARE. */ // This module is split into files, cause it's just too big -pub(self) mod actions; -pub(self) mod lib; -pub(self) mod misc; -pub(self) mod session; -pub(self) mod update; -pub(self) mod view; +mod actions; +mod components; +mod lib; +mod misc; +mod session; +mod update; +mod view; // locals use super::{Activity, Context, ExitReason}; use crate::config::themes::Theme; use crate::filetransfer::{FileTransfer, FileTransferProtocol}; use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer}; -use crate::fs::explorer::FileExplorer; +use crate::fs::explorer::{FileExplorer, FileSorting}; use crate::fs::FsEntry; use crate::host::Localhost; use crate::system::config_client::ConfigClient; @@ -49,10 +50,10 @@ pub(self) use session::TransferPayload; // Includes use chrono::{DateTime, Local}; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::collections::VecDeque; +use std::time::Duration; use tempfile::TempDir; -use tuirealm::View; +use tuirealm::{Application, EventListenerCfg, NoUserEvent, PollStrategy}; // -- Storage keys @@ -61,34 +62,111 @@ const STORAGE_PENDING_TRANSFER: &str = "FILETRANSFER_PENDING_TRANSFER"; // -- components -const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL"; -const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE"; -const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND"; -const COMPONENT_LOG_BOX: &str = "LOG_BOX"; -const COMPONENT_PROGRESS_BAR_FULL: &str = "PROGRESS_BAR_FULL"; -const COMPONENT_PROGRESS_BAR_PARTIAL: &str = "PROGRESS_BAR_PARTIAL"; -const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR"; -const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL"; -const COMPONENT_TEXT_HELP: &str = "TEXT_HELP"; -const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT"; -const COMPONENT_INPUT_COPY: &str = "INPUT_COPY"; -const COMPONENT_INPUT_EXEC: &str = "INPUT_EXEC"; -const COMPONENT_INPUT_FIND: &str = "INPUT_FIND"; -const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO"; -const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR"; -const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE"; -const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH"; -const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME"; -const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS"; -const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE"; -const COMPONENT_RADIO_REPLACE: &str = "RADIO_REPLACE"; // NOTE: used for file transfers, to choose whether to replace files -const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT"; -const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT"; -const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING"; -const COMPONENT_SPAN_STATUS_BAR_LOCAL: &str = "STATUS_BAR_LOCAL"; -const COMPONENT_SPAN_STATUS_BAR_REMOTE: &str = "STATUS_BAR_REMOTE"; -const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO"; -const COMPONENT_LIST_REPLACING_FILES: &str = "LIST_REPLACING_FILES"; // NOTE: used for file transfers, to list files which are going to be replaced +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +enum Id { + CopyPopup, + DeletePopup, + DisconnectPopup, + ErrorPopup, + ExecPopup, + ExplorerFind, + ExplorerLocal, + ExplorerRemote, + FatalPopup, + FileInfoPopup, + FindPopup, + GlobalListener, + GotoPopup, + KeybindingsPopup, + Log, + MkdirPopup, + NewfilePopup, + OpenWithPopup, + ProgressBarFull, + ProgressBarPartial, + QuitPopup, + RenamePopup, + ReplacePopup, + ReplacingFilesListPopup, + SaveAsPopup, + SortingPopup, + StatusBarLocal, + StatusBarRemote, + WaitPopup, +} + +#[derive(Debug, PartialEq)] +enum Msg { + Transfer(TransferMsg), + Ui(UiMsg), + None, +} + +#[derive(Debug, PartialEq)] +enum TransferMsg { + CopyFileTo(String), + DeleteFile, + EnterDirectory, + ExecuteCmd(String), + GoTo(String), + GoToParentDirectory, + GoToPreviousDirectory, + Mkdir, + NewFile, + OpenFile, + OpenFileWith(String), + OpenTextFile, + ReloadDir, + RenameFile(String), + SaveFileAs(String), + SearchFile(String), + SelectAllFiles, + StatFile, + TransferFile, +} + +#[derive(Debug, PartialEq)] +enum UiMsg { + ChangeFileSorting(FileSorting), + ChangeTransferWindow, + CloseCopyPopup, + CloseDeletePopup, + CloseDisconnectPopup, + CloseExecPopup, + CloseFileInfoPopup, + CloseFileSortingPopup, + CloseFindExplorer, + CloseGotoPopup, + CloseKeybindingsPopup, + CloseMkdirPopup, + CloseNewFilePopup, + CloseOpenWithPopup, + CloseQuitPopup, + CloseRenamePopup, + CloseSaveAsPopup, + Disconnect, + ExplorerTabbed, + InterruptTransfer, + LogTabbed, + Quit, + SelectFile(usize), + ShowCopyPopup, + ShowDeletePopup, + ShowDisconnectPopup, + ShowExecPopup, + ShowFileInfoPopup, + ShowFileSortingPopup, + ShowGotoPopup, + ShowKeybindingsPopup, + ShowMkdirPopup, + ShowNewFilePopup, + ShowOpenWithPopup, + ShowQuitPopup, + ShowRenamePopup, + ShowSaveAsPopup, + ToggleHiddenFiles, + ToggleSyncBrowsing, +} /// ## LogLevel /// @@ -125,9 +203,9 @@ impl LogRecord { /// /// FileTransferActivity is the data holder for the file transfer activity pub struct FileTransferActivity { - exit_reason: Option, // Exit reason - context: Option, // Context holder - view: View, // View + exit_reason: Option, // Exit reason + context: Option, // Context holder + app: Application, host: Localhost, // Localhost client: Box, // File transfer client browser: Browser, // Browser @@ -140,13 +218,21 @@ impl FileTransferActivity { /// ### new /// /// Instantiates a new FileTransferActivity - pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity { + pub fn new( + host: Localhost, + protocol: FileTransferProtocol, + ticks: Duration, + ) -> FileTransferActivity { // Get config client let config_client: ConfigClient = Self::init_config_client(); FileTransferActivity { exit_reason: None, context: None, - view: View::init(), + app: Application::init( + EventListenerCfg::default() + .poll_timeout(ticks) + .default_input_listener(ticks), + ), host, client: match protocol { FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new( @@ -257,9 +343,9 @@ impl Activity for FileTransferActivity { // Set context self.context = Some(context); // Clear terminal - self.context_mut().clear_screen(); + self.context.as_mut().unwrap().clear_screen(); // Put raw mode on enabled - if let Err(err) = enable_raw_mode() { + if let Err(err) = self.context_mut().terminal().enable_raw_mode() { error!("Failed to enter raw mode: {}", err); } // Get files at current pwd @@ -291,7 +377,7 @@ impl Activity for FileTransferActivity { return; } // Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error) - if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() { + if !self.client.is_connected() && !self.app.mounted(&Id::FatalPopup) { let ftparams = self.context().ft_params().unwrap(); // print params let msg: String = Self::get_connection_msg(&ftparams.params); @@ -304,10 +390,21 @@ impl Activity for FileTransferActivity { // Redraw redraw = true; } - // Handle input events (if false, becomes true; otherwise remains true) - redraw |= self.read_input_event(); - // @! draw interface - if redraw { + match self.app.tick(PollStrategy::UpTo(3)) { + Ok(messages) => { + for msg in messages.into_iter() { + let mut msg = Some(msg); + while msg.is_some() { + msg = self.update(msg); + } + } + } + Err(err) => { + self.mount_error(format!("Application error: {}", err)); + } + } + // View + if self.redraw { self.view(); } } @@ -333,7 +430,7 @@ impl Activity for FileTransferActivity { } } // Disable raw mode - if let Err(err) = disable_raw_mode() { + if let Err(err) = self.context_mut().terminal().disable_raw_mode() { error!("Failed to disable raw mode: {}", err); } // Disconnect client @@ -343,7 +440,7 @@ impl Activity for FileTransferActivity { // Clear terminal and return match self.context.take() { Some(mut ctx) => { - ctx.clear_screen(); + ctx.terminal().clear_screen(); Some(ctx) } None => None, diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index da003bf..c95069f 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -29,29 +29,19 @@ use super::{ actions::SelectedEntry, browser::{FileExplorerTab, FoundExplorerTab}, - FileTransferActivity, LogLevel, TransferOpts, COMPONENT_EXPLORER_FIND, - COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY, - COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR, - COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH, COMPONENT_INPUT_RENAME, - COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LIST_REPLACING_FILES, - COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, - COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, - COMPONENT_RADIO_REPLACE, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, - COMPONENT_TEXT_HELP, + FileTransferActivity, Id, LogLevel, Msg, TransferMsg, TransferOpts, UiMsg, }; use crate::fs::explorer::FileSorting; use crate::fs::FsEntry; -use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder}; -use crate::ui::keymap::*; use crate::utils::fmt::fmt_path_elide_ex; // externals -use tui_realm_stdlib::ProgressBarPropsBuilder; use tuirealm::{ - props::{Alignment, PropsBuilder, TableBuilder, TextSpan}, + props::{Alignment, AttrValue, Attribute, TableBuilder, TextSpan}, tui::style::Color, - Msg, Payload, Update, Value, + State, StateValue, Update, }; +/* impl Update for FileTransferActivity { // -- update @@ -686,11 +676,11 @@ impl Update for FileTransferActivity { self.action_find_delete(); // Delete entries match self.view.get_state(COMPONENT_EXPLORER_FIND) { - Some(Payload::One(Value::Usize(idx))) => { + Ok(State::One(Value::Usize(idx))) => { // Reload entries self.found_mut().unwrap().del_entry(idx); } - Some(Payload::Vec(values)) => { + Ok(State::Vec(values)) => { values .iter() .map(|x| match x { @@ -1028,3 +1018,4 @@ impl FileTransferActivity { } } } +*/ diff --git a/src/ui/activities/setup/components/config.rs b/src/ui/activities/setup/components/config.rs index 05e94c5..eaffcd9 100644 --- a/src/ui/activities/setup/components/config.rs +++ b/src/ui/activities/setup/components/config.rs @@ -411,7 +411,7 @@ impl Component for TextEditor { // -- event handler fn handle_input_ev( - component: &mut dyn Component, + component: &mut dyn Component, ev: Event, on_key_down: Msg, on_key_up: Msg, @@ -468,7 +468,7 @@ fn handle_input_ev( } fn handle_radio_ev( - component: &mut dyn Component, + component: &mut dyn Component, ev: Event, on_key_down: Msg, on_key_up: Msg, diff --git a/src/ui/activities/setup/components/theme.rs b/src/ui/activities/setup/components/theme.rs index 606dffb..1d40ba5 100644 --- a/src/ui/activities/setup/components/theme.rs +++ b/src/ui/activities/setup/components/theme.rs @@ -84,7 +84,7 @@ pub struct TransferTitle { component: Label, } -impl Default for AuthTitle { +impl Default for TransferTitle { fn default() -> Self { Self { component: Label::default() @@ -105,7 +105,7 @@ pub struct TransferTitle2 { component: Label, } -impl Default for AuthTitle { +impl Default for TransferTitle2 { fn default() -> Self { Self { component: Label::default() diff --git a/src/ui/activities/setup/view/mod.rs b/src/ui/activities/setup/view/mod.rs index c529838..8bc0656 100644 --- a/src/ui/activities/setup/view/mod.rs +++ b/src/ui/activities/setup/view/mod.rs @@ -269,7 +269,7 @@ impl SetupActivity { /// ### no_popup_mounted_clause /// /// Returns a sub clause which requires that no popup is mounted in order to be satisfied - fn no_popup_mounted_clause() -> SubClause { + fn no_popup_mounted_clause() -> SubClause { SubClause::And( Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common( IdCommon::ErrorPopup, diff --git a/src/ui/components/logbox.rs b/src/ui/components/logbox.rs deleted file mode 100644 index 41ac7d1..0000000 --- a/src/ui/components/logbox.rs +++ /dev/null @@ -1,433 +0,0 @@ -//! ## LogBox -//! -//! `LogBox` component renders a log box view - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// ext -use tui_realm_stdlib::utils::{get_block, wrap_spans}; -use tuirealm::event::{Event, KeyCode}; -use tuirealm::props::{ - Alignment, BlockTitle, BordersProps, Props, PropsBuilder, Table as TextTable, -}; -use tuirealm::tui::{ - layout::{Corner, Rect}, - style::{Color, Style}, - widgets::{BorderType, Borders, List, ListItem, ListState}, -}; -use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value}; - -// -- props - -const PROP_TABLE: &str = "table"; - -pub struct LogboxPropsBuilder { - props: Option, -} - -impl Default for LogboxPropsBuilder { - fn default() -> Self { - LogboxPropsBuilder { - props: Some(Props::default()), - } - } -} - -impl PropsBuilder for LogboxPropsBuilder { - fn build(&mut self) -> Props { - self.props.take().unwrap() - } - - fn hidden(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = false; - } - self - } - - fn visible(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = true; - } - self - } -} - -impl From for LogboxPropsBuilder { - fn from(props: Props) -> Self { - LogboxPropsBuilder { props: Some(props) } - } -} - -impl LogboxPropsBuilder { - /// ### with_borders - /// - /// Set component borders style - pub fn with_borders( - &mut self, - borders: Borders, - variant: BorderType, - color: Color, - ) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.borders = BordersProps { - borders, - variant, - color, - } - } - self - } - - /// ### with_background - /// - /// Set background color for area - pub fn with_background(&mut self, color: Color) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.background = color; - } - self - } - - pub fn with_title>(&mut self, text: S, alignment: Alignment) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.title = Some(BlockTitle::new(text, alignment)); - } - self - } - - pub fn with_log(&mut self, table: TextTable) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props - .own - .insert(PROP_TABLE, PropPayload::One(PropValue::Table(table))); - } - self - } -} - -// -- states - -/// ## OwnStates -/// -/// OwnStates contains states for this component -#[derive(Clone)] -struct OwnStates { - list_index: usize, // Index of selected element in list - list_len: usize, // Length of file list - focus: bool, // Has focus? -} - -impl Default for OwnStates { - fn default() -> Self { - OwnStates { - list_index: 0, - list_len: 0, - focus: false, - } - } -} - -impl OwnStates { - /// ### set_list_len - /// - /// Set list length - pub fn set_list_len(&mut self, len: usize) { - self.list_len = len; - } - - /// ### get_list_index - /// - /// Return current value for list index - pub fn get_list_index(&self) -> usize { - self.list_index - } - - /// ### incr_list_index - /// - /// Incremenet list index - pub fn incr_list_index(&mut self) { - // Check if index is at last element - if self.list_index + 1 < self.list_len { - self.list_index += 1; - } - } - - /// ### decr_list_index - /// - /// Decrement list index - pub fn decr_list_index(&mut self) { - // Check if index is bigger than 0 - if self.list_index > 0 { - self.list_index -= 1; - } - } - - /// ### reset_list_index - /// - /// Reset list index to last element - pub fn reset_list_index(&mut self) { - self.list_index = 0; // Last element is always 0 - } -} - -// -- Component - -/// ## LogBox -/// -/// LogBox list component -pub struct LogBox { - props: Props, - states: OwnStates, -} - -impl LogBox { - /// ### new - /// - /// Instantiates a new FileList starting from Props - /// The method also initializes the component states. - pub fn new(props: Props) -> Self { - // Initialize states - let mut states: OwnStates = OwnStates::default(); - // Set list length - states.set_list_len(Self::table_len(&props)); - // Reset list index - states.reset_list_index(); - LogBox { props, states } - } - - fn table_len(props: &Props) -> usize { - match props.own.get(PROP_TABLE) { - Some(PropPayload::One(PropValue::Table(table))) => table.len(), - _ => 0, - } - } -} - -impl Component for LogBox { - #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Frame, area: Rect) { - if self.props.visible { - let width: usize = area.width as usize - 4; - // Make list - let list_items: Vec = match self.props.own.get(PROP_TABLE) { - Some(PropPayload::One(PropValue::Table(table))) => table - .iter() - .map(|row| ListItem::new(wrap_spans(row, width, &self.props))) - .collect(), // Make List item from TextSpan - _ => Vec::new(), - }; - let w = List::new(list_items) - .block(get_block( - &self.props.borders, - self.props.title.as_ref(), - self.states.focus, - )) - .start_corner(Corner::BottomLeft) - .highlight_symbol(">> ") - .style(Style::default().bg(self.props.background)) - .highlight_style(Style::default().add_modifier(self.props.modifiers)); - let mut state: ListState = ListState::default(); - state.select(Some(self.states.list_index)); - render.render_stateful_widget(w, area, &mut state); - } - } - - fn update(&mut self, props: Props) -> Msg { - self.props = props; - // re-Set list length - self.states.set_list_len(Self::table_len(&self.props)); - // Reset list index - self.states.reset_list_index(); - Msg::None - } - - fn get_props(&self) -> Props { - self.props.clone() - } - - fn on(&mut self, ev: Event) -> Msg { - // Match event - if let Event::Key(key) = ev { - match key.code { - KeyCode::Up => { - // Update states - self.states.incr_list_index(); - Msg::None - } - KeyCode::Down => { - // Update states - self.states.decr_list_index(); - Msg::None - } - KeyCode::PageUp => { - // Update states - for _ in 0..8 { - self.states.incr_list_index(); - } - Msg::None - } - KeyCode::PageDown => { - // Update states - for _ in 0..8 { - self.states.decr_list_index(); - } - Msg::None - } - _ => { - // Return key event to activity - Msg::OnKey(key) - } - } - } else { - // Unhandled event - Msg::None - } - } - - fn get_state(&self) -> Payload { - Payload::One(Value::Usize(self.states.get_list_index())) - } - - fn blur(&mut self) { - self.states.focus = false; - } - - fn active(&mut self) { - self.states.focus = true; - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - use pretty_assertions::assert_eq; - use tuirealm::event::{KeyCode, KeyEvent}; - use tuirealm::props::{TableBuilder, TextSpan}; - use tuirealm::tui::style::Color; - - #[test] - fn test_ui_components_logbox() { - let mut component: LogBox = LogBox::new( - LogboxPropsBuilder::default() - .hidden() - .visible() - .with_borders(Borders::ALL, BorderType::Double, Color::Red) - .with_background(Color::Blue) - .with_title("Log", Alignment::Left) - .with_log( - TableBuilder::default() - .add_col(TextSpan::from("12:29")) - .add_col(TextSpan::from("system crashed")) - .add_row() - .add_col(TextSpan::from("12:38")) - .add_col(TextSpan::from("system alive")) - .build(), - ) - .build(), - ); - assert_eq!(component.props.visible, true); - assert_eq!(component.props.background, Color::Blue); - assert_eq!(component.props.title.as_ref().unwrap().text(), "Log"); - // Verify states - assert_eq!(component.states.list_index, 0); - assert_eq!(component.states.list_len, 2); - assert_eq!(component.states.focus, false); - // Focus - component.active(); - assert_eq!(component.states.focus, true); - component.blur(); - assert_eq!(component.states.focus, false); - // Update - let props = LogboxPropsBuilder::from(component.get_props()) - .hidden() - .build(); - assert_eq!(component.update(props), Msg::None); - assert_eq!(component.props.visible, false); - // Increment list index - component.states.list_index += 1; - assert_eq!(component.states.list_index, 1); - // Update - component.update( - LogboxPropsBuilder::from(component.get_props()) - .with_log( - TableBuilder::default() - .add_col(TextSpan::from("12:29")) - .add_col(TextSpan::from("system crashed")) - .add_row() - .add_col(TextSpan::from("12:38")) - .add_col(TextSpan::from("system alive")) - .add_row() - .add_col(TextSpan::from("12:41")) - .add_col(TextSpan::from("system is going down for REBOOT")) - .build(), - ) - .build(), - ); - // Verify states - assert_eq!(component.states.list_index, 0); // Last item - assert_eq!(component.states.list_len, 3); - // get value - assert_eq!(component.get_state(), Payload::One(Value::Usize(0))); - // RenderData - assert_eq!(component.states.list_index, 0); - // Set cursor to 0 - component.states.list_index = 0; - // Handle inputs - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Up))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 1); - // Index should be decremented - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Down))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 0); - // Index should be 2 - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 2); - // Index should be 0 - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 0); - // On key - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))), - Msg::OnKey(KeyEvent::from(KeyCode::Backspace)) - ); - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2be0569..4b7cc90 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -27,6 +27,5 @@ */ // Modules pub mod activities; -pub(crate) mod components; pub mod context; pub(crate) mod store;