Migrated termscp to tui-realm 1.x

This commit is contained in:
veeso
2021-11-21 10:02:03 +01:00
committed by Christian Visintin
parent 30851a78e8
commit 54b5583d1a
54 changed files with 10994 additions and 7691 deletions

View File

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

View File

@@ -26,10 +26,10 @@
* SOFTWARE.
*/
pub(self) use super::{
browser::FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, TransferOpts,
browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts,
TransferPayload,
};
use tuirealm::{Payload, Value};
use tuirealm::{State, StateValue};
// actions
pub(crate) mod change_dir;
@@ -79,7 +79,7 @@ impl FileTransferActivity {
///
/// Get local file entry
pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) {
match self.get_selected_index(&Id::ExplorerLocal) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
@@ -97,7 +97,7 @@ impl FileTransferActivity {
///
/// Get remote file entry
pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) {
match self.get_selected_index(&Id::ExplorerRemote) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
@@ -115,7 +115,7 @@ impl FileTransferActivity {
///
/// Get remote file entry
pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_FIND) {
match self.get_selected_index(&Id::ExplorerFind) {
SelectedEntryIndex::One(idx) => {
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
}
@@ -133,14 +133,14 @@ impl FileTransferActivity {
// -- private
fn get_selected_index(&self, component: &str) -> SelectedEntryIndex {
match self.view.get_state(component) {
Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx),
Some(Payload::Vec(files)) => {
fn get_selected_index(&self, id: &Id) -> SelectedEntryIndex {
match self.app.state(id) {
Ok(State::One(StateValue::Usize(idx))) => SelectedEntryIndex::One(idx),
Ok(State::Vec(files)) => {
let list: Vec<usize> = files
.iter()
.map(|x| match x {
Value::Usize(v) => *v,
StateValue::Usize(v) => *v,
_ => 0,
})
.collect();

View File

@@ -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);
}
}
}
}

View 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
}
}

View 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,
}
}
}

File diff suppressed because it is too large Load Diff

View 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,
}
}
}

View 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,
}
}
}

View File

@@ -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,

View File

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

View File

@@ -26,19 +26,20 @@
* SOFTWARE.
*/
// This module is split into files, cause it's just too big
pub(self) mod actions;
pub(self) mod lib;
pub(self) mod misc;
pub(self) mod session;
pub(self) mod update;
pub(self) mod view;
mod actions;
mod components;
mod lib;
mod misc;
mod session;
mod update;
mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
use crate::fs::explorer::FileExplorer;
use crate::fs::explorer::{FileExplorer, FileSorting};
use crate::fs::FsEntry;
use crate::host::Localhost;
use crate::system::config_client::ConfigClient;
@@ -49,10 +50,10 @@ pub(self) use session::TransferPayload;
// Includes
use chrono::{DateTime, Local};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::collections::VecDeque;
use std::time::Duration;
use tempfile::TempDir;
use tuirealm::View;
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
// -- 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()
}
}

View File

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