wip
This commit is contained in:
parent
eab9a32755
commit
4ed7405edf
|
@ -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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,5 @@
|
|||
*/
|
||||
// Modules
|
||||
pub mod activities;
|
||||
pub(crate) mod components;
|
||||
pub mod context;
|
||||
pub(crate) mod store;
|
||||
|
|
Loading…
Reference in New Issue