This commit is contained in:
veeso 2021-11-26 17:12:04 +01:00
parent eab9a32755
commit 4ed7405edf
13 changed files with 963 additions and 522 deletions

View File

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

View File

@ -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<Id> {
SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ErrorPopup,

View File

@ -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<usize> = files
.iter()
.map(|x| match x {
Value::Usize(v) => *v,
StateValue::Usize(v) => *v,
_ => 0,
})
.collect();

View File

@ -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<ListItem> = 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<AttrValue> {
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<Msg, NoUserEvent> for Log {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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
}
}

View File

@ -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<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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)),
}
}
}

View File

@ -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<Msg, NoUserEvent> for ExplorerFind {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for ExplorerLocal {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for ExplorerRemote {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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)),
}
}
}

View File

@ -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<ExitReason>, // Exit reason
context: Option<Context>, // Context holder
view: View, // View
exit_reason: Option<ExitReason>, // Exit reason
context: Option<Context>, // Context holder
app: Application<Id, Msg, NoUserEvent>,
host: Localhost, // Localhost
client: Box<dyn FileTransfer>, // 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,

View File

@ -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 {
}
}
}
*/

View File

@ -411,7 +411,7 @@ impl Component<Msg, NoUserEvent> for TextEditor {
// -- event handler
fn handle_input_ev(
component: &mut dyn Component<Msg>,
component: &mut dyn Component<Msg, NoUserEvent>,
ev: Event<NoUserEvent>,
on_key_down: Msg,
on_key_up: Msg,
@ -468,7 +468,7 @@ fn handle_input_ev(
}
fn handle_radio_ev(
component: &mut dyn Component<Msg>,
component: &mut dyn Component<Msg, NoUserEvent>,
ev: Event<NoUserEvent>,
on_key_down: Msg,
on_key_up: Msg,

View File

@ -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()

View File

@ -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<Id> {
SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::ErrorPopup,

View File

@ -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<Props>,
}
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<Props> 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<S: AsRef<str>>(&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<ListItem> = 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))
);
}
}

View File

@ -27,6 +27,5 @@
*/
// Modules
pub mod activities;
pub(crate) mod components;
pub mod context;
pub(crate) mod store;