mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Migrated termscp to tui-realm 1.x
This commit is contained in:
committed by
Christian Visintin
parent
30851a78e8
commit
54b5583d1a
@@ -29,7 +29,6 @@
|
||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
|
||||
use crate::fs::FsFile;
|
||||
// ext
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -109,13 +108,15 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
// Put input mode back to normal
|
||||
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);
|
||||
}
|
||||
// Leave alternate mode
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
if let Err(err) = self.context_mut().terminal().leave_alternate_screen() {
|
||||
error!("Could not leave alternate screen: {}", err);
|
||||
}
|
||||
// Lock ports
|
||||
assert!(self.app.lock_ports().is_ok());
|
||||
// Open editor
|
||||
match edit::edit_file(path) {
|
||||
Ok(_) => self.log(
|
||||
@@ -128,13 +129,20 @@ impl FileTransferActivity {
|
||||
Err(err) => return Err(format!("Could not open editor: {}", err)),
|
||||
}
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
if let Err(err) = ctx.terminal().clear_screen() {
|
||||
error!("Could not clear screen screen: {}", err);
|
||||
}
|
||||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
if let Err(err) = ctx.terminal().enter_alternate_screen() {
|
||||
error!("Could not enter alternate screen: {}", err);
|
||||
}
|
||||
// Re-enable raw mode
|
||||
if let Err(err) = ctx.terminal().enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Unlock ports
|
||||
assert!(self.app.unlock_ports().is_ok());
|
||||
}
|
||||
// Re-enable raw mode
|
||||
let _ = enable_raw_mode();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -160,7 +160,9 @@ impl FileTransferActivity {
|
||||
// NOTE: clear screen in order to prevent crap on stderr
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
if let Err(err) = ctx.terminal().clear_screen() {
|
||||
error!("Could not clear screen screen: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
296
src/ui/activities/filetransfer/components/log.rs
Normal file
296
src/ui/activities/filetransfer/components/log.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
//! ## 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::Borders,
|
||||
AttrValue::Borders(Borders::default().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.get_list_index()));
|
||||
frame.render_stateful_widget(w, area, &mut state);
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
if matches!(attr, Attribute::Content) {
|
||||
self.states.set_list_len(
|
||||
match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
|
||||
Some(spans) => spans.len(),
|
||||
_ => 0,
|
||||
},
|
||||
);
|
||||
self.states.reset_list_index();
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::One(StateValue::Usize(self.states.get_list_index()))
|
||||
}
|
||||
|
||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||
match cmd {
|
||||
Cmd::Move(Direction::Down) => {
|
||||
let prev = self.states.get_list_index();
|
||||
self.states.incr_list_index();
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Move(Direction::Up) => {
|
||||
let prev = self.states.get_list_index();
|
||||
self.states.decr_list_index();
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Scroll(Direction::Down) => {
|
||||
let prev = self.states.get_list_index();
|
||||
(0..8).for_each(|_| self.states.incr_list_index());
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Scroll(Direction::Up) => {
|
||||
let prev = self.states.get_list_index();
|
||||
(0..8).for_each(|_| self.states.decr_list_index());
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::GoTo(Position::Begin) => {
|
||||
let prev = self.states.get_list_index();
|
||||
self.states.reset_list_index();
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::GoTo(Position::End) => {
|
||||
let prev = self.states.get_list_index();
|
||||
self.states.list_index_at_last();
|
||||
if prev != self.states.get_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::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### list_index_at_last
|
||||
///
|
||||
/// Set list index at last item
|
||||
pub fn list_index_at_last(&mut self) {
|
||||
self.list_index = match self.list_len {
|
||||
0 => 0,
|
||||
len => len - 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
|
||||
}
|
||||
}
|
||||
72
src/ui/activities/filetransfer/components/mod.rs
Normal file
72
src/ui/activities/filetransfer/components/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
//! ## Components
|
||||
//!
|
||||
//! file transfer activity 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::Phantom;
|
||||
use tuirealm::{
|
||||
event::{Event, Key, KeyEvent, KeyModifiers},
|
||||
Component, MockComponent, NoUserEvent,
|
||||
};
|
||||
|
||||
// -- export
|
||||
mod log;
|
||||
mod popups;
|
||||
mod transfer;
|
||||
|
||||
pub use self::log::Log;
|
||||
pub use popups::{
|
||||
CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup, FileInfoPopup,
|
||||
FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup,
|
||||
ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, WaitPopup,
|
||||
};
|
||||
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)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
1689
src/ui/activities/filetransfer/components/popups.rs
Normal file
1689
src/ui/activities/filetransfer/components/popups.rs
Normal file
File diff suppressed because it is too large
Load Diff
400
src/ui/activities/filetransfer/components/transfer/file_list.rs
Normal file
400
src/ui/activities/filetransfer/components/transfer/file_list.rs
Normal file
@@ -0,0 +1,400 @@
|
||||
//! ## FileList
|
||||
//!
|
||||
//! `FileList` component renders a file list tab
|
||||
|
||||
/**
|
||||
* 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 tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::props::{
|
||||
Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers,
|
||||
};
|
||||
use tuirealm::tui::layout::Corner;
|
||||
use tuirealm::tui::text::{Span, Spans};
|
||||
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
|
||||
use tuirealm::{MockComponent, Props, State, StateValue};
|
||||
|
||||
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
|
||||
|
||||
/// ## OwnStates
|
||||
///
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone)]
|
||||
struct OwnStates {
|
||||
list_index: usize, // Index of selected element in list
|
||||
selected: Vec<usize>, // Selected files
|
||||
}
|
||||
|
||||
impl Default for OwnStates {
|
||||
fn default() -> Self {
|
||||
OwnStates {
|
||||
list_index: 0,
|
||||
selected: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// ### init_list_states
|
||||
///
|
||||
/// Initialize list states
|
||||
pub fn init_list_states(&mut self, len: usize) {
|
||||
self.selected = Vec::with_capacity(len);
|
||||
self.fix_list_index();
|
||||
}
|
||||
|
||||
/// ### list_index
|
||||
///
|
||||
/// Return current value for list index
|
||||
pub fn list_index(&self) -> usize {
|
||||
self.list_index
|
||||
}
|
||||
|
||||
/// ### incr_list_index
|
||||
///
|
||||
/// Incremenet list index.
|
||||
/// If `can_rewind` is `true` the index rewinds when boundary is reached
|
||||
pub fn incr_list_index(&mut self, can_rewind: bool) {
|
||||
// Check if index is at last element
|
||||
if self.list_index + 1 < self.list_len() {
|
||||
self.list_index += 1;
|
||||
} else if can_rewind {
|
||||
self.list_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_list_index
|
||||
///
|
||||
/// Decrement list index
|
||||
/// If `can_rewind` is `true` the index rewinds when boundary is reached
|
||||
pub fn decr_list_index(&mut self, can_rewind: bool) {
|
||||
// Check if index is bigger than 0
|
||||
if self.list_index > 0 {
|
||||
self.list_index -= 1;
|
||||
} else if self.list_len() > 0 && can_rewind {
|
||||
self.list_index = self.list_len() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_index_at_first(&mut self) {
|
||||
self.list_index = 0;
|
||||
}
|
||||
|
||||
pub fn list_index_at_last(&mut self) {
|
||||
self.list_index = match self.list_len() {
|
||||
0 => 0,
|
||||
len => len - 1,
|
||||
};
|
||||
}
|
||||
|
||||
/// ### list_len
|
||||
///
|
||||
/// Returns the length of the file list, which is actually the capacity of the selection vector
|
||||
pub fn list_len(&self) -> usize {
|
||||
self.selected.capacity()
|
||||
}
|
||||
|
||||
/// ### is_selected
|
||||
///
|
||||
/// Returns whether the file with index `entry` is selected
|
||||
pub fn is_selected(&self, entry: usize) -> bool {
|
||||
self.selected.contains(&entry)
|
||||
}
|
||||
|
||||
/// ### is_selection_empty
|
||||
///
|
||||
/// Returns whether the selection is currently empty
|
||||
pub fn is_selection_empty(&self) -> bool {
|
||||
self.selected.is_empty()
|
||||
}
|
||||
|
||||
/// ### get_selection
|
||||
///
|
||||
/// Returns current file selection
|
||||
pub fn get_selection(&self) -> Vec<usize> {
|
||||
self.selected.clone()
|
||||
}
|
||||
|
||||
/// ### fix_list_index
|
||||
///
|
||||
/// Keep index if possible, otherwise set to lenght - 1
|
||||
fn fix_list_index(&mut self) {
|
||||
if self.list_index >= self.list_len() && self.list_len() > 0 {
|
||||
self.list_index = self.list_len() - 1;
|
||||
} else if self.list_len() == 0 {
|
||||
self.list_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// -- select manipulation
|
||||
|
||||
/// ### toggle_file
|
||||
///
|
||||
/// Select or deselect file with provided entry index
|
||||
pub fn toggle_file(&mut self, entry: usize) {
|
||||
match self.is_selected(entry) {
|
||||
true => self.deselect(entry),
|
||||
false => self.select(entry),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### select_all
|
||||
///
|
||||
/// Select all files
|
||||
pub fn select_all(&mut self) {
|
||||
for i in 0..self.list_len() {
|
||||
self.select(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### select
|
||||
///
|
||||
/// Select provided index if not selected yet
|
||||
fn select(&mut self, entry: usize) {
|
||||
if !self.is_selected(entry) {
|
||||
self.selected.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deselect
|
||||
///
|
||||
/// Remove element file with associated index
|
||||
fn deselect(&mut self, entry: usize) {
|
||||
if self.is_selected(entry) {
|
||||
self.selected.retain(|&x| x != entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FileList {
|
||||
props: Props,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl FileList {
|
||||
pub fn foreground(mut self, fg: Color) -> Self {
|
||||
self.attr(Attribute::Foreground, AttrValue::Color(fg));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background(mut self, bg: Color) -> Self {
|
||||
self.attr(Attribute::Background, AttrValue::Color(bg));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn borders(mut self, b: Borders) -> Self {
|
||||
self.attr(Attribute::Borders, AttrValue::Borders(b));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
|
||||
self.attr(
|
||||
Attribute::Title,
|
||||
AttrValue::Title((t.as_ref().to_string(), a)),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlighted_color(mut self, c: Color) -> Self {
|
||||
self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows(mut self, rows: Table) -> Self {
|
||||
self.attr(Attribute::Content, AttrValue::Table(rows));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MockComponent for FileList {
|
||||
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) {
|
||||
let title = self
|
||||
.props
|
||||
.get_or(
|
||||
Attribute::Title,
|
||||
AttrValue::Title((String::default(), Alignment::Left)),
|
||||
)
|
||||
.unwrap_title();
|
||||
let borders = self
|
||||
.props
|
||||
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
|
||||
.unwrap_borders();
|
||||
let focus = self
|
||||
.props
|
||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
||||
.unwrap_flag();
|
||||
let div = tui_realm_stdlib::utils::get_block(borders, Some(title), focus, None);
|
||||
// Make list entries
|
||||
let list_items: Vec<ListItem> = match self
|
||||
.props
|
||||
.get(Attribute::Content)
|
||||
.map(|x| x.unwrap_table())
|
||||
{
|
||||
Some(table) => table
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(num, row)| {
|
||||
let columns: Vec<Span> = row
|
||||
.iter()
|
||||
.map(|col| {
|
||||
let (fg, bg, mut modifiers) =
|
||||
tui_realm_stdlib::utils::use_or_default_styles(&self.props, col);
|
||||
if self.states.is_selected(num) {
|
||||
modifiers |= TextModifiers::REVERSED
|
||||
| TextModifiers::UNDERLINED
|
||||
| TextModifiers::ITALIC;
|
||||
}
|
||||
Span::styled(
|
||||
col.content.clone(),
|
||||
Style::default().add_modifier(modifiers).fg(fg).bg(bg),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
ListItem::new(Spans::from(columns))
|
||||
})
|
||||
.collect(), // Make List item from TextSpan
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let highlighted_color = self
|
||||
.props
|
||||
.get(Attribute::HighlightedColor)
|
||||
.map(|x| x.unwrap_color());
|
||||
let modifiers = match focus {
|
||||
true => TextModifiers::REVERSED,
|
||||
false => TextModifiers::empty(),
|
||||
};
|
||||
// Make list
|
||||
let mut list = TuiList::new(list_items)
|
||||
.block(div)
|
||||
.start_corner(Corner::TopLeft);
|
||||
if let Some(highlighted_color) = highlighted_color {
|
||||
list = list.highlight_style(
|
||||
Style::default()
|
||||
.fg(highlighted_color)
|
||||
.add_modifier(modifiers),
|
||||
);
|
||||
}
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
frame.render_stateful_widget(list, area, &mut state);
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
if matches!(attr, Attribute::Content) {
|
||||
self.states.init_list_states(
|
||||
match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
|
||||
Some(spans) => spans.len(),
|
||||
_ => 0,
|
||||
},
|
||||
);
|
||||
self.states.fix_list_index();
|
||||
}
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
match self.states.is_selection_empty() {
|
||||
true => State::One(StateValue::Usize(self.states.list_index())),
|
||||
false => State::Vec(
|
||||
self.states
|
||||
.get_selection()
|
||||
.into_iter()
|
||||
.map(StateValue::Usize)
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||
match cmd {
|
||||
Cmd::Move(Direction::Down) => {
|
||||
let prev = self.states.list_index;
|
||||
self.states.incr_list_index(true);
|
||||
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(true);
|
||||
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(false));
|
||||
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(false));
|
||||
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
|
||||
}
|
||||
}
|
||||
Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => {
|
||||
self.states.select_all();
|
||||
CmdResult::None
|
||||
}
|
||||
Cmd::Toggle => {
|
||||
self.states.toggle_file(self.states.list_index());
|
||||
CmdResult::None
|
||||
}
|
||||
_ => CmdResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
494
src/ui/activities/filetransfer/components/transfer/mod.rs
Normal file
494
src/ui/activities/filetransfer/components/transfer/mod.rs
Normal file
@@ -0,0 +1,494 @@
|
||||
//! ## 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};
|
||||
|
||||
mod file_list;
|
||||
use file_list::FileList;
|
||||
|
||||
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};
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerFind {
|
||||
component: FileList,
|
||||
}
|
||||
|
||||
impl ExplorerFind {
|
||||
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
|
||||
Self {
|
||||
component: FileList::default()
|
||||
.background(bg)
|
||||
.borders(Borders::default().color(hg))
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.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)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
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('i'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
||||
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)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerLocal {
|
||||
component: FileList,
|
||||
}
|
||||
|
||||
impl ExplorerLocal {
|
||||
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
|
||||
Self {
|
||||
component: FileList::default()
|
||||
.background(bg)
|
||||
.borders(Borders::default().color(hg))
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.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)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
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::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::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('f'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
|
||||
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('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)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerRemote {
|
||||
component: FileList,
|
||||
}
|
||||
|
||||
impl ExplorerRemote {
|
||||
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
|
||||
Self {
|
||||
component: FileList::default()
|
||||
.background(bg)
|
||||
.borders(Borders::default().color(hg))
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.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)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
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, ..
|
||||
}) => 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::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('f'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
|
||||
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('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)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ use std::path::Path;
|
||||
/// ## FileExplorerTab
|
||||
///
|
||||
/// File explorer tab
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileExplorerTab {
|
||||
Local,
|
||||
Remote,
|
||||
|
||||
@@ -22,22 +22,50 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord, TransferPayload};
|
||||
use super::{
|
||||
browser::FileExplorerTab, ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord,
|
||||
TransferPayload,
|
||||
};
|
||||
use crate::filetransfer::ProtocolParams;
|
||||
use crate::system::environment;
|
||||
use crate::system::notifications::Notification;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::fmt::fmt_millis;
|
||||
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
|
||||
use crate::utils::path;
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tuirealm::Update;
|
||||
use tuirealm::props::{
|
||||
Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan,
|
||||
};
|
||||
use tuirealm::{PollStrategy, Update};
|
||||
|
||||
const LOG_CAPACITY: usize = 256;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### tick
|
||||
///
|
||||
/// Call `Application::tick()` and process messages in `Update`
|
||||
pub(super) fn tick(&mut self) {
|
||||
match self.app.tick(PollStrategy::UpTo(3)) {
|
||||
Ok(messages) => {
|
||||
if !messages.is_empty() {
|
||||
self.redraw = true;
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### log
|
||||
///
|
||||
/// Add message to log events
|
||||
@@ -57,8 +85,7 @@ impl FileTransferActivity {
|
||||
// Eventually push front the new record
|
||||
self.log_records.push_front(record);
|
||||
// Update log
|
||||
let msg = self.update_logbox();
|
||||
self.update(msg);
|
||||
self.update_logbox();
|
||||
}
|
||||
|
||||
/// ### log_and_alert
|
||||
@@ -68,8 +95,7 @@ impl FileTransferActivity {
|
||||
self.mount_error(msg.as_str());
|
||||
self.log(level, msg);
|
||||
// Update log
|
||||
let msg = self.update_logbox();
|
||||
self.update(msg);
|
||||
self.update_logbox();
|
||||
}
|
||||
|
||||
/// ### init_config_client
|
||||
@@ -108,23 +134,6 @@ impl FileTransferActivity {
|
||||
env::set_var("EDITOR", self.config().get_text_editor());
|
||||
}
|
||||
|
||||
/// ### read_input_event
|
||||
///
|
||||
/// Read one event.
|
||||
/// Returns whether at least one event has been handled
|
||||
pub(super) fn read_input_event(&mut self) -> bool {
|
||||
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
|
||||
// Handle event
|
||||
let msg = self.view.on(event);
|
||||
self.update(msg);
|
||||
// Return true
|
||||
true
|
||||
} else {
|
||||
// Error
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// ### local_to_abs_path
|
||||
///
|
||||
/// Convert a path to absolute according to local explorer
|
||||
@@ -231,4 +240,245 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update_local_filelist
|
||||
///
|
||||
/// Update local file list
|
||||
pub(super) fn update_local_filelist(&mut self) {
|
||||
// Get width
|
||||
let width: usize = self
|
||||
.context()
|
||||
.store()
|
||||
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
|
||||
.unwrap_or(256);
|
||||
let hostname: String = match hostname::get() {
|
||||
Ok(h) => {
|
||||
let hostname: String = h.as_os_str().to_string_lossy().to_string();
|
||||
let tokens: Vec<&str> = hostname.split('.').collect();
|
||||
String::from(*tokens.get(0).unwrap_or(&"localhost"))
|
||||
}
|
||||
Err(_) => String::from("localhost"),
|
||||
};
|
||||
let hostname: String = format!(
|
||||
"{}:{} ",
|
||||
hostname,
|
||||
fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/'
|
||||
);
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.local()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.local().fmt_file(x))])
|
||||
.collect();
|
||||
// Update content and title
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerLocal,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerLocal,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### update_remote_filelist
|
||||
///
|
||||
/// Update remote file list
|
||||
pub(super) fn update_remote_filelist(&mut self) {
|
||||
let width: usize = self
|
||||
.context()
|
||||
.store()
|
||||
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
|
||||
.unwrap_or(256);
|
||||
let hostname = self.get_remote_hostname();
|
||||
let hostname: String = format!(
|
||||
"{}:{} ",
|
||||
hostname,
|
||||
fmt_path_elide_ex(
|
||||
self.remote().wrkdir.as_path(),
|
||||
width,
|
||||
hostname.len() + 3 // 3 because of '/…/'
|
||||
)
|
||||
);
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.remote()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.remote().fmt_file(x))])
|
||||
.collect();
|
||||
// Update content and title
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerRemote,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerRemote,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### update_logbox
|
||||
///
|
||||
/// Update log box
|
||||
pub(super) fn update_logbox(&mut self) {
|
||||
let mut table: TableBuilder = TableBuilder::default();
|
||||
for (idx, record) in self.log_records.iter().enumerate() {
|
||||
// Add row if not first row
|
||||
if idx > 0 {
|
||||
table.add_row();
|
||||
}
|
||||
let fg = match record.level {
|
||||
LogLevel::Error => Color::Red,
|
||||
LogLevel::Warn => Color::Yellow,
|
||||
LogLevel::Info => Color::Green,
|
||||
};
|
||||
table
|
||||
.add_col(TextSpan::from(format!(
|
||||
"{}",
|
||||
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
|
||||
)))
|
||||
.add_col(TextSpan::from(" ["))
|
||||
.add_col(
|
||||
TextSpan::new(
|
||||
format!(
|
||||
"{:5}",
|
||||
match record.level {
|
||||
LogLevel::Error => "ERROR",
|
||||
LogLevel::Warn => "WARN",
|
||||
LogLevel::Info => "INFO",
|
||||
}
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.fg(fg),
|
||||
)
|
||||
.add_col(TextSpan::from("]: "))
|
||||
.add_col(TextSpan::from(record.msg.as_str()));
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::Log,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(table.build())
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn update_progress_bar(&mut self, filename: String) {
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarFull,
|
||||
Attribute::Text,
|
||||
AttrValue::String(self.transfer.full.to_string())
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarFull,
|
||||
Attribute::Value,
|
||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||
self.transfer.full.calc_progress()
|
||||
)))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Text,
|
||||
AttrValue::String(self.transfer.partial.to_string())
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Value,
|
||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||
self.transfer.partial.calc_progress()
|
||||
)))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((filename, Alignment::Left))
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### finalize_find
|
||||
///
|
||||
/// Finalize find process
|
||||
pub(super) fn finalize_find(&mut self) {
|
||||
// Set found to none
|
||||
self.browser.del_found();
|
||||
// Restore tab
|
||||
let new_tab = match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal => FileExplorerTab::Local,
|
||||
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
|
||||
_ => FileExplorerTab::Local,
|
||||
};
|
||||
// Give focus to new tab
|
||||
match new_tab {
|
||||
FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()),
|
||||
FileExplorerTab::Remote => {
|
||||
assert!(self.app.active(&Id::ExplorerRemote).is_ok())
|
||||
}
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
assert!(self.app.active(&Id::ExplorerFind).is_ok())
|
||||
}
|
||||
}
|
||||
self.browser.change_tab(new_tab);
|
||||
}
|
||||
|
||||
pub(super) fn update_find_list(&mut self) {
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.found()
|
||||
.unwrap()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.found().unwrap().fmt_file(x))])
|
||||
.collect();
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerFind,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn update_browser_file_list(&mut self) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_local_filelist(),
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_browser_file_list_swapped(&mut self) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_remote_filelist(),
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_local_filelist(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
// -- Storage keys
|
||||
|
||||
@@ -61,34 +62,115 @@ 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 {
|
||||
AbortTransfer,
|
||||
CopyFileTo(String),
|
||||
DeleteFile,
|
||||
EnterDirectory,
|
||||
ExecuteCmd(String),
|
||||
GoTo(String),
|
||||
GoToParentDirectory,
|
||||
GoToPreviousDirectory,
|
||||
Mkdir(String),
|
||||
NewFile(String),
|
||||
OpenFile,
|
||||
OpenFileWith(String),
|
||||
OpenTextFile,
|
||||
ReloadDir,
|
||||
RenameFile(String),
|
||||
SaveFileAs(String),
|
||||
SearchFile(String),
|
||||
TransferFile,
|
||||
TransferPendingFile,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum UiMsg {
|
||||
ChangeFileSorting(FileSorting),
|
||||
ChangeTransferWindow,
|
||||
CloseCopyPopup,
|
||||
CloseDeletePopup,
|
||||
CloseDisconnectPopup,
|
||||
CloseErrorPopup,
|
||||
CloseExecPopup,
|
||||
CloseFatalPopup,
|
||||
CloseFileInfoPopup,
|
||||
CloseFileSortingPopup,
|
||||
CloseFindExplorer,
|
||||
CloseFindPopup,
|
||||
CloseGotoPopup,
|
||||
CloseKeybindingsPopup,
|
||||
CloseMkdirPopup,
|
||||
CloseNewFilePopup,
|
||||
CloseOpenWithPopup,
|
||||
CloseQuitPopup,
|
||||
CloseReplacePopups,
|
||||
CloseRenamePopup,
|
||||
CloseSaveAsPopup,
|
||||
Disconnect,
|
||||
ExplorerTabbed,
|
||||
LogTabbed,
|
||||
Quit,
|
||||
ReplacePopupTabbed,
|
||||
ShowCopyPopup,
|
||||
ShowDeletePopup,
|
||||
ShowDisconnectPopup,
|
||||
ShowExecPopup,
|
||||
ShowFileInfoPopup,
|
||||
ShowFileSortingPopup,
|
||||
ShowFindPopup,
|
||||
ShowGotoPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowMkdirPopup,
|
||||
ShowNewFilePopup,
|
||||
ShowOpenWithPopup,
|
||||
ShowQuitPopup,
|
||||
ShowRenamePopup,
|
||||
ShowSaveAsPopup,
|
||||
ToggleHiddenFiles,
|
||||
ToggleSyncBrowsing,
|
||||
}
|
||||
|
||||
/// ## LogLevel
|
||||
///
|
||||
@@ -125,28 +207,43 @@ 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
|
||||
host: Localhost, // Localhost
|
||||
client: Box<dyn FileTransfer>, // File transfer client
|
||||
browser: Browser, // Browser
|
||||
log_records: VecDeque<LogRecord>, // Log records
|
||||
transfer: TransferStates, // Transfer states
|
||||
cache: Option<TempDir>, // Temporary directory where to store stuff
|
||||
/// Exit reason
|
||||
exit_reason: Option<ExitReason>,
|
||||
/// Context holder
|
||||
context: Option<Context>,
|
||||
/// Tui-realm application
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
/// Whether should redraw UI
|
||||
redraw: bool,
|
||||
/// Localhost bridge
|
||||
host: Localhost,
|
||||
/// Remote host
|
||||
client: Box<dyn FileTransfer>,
|
||||
/// Browser
|
||||
browser: Browser,
|
||||
/// Current log lines
|
||||
log_records: VecDeque<LogRecord>,
|
||||
transfer: TransferStates,
|
||||
/// Temporary directory where to store temporary stuff
|
||||
cache: Option<TempDir>,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new FileTransferActivity
|
||||
pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity {
|
||||
pub fn new(host: Localhost, protocol: FileTransferProtocol, ticks: Duration) -> Self {
|
||||
// Get config client
|
||||
let config_client: ConfigClient = Self::init_config_client();
|
||||
FileTransferActivity {
|
||||
Self {
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
view: View::init(),
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.poll_timeout(ticks)
|
||||
.default_input_listener(ticks),
|
||||
),
|
||||
redraw: true,
|
||||
host,
|
||||
client: match protocol {
|
||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
||||
@@ -257,9 +354,11 @@ impl Activity for FileTransferActivity {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context_mut().clear_screen();
|
||||
if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
// 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
|
||||
@@ -284,14 +383,12 @@ impl Activity for FileTransferActivity {
|
||||
/// `on_draw` is the function which draws the graphical interface.
|
||||
/// This function must be called at each tick to refresh the interface
|
||||
fn on_draw(&mut self) {
|
||||
// Should ui actually be redrawned?
|
||||
let mut redraw: bool = false;
|
||||
// Context must be something
|
||||
if self.context.is_none() {
|
||||
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);
|
||||
@@ -302,12 +399,11 @@ impl Activity for FileTransferActivity {
|
||||
// Connect to remote
|
||||
self.connect();
|
||||
// Redraw
|
||||
redraw = true;
|
||||
self.redraw = true;
|
||||
}
|
||||
// Handle input events (if false, becomes true; otherwise remains true)
|
||||
redraw |= self.read_input_event();
|
||||
// @! draw interface
|
||||
if redraw {
|
||||
self.tick();
|
||||
// View
|
||||
if self.redraw {
|
||||
self.view();
|
||||
}
|
||||
}
|
||||
@@ -333,20 +429,16 @@ 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);
|
||||
}
|
||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
// Disconnect client
|
||||
if self.client.is_connected() {
|
||||
let _ = self.client.disconnect();
|
||||
}
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
ctx.clear_screen();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
self.context.take()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +505,7 @@ impl FileTransferActivity {
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
self.tick();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
@@ -937,7 +937,7 @@ impl FileTransferActivity {
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
self.tick();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user