diff --git a/src/ui/activities/filetransfer_activity.rs b/src/ui/activities/filetransfer_activity.rs
deleted file mode 100644
index b4ffd1d..0000000
--- a/src/ui/activities/filetransfer_activity.rs
+++ /dev/null
@@ -1,2394 +0,0 @@
-//! ## FileTransferActivity
-//!
-//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
-
-/*
-*
-* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
-*
-* This file is part of "TermSCP"
-*
-* TermSCP is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* TermSCP is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with TermSCP. If not, see .
-*
-*/
-
-// Dependencies
-extern crate chrono;
-extern crate crossterm;
-extern crate textwrap;
-extern crate tui;
-extern crate unicode_width;
-
-// locals
-use super::{Activity, Context};
-use crate::filetransfer::FileTransferProtocol;
-
-// File transfer
-use crate::filetransfer::sftp_transfer::SftpFileTransfer;
-use crate::filetransfer::FileTransfer;
-use crate::fs::FsEntry;
-
-// Includes
-use chrono::{DateTime, Local};
-use crossterm::event::Event as InputEvent;
-use crossterm::event::{KeyCode, KeyModifiers};
-use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
-use std::collections::VecDeque;
-use std::io::{Read, Seek, Write};
-use std::path::{Path, PathBuf};
-use std::time::Instant;
-use tui::{
- layout::{Constraint, Corner, Direction, Layout, Rect},
- style::{Color, Modifier, Style},
- text::{Span, Spans},
- widgets::{Block, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, Tabs},
-};
-use unicode_width::UnicodeWidthStr;
-
-// Types
-type DialogCallback = fn(&mut FileTransferActivity);
-type OnInputSubmitCallback = fn(&mut FileTransferActivity, String);
-
-/// ### FileTransferParams
-///
-/// Holds connection parameters for file transfers
-pub struct FileTransferParams {
- pub address: String,
- pub port: u16,
- pub protocol: FileTransferProtocol,
- pub username: Option,
- pub password: Option,
-}
-
-/// ### InputField
-///
-/// Input field selected
-#[derive(std::cmp::PartialEq)]
-enum InputField {
- Explorer,
- Logs,
-}
-
-/// ### DialogYesNoOption
-///
-/// Current yes/no dialog option
-#[derive(std::cmp::PartialEq, Clone)]
-enum DialogYesNoOption {
- Yes,
- No,
-}
-
-/// ## PopupType
-///
-/// PopupType describes the type of popup
-#[derive(Clone)]
-enum PopupType {
- Alert(Color, String), // Block color; Block text
- Fatal(String), // Must quit after being hidden
- Help, // Show Help
- Input(String, OnInputSubmitCallback), // Input description; Callback for submit
- Progress(String), // Progress block text
- Wait(String), // Wait block text
- YesNo(String, DialogCallback, DialogCallback), // Yes, no callback
-}
-
-/// ## InputMode
-///
-/// InputMode describes the current input mode
-/// Each input mode handle the input events in a different way
-#[derive(Clone)]
-enum InputMode {
- Explorer,
- Popup(PopupType),
-}
-
-/// ## FileExplorer
-///
-/// File explorer states
-struct FileExplorer {
- pub index: usize,
- pub files: Vec,
- dirstack: VecDeque,
-}
-
-impl FileExplorer {
- /// ### new
- ///
- /// Instantiates a new FileExplorer
- pub fn new() -> FileExplorer {
- FileExplorer {
- index: 0,
- files: Vec::new(),
- dirstack: VecDeque::with_capacity(16),
- }
- }
-
- /// ### pushd
- ///
- /// push directory to stack
- pub fn pushd(&mut self, dir: &Path) {
- // Check if stack overflows the size
- if self.dirstack.len() + 1 > 16 {
- self.dirstack.pop_back(); // Start cleaning events from back
- }
- // Eventually push front the new record
- self.dirstack.push_front(PathBuf::from(dir));
- }
-
- /// ### popd
- ///
- /// Pop directory from the stack and return the directory
- pub fn popd(&mut self) -> Option {
- self.dirstack.pop_front()
- }
-
- /// ### sort_files_by_name
- ///
- /// Sort explorer files by their name
- pub fn sort_files_by_name(&mut self) {
- self.files.sort_by_key(|x: &FsEntry| match x {
- FsEntry::Directory(dir) => dir.name.clone(),
- FsEntry::File(file) => file.name.clone(),
- });
- }
-}
-
-/// ## FileExplorerTab
-///
-/// File explorer tab
-enum FileExplorerTab {
- Local,
- Remote,
-}
-
-/// ## LogLevel
-///
-/// Log level type
-#[allow(dead_code)]
-enum LogLevel {
- Error,
- Warn,
- Info,
-}
-
-/// ## LogRecord
-///
-/// Log record entry
-struct LogRecord {
- pub time: DateTime,
- pub level: LogLevel,
- pub msg: String,
-}
-
-impl LogRecord {
- /// ### new
- ///
- /// Instantiates a new LogRecord
- pub fn new(level: LogLevel, msg: &str) -> LogRecord {
- LogRecord {
- time: Local::now(),
- level: level,
- msg: String::from(msg),
- }
- }
-}
-
-/// ## FileTransferActivity
-///
-/// FileTransferActivity is the data holder for the file transfer activity
-pub struct FileTransferActivity {
- pub disconnected: bool, // Has disconnected from remote?
- pub quit: bool, // Has quit term scp?
- context: Option, // Context holder
- params: FileTransferParams, // FT connection params
- client: Box, // File transfer client
- local: FileExplorer, // Local File explorer state
- remote: FileExplorer, // Remote File explorer state
- tab: FileExplorerTab, // Current selected tab
- log_index: usize, // Current log index entry selected
- log_records: VecDeque, // Log records
- log_size: usize, // Log records size (max)
- input_mode: InputMode, // Current input mode
- input_field: InputField, // Current selected input mode
- input_txt: String, // Input text
- choice_opt: DialogYesNoOption, // Dialog popup selected option
- transfer_progress: f64, // Current write/read progress (percentage)
- transfer_started: Instant, // Instant when progress has started
-}
-
-impl FileTransferActivity {
- /// ### new
- ///
- /// Instantiates a new FileTransferActivity
- pub fn new(params: FileTransferParams) -> FileTransferActivity {
- let protocol: FileTransferProtocol = params.protocol.clone();
- FileTransferActivity {
- disconnected: false,
- quit: false,
- context: None,
- params: params,
- client: match protocol {
- FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new()),
- FileTransferProtocol::Ftp => panic!("FTP is not supported YET!"), // FIXME: FTP
- },
- local: FileExplorer::new(),
- remote: FileExplorer::new(),
- tab: FileExplorerTab::Local,
- log_index: 0,
- log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
- log_size: 256, // Must match with capacity
- input_mode: InputMode::Explorer,
- input_field: InputField::Explorer,
- input_txt: String::new(),
- choice_opt: DialogYesNoOption::Yes,
- transfer_progress: 0.0,
- transfer_started: Instant::now(),
- }
- }
-
- // @! Session
-
- /// ### connect
- ///
- /// Connect to remote
- fn connect(&mut self) {
- // Connect to remote
- match self.client.connect(
- self.params.address.clone(),
- self.params.port,
- self.params.username.clone(),
- self.params.password.clone(),
- ) {
- Ok(_) => {
- // Set state to explorer
- self.input_mode = InputMode::Explorer;
- self.reload_remote_dir();
- }
- Err(err) => {
- // Set popup fatal error
- self.input_mode = InputMode::Popup(PopupType::Fatal(format!("{}", err)));
- }
- }
- }
-
- /// ### disconnect
- ///
- /// disconnect from remote
- fn disconnect(&mut self) {
- // Show popup disconnecting
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- String::from("Disconnecting from remote..."),
- ));
- // Disconnect
- let _ = self.client.disconnect();
- // Quit
- self.disconnected = true;
- }
-
- /// ### reload_remote_dir
- ///
- /// Reload remote directory entries
- fn reload_remote_dir(&mut self) {
- // Get current entries
- if let Ok(pwd) = self.client.pwd() {
- self.remote_scan(pwd.as_path());
- }
- }
-
- /// ### filetransfer_send
- ///
- /// Send fs entry to remote.
- /// If dst_name is Some, entry will be saved with a different name.
- /// If entry is a directory, this applies to directory only
- fn filetransfer_send(
- &mut self,
- entry: &FsEntry,
- curr_remote_path: &Path,
- dst_name: Option,
- ) {
- // Write popup
- let file_name: String = match entry {
- FsEntry::Directory(dir) => dir.name.clone(),
- FsEntry::File(file) => file.name.clone(),
- };
- self.input_mode = InputMode::Popup(PopupType::Wait(format!("Uploading \"{}\"", file_name)));
- // Draw
- self.draw();
- // Get remote path
- let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
- let remote_file_name: PathBuf = match dst_name {
- Some(s) => PathBuf::from(s.as_str()),
- None => PathBuf::from(file_name.as_str()),
- };
- remote_path.push(remote_file_name);
- // Match entry
- match entry {
- FsEntry::File(file) => {
- // Upload file
- // Try to open local file
- match self
- .context
- .as_ref()
- .unwrap()
- .local
- .open_file_read(file.abs_path.as_path())
- {
- Ok(mut fhnd) => match self.client.send_file(remote_path.as_path()) {
- Ok(mut rhnd) => {
- // Write file
- let file_size: usize =
- fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
- // rewind
- if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) {
- self.log(
- LogLevel::Error,
- format!("Could not rewind local file: {}", err).as_ref(),
- );
- }
- // Write remote file
- let mut total_bytes_written: usize = 0;
- // Set input state to popup progress
- self.input_mode = InputMode::Popup(PopupType::Progress(format!(
- "Uploading \"{}\"",
- file_name
- )));
- // Set started time
- self.transfer_started = Instant::now();
- let mut last_progress_val: f64 = 0.0;
- loop {
- // Read till you can
- let mut buffer: [u8; 8192] = [0; 8192];
- match fhnd.read(&mut buffer) {
- Ok(bytes_read) => {
- total_bytes_written += bytes_read;
- if bytes_read == 0 {
- break;
- } else {
- // Write bytes
- if let Err(err) = rhnd.write(&buffer[0..bytes_read]) {
- self.log(
- LogLevel::Error,
- format!("Could not write remote file: {}", err)
- .as_ref(),
- );
- }
- }
- }
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!("Could not read local file: {}", err).as_ref(),
- );
- }
- }
- // Increase progress
- self.set_progress(total_bytes_written, file_size);
- // Draw only if a significant progress has been made (performance improvement)
- if last_progress_val + 0.5 >= self.transfer_progress {
- // Draw
- self.draw();
- last_progress_val = self.transfer_progress;
- }
- }
- self.log(
- LogLevel::Info,
- format!(
- "Saved file \"{}\" to \"{}\"",
- file.abs_path.display(),
- remote_path.display()
- )
- .as_ref(),
- );
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!(
- "Failed to upload file \"{}\": {}",
- file.abs_path.display(),
- err
- )
- .as_ref(),
- ),
- },
- Err(err) => {
- // Report error
- self.log(
- LogLevel::Error,
- format!(
- "Failed to open file \"{}\": {}",
- file.abs_path.display(),
- err
- )
- .as_ref(),
- );
- }
- }
- }
- FsEntry::Directory(dir) => {
- // Create directory on remote
- match self.client.mkdir(remote_path.as_path()) {
- Ok(_) => {
- self.log(
- LogLevel::Info,
- format!("Created directory \"{}\"", remote_path.display()).as_ref(),
- );
- // Get files in dir
- match self
- .context
- .as_ref()
- .unwrap()
- .local
- .scan_dir(dir.abs_path.as_path())
- {
- Ok(entries) => {
- // Iterate over files
- for entry in entries.iter() {
- // Send entry; name is always None after first call
- self.filetransfer_send(&entry, remote_path.as_path(), None);
- }
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!(
- "Could not scan directory \"{}\": {}",
- dir.abs_path.display(),
- err
- )
- .as_ref(),
- ),
- }
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!(
- "Failed to create directory \"{}\": {}",
- remote_path.display(),
- err
- )
- .as_ref(),
- ),
- }
- }
- }
- // Scan dir on remote
- if let Ok(path) = self.client.pwd() {
- self.remote_scan(path.as_path());
- }
- // Eventually, Reset input mode to explorer
- self.input_mode = InputMode::Explorer;
- }
-
- /// ### filetransfer_recv
- ///
- /// Recv fs entry from remote.
- /// If dst_name is Some, entry will be saved with a different name.
- /// If entry is a directory, this applies to directory only
- fn filetransfer_recv(&mut self, entry: &FsEntry, local_path: &Path, dst_name: Option) {
- // Write popup
- let file_name: String = match entry {
- FsEntry::Directory(dir) => dir.name.clone(),
- FsEntry::File(file) => file.name.clone(),
- };
- self.input_mode =
- InputMode::Popup(PopupType::Wait(format!("Downloading \"{}\"...", file_name)));
- // Draw
- self.draw();
- // Match entry
- match entry {
- FsEntry::File(file) => {
- // Get local file
- let mut local_file_path: PathBuf = PathBuf::from(local_path);
- let local_file_name: String = match dst_name {
- Some(n) => n.clone(),
- None => file.name.clone(),
- };
- local_file_path.push(local_file_name.as_str());
- // Try to open local file
- match self
- .context
- .as_ref()
- .unwrap()
- .local
- .open_file_write(local_file_path.as_path())
- {
- Ok(mut local_file) => {
- // Download file from remote
- match self.client.recv_file(file.abs_path.as_path()) {
- Ok((mut rhnd, file_size)) => {
- // Set popup progress
- self.input_mode = InputMode::Popup(PopupType::Progress(format!(
- "Downloading \"{}\"...",
- file_name
- )));
- let mut total_bytes_written: usize = 0;
- // Set started time
- self.transfer_started = Instant::now();
- // Write local file
- let mut last_progress_val: f64 = 0.0;
- loop {
- // Read till you can
- let mut buffer: [u8; 8192] = [0; 8192];
- match rhnd.read(&mut buffer) {
- Ok(bytes_read) => {
- total_bytes_written += bytes_read;
- if bytes_read == 0 {
- break;
- } else {
- // Write bytes
- if let Err(err) =
- local_file.write(&buffer[0..bytes_read])
- {
- self.log(
- LogLevel::Error,
- format!(
- "Could not write local file: {}",
- err
- )
- .as_ref(),
- );
- }
- }
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!("Could not read remote file: {}", err).as_ref(),
- ),
- }
- // Set progress
- self.set_progress(total_bytes_written, file_size);
- // Draw only if a significant progress has been made (performance improvement)
- if last_progress_val + 0.5 >= self.transfer_progress {
- // Draw
- self.draw();
- last_progress_val = self.transfer_progress;
- }
- }
- // Log
- self.log(
- LogLevel::Info,
- format!(
- "Saved file \"{}\" to \"{}\"",
- file.abs_path.display(),
- local_file_path.display()
- )
- .as_ref(),
- );
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!(
- "Failed to download file \"{}\": {}",
- file.abs_path.display(),
- err
- )
- .as_ref(),
- ),
- }
- }
- Err(err) => {
- // Report error
- self.log(
- LogLevel::Error,
- format!(
- "Failed to open local file for write \"{}\": {}",
- local_file_path.display(),
- err
- )
- .as_ref(),
- );
- }
- }
- }
- FsEntry::Directory(dir) => {
- // Get dir name
- let mut local_dir_path: PathBuf = PathBuf::from(local_path);
- match dst_name {
- Some(name) => local_dir_path.push(name),
- None => local_dir_path.push(dir.name.as_str()),
- }
- // Create directory on local
- match self
- .context
- .as_mut()
- .unwrap()
- .local
- .mkdir_ex(local_dir_path.as_path(), true)
- {
- Ok(_) => {
- self.log(
- LogLevel::Info,
- format!("Created directory \"{}\"", local_dir_path.display()).as_ref(),
- );
- // Get files in dir
- match self.client.list_dir(dir.abs_path.as_path()) {
- Ok(entries) => {
- // Iterate over files
- for entry in entries.iter() {
- // Receive entry; name is always None after first call
- // Local path becomes local_dir_path
- self.filetransfer_recv(&entry, local_dir_path.as_path(), None);
- }
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!(
- "Could not scan directory \"{}\": {}",
- dir.abs_path.display(),
- err
- )
- .as_ref(),
- ),
- }
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!(
- "Failed to create directory \"{}\": {}",
- local_dir_path.display(),
- err
- )
- .as_ref(),
- ),
- }
- }
- }
- // Reload directory on local
- self.local_scan(local_path);
- // Eventually, Reset input mode to explorer
- self.input_mode = InputMode::Explorer;
- }
-
- /// ### local_scan
- ///
- /// Scan current local directory
- fn local_scan(&mut self, path: &Path) {
- match self.context.as_ref().unwrap().local.scan_dir(path) {
- Ok(files) => {
- // Reset index
- self.local.index = 0;
- self.local.files = files;
- // Sort files
- self.local.sort_files_by_name();
- }
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!("Could not scan current directory: {}", err).as_str(),
- );
- }
- }
- }
-
- /// ### remote_scan
- ///
- /// Scan current remote directory
- fn remote_scan(&mut self, path: &Path) {
- match self.client.list_dir(path) {
- Ok(files) => {
- // Reset index
- self.remote.index = 0;
- self.remote.files = files;
- // Sort files
- self.remote.sort_files_by_name();
- }
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!("Could not scan current directory: {}", err).as_str(),
- );
- }
- }
- }
-
- /// ### local_changedir
- ///
- /// Change directory for local
- fn local_changedir(&mut self, path: &Path, push: bool) {
- // Get current directory
- let prev_dir: PathBuf = self.context.as_ref().unwrap().local.pwd();
- // Change directory
- match self
- .context
- .as_mut()
- .unwrap()
- .local
- .change_wrkdir(PathBuf::from(path))
- {
- Ok(_) => {
- self.log(
- LogLevel::Info,
- format!("Changed directory on local: {}", path.display()).as_str(),
- );
- // Reload files
- self.local_scan(path);
- // Push prev_dir to stack
- if push {
- self.local.pushd(prev_dir.as_path())
- }
- }
- Err(err) => {
- // Report err
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not change working directory: {}", err),
- ));
- }
- }
- }
-
- fn remote_changedir(&mut self, path: &Path, push: bool) {
- // Get current directory
- match self.client.pwd() {
- Ok(prev_dir) => {
- // Change directory
- match self.client.change_dir(path) {
- Ok(_) => {
- self.log(
- LogLevel::Info,
- format!("Changed directory on remote: {}", path.display()).as_str(),
- );
- // Update files
- self.remote_scan(path);
- // Push prev_dir to stack
- if push {
- self.remote.pushd(prev_dir.as_path())
- }
- }
- Err(err) => {
- // Report err
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not change working directory: {}", err),
- ));
- }
- }
- }
- Err(err) => {
- // Report err
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not change working directory: {}", err),
- ));
- }
- }
- }
-
- /// ### log
- ///
- /// Add message to log events
- fn log(&mut self, level: LogLevel, msg: &str) {
- // Create log record
- let record: LogRecord = LogRecord::new(level, msg);
- //Check if history overflows the size
- if self.log_records.len() + 1 > self.log_size {
- self.log_records.pop_back(); // Start cleaning events from back
- }
- // Eventually push front the new record
- self.log_records.push_front(record);
- // Set log index
- self.log_index = 0;
- }
-
- /// ### create_quit_popup
- ///
- /// Create quit popup input mode (since must be shared between different input handlers)
- fn create_quit_popup(&mut self) -> InputMode {
- InputMode::Popup(PopupType::YesNo(
- String::from("Are you sure you want to quit?"),
- FileTransferActivity::disconnect,
- FileTransferActivity::callback_nothing_to_do,
- ))
- }
-
- /// ### switch_input_field
- ///
- /// Switch input field based on current input field
- fn switch_input_field(&mut self) {
- self.input_field = match self.input_field {
- InputField::Explorer => InputField::Logs,
- InputField::Logs => InputField::Explorer,
- }
- }
-
- /// ### set_progress
- ///
- /// Calculate progress percentage based on current progress
- fn set_progress(&mut self, it: usize, sz: usize) {
- self.transfer_progress = ((it as f64) * 100.0) / (sz as f64);
- }
-
- // @! input listeners
-
- /// ### handle_input_event
- ///
- /// Handle input event based on current input mode
- fn handle_input_event(&mut self, ev: &InputEvent) {
- // NOTE: this is necessary due to this
- // NOTE: Do you want my opinion about that issue? It's a bs and doesn't make any sense.
- let popup: Option = match &self.input_mode {
- InputMode::Popup(ptype) => Some(ptype.clone()),
- _ => None,
- };
- match &self.input_mode {
- InputMode::Explorer => self.handle_input_event_mode_explorer(ev),
- InputMode::Popup(_) => {
- if let Some(popup) = popup {
- self.handle_input_event_mode_popup(ev, popup);
- }
- }
- }
- }
-
- /// ### handle_input_event_mode_explorer
- ///
- /// Input event handler for explorer mode
- fn handle_input_event_mode_explorer(&mut self, ev: &InputEvent) {
- // Match input field
- match self.input_field {
- InputField::Explorer => match self.tab {
- // Match current selected tab
- FileExplorerTab::Local => self.handle_input_event_mode_explorer_tab_local(ev),
- FileExplorerTab::Remote => self.handle_input_event_mode_explorer_tab_remote(ev),
- },
- InputField::Logs => self.handle_input_event_mode_explorer_log(ev),
- }
- }
-
- /// ### handle_input_event_mode_explorer_tab_local
- ///
- /// Input event handler for explorer mode when localhost tab is selected
- fn handle_input_event_mode_explorer_tab_local(&mut self, ev: &InputEvent) {
- // Match events
- match ev {
- InputEvent::Key(key) => {
- match key.code {
- KeyCode::Esc => {
- // Handle quit event
- // Create quit prompt dialog
- self.input_mode = self.create_quit_popup();
- }
- KeyCode::Tab => self.switch_input_field(), // switch tab
- KeyCode::Right => self.tab = FileExplorerTab::Remote, // switch to right tab
- KeyCode::Up => {
- // Move index up
- if self.local.index > 0 {
- self.local.index -= 1;
- }
- }
- KeyCode::Down => {
- // Move index down
- if self.local.index + 1 < self.local.files.len() {
- self.local.index += 1;
- }
- }
- KeyCode::PageUp => {
- // Move index up (fast)
- if self.local.index > 8 {
- self.local.index = self.local.index - 8; // Decrease by `8` if possible
- } else {
- self.local.index = 0; // Set to 0 otherwise
- }
- }
- KeyCode::PageDown => {
- // Move index down (fast)
- if self.local.index + 8 >= self.local.files.len() {
- // If overflows, set to size
- self.local.index = self.local.files.len() - 1;
- } else {
- self.local.index = self.local.index + 8; // Increase by `8`
- }
- }
- KeyCode::Enter => {
- // Match selected file
- let local_files: Vec = self.local.files.clone();
- if let Some(entry) = local_files.get(self.local.index) {
- // If directory, enter directory, otherwise check if symlink
- match entry {
- FsEntry::Directory(dir) => {
- self.local_changedir(dir.abs_path.as_path(), true)
- }
- FsEntry::File(file) => {
- // Check if symlink
- if let Some(realpath) = &file.symlink {
- // Stat realpath
- match self
- .context
- .as_ref()
- .unwrap()
- .local
- .stat(realpath.as_path())
- {
- Ok(real_file) => {
- // If real file is a directory, enter directory
- if let FsEntry::Directory(real_dir) = real_file {
- self.local_changedir(
- real_dir.abs_path.as_path(),
- true,
- )
- }
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!(
- "Failed to stat file \"{}\": {}",
- realpath.display(),
- err
- )
- .as_ref(),
- ),
- }
- }
- }
- }
- }
- }
- KeyCode::Backspace => {
- // Go to previous directory
- if let Some(d) = self.local.popd() {
- self.local_changedir(d.as_path(), false);
- }
- }
- KeyCode::Delete => {
- // Get file at index
- if let Some(entry) = self.local.files.get(self.local.index) {
- // Get file name
- let file_name: String = match entry {
- FsEntry::Directory(dir) => dir.name.clone(),
- FsEntry::File(file) => file.name.clone(),
- };
- // Show delete prompt
- self.input_mode = InputMode::Popup(PopupType::YesNo(
- format!("Delete file \"{}\"", file_name),
- FileTransferActivity::callback_delete_fsentry,
- FileTransferActivity::callback_nothing_to_do,
- ))
- }
- }
- KeyCode::Char(ch) => match ch {
- 'g' | 'G' => {
- // Goto
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- // Show input popup
- self.input_mode = InputMode::Popup(PopupType::Input(
- String::from("Change working directory"),
- FileTransferActivity::callback_change_directory,
- ));
- }
- }
- 'd' | 'D' => {
- // Make directory
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- self.input_mode = InputMode::Popup(PopupType::Input(
- String::from("Insert directory name"),
- FileTransferActivity::callback_mkdir,
- ));
- }
- }
- 'h' | 'H' => {
- // Show help
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- self.input_mode = InputMode::Popup(PopupType::Help);
- }
- }
- 'r' | 'R' => {
- // Rename
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- self.input_mode = InputMode::Popup(PopupType::Input(
- String::from("Insert new name"),
- FileTransferActivity::callback_rename,
- ));
- }
- }
- 's' | 'S' => {
- // Save as...
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- // Ask for input
- self.input_mode = InputMode::Popup(PopupType::Input(
- String::from("Save as..."),
- FileTransferActivity::callback_save_as,
- ));
- }
- }
- 'u' | 'U' => {
- // Go to parent directory
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- // Get pwd
- let path: PathBuf = self.context.as_ref().unwrap().local.pwd();
- if let Some(parent) = path.as_path().parent() {
- self.local_changedir(parent, true);
- }
- }
- }
- ' ' => {
- // Get pwd
- let wrkdir: PathBuf = match self.client.pwd() {
- Ok(p) => p,
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!("Could not get current remote path: {}", err)
- .as_ref(),
- );
- return;
- }
- };
- // Get files
- let files: Vec = self.local.files.clone(); // Otherwise self is borrowed both as mutable and immutable...
- // Get file at index
- if let Some(entry) = files.get(self.local.index) {
- // Call upload
- self.filetransfer_send(entry, wrkdir.as_path(), None);
- }
- }
- _ => { /* Nothing to do */ }
- },
- _ => { /* Nothing to do */ }
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_explorer_tab_local
- ///
- /// Input event handler for explorer mode when remote tab is selected
- fn handle_input_event_mode_explorer_tab_remote(&mut self, ev: &InputEvent) {
- // Match events
- match ev {
- InputEvent::Key(key) => {
- match key.code {
- KeyCode::Esc => {
- // Handle quit event
- // Create quit prompt dialog
- self.input_mode = self.create_quit_popup();
- }
- KeyCode::Tab => self.switch_input_field(), // switch tab
- KeyCode::Left => self.tab = FileExplorerTab::Local, // switch to local tab
- KeyCode::Up => {
- // Move index up
- if self.remote.index > 0 {
- self.remote.index -= 1;
- }
- }
- KeyCode::Down => {
- // Move index down
- if self.remote.index + 1 < self.remote.files.len() {
- self.remote.index += 1;
- }
- }
- KeyCode::PageUp => {
- // Move index up (fast)
- if self.remote.index > 8 {
- self.remote.index = self.remote.index - 8; // Decrease by `8` if possible
- } else {
- self.remote.index = 0; // Set to 0 otherwise
- }
- }
- KeyCode::PageDown => {
- // Move index down (fast)
- if self.remote.index + 8 >= self.remote.files.len() {
- // If overflows, set to size
- self.remote.index = self.remote.files.len() - 1;
- } else {
- self.remote.index = self.remote.index + 8; // Increase by `8`
- }
- }
- KeyCode::Enter => {
- // Match selected file
- let files: Vec = self.remote.files.clone();
- if let Some(entry) = files.get(self.remote.index) {
- // If directory, enter directory; if file, check if is symlink
- match entry {
- FsEntry::Directory(dir) => {
- self.remote_changedir(dir.abs_path.as_path(), true)
- }
- FsEntry::File(file) => {
- // Check if symlink
- if let Some(realpath) = &file.symlink {
- // Stat realpath
- match self.client.stat(realpath.as_path()) {
- Ok(real_file) => {
- // If real file is a directory, enter directory
- if let FsEntry::Directory(real_dir) = real_file {
- self.remote_changedir(
- real_dir.abs_path.as_path(),
- true,
- )
- }
- }
- Err(err) => self.log(
- LogLevel::Error,
- format!(
- "Failed to stat file \"{}\": {}",
- realpath.display(),
- err
- )
- .as_ref(),
- ),
- }
- }
- }
- }
- }
- }
- KeyCode::Backspace => {
- // Go to previous directory
- if let Some(d) = self.remote.popd() {
- self.remote_changedir(d.as_path(), false);
- }
- }
- KeyCode::Delete => {
- // Get file at index
- if let Some(entry) = self.remote.files.get(self.remote.index) {
- // Get file name
- let file_name: String = match entry {
- FsEntry::Directory(dir) => dir.name.clone(),
- FsEntry::File(file) => file.name.clone(),
- };
- // Show delete prompt
- self.input_mode = InputMode::Popup(PopupType::YesNo(
- format!("Delete file \"{}\"", file_name),
- FileTransferActivity::callback_delete_fsentry,
- FileTransferActivity::callback_nothing_to_do,
- ))
- }
- }
- KeyCode::Char(ch) => match ch {
- 'g' | 'G' => {
- // Goto
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- // Show input popup
- self.input_mode = InputMode::Popup(PopupType::Input(
- String::from("Change working directory"),
- FileTransferActivity::callback_change_directory,
- ));
- }
- }
- 'd' | 'D' => {
- // Make directory
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- self.input_mode = InputMode::Popup(PopupType::Input(
- String::from("Insert directory name"),
- FileTransferActivity::callback_mkdir,
- ));
- }
- }
- 'h' | 'H' => {
- // Show help
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- self.input_mode = InputMode::Popup(PopupType::Help);
- }
- }
- 'r' | 'R' => {
- // Rename
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- self.input_mode = InputMode::Popup(PopupType::Input(
- String::from("Insert new name"),
- FileTransferActivity::callback_rename,
- ));
- }
- }
- 's' | 'S' => {
- // Save as...
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- // Ask for input
- self.input_mode = InputMode::Popup(PopupType::Input(
- String::from("Save as..."),
- FileTransferActivity::callback_save_as,
- ));
- }
- }
- 'u' | 'U' => {
- // Go to parent directory
- // If ctrl is enabled...
- if key.modifiers.intersects(KeyModifiers::CONTROL) {
- // Get pwd
- match self.client.pwd() {
- Ok(path) => {
- if let Some(parent) = path.as_path().parent() {
- self.remote_changedir(parent, true);
- }
- }
- Err(err) => {
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not change working directory: {}", err),
- ))
- }
- }
- }
- }
- ' ' => {
- // Get files
- let files: Vec = self.remote.files.clone(); // Otherwise self is borrowed both as mutable and immutable...
- // Get file at index
- if let Some(entry) = files.get(self.remote.index) {
- // Call upload
- self.filetransfer_recv(
- entry,
- self.context.as_ref().unwrap().local.pwd().as_path(),
- None,
- );
- }
- }
- _ => { /* Nothing to do */ }
- },
- _ => { /* Nothing to do */ }
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_explorer_log
- ///
- /// Input even handler for explorer mode when log tab is selected
- fn handle_input_event_mode_explorer_log(&mut self, ev: &InputEvent) {
- // Match event
- let records_block: usize = 16;
- match ev {
- InputEvent::Key(key) => {
- match key.code {
- KeyCode::Esc => {
- // Handle quit event
- // Create quit prompt dialog
- self.input_mode = self.create_quit_popup();
- }
- KeyCode::Tab => self.switch_input_field(), // switch tab
- KeyCode::Down => {
- // NOTE: Twisted logic
- // Decrease log index
- if self.log_index > 0 {
- self.log_index = self.log_index - 1;
- }
- }
- KeyCode::Up => {
- // NOTE: Twisted logic
- // Increase log index
- if self.log_index + 1 < self.log_records.len() {
- self.log_index = self.log_index + 1;
- }
- }
- KeyCode::PageDown => {
- // NOTE: Twisted logic
- // Fast decreasing of log index
- if self.log_index >= records_block {
- self.log_index = self.log_index - records_block; // Decrease by `records_block` if possible
- } else {
- self.log_index = 0; // Set to 0 otherwise
- }
- }
- KeyCode::PageUp => {
- // NOTE: Twisted logic
- // Fast increasing of log index
- if self.log_index + records_block >= self.log_records.len() {
- // If overflows, set to size
- self.log_index = self.log_records.len() - 1;
- } else {
- self.log_index = self.log_index + records_block; // Increase by `records_block`
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_explorer
- ///
- /// Input event handler for popup mode. Handler is then based on Popup type
- fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: PopupType) {
- match popup {
- PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
- PopupType::Help => self.handle_input_event_mode_popup_help(ev),
- PopupType::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev),
- PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb),
- PopupType::Progress(_) => self.handle_input_event_mode_popup_progress(ev),
- PopupType::Wait(_) => self.handle_input_event_mode_popup_wait(ev),
- PopupType::YesNo(_, yes_cb, no_cb) => {
- self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb)
- }
- }
- }
-
- /// ### handle_input_event_mode_popup_alert
- ///
- /// Input event handler for popup alert
- fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
- // If enter, close popup
- match ev {
- InputEvent::Key(key) => {
- match key.code {
- KeyCode::Enter => {
- // Set input mode back to explorer
- self.input_mode = InputMode::Explorer;
- }
- _ => { /* Nothing to do */ }
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_popup_help
- ///
- /// Input event handler for popup help
- fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
- // If enter, close popup
- match ev {
- InputEvent::Key(key) => {
- match key.code {
- KeyCode::Enter | KeyCode::Esc => {
- // Set input mode back to explorer
- self.input_mode = InputMode::Explorer;
- }
- _ => { /* Nothing to do */ }
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_popup_fatal
- ///
- /// Input event handler for popup alert
- fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) {
- // If enter, close popup
- match ev {
- InputEvent::Key(key) => {
- match key.code {
- KeyCode::Enter => {
- // Set quit to true; since a fatal error happened
- self.disconnect();
- }
- _ => { /* Nothing to do */ }
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_popup_input
- ///
- /// Input event handler for input popup
- fn handle_input_event_mode_popup_input(&mut self, ev: &InputEvent, cb: OnInputSubmitCallback) {
- // If enter, close popup, otherwise push chars to input
- match ev {
- InputEvent::Key(key) => {
- match key.code {
- KeyCode::Esc => {
- // Abort input
- // Clear current input text
- self.input_txt.clear();
- // Set mode back to explorer
- self.input_mode = InputMode::Explorer;
- }
- KeyCode::Enter => {
- // Submit
- let input_text: String = self.input_txt.clone();
- // Clear current input text
- self.input_txt.clear();
- // Set mode back to explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
- self.input_mode = InputMode::Explorer;
- // Call cb
- cb(self, input_text);
- }
- KeyCode::Char(ch) => self.input_txt.push(ch),
- KeyCode::Backspace => {
- let _ = self.input_txt.pop();
- }
- _ => { /* Nothing to do */ }
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_explorer_alert
- ///
- /// Input event handler for popup alert
- fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) {
- // There's nothing you can do here I guess... maybe ctrl+c in the future idk
- match ev {
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_explorer_alert
- ///
- /// Input event handler for popup alert
- fn handle_input_event_mode_popup_wait(&mut self, ev: &InputEvent) {
- // There's nothing you can do here I guess... maybe ctrl+c in the future idk
- match ev {
- _ => { /* Nothing to do */ }
- }
- }
-
- /// ### handle_input_event_mode_explorer_alert
- ///
- /// Input event handler for popup alert
- fn handle_input_event_mode_popup_yesno(
- &mut self,
- ev: &InputEvent,
- yes_cb: DialogCallback,
- no_cb: DialogCallback,
- ) {
- // If enter, close popup, otherwise move dialog option
- match ev {
- InputEvent::Key(key) => {
- match key.code {
- KeyCode::Enter => {
- // @! Set input mode to Explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
- self.input_mode = InputMode::Explorer;
- // Check if user selected yes or not
- match self.choice_opt {
- DialogYesNoOption::No => no_cb(self),
- DialogYesNoOption::Yes => yes_cb(self),
- }
- // Reset choice option to yes
- self.choice_opt = DialogYesNoOption::Yes;
- }
- KeyCode::Right => self.choice_opt = DialogYesNoOption::No, // Set to NO
- KeyCode::Left => self.choice_opt = DialogYesNoOption::Yes, // Set to YES
- _ => { /* Nothing to do */ }
- }
- }
- _ => { /* Nothing to do */ }
- }
- }
-
- // @! Callbacks
-
- /// ### callback_nothing_to_do
- ///
- /// Self titled
- fn callback_nothing_to_do(&mut self) {}
-
- /// ### callback_change_directory
- ///
- /// Callback for GOTO command
- fn callback_change_directory(&mut self, input: String) {
- let dir_path: PathBuf = PathBuf::from(input);
- match self.tab {
- FileExplorerTab::Local => {
- // If path is relative, concat pwd
- let abs_dir_path: PathBuf = match dir_path.is_relative() {
- true => {
- let mut d: PathBuf = self.context.as_ref().unwrap().local.pwd();
- d.push(dir_path);
- d
- }
- false => dir_path,
- };
- self.local_changedir(abs_dir_path.as_path(), true);
- }
- FileExplorerTab::Remote => {
- // If path is relative, concat pwd
- let abs_dir_path: PathBuf = match dir_path.is_relative() {
- true => match self.client.pwd() {
- Ok(mut wkrdir) => {
- wkrdir.push(dir_path);
- wkrdir
- }
- Err(err) => {
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not retrieve current directory: {}", err),
- ));
- return;
- }
- },
- false => dir_path,
- };
- self.remote_changedir(abs_dir_path.as_path(), true);
- }
- }
- }
-
- /// ### callback_mkdir
- ///
- /// Callback for MKDIR command (supports both local and remote)
- fn callback_mkdir(&mut self, input: String) {
- match self.tab {
- FileExplorerTab::Local => {
- match self
- .context
- .as_mut()
- .unwrap()
- .local
- .mkdir(PathBuf::from(input.as_str()).as_path())
- {
- Ok(_) => {
- // Reload files
- self.log(
- LogLevel::Info,
- format!("Created directory \"{}\"", input).as_ref(),
- );
- let wrkdir: PathBuf = self.context.as_ref().unwrap().local.pwd();
- self.local_scan(wrkdir.as_path());
- }
- Err(err) => {
- // Report err
- self.log(
- LogLevel::Error,
- format!("Could not create directory \"{}\": {}", input, err).as_ref(),
- );
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not create directory \"{}\": {}", input, err),
- ));
- }
- }
- }
- FileExplorerTab::Remote => {
- match self.client.mkdir(PathBuf::from(input.as_str()).as_path()) {
- Ok(_) => {
- // Reload files
- self.log(
- LogLevel::Info,
- format!("Created directory \"{}\"", input).as_ref(),
- );
- self.reload_remote_dir();
- }
- Err(err) => {
- // Report err
- self.log(
- LogLevel::Error,
- format!("Could not create directory \"{}\": {}", input, err).as_ref(),
- );
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not create directory \"{}\": {}", input, err),
- ));
- }
- }
- }
- }
- }
-
- /// ### callback_rename
- ///
- /// Callback for RENAME command (supports borth local and remote)
- fn callback_rename(&mut self, input: String) {
- match self.tab {
- FileExplorerTab::Local => {
- let mut dst_path: PathBuf = PathBuf::from(input);
- // Check if path is relative
- if dst_path.as_path().is_relative() {
- let mut wrkdir: PathBuf = self.context.as_ref().unwrap().local.pwd();
- wrkdir.push(dst_path);
- dst_path = wrkdir;
- }
- // Check if file entry exists
- if let Some(entry) = self.local.files.get(self.local.index) {
- let full_path: PathBuf = match entry {
- FsEntry::Directory(dir) => dir.abs_path.clone(),
- FsEntry::File(file) => file.abs_path.clone(),
- };
- // Rename file or directory and report status as popup
- match self
- .context
- .as_mut()
- .unwrap()
- .local
- .rename(entry, dst_path.as_path())
- {
- Ok(_) => {
- // Reload files
- self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
- // Log
- self.log(
- LogLevel::Info,
- format!(
- "Renamed file \"{}\" to \"{}\"",
- full_path.display(),
- dst_path.display()
- )
- .as_ref(),
- );
- }
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!(
- "Could not rename file \"{}\": {}",
- full_path.display(),
- err
- )
- .as_ref(),
- );
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not rename file: {}", err),
- ))
- }
- }
- }
- }
- FileExplorerTab::Remote => {
- // Check if file entry exists
- if let Some(entry) = self.remote.files.get(self.remote.index) {
- let full_path: PathBuf = match entry {
- FsEntry::Directory(dir) => dir.abs_path.clone(),
- FsEntry::File(file) => file.abs_path.clone(),
- };
- // Rename file or directory and report status as popup
- let dst_path: PathBuf = PathBuf::from(input);
- match self.client.rename(entry, dst_path.as_path()) {
- Ok(_) => {
- // Reload files
- if let Ok(path) = self.client.pwd() {
- self.remote_scan(path.as_path());
- }
- // Log
- self.log(
- LogLevel::Info,
- format!(
- "Renamed file \"{}\" to \"{}\"",
- full_path.display(),
- dst_path.display()
- )
- .as_ref(),
- );
- }
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!(
- "Could not rename file \"{}\": {}",
- full_path.display(),
- err
- )
- .as_ref(),
- );
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not rename file: {}", err),
- ))
- }
- }
- }
- }
- }
- }
-
- /// ### callback_delete_fsentry
- ///
- /// Delete current selected fsentry in the currently selected TAB
- fn callback_delete_fsentry(&mut self) {
- // Match current selected tab
- match self.tab {
- FileExplorerTab::Local => {
- // Check if file entry exists
- if let Some(entry) = self.local.files.get(self.local.index) {
- let full_path: PathBuf = match entry {
- FsEntry::Directory(dir) => dir.abs_path.clone(),
- FsEntry::File(file) => file.abs_path.clone(),
- };
- // Delete file or directory and report status as popup
- match self.context.as_mut().unwrap().local.remove(entry) {
- Ok(_) => {
- // Reload files
- self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
- // Log
- self.log(
- LogLevel::Info,
- format!("Removed file \"{}\"", full_path.display()).as_ref(),
- );
- }
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!(
- "Could not delete file \"{}\": {}",
- full_path.display(),
- err
- )
- .as_ref(),
- );
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not delete file: {}", err),
- ))
- }
- }
- }
- }
- FileExplorerTab::Remote => {
- // Check if file entry exists
- if let Some(entry) = self.remote.files.get(self.remote.index) {
- let full_path: PathBuf = match entry {
- FsEntry::Directory(dir) => dir.abs_path.clone(),
- FsEntry::File(file) => file.abs_path.clone(),
- };
- // Delete file
- match self.client.remove(entry) {
- Ok(_) => {
- self.reload_remote_dir();
- self.log(
- LogLevel::Info,
- format!("Removed file \"{}\"", full_path.display()).as_ref(),
- );
- }
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!(
- "Could not delete file \"{}\": {}",
- full_path.display(),
- err
- )
- .as_ref(),
- );
- self.input_mode = InputMode::Popup(PopupType::Alert(
- Color::Red,
- format!("Could not delete file: {}", err),
- ))
- }
- }
- }
- }
- }
- }
-
- /// ### callback_save_as
- ///
- /// Call file upload, but save with input as name
- /// Handled both local and remote tab
- fn callback_save_as(&mut self, input: String) {
- match self.tab {
- FileExplorerTab::Local => {
- // Get pwd
- let wrkdir: PathBuf = match self.client.pwd() {
- Ok(p) => p,
- Err(err) => {
- self.log(
- LogLevel::Error,
- format!("Could not get current remote path: {}", err).as_ref(),
- );
- return;
- }
- };
- let files: Vec = self.local.files.clone();
- // Get file at index
- if let Some(entry) = files.get(self.local.index) {
- // Call send (upload)
- self.filetransfer_send(entry, wrkdir.as_path(), Some(input));
- }
- }
- FileExplorerTab::Remote => {
- let files: Vec = self.remote.files.clone();
- // Get file at index
- if let Some(entry) = files.get(self.remote.index) {
- // Call receive (download)
- self.filetransfer_recv(
- entry,
- self.context.as_ref().unwrap().local.pwd().as_path(),
- Some(input),
- );
- }
- }
- }
- }
-
- // @! Gfx
-
- /// ### draw
- ///
- /// Draw UI
- fn draw(&mut self) {
- let mut ctx: Context = self.context.take().unwrap();
- let _ = ctx.terminal.draw(|f| {
- // Prepare chunks
- let chunks = Layout::default()
- .direction(Direction::Vertical)
- .margin(2)
- .constraints(
- [
- Constraint::Length(5), // Header
- Constraint::Length(20), // Explorer
- Constraint::Length(16), // Log
- ]
- .as_ref(),
- )
- .split(f.size());
- // Create explorer chunks
- let tabs_chunks = Layout::default()
- .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
- .direction(Direction::Horizontal)
- .split(chunks[1]);
- // Draw header
- f.render_widget(self.draw_header(), chunks[0]);
- // Set localhost state
- let mut localhost_state: ListState = ListState::default();
- localhost_state.select(Some(self.local.index));
- // Set remote state
- let mut remote_state: ListState = ListState::default();
- remote_state.select(Some(self.remote.index));
- // Draw tabs
- f.render_stateful_widget(
- self.draw_local_explorer(),
- tabs_chunks[0],
- &mut localhost_state,
- );
- f.render_stateful_widget(
- self.draw_remote_explorer(),
- tabs_chunks[1],
- &mut remote_state,
- );
- // Set log state
- let mut log_state: ListState = ListState::default();
- log_state.select(Some(self.log_index));
- // Draw log
- f.render_stateful_widget(
- self.draw_log_list(chunks[2].width),
- chunks[2],
- &mut log_state,
- );
- // Draw popup
- if let InputMode::Popup(popup) = &self.input_mode {
- // Calculate popup size
- let (width, height): (u16, u16) = match popup {
- PopupType::Alert(_, _) => (30, 10),
- PopupType::Fatal(_) => (30, 10),
- PopupType::Help => (50, 70),
- PopupType::Input(_, _) => (30, 10),
- PopupType::Progress(_) => (40, 10),
- PopupType::Wait(_) => (50, 10),
- PopupType::YesNo(_, _, _) => (20, 10),
- };
- let popup_area: Rect = self.draw_popup_area(f.size(), width, height);
- f.render_widget(Clear, popup_area); //this clears out the background
- match popup {
- PopupType::Alert(color, txt) => f.render_widget(
- self.draw_popup_alert(color.clone(), txt.clone()),
- popup_area,
- ),
- PopupType::Fatal(txt) => {
- f.render_widget(self.draw_popup_fatal(txt.clone()), popup_area)
- }
- PopupType::Help => f.render_widget(self.draw_popup_help(), popup_area),
- PopupType::Input(txt, _) => {
- f.render_widget(self.draw_popup_input(txt.clone()), popup_area);
- // Set cursor
- f.set_cursor(
- popup_area.x + self.input_txt.width() as u16 + 1,
- popup_area.y + 1,
- )
- }
- PopupType::Progress(txt) => {
- f.render_widget(self.draw_popup_progress(txt.clone()), popup_area)
- }
- PopupType::Wait(txt) => {
- f.render_widget(self.draw_popup_wait(txt.clone()), popup_area)
- }
- PopupType::YesNo(txt, _, _) => {
- f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area)
- }
- }
- }
- });
- self.context = Some(ctx);
- }
-
- /// ### draw_header
- ///
- /// Draw header
- fn draw_header(&self) -> Paragraph {
- Paragraph::new(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
- .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
- }
-
- /// ### draw_local_explorer
- ///
- /// Draw local explorer list
- fn draw_local_explorer(&self) -> List {
- let files: Vec = self
- .local
- .files
- .iter()
- .map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
- .collect();
- List::new(files)
- .block(
- Block::default()
- .borders(Borders::ALL)
- .border_style(match self.input_field {
- InputField::Explorer => match self.tab {
- FileExplorerTab::Local => Style::default().fg(Color::Yellow),
- _ => Style::default(),
- },
- _ => Style::default(),
- })
- .title("Localhost"),
- )
- .start_corner(Corner::TopLeft)
- .highlight_style(
- Style::default()
- .fg(Color::Yellow)
- .add_modifier(Modifier::BOLD),
- )
- }
-
- /// ### draw_remote_explorer
- ///
- /// Draw remote explorer list
- fn draw_remote_explorer(&self) -> List {
- let files: Vec = self
- .remote
- .files
- .iter()
- .map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
- .collect();
- List::new(files)
- .block(
- Block::default()
- .borders(Borders::ALL)
- .border_style(match self.input_field {
- InputField::Explorer => match self.tab {
- FileExplorerTab::Remote => Style::default().fg(Color::LightBlue),
- _ => Style::default(),
- },
- _ => Style::default(),
- })
- .title(self.params.address.clone()),
- )
- .start_corner(Corner::TopLeft)
- .highlight_style(
- Style::default()
- .fg(Color::LightBlue)
- .add_modifier(Modifier::BOLD),
- )
- }
-
- /// ### draw_log_list
- ///
- /// Draw log list
- /// Chunk width must be provided to wrap text
- fn draw_log_list(&self, width: u16) -> List {
- let events: Vec = self
- .log_records
- .iter()
- .map(|record: &LogRecord| {
- let record_rows = textwrap::wrap(record.msg.as_str(), (width as usize) - 35); // -35 'cause log prefix
- let s = match record.level {
- LogLevel::Error => Style::default().fg(Color::Red),
- LogLevel::Warn => Style::default().fg(Color::Yellow),
- LogLevel::Info => Style::default().fg(Color::Green),
- };
- let mut rows: Vec = Vec::with_capacity(record_rows.len());
- // Iterate over remaining rows
- for (idx, row) in record_rows.iter().enumerate() {
- let row: Spans = match idx {
- 0 => Spans::from(vec![
- Span::from(format!("{}", record.time.format("%Y-%m-%dT%H:%M:%S%Z"))),
- Span::raw(" ["),
- Span::styled(
- format!(
- "{:5}",
- match record.level {
- LogLevel::Error => "ERROR",
- LogLevel::Warn => "WARN",
- LogLevel::Info => "INFO",
- }
- ),
- s,
- ),
- Span::raw("]: "),
- Span::from(String::from(row.as_ref())),
- ]),
- _ => Spans::from(vec![Span::from(textwrap::indent(
- row.as_ref(),
- " ",
- ))]),
- };
- rows.push(row);
- }
- ListItem::new(rows)
- })
- .collect();
- List::new(events)
- .block(
- Block::default()
- .borders(Borders::ALL)
- .border_style(match self.input_field {
- InputField::Logs => Style::default().fg(Color::LightGreen),
- _ => Style::default(),
- })
- .title("Log"),
- )
- .start_corner(Corner::BottomLeft)
- .highlight_style(Style::default().add_modifier(Modifier::BOLD))
- }
-
- /// ### draw_popup_area
- ///
- /// Draw popup area
- fn draw_popup_area(&self, area: Rect, width: u16, height: u16) -> Rect {
- let popup_layout = Layout::default()
- .direction(Direction::Vertical)
- .constraints(
- [
- Constraint::Percentage((100 - height) / 2),
- Constraint::Percentage(height),
- Constraint::Percentage((100 - height) / 2),
- ]
- .as_ref(),
- )
- .split(area);
- Layout::default()
- .direction(Direction::Horizontal)
- .constraints(
- [
- Constraint::Percentage((100 - width) / 2),
- Constraint::Percentage(width),
- Constraint::Percentage((100 - width) / 2),
- ]
- .as_ref(),
- )
- .split(popup_layout[1])[1]
- }
-
- /// ### draw_popup_alert
- ///
- /// Draw alert popup
- fn draw_popup_alert(&self, color: Color, text: String) -> Paragraph {
- Paragraph::new(text)
- .style(Style::default().fg(color))
- .block(Block::default().borders(Borders::ALL).title("Alert"))
- }
-
- /// ### draw_popup_fatal
- ///
- /// Draw fatal error popup
- fn draw_popup_fatal(&self, text: String) -> Paragraph {
- Paragraph::new(text)
- .style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
- .block(Block::default().borders(Borders::ALL).title("Fatal error"))
- }
- /// ### draw_popup_input
- ///
- /// Draw input popup
- fn draw_popup_input(&self, text: String) -> Paragraph {
- Paragraph::new(self.input_txt.as_ref())
- .style(Style::default().fg(Color::White))
- .block(Block::default().borders(Borders::ALL).title(text))
- }
-
- /// ### draw_popup_progress
- ///
- /// Draw progress popup
- fn draw_popup_progress(&self, text: String) -> Gauge {
- // Calculate ETA
- let eta: String = match self.transfer_progress as u64 {
- 0 => String::from("--:--"), // NOTE: would divide by 0 :D
- _ => {
- let elapsed_secs: u64 = self.transfer_started.elapsed().as_secs();
- let eta: u64 =
- ((elapsed_secs * 100) / (self.transfer_progress as u64)) - elapsed_secs;
- format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2)
- }
- };
- let label = format!("{:.2}% - ETA {}", self.transfer_progress, eta);
- Gauge::default()
- .block(Block::default().borders(Borders::ALL).title(text))
- .gauge_style(
- Style::default()
- .fg(Color::Green)
- .bg(Color::Black)
- .add_modifier(Modifier::BOLD),
- )
- .label(label)
- .ratio(self.transfer_progress / 100.0)
- }
-
- /// ### draw_popup_wait
- ///
- /// Draw wait popup
- fn draw_popup_wait(&self, text: String) -> Paragraph {
- Paragraph::new(text)
- .style(Style::default().add_modifier(Modifier::BOLD))
- .block(Block::default().borders(Borders::ALL).title("Please wait"))
- }
-
- /// ### draw_popup_yesno
- ///
- /// Draw yes/no select popup
- fn draw_popup_yesno(&self, text: String) -> Tabs {
- let choices: Vec = vec![Spans::from("Yes"), Spans::from("No")];
- let index: usize = match self.choice_opt {
- DialogYesNoOption::Yes => 0,
- DialogYesNoOption::No => 1,
- };
- Tabs::new(choices)
- .block(Block::default().borders(Borders::ALL).title(text))
- .select(index)
- .style(Style::default())
- .highlight_style(
- Style::default()
- .add_modifier(Modifier::BOLD)
- .fg(Color::Yellow),
- )
- }
-
- /// ### draw_footer
- ///
- /// Draw authentication page footer
- fn draw_popup_help(&self) -> List {
- // Write header
- let cmds: Vec = vec![
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("quit"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("change input field"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("change explorer tab"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("move up/down in list"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("scroll up/down in list quickly"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("enter directory"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("upload/download file"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("make directory"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("goto path"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("rename file"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("go to parent directory"),
- ])),
- ListItem::new(Spans::from(vec![
- Span::styled(
- "",
- Style::default()
- .bg(Color::Cyan)
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- ),
- Span::raw(" "),
- Span::raw("delete file"),
- ])),
- ];
- List::new(cmds)
- .block(
- Block::default()
- .borders(Borders::ALL)
- .border_style(Style::default())
- .title("Help"),
- )
- .start_corner(Corner::TopLeft)
- }
-}
-
-/**
- * Activity Trait
- * Keep it clean :)
- * Use methods instead!
- */
-
-impl Activity for FileTransferActivity {
- /// ### on_create
- ///
- /// `on_create` is the function which must be called to initialize the activity.
- /// `on_create` must initialize all the data structures used by the activity
- fn on_create(&mut self, context: Context) {
- // Set context
- self.context = Some(context);
- // Clear terminal
- let _ = self.context.as_mut().unwrap().terminal.clear();
- // Put raw mode on enabled
- let _ = enable_raw_mode();
- // Get files at current wd
- self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
- }
-
- /// ### on_draw
- ///
- /// `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) {
- let mut redraw: bool = false; // Should ui actually be redrawned?
- // Context must be something
- if self.context.is_none() {
- return;
- }
- let is_explorer_mode: bool = match self.input_mode {
- InputMode::Explorer => true,
- _ => false,
- };
- // Check if connected
- if !self.client.is_connected() && is_explorer_mode {
- // Set init state to connecting popup
- self.input_mode = InputMode::Popup(PopupType::Wait(format!(
- "Connecting to {}:{}...",
- self.params.address, self.params.port
- )));
- // Force ui draw
- self.draw();
- // Connect to remote
- self.connect();
- // Redraw
- redraw = true;
- }
- // Handle input events
- if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
- // Iterate over input events
- if let Some(event) = event {
- // Handle event
- self.handle_input_event(&event);
- // Set redraw to true
- redraw = true;
- }
- }
- // @! draw interface
- if redraw {
- self.draw();
- }
- }
-
- /// ### on_destroy
- ///
- /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
- /// This function must be called once before terminating the activity.
- fn on_destroy(&mut self) -> Option {
- // Disable raw mode
- let _ = disable_raw_mode();
- // Disconnect client
- if self.client.is_connected() {
- let _ = self.client.disconnect();
- }
- // Clear terminal and return
- match self.context.take() {
- Some(mut ctx) => {
- let _ = ctx.terminal.clear();
- Some(ctx)
- }
- None => None,
- }
- }
-}
diff --git a/src/ui/activities/filetransfer_activity/callbacks.rs b/src/ui/activities/filetransfer_activity/callbacks.rs
new file mode 100644
index 0000000..aacb5a2
--- /dev/null
+++ b/src/ui/activities/filetransfer_activity/callbacks.rs
@@ -0,0 +1,359 @@
+/*
+*
+* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
+*
+* This file is part of "TermSCP"
+*
+* TermSCP is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* TermSCP is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with TermSCP. If not, see .
+*
+*/
+
+use super::{FileExplorerTab, FileTransferActivity, FsEntry, InputMode, LogLevel, PopupType};
+
+use std::path::PathBuf;
+use tui::style::Color;
+
+impl FileTransferActivity {
+ /// ### callback_nothing_to_do
+ ///
+ /// Self titled
+ pub(super) fn callback_nothing_to_do(&mut self) {}
+
+ /// ### callback_change_directory
+ ///
+ /// Callback for GOTO command
+ pub(super) fn callback_change_directory(&mut self, input: String) {
+ let dir_path: PathBuf = PathBuf::from(input);
+ match self.tab {
+ FileExplorerTab::Local => {
+ // If path is relative, concat pwd
+ let abs_dir_path: PathBuf = match dir_path.is_relative() {
+ true => {
+ let mut d: PathBuf = self.context.as_ref().unwrap().local.pwd();
+ d.push(dir_path);
+ d
+ }
+ false => dir_path,
+ };
+ self.local_changedir(abs_dir_path.as_path(), true);
+ }
+ FileExplorerTab::Remote => {
+ // If path is relative, concat pwd
+ let abs_dir_path: PathBuf = match dir_path.is_relative() {
+ true => match self.client.pwd() {
+ Ok(mut wkrdir) => {
+ wkrdir.push(dir_path);
+ wkrdir
+ }
+ Err(err) => {
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not retrieve current directory: {}", err),
+ ));
+ return;
+ }
+ },
+ false => dir_path,
+ };
+ self.remote_changedir(abs_dir_path.as_path(), true);
+ }
+ }
+ }
+
+ /// ### callback_mkdir
+ ///
+ /// Callback for MKDIR command (supports both local and remote)
+ pub(super) fn callback_mkdir(&mut self, input: String) {
+ match self.tab {
+ FileExplorerTab::Local => {
+ match self
+ .context
+ .as_mut()
+ .unwrap()
+ .local
+ .mkdir(PathBuf::from(input.as_str()).as_path())
+ {
+ Ok(_) => {
+ // Reload files
+ self.log(
+ LogLevel::Info,
+ format!("Created directory \"{}\"", input).as_ref(),
+ );
+ let wrkdir: PathBuf = self.context.as_ref().unwrap().local.pwd();
+ self.local_scan(wrkdir.as_path());
+ }
+ Err(err) => {
+ // Report err
+ self.log(
+ LogLevel::Error,
+ format!("Could not create directory \"{}\": {}", input, err).as_ref(),
+ );
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not create directory \"{}\": {}", input, err),
+ ));
+ }
+ }
+ }
+ FileExplorerTab::Remote => {
+ match self.client.mkdir(PathBuf::from(input.as_str()).as_path()) {
+ Ok(_) => {
+ // Reload files
+ self.log(
+ LogLevel::Info,
+ format!("Created directory \"{}\"", input).as_ref(),
+ );
+ self.reload_remote_dir();
+ }
+ Err(err) => {
+ // Report err
+ self.log(
+ LogLevel::Error,
+ format!("Could not create directory \"{}\": {}", input, err).as_ref(),
+ );
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not create directory \"{}\": {}", input, err),
+ ));
+ }
+ }
+ }
+ }
+ }
+
+ /// ### callback_rename
+ ///
+ /// Callback for RENAME command (supports borth local and remote)
+ pub(super) fn callback_rename(&mut self, input: String) {
+ match self.tab {
+ FileExplorerTab::Local => {
+ let mut dst_path: PathBuf = PathBuf::from(input);
+ // Check if path is relative
+ if dst_path.as_path().is_relative() {
+ let mut wrkdir: PathBuf = self.context.as_ref().unwrap().local.pwd();
+ wrkdir.push(dst_path);
+ dst_path = wrkdir;
+ }
+ // Check if file entry exists
+ if let Some(entry) = self.local.files.get(self.local.index) {
+ let full_path: PathBuf = match entry {
+ FsEntry::Directory(dir) => dir.abs_path.clone(),
+ FsEntry::File(file) => file.abs_path.clone(),
+ };
+ // Rename file or directory and report status as popup
+ match self
+ .context
+ .as_mut()
+ .unwrap()
+ .local
+ .rename(entry, dst_path.as_path())
+ {
+ Ok(_) => {
+ // Reload files
+ self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
+ // Log
+ self.log(
+ LogLevel::Info,
+ format!(
+ "Renamed file \"{}\" to \"{}\"",
+ full_path.display(),
+ dst_path.display()
+ )
+ .as_ref(),
+ );
+ }
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!(
+ "Could not rename file \"{}\": {}",
+ full_path.display(),
+ err
+ )
+ .as_ref(),
+ );
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not rename file: {}", err),
+ ))
+ }
+ }
+ }
+ }
+ FileExplorerTab::Remote => {
+ // Check if file entry exists
+ if let Some(entry) = self.remote.files.get(self.remote.index) {
+ let full_path: PathBuf = match entry {
+ FsEntry::Directory(dir) => dir.abs_path.clone(),
+ FsEntry::File(file) => file.abs_path.clone(),
+ };
+ // Rename file or directory and report status as popup
+ let dst_path: PathBuf = PathBuf::from(input);
+ match self.client.rename(entry, dst_path.as_path()) {
+ Ok(_) => {
+ // Reload files
+ if let Ok(path) = self.client.pwd() {
+ self.remote_scan(path.as_path());
+ }
+ // Log
+ self.log(
+ LogLevel::Info,
+ format!(
+ "Renamed file \"{}\" to \"{}\"",
+ full_path.display(),
+ dst_path.display()
+ )
+ .as_ref(),
+ );
+ }
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!(
+ "Could not rename file \"{}\": {}",
+ full_path.display(),
+ err
+ )
+ .as_ref(),
+ );
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not rename file: {}", err),
+ ))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /// ### callback_delete_fsentry
+ ///
+ /// Delete current selected fsentry in the currently selected TAB
+ pub(super) fn callback_delete_fsentry(&mut self) {
+ // Match current selected tab
+ match self.tab {
+ FileExplorerTab::Local => {
+ // Check if file entry exists
+ if let Some(entry) = self.local.files.get(self.local.index) {
+ let full_path: PathBuf = match entry {
+ FsEntry::Directory(dir) => dir.abs_path.clone(),
+ FsEntry::File(file) => file.abs_path.clone(),
+ };
+ // Delete file or directory and report status as popup
+ match self.context.as_mut().unwrap().local.remove(entry) {
+ Ok(_) => {
+ // Reload files
+ self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
+ // Log
+ self.log(
+ LogLevel::Info,
+ format!("Removed file \"{}\"", full_path.display()).as_ref(),
+ );
+ }
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!(
+ "Could not delete file \"{}\": {}",
+ full_path.display(),
+ err
+ )
+ .as_ref(),
+ );
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not delete file: {}", err),
+ ))
+ }
+ }
+ }
+ }
+ FileExplorerTab::Remote => {
+ // Check if file entry exists
+ if let Some(entry) = self.remote.files.get(self.remote.index) {
+ let full_path: PathBuf = match entry {
+ FsEntry::Directory(dir) => dir.abs_path.clone(),
+ FsEntry::File(file) => file.abs_path.clone(),
+ };
+ // Delete file
+ match self.client.remove(entry) {
+ Ok(_) => {
+ self.reload_remote_dir();
+ self.log(
+ LogLevel::Info,
+ format!("Removed file \"{}\"", full_path.display()).as_ref(),
+ );
+ }
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!(
+ "Could not delete file \"{}\": {}",
+ full_path.display(),
+ err
+ )
+ .as_ref(),
+ );
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not delete file: {}", err),
+ ))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /// ### callback_save_as
+ ///
+ /// Call file upload, but save with input as name
+ /// Handled both local and remote tab
+ pub(super) fn callback_save_as(&mut self, input: String) {
+ match self.tab {
+ FileExplorerTab::Local => {
+ // Get pwd
+ let wrkdir: PathBuf = match self.client.pwd() {
+ Ok(p) => p,
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!("Could not get current remote path: {}", err).as_ref(),
+ );
+ return;
+ }
+ };
+ let files: Vec = self.local.files.clone();
+ // Get file at index
+ if let Some(entry) = files.get(self.local.index) {
+ // Call send (upload)
+ self.filetransfer_send(entry, wrkdir.as_path(), Some(input));
+ }
+ }
+ FileExplorerTab::Remote => {
+ let files: Vec = self.remote.files.clone();
+ // Get file at index
+ if let Some(entry) = files.get(self.remote.index) {
+ // Call receive (download)
+ self.filetransfer_recv(
+ entry,
+ self.context.as_ref().unwrap().local.pwd().as_path(),
+ Some(input),
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/src/ui/activities/filetransfer_activity/input.rs b/src/ui/activities/filetransfer_activity/input.rs
new file mode 100644
index 0000000..bbbde0a
--- /dev/null
+++ b/src/ui/activities/filetransfer_activity/input.rs
@@ -0,0 +1,682 @@
+/*
+*
+* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
+*
+* This file is part of "TermSCP"
+*
+* TermSCP is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* TermSCP is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with TermSCP. If not, see .
+*
+*/
+
+use super::{DialogCallback, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputEvent, InputField, InputMode, LogLevel, OnInputSubmitCallback, PopupType};
+
+use crossterm::event::{KeyCode, KeyModifiers};
+use std::path::PathBuf;
+use tui::style::Color;
+
+impl FileTransferActivity {
+ /// ### handle_input_event
+ ///
+ /// Handle input event based on current input mode
+ pub(super) fn handle_input_event(&mut self, ev: &InputEvent) {
+ // NOTE: this is necessary due to this
+ // NOTE: Do you want my opinion about that issue? It's a bs and doesn't make any sense.
+ let popup: Option = match &self.input_mode {
+ InputMode::Popup(ptype) => Some(ptype.clone()),
+ _ => None,
+ };
+ match &self.input_mode {
+ InputMode::Explorer => self.handle_input_event_mode_explorer(ev),
+ InputMode::Popup(_) => {
+ if let Some(popup) = popup {
+ self.handle_input_event_mode_popup(ev, popup);
+ }
+ }
+ }
+ }
+
+ /// ### handle_input_event_mode_explorer
+ ///
+ /// Input event handler for explorer mode
+ pub(super) fn handle_input_event_mode_explorer(&mut self, ev: &InputEvent) {
+ // Match input field
+ match self.input_field {
+ InputField::Explorer => match self.tab {
+ // Match current selected tab
+ FileExplorerTab::Local => self.handle_input_event_mode_explorer_tab_local(ev),
+ FileExplorerTab::Remote => self.handle_input_event_mode_explorer_tab_remote(ev),
+ },
+ InputField::Logs => self.handle_input_event_mode_explorer_log(ev),
+ }
+ }
+
+ /// ### handle_input_event_mode_explorer_tab_local
+ ///
+ /// Input event handler for explorer mode when localhost tab is selected
+ pub(super) fn handle_input_event_mode_explorer_tab_local(&mut self, ev: &InputEvent) {
+ // Match events
+ match ev {
+ InputEvent::Key(key) => {
+ match key.code {
+ KeyCode::Esc => {
+ // Handle quit event
+ // Create quit prompt dialog
+ self.input_mode = self.create_quit_popup();
+ }
+ KeyCode::Tab => self.switch_input_field(), // switch tab
+ KeyCode::Right => self.tab = FileExplorerTab::Remote, // switch to right tab
+ KeyCode::Up => {
+ // Move index up
+ if self.local.index > 0 {
+ self.local.index -= 1;
+ }
+ }
+ KeyCode::Down => {
+ // Move index down
+ if self.local.index + 1 < self.local.files.len() {
+ self.local.index += 1;
+ }
+ }
+ KeyCode::PageUp => {
+ // Move index up (fast)
+ if self.local.index > 8 {
+ self.local.index = self.local.index - 8; // Decrease by `8` if possible
+ } else {
+ self.local.index = 0; // Set to 0 otherwise
+ }
+ }
+ KeyCode::PageDown => {
+ // Move index down (fast)
+ if self.local.index + 8 >= self.local.files.len() {
+ // If overflows, set to size
+ self.local.index = self.local.files.len() - 1;
+ } else {
+ self.local.index = self.local.index + 8; // Increase by `8`
+ }
+ }
+ KeyCode::Enter => {
+ // Match selected file
+ let local_files: Vec = self.local.files.clone();
+ if let Some(entry) = local_files.get(self.local.index) {
+ // If directory, enter directory, otherwise check if symlink
+ match entry {
+ FsEntry::Directory(dir) => {
+ self.local_changedir(dir.abs_path.as_path(), true)
+ }
+ FsEntry::File(file) => {
+ // Check if symlink
+ if let Some(realpath) = &file.symlink {
+ // Stat realpath
+ match self
+ .context
+ .as_ref()
+ .unwrap()
+ .local
+ .stat(realpath.as_path())
+ {
+ Ok(real_file) => {
+ // If real file is a directory, enter directory
+ if let FsEntry::Directory(real_dir) = real_file {
+ self.local_changedir(
+ real_dir.abs_path.as_path(),
+ true,
+ )
+ }
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!(
+ "Failed to stat file \"{}\": {}",
+ realpath.display(),
+ err
+ )
+ .as_ref(),
+ ),
+ }
+ }
+ }
+ }
+ }
+ }
+ KeyCode::Backspace => {
+ // Go to previous directory
+ if let Some(d) = self.local.popd() {
+ self.local_changedir(d.as_path(), false);
+ }
+ }
+ KeyCode::Delete => {
+ // Get file at index
+ if let Some(entry) = self.local.files.get(self.local.index) {
+ // Get file name
+ let file_name: String = match entry {
+ FsEntry::Directory(dir) => dir.name.clone(),
+ FsEntry::File(file) => file.name.clone(),
+ };
+ // Show delete prompt
+ self.input_mode = InputMode::Popup(PopupType::YesNo(
+ format!("Delete file \"{}\"", file_name),
+ FileTransferActivity::callback_delete_fsentry,
+ FileTransferActivity::callback_nothing_to_do,
+ ))
+ }
+ }
+ KeyCode::Char(ch) => match ch {
+ 'g' | 'G' => {
+ // Goto
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ // Show input popup
+ self.input_mode = InputMode::Popup(PopupType::Input(
+ String::from("Change working directory"),
+ FileTransferActivity::callback_change_directory,
+ ));
+ }
+ }
+ 'd' | 'D' => {
+ // Make directory
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ self.input_mode = InputMode::Popup(PopupType::Input(
+ String::from("Insert directory name"),
+ FileTransferActivity::callback_mkdir,
+ ));
+ }
+ }
+ 'h' | 'H' => {
+ // Show help
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ self.input_mode = InputMode::Popup(PopupType::Help);
+ }
+ }
+ 'r' | 'R' => {
+ // Rename
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ self.input_mode = InputMode::Popup(PopupType::Input(
+ String::from("Insert new name"),
+ FileTransferActivity::callback_rename,
+ ));
+ }
+ }
+ 's' | 'S' => {
+ // Save as...
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ // Ask for input
+ self.input_mode = InputMode::Popup(PopupType::Input(
+ String::from("Save as..."),
+ FileTransferActivity::callback_save_as,
+ ));
+ }
+ }
+ 'u' | 'U' => {
+ // Go to parent directory
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ // Get pwd
+ let path: PathBuf = self.context.as_ref().unwrap().local.pwd();
+ if let Some(parent) = path.as_path().parent() {
+ self.local_changedir(parent, true);
+ }
+ }
+ }
+ ' ' => {
+ // Get pwd
+ let wrkdir: PathBuf = match self.client.pwd() {
+ Ok(p) => p,
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!("Could not get current remote path: {}", err)
+ .as_ref(),
+ );
+ return;
+ }
+ };
+ // Get files
+ let files: Vec = self.local.files.clone(); // Otherwise self is borrowed both as mutable and immutable...
+ // Get file at index
+ if let Some(entry) = files.get(self.local.index) {
+ // Call upload
+ self.filetransfer_send(entry, wrkdir.as_path(), None);
+ }
+ }
+ _ => { /* Nothing to do */ }
+ },
+ _ => { /* Nothing to do */ }
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_explorer_tab_local
+ ///
+ /// Input event handler for explorer mode when remote tab is selected
+ pub(super) fn handle_input_event_mode_explorer_tab_remote(&mut self, ev: &InputEvent) {
+ // Match events
+ match ev {
+ InputEvent::Key(key) => {
+ match key.code {
+ KeyCode::Esc => {
+ // Handle quit event
+ // Create quit prompt dialog
+ self.input_mode = self.create_quit_popup();
+ }
+ KeyCode::Tab => self.switch_input_field(), // switch tab
+ KeyCode::Left => self.tab = FileExplorerTab::Local, // switch to local tab
+ KeyCode::Up => {
+ // Move index up
+ if self.remote.index > 0 {
+ self.remote.index -= 1;
+ }
+ }
+ KeyCode::Down => {
+ // Move index down
+ if self.remote.index + 1 < self.remote.files.len() {
+ self.remote.index += 1;
+ }
+ }
+ KeyCode::PageUp => {
+ // Move index up (fast)
+ if self.remote.index > 8 {
+ self.remote.index = self.remote.index - 8; // Decrease by `8` if possible
+ } else {
+ self.remote.index = 0; // Set to 0 otherwise
+ }
+ }
+ KeyCode::PageDown => {
+ // Move index down (fast)
+ if self.remote.index + 8 >= self.remote.files.len() {
+ // If overflows, set to size
+ self.remote.index = self.remote.files.len() - 1;
+ } else {
+ self.remote.index = self.remote.index + 8; // Increase by `8`
+ }
+ }
+ KeyCode::Enter => {
+ // Match selected file
+ let files: Vec = self.remote.files.clone();
+ if let Some(entry) = files.get(self.remote.index) {
+ // If directory, enter directory; if file, check if is symlink
+ match entry {
+ FsEntry::Directory(dir) => {
+ self.remote_changedir(dir.abs_path.as_path(), true)
+ }
+ FsEntry::File(file) => {
+ // Check if symlink
+ if let Some(realpath) = &file.symlink {
+ // Stat realpath
+ match self.client.stat(realpath.as_path()) {
+ Ok(real_file) => {
+ // If real file is a directory, enter directory
+ if let FsEntry::Directory(real_dir) = real_file {
+ self.remote_changedir(
+ real_dir.abs_path.as_path(),
+ true,
+ )
+ }
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!(
+ "Failed to stat file \"{}\": {}",
+ realpath.display(),
+ err
+ )
+ .as_ref(),
+ ),
+ }
+ }
+ }
+ }
+ }
+ }
+ KeyCode::Backspace => {
+ // Go to previous directory
+ if let Some(d) = self.remote.popd() {
+ self.remote_changedir(d.as_path(), false);
+ }
+ }
+ KeyCode::Delete => {
+ // Get file at index
+ if let Some(entry) = self.remote.files.get(self.remote.index) {
+ // Get file name
+ let file_name: String = match entry {
+ FsEntry::Directory(dir) => dir.name.clone(),
+ FsEntry::File(file) => file.name.clone(),
+ };
+ // Show delete prompt
+ self.input_mode = InputMode::Popup(PopupType::YesNo(
+ format!("Delete file \"{}\"", file_name),
+ FileTransferActivity::callback_delete_fsentry,
+ FileTransferActivity::callback_nothing_to_do,
+ ))
+ }
+ }
+ KeyCode::Char(ch) => match ch {
+ 'g' | 'G' => {
+ // Goto
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ // Show input popup
+ self.input_mode = InputMode::Popup(PopupType::Input(
+ String::from("Change working directory"),
+ FileTransferActivity::callback_change_directory,
+ ));
+ }
+ }
+ 'd' | 'D' => {
+ // Make directory
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ self.input_mode = InputMode::Popup(PopupType::Input(
+ String::from("Insert directory name"),
+ FileTransferActivity::callback_mkdir,
+ ));
+ }
+ }
+ 'h' | 'H' => {
+ // Show help
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ self.input_mode = InputMode::Popup(PopupType::Help);
+ }
+ }
+ 'r' | 'R' => {
+ // Rename
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ self.input_mode = InputMode::Popup(PopupType::Input(
+ String::from("Insert new name"),
+ FileTransferActivity::callback_rename,
+ ));
+ }
+ }
+ 's' | 'S' => {
+ // Save as...
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ // Ask for input
+ self.input_mode = InputMode::Popup(PopupType::Input(
+ String::from("Save as..."),
+ FileTransferActivity::callback_save_as,
+ ));
+ }
+ }
+ 'u' | 'U' => {
+ // Go to parent directory
+ // If ctrl is enabled...
+ if key.modifiers.intersects(KeyModifiers::CONTROL) {
+ // Get pwd
+ match self.client.pwd() {
+ Ok(path) => {
+ if let Some(parent) = path.as_path().parent() {
+ self.remote_changedir(parent, true);
+ }
+ }
+ Err(err) => {
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not change working directory: {}", err),
+ ))
+ }
+ }
+ }
+ }
+ ' ' => {
+ // Get files
+ let files: Vec = self.remote.files.clone(); // Otherwise self is borrowed both as mutable and immutable...
+ // Get file at index
+ if let Some(entry) = files.get(self.remote.index) {
+ // Call upload
+ self.filetransfer_recv(
+ entry,
+ self.context.as_ref().unwrap().local.pwd().as_path(),
+ None,
+ );
+ }
+ }
+ _ => { /* Nothing to do */ }
+ },
+ _ => { /* Nothing to do */ }
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_explorer_log
+ ///
+ /// Input even handler for explorer mode when log tab is selected
+ pub(super) fn handle_input_event_mode_explorer_log(&mut self, ev: &InputEvent) {
+ // Match event
+ let records_block: usize = 16;
+ match ev {
+ InputEvent::Key(key) => {
+ match key.code {
+ KeyCode::Esc => {
+ // Handle quit event
+ // Create quit prompt dialog
+ self.input_mode = self.create_quit_popup();
+ }
+ KeyCode::Tab => self.switch_input_field(), // switch tab
+ KeyCode::Down => {
+ // NOTE: Twisted logic
+ // Decrease log index
+ if self.log_index > 0 {
+ self.log_index = self.log_index - 1;
+ }
+ }
+ KeyCode::Up => {
+ // NOTE: Twisted logic
+ // Increase log index
+ if self.log_index + 1 < self.log_records.len() {
+ self.log_index = self.log_index + 1;
+ }
+ }
+ KeyCode::PageDown => {
+ // NOTE: Twisted logic
+ // Fast decreasing of log index
+ if self.log_index >= records_block {
+ self.log_index = self.log_index - records_block; // Decrease by `records_block` if possible
+ } else {
+ self.log_index = 0; // Set to 0 otherwise
+ }
+ }
+ KeyCode::PageUp => {
+ // NOTE: Twisted logic
+ // Fast increasing of log index
+ if self.log_index + records_block >= self.log_records.len() {
+ // If overflows, set to size
+ self.log_index = self.log_records.len() - 1;
+ } else {
+ self.log_index = self.log_index + records_block; // Increase by `records_block`
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_explorer
+ ///
+ /// Input event handler for popup mode. Handler is then based on Popup type
+ pub(super) fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: PopupType) {
+ match popup {
+ PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
+ PopupType::Help => self.handle_input_event_mode_popup_help(ev),
+ PopupType::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev),
+ PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb),
+ PopupType::Progress(_) => self.handle_input_event_mode_popup_progress(ev),
+ PopupType::Wait(_) => self.handle_input_event_mode_popup_wait(ev),
+ PopupType::YesNo(_, yes_cb, no_cb) => {
+ self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb)
+ }
+ }
+ }
+
+ /// ### handle_input_event_mode_popup_alert
+ ///
+ /// Input event handler for popup alert
+ pub(super) fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
+ // If enter, close popup
+ match ev {
+ InputEvent::Key(key) => {
+ match key.code {
+ KeyCode::Enter => {
+ // Set input mode back to explorer
+ self.input_mode = InputMode::Explorer;
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_popup_help
+ ///
+ /// Input event handler for popup help
+ pub(super) fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
+ // If enter, close popup
+ match ev {
+ InputEvent::Key(key) => {
+ match key.code {
+ KeyCode::Enter | KeyCode::Esc => {
+ // Set input mode back to explorer
+ self.input_mode = InputMode::Explorer;
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_popup_fatal
+ ///
+ /// Input event handler for popup alert
+ pub(super) fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) {
+ // If enter, close popup
+ match ev {
+ InputEvent::Key(key) => {
+ match key.code {
+ KeyCode::Enter => {
+ // Set quit to true; since a fatal error happened
+ self.disconnect();
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_popup_input
+ ///
+ /// Input event handler for input popup
+ pub(super) fn handle_input_event_mode_popup_input(
+ &mut self,
+ ev: &InputEvent,
+ cb: OnInputSubmitCallback,
+ ) {
+ // If enter, close popup, otherwise push chars to input
+ match ev {
+ InputEvent::Key(key) => {
+ match key.code {
+ KeyCode::Esc => {
+ // Abort input
+ // Clear current input text
+ self.input_txt.clear();
+ // Set mode back to explorer
+ self.input_mode = InputMode::Explorer;
+ }
+ KeyCode::Enter => {
+ // Submit
+ let input_text: String = self.input_txt.clone();
+ // Clear current input text
+ self.input_txt.clear();
+ // Set mode back to explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
+ self.input_mode = InputMode::Explorer;
+ // Call cb
+ cb(self, input_text);
+ }
+ KeyCode::Char(ch) => self.input_txt.push(ch),
+ KeyCode::Backspace => {
+ let _ = self.input_txt.pop();
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_explorer_alert
+ ///
+ /// Input event handler for popup alert
+ pub(super) fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) {
+ // There's nothing you can do here I guess... maybe ctrl+c in the future idk
+ match ev {
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_explorer_alert
+ ///
+ /// Input event handler for popup alert
+ pub(super) fn handle_input_event_mode_popup_wait(&mut self, ev: &InputEvent) {
+ // There's nothing you can do here I guess... maybe ctrl+c in the future idk
+ match ev {
+ _ => { /* Nothing to do */ }
+ }
+ }
+
+ /// ### handle_input_event_mode_explorer_alert
+ ///
+ /// Input event handler for popup alert
+ pub(super) fn handle_input_event_mode_popup_yesno(
+ &mut self,
+ ev: &InputEvent,
+ yes_cb: DialogCallback,
+ no_cb: DialogCallback,
+ ) {
+ // If enter, close popup, otherwise move dialog option
+ match ev {
+ InputEvent::Key(key) => {
+ match key.code {
+ KeyCode::Enter => {
+ // @! Set input mode to Explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
+ self.input_mode = InputMode::Explorer;
+ // Check if user selected yes or not
+ match self.choice_opt {
+ DialogYesNoOption::No => no_cb(self),
+ DialogYesNoOption::Yes => yes_cb(self),
+ }
+ // Reset choice option to yes
+ self.choice_opt = DialogYesNoOption::Yes;
+ }
+ KeyCode::Right => self.choice_opt = DialogYesNoOption::No, // Set to NO
+ KeyCode::Left => self.choice_opt = DialogYesNoOption::Yes, // Set to YES
+ _ => { /* Nothing to do */ }
+ }
+ }
+ _ => { /* Nothing to do */ }
+ }
+ }
+}
diff --git a/src/ui/activities/filetransfer_activity/layout.rs b/src/ui/activities/filetransfer_activity/layout.rs
new file mode 100644
index 0000000..856ab23
--- /dev/null
+++ b/src/ui/activities/filetransfer_activity/layout.rs
@@ -0,0 +1,521 @@
+/*
+*
+* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
+*
+* This file is part of "TermSCP"
+*
+* TermSCP is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* TermSCP is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with TermSCP. If not, see .
+*
+*/
+
+use super::{
+ Context, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputField,
+ InputMode, LogLevel, LogRecord, PopupType,
+};
+use tui::{
+ layout::{Constraint, Corner, Direction, Layout, Rect},
+ style::{Color, Modifier, Style},
+ text::{Span, Spans},
+ widgets::{Block, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, Tabs},
+};
+use unicode_width::UnicodeWidthStr;
+
+impl FileTransferActivity {
+ /// ### draw
+ ///
+ /// Draw UI
+ pub(super) fn draw(&mut self) {
+ let mut ctx: Context = self.context.take().unwrap();
+ let _ = ctx.terminal.draw(|f| {
+ // Prepare chunks
+ let chunks = Layout::default()
+ .direction(Direction::Vertical)
+ .margin(2)
+ .constraints(
+ [
+ Constraint::Length(5), // Header
+ Constraint::Length(20), // Explorer
+ Constraint::Length(16), // Log
+ ]
+ .as_ref(),
+ )
+ .split(f.size());
+ // Create explorer chunks
+ let tabs_chunks = Layout::default()
+ .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
+ .direction(Direction::Horizontal)
+ .split(chunks[1]);
+ // Draw header
+ f.render_widget(self.draw_header(), chunks[0]);
+ // Set localhost state
+ let mut localhost_state: ListState = ListState::default();
+ localhost_state.select(Some(self.local.index));
+ // Set remote state
+ let mut remote_state: ListState = ListState::default();
+ remote_state.select(Some(self.remote.index));
+ // Draw tabs
+ f.render_stateful_widget(
+ self.draw_local_explorer(),
+ tabs_chunks[0],
+ &mut localhost_state,
+ );
+ f.render_stateful_widget(
+ self.draw_remote_explorer(),
+ tabs_chunks[1],
+ &mut remote_state,
+ );
+ // Set log state
+ let mut log_state: ListState = ListState::default();
+ log_state.select(Some(self.log_index));
+ // Draw log
+ f.render_stateful_widget(
+ self.draw_log_list(chunks[2].width),
+ chunks[2],
+ &mut log_state,
+ );
+ // Draw popup
+ if let InputMode::Popup(popup) = &self.input_mode {
+ // Calculate popup size
+ let (width, height): (u16, u16) = match popup {
+ PopupType::Alert(_, _) => (30, 10),
+ PopupType::Fatal(_) => (30, 10),
+ PopupType::Help => (50, 70),
+ PopupType::Input(_, _) => (30, 10),
+ PopupType::Progress(_) => (40, 10),
+ PopupType::Wait(_) => (50, 10),
+ PopupType::YesNo(_, _, _) => (20, 10),
+ };
+ let popup_area: Rect = self.draw_popup_area(f.size(), width, height);
+ f.render_widget(Clear, popup_area); //this clears out the background
+ match popup {
+ PopupType::Alert(color, txt) => f.render_widget(
+ self.draw_popup_alert(color.clone(), txt.clone()),
+ popup_area,
+ ),
+ PopupType::Fatal(txt) => {
+ f.render_widget(self.draw_popup_fatal(txt.clone()), popup_area)
+ }
+ PopupType::Help => f.render_widget(self.draw_popup_help(), popup_area),
+ PopupType::Input(txt, _) => {
+ f.render_widget(self.draw_popup_input(txt.clone()), popup_area);
+ // Set cursor
+ f.set_cursor(
+ popup_area.x + self.input_txt.width() as u16 + 1,
+ popup_area.y + 1,
+ )
+ }
+ PopupType::Progress(txt) => {
+ f.render_widget(self.draw_popup_progress(txt.clone()), popup_area)
+ }
+ PopupType::Wait(txt) => {
+ f.render_widget(self.draw_popup_wait(txt.clone()), popup_area)
+ }
+ PopupType::YesNo(txt, _, _) => {
+ f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area)
+ }
+ }
+ }
+ });
+ self.context = Some(ctx);
+ }
+
+ /// ### draw_header
+ ///
+ /// Draw header
+ pub(super) fn draw_header(&self) -> Paragraph {
+ Paragraph::new(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
+ .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
+ }
+
+ /// ### draw_local_explorer
+ ///
+ /// Draw local explorer list
+ pub(super) fn draw_local_explorer(&self) -> List {
+ let files: Vec = self
+ .local
+ .files
+ .iter()
+ .map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
+ .collect();
+ List::new(files)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(match self.input_field {
+ InputField::Explorer => match self.tab {
+ FileExplorerTab::Local => Style::default().fg(Color::Yellow),
+ _ => Style::default(),
+ },
+ _ => Style::default(),
+ })
+ .title("Localhost"),
+ )
+ .start_corner(Corner::TopLeft)
+ .highlight_style(
+ Style::default()
+ .fg(Color::Yellow)
+ .add_modifier(Modifier::BOLD),
+ )
+ }
+
+ /// ### draw_remote_explorer
+ ///
+ /// Draw remote explorer list
+ pub(super) fn draw_remote_explorer(&self) -> List {
+ let files: Vec = self
+ .remote
+ .files
+ .iter()
+ .map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
+ .collect();
+ List::new(files)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(match self.input_field {
+ InputField::Explorer => match self.tab {
+ FileExplorerTab::Remote => Style::default().fg(Color::LightBlue),
+ _ => Style::default(),
+ },
+ _ => Style::default(),
+ })
+ .title(self.params.address.clone()),
+ )
+ .start_corner(Corner::TopLeft)
+ .highlight_style(
+ Style::default()
+ .fg(Color::LightBlue)
+ .add_modifier(Modifier::BOLD),
+ )
+ }
+
+ /// ### draw_log_list
+ ///
+ /// Draw log list
+ /// Chunk width must be provided to wrap text
+ pub(super) fn draw_log_list(&self, width: u16) -> List {
+ let events: Vec = self
+ .log_records
+ .iter()
+ .map(|record: &LogRecord| {
+ let record_rows = textwrap::wrap(record.msg.as_str(), (width as usize) - 35); // -35 'cause log prefix
+ let s = match record.level {
+ LogLevel::Error => Style::default().fg(Color::Red),
+ LogLevel::Warn => Style::default().fg(Color::Yellow),
+ LogLevel::Info => Style::default().fg(Color::Green),
+ };
+ let mut rows: Vec = Vec::with_capacity(record_rows.len());
+ // Iterate over remaining rows
+ for (idx, row) in record_rows.iter().enumerate() {
+ let row: Spans = match idx {
+ 0 => Spans::from(vec![
+ Span::from(format!("{}", record.time.format("%Y-%m-%dT%H:%M:%S%Z"))),
+ Span::raw(" ["),
+ Span::styled(
+ format!(
+ "{:5}",
+ match record.level {
+ LogLevel::Error => "ERROR",
+ LogLevel::Warn => "WARN",
+ LogLevel::Info => "INFO",
+ }
+ ),
+ s,
+ ),
+ Span::raw("]: "),
+ Span::from(String::from(row.as_ref())),
+ ]),
+ _ => Spans::from(vec![Span::from(textwrap::indent(
+ row.as_ref(),
+ " ",
+ ))]),
+ };
+ rows.push(row);
+ }
+ ListItem::new(rows)
+ })
+ .collect();
+ List::new(events)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(match self.input_field {
+ InputField::Logs => Style::default().fg(Color::LightGreen),
+ _ => Style::default(),
+ })
+ .title("Log"),
+ )
+ .start_corner(Corner::BottomLeft)
+ .highlight_style(Style::default().add_modifier(Modifier::BOLD))
+ }
+
+ /// ### draw_popup_area
+ ///
+ /// Draw popup area
+ pub(super) fn draw_popup_area(&self, area: Rect, width: u16, height: u16) -> Rect {
+ let popup_layout = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints(
+ [
+ Constraint::Percentage((100 - height) / 2),
+ Constraint::Percentage(height),
+ Constraint::Percentage((100 - height) / 2),
+ ]
+ .as_ref(),
+ )
+ .split(area);
+ Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints(
+ [
+ Constraint::Percentage((100 - width) / 2),
+ Constraint::Percentage(width),
+ Constraint::Percentage((100 - width) / 2),
+ ]
+ .as_ref(),
+ )
+ .split(popup_layout[1])[1]
+ }
+
+ /// ### draw_popup_alert
+ ///
+ /// Draw alert popup
+ pub(super) fn draw_popup_alert(&self, color: Color, text: String) -> Paragraph {
+ Paragraph::new(text)
+ .style(Style::default().fg(color))
+ .block(Block::default().borders(Borders::ALL).title("Alert"))
+ }
+
+ /// ### draw_popup_fatal
+ ///
+ /// Draw fatal error popup
+ pub(super) fn draw_popup_fatal(&self, text: String) -> Paragraph {
+ Paragraph::new(text)
+ .style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
+ .block(Block::default().borders(Borders::ALL).title("Fatal error"))
+ }
+ /// ### draw_popup_input
+ ///
+ /// Draw input popup
+ pub(super) fn draw_popup_input(&self, text: String) -> Paragraph {
+ Paragraph::new(self.input_txt.as_ref())
+ .style(Style::default().fg(Color::White))
+ .block(Block::default().borders(Borders::ALL).title(text))
+ }
+
+ /// ### draw_popup_progress
+ ///
+ /// Draw progress popup
+ pub(super) fn draw_popup_progress(&self, text: String) -> Gauge {
+ // Calculate ETA
+ let eta: String = match self.transfer_progress as u64 {
+ 0 => String::from("--:--"), // NOTE: would divide by 0 :D
+ _ => {
+ let elapsed_secs: u64 = self.transfer_started.elapsed().as_secs();
+ let eta: u64 =
+ ((elapsed_secs * 100) / (self.transfer_progress as u64)) - elapsed_secs;
+ format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2)
+ }
+ };
+ let label = format!("{:.2}% - ETA {}", self.transfer_progress, eta);
+ Gauge::default()
+ .block(Block::default().borders(Borders::ALL).title(text))
+ .gauge_style(
+ Style::default()
+ .fg(Color::Green)
+ .bg(Color::Black)
+ .add_modifier(Modifier::BOLD),
+ )
+ .label(label)
+ .ratio(self.transfer_progress / 100.0)
+ }
+
+ /// ### draw_popup_wait
+ ///
+ /// Draw wait popup
+ pub(super) fn draw_popup_wait(&self, text: String) -> Paragraph {
+ Paragraph::new(text)
+ .style(Style::default().add_modifier(Modifier::BOLD))
+ .block(Block::default().borders(Borders::ALL).title("Please wait"))
+ }
+
+ /// ### draw_popup_yesno
+ ///
+ /// Draw yes/no select popup
+ pub(super) fn draw_popup_yesno(&self, text: String) -> Tabs {
+ let choices: Vec = vec![Spans::from("Yes"), Spans::from("No")];
+ let index: usize = match self.choice_opt {
+ DialogYesNoOption::Yes => 0,
+ DialogYesNoOption::No => 1,
+ };
+ Tabs::new(choices)
+ .block(Block::default().borders(Borders::ALL).title(text))
+ .select(index)
+ .style(Style::default())
+ .highlight_style(
+ Style::default()
+ .add_modifier(Modifier::BOLD)
+ .fg(Color::Yellow),
+ )
+ }
+
+ /// ### draw_footer
+ ///
+ /// Draw authentication page footer
+ pub(super) fn draw_popup_help(&self) -> List {
+ // Write header
+ let cmds: Vec = vec![
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("quit"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("change input field"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("change explorer tab"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("move up/down in list"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("scroll up/down in list quickly"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("enter directory"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("upload/download file"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("make directory"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("goto path"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("rename file"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("go to parent directory"),
+ ])),
+ ListItem::new(Spans::from(vec![
+ Span::styled(
+ "",
+ Style::default()
+ .bg(Color::Cyan)
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(" "),
+ Span::raw("delete file"),
+ ])),
+ ];
+ List::new(cmds)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(Style::default())
+ .title("Help"),
+ )
+ .start_corner(Corner::TopLeft)
+ }
+}
diff --git a/src/ui/activities/filetransfer_activity/misc.rs b/src/ui/activities/filetransfer_activity/misc.rs
new file mode 100644
index 0000000..1a777ff
--- /dev/null
+++ b/src/ui/activities/filetransfer_activity/misc.rs
@@ -0,0 +1,68 @@
+/*
+*
+* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
+*
+* This file is part of "TermSCP"
+*
+* TermSCP is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* TermSCP is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with TermSCP. If not, see .
+*
+*/
+
+use super::{FileTransferActivity, InputField, InputMode, LogLevel, LogRecord, PopupType};
+
+impl FileTransferActivity {
+ /// ### log
+ ///
+ /// Add message to log events
+ pub(super) fn log(&mut self, level: LogLevel, msg: &str) {
+ // Create log record
+ let record: LogRecord = LogRecord::new(level, msg);
+ //Check if history overflows the size
+ if self.log_records.len() + 1 > self.log_size {
+ self.log_records.pop_back(); // Start cleaning events from back
+ }
+ // Eventually push front the new record
+ self.log_records.push_front(record);
+ // Set log index
+ self.log_index = 0;
+ }
+
+ /// ### create_quit_popup
+ ///
+ /// Create quit popup input mode (since must be shared between different input handlers)
+ pub(super) fn create_quit_popup(&mut self) -> InputMode {
+ InputMode::Popup(PopupType::YesNo(
+ String::from("Are you sure you want to quit?"),
+ FileTransferActivity::disconnect,
+ FileTransferActivity::callback_nothing_to_do,
+ ))
+ }
+
+ /// ### switch_input_field
+ ///
+ /// Switch input field based on current input field
+ pub(super) fn switch_input_field(&mut self) {
+ self.input_field = match self.input_field {
+ InputField::Explorer => InputField::Logs,
+ InputField::Logs => InputField::Explorer,
+ }
+ }
+
+ /// ### set_progress
+ ///
+ /// Calculate progress percentage based on current progress
+ pub(super) fn set_progress(&mut self, it: usize, sz: usize) {
+ self.transfer_progress = ((it as f64) * 100.0) / (sz as f64);
+ }
+}
diff --git a/src/ui/activities/filetransfer_activity/mod.rs b/src/ui/activities/filetransfer_activity/mod.rs
new file mode 100644
index 0000000..8d2e075
--- /dev/null
+++ b/src/ui/activities/filetransfer_activity/mod.rs
@@ -0,0 +1,347 @@
+//! ## FileTransferActivity
+//!
+//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
+
+/*
+*
+* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
+*
+* This file is part of "TermSCP"
+*
+* TermSCP is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* TermSCP is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with TermSCP. If not, see .
+*
+*/
+
+// This module is split into files, cause it's just too big
+mod callbacks;
+mod input;
+mod layout;
+mod misc;
+mod session;
+
+// Dependencies
+extern crate chrono;
+extern crate crossterm;
+extern crate textwrap;
+extern crate tui;
+extern crate unicode_width;
+
+// locals
+use super::{Activity, Context};
+use crate::filetransfer::FileTransferProtocol;
+
+// File transfer
+use crate::filetransfer::sftp_transfer::SftpFileTransfer;
+use crate::filetransfer::FileTransfer;
+use crate::fs::FsEntry;
+
+// Includes
+use chrono::{DateTime, Local};
+use crossterm::event::Event as InputEvent;
+use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
+use std::collections::VecDeque;
+use std::path::{Path, PathBuf};
+use std::time::Instant;
+use tui::style::Color;
+
+// Types
+type DialogCallback = fn(&mut FileTransferActivity);
+type OnInputSubmitCallback = fn(&mut FileTransferActivity, String);
+
+/// ### FileTransferParams
+///
+/// Holds connection parameters for file transfers
+pub struct FileTransferParams {
+ pub address: String,
+ pub port: u16,
+ pub protocol: FileTransferProtocol,
+ pub username: Option,
+ pub password: Option,
+}
+
+/// ### InputField
+///
+/// Input field selected
+#[derive(std::cmp::PartialEq)]
+enum InputField {
+ Explorer,
+ Logs,
+}
+
+/// ### DialogYesNoOption
+///
+/// Current yes/no dialog option
+#[derive(std::cmp::PartialEq, Clone)]
+enum DialogYesNoOption {
+ Yes,
+ No,
+}
+
+/// ## PopupType
+///
+/// PopupType describes the type of popup
+#[derive(Clone)]
+enum PopupType {
+ Alert(Color, String), // Block color; Block text
+ Fatal(String), // Must quit after being hidden
+ Help, // Show Help
+ Input(String, OnInputSubmitCallback), // Input description; Callback for submit
+ Progress(String), // Progress block text
+ Wait(String), // Wait block text
+ YesNo(String, DialogCallback, DialogCallback), // Yes, no callback
+}
+
+/// ## InputMode
+///
+/// InputMode describes the current input mode
+/// Each input mode handle the input events in a different way
+#[derive(Clone)]
+enum InputMode {
+ Explorer,
+ Popup(PopupType),
+}
+
+/// ## FileExplorer
+///
+/// File explorer states
+struct FileExplorer {
+ pub index: usize,
+ pub files: Vec,
+ dirstack: VecDeque,
+}
+
+impl FileExplorer {
+ /// ### new
+ ///
+ /// Instantiates a new FileExplorer
+ pub fn new() -> FileExplorer {
+ FileExplorer {
+ index: 0,
+ files: Vec::new(),
+ dirstack: VecDeque::with_capacity(16),
+ }
+ }
+
+ /// ### pushd
+ ///
+ /// push directory to stack
+ pub fn pushd(&mut self, dir: &Path) {
+ // Check if stack overflows the size
+ if self.dirstack.len() + 1 > 16 {
+ self.dirstack.pop_back(); // Start cleaning events from back
+ }
+ // Eventually push front the new record
+ self.dirstack.push_front(PathBuf::from(dir));
+ }
+
+ /// ### popd
+ ///
+ /// Pop directory from the stack and return the directory
+ pub fn popd(&mut self) -> Option {
+ self.dirstack.pop_front()
+ }
+
+ /// ### sort_files_by_name
+ ///
+ /// Sort explorer files by their name
+ pub fn sort_files_by_name(&mut self) {
+ self.files.sort_by_key(|x: &FsEntry| match x {
+ FsEntry::Directory(dir) => dir.name.clone(),
+ FsEntry::File(file) => file.name.clone(),
+ });
+ }
+}
+
+/// ## FileExplorerTab
+///
+/// File explorer tab
+enum FileExplorerTab {
+ Local,
+ Remote,
+}
+
+/// ## LogLevel
+///
+/// Log level type
+#[allow(dead_code)]
+enum LogLevel {
+ Error,
+ Warn,
+ Info,
+}
+
+/// ## LogRecord
+///
+/// Log record entry
+struct LogRecord {
+ pub time: DateTime,
+ pub level: LogLevel,
+ pub msg: String,
+}
+
+impl LogRecord {
+ /// ### new
+ ///
+ /// Instantiates a new LogRecord
+ pub fn new(level: LogLevel, msg: &str) -> LogRecord {
+ LogRecord {
+ time: Local::now(),
+ level: level,
+ msg: String::from(msg),
+ }
+ }
+}
+
+/// ## FileTransferActivity
+///
+/// FileTransferActivity is the data holder for the file transfer activity
+pub struct FileTransferActivity {
+ pub disconnected: bool, // Has disconnected from remote?
+ pub quit: bool, // Has quit term scp?
+ context: Option, // Context holder
+ params: FileTransferParams, // FT connection params
+ client: Box, // File transfer client
+ local: FileExplorer, // Local File explorer state
+ remote: FileExplorer, // Remote File explorer state
+ tab: FileExplorerTab, // Current selected tab
+ log_index: usize, // Current log index entry selected
+ log_records: VecDeque, // Log records
+ log_size: usize, // Log records size (max)
+ input_mode: InputMode, // Current input mode
+ input_field: InputField, // Current selected input mode
+ input_txt: String, // Input text
+ choice_opt: DialogYesNoOption, // Dialog popup selected option
+ transfer_progress: f64, // Current write/read progress (percentage)
+ transfer_started: Instant, // Instant when progress has started
+}
+
+impl FileTransferActivity {
+ /// ### new
+ ///
+ /// Instantiates a new FileTransferActivity
+ pub fn new(params: FileTransferParams) -> FileTransferActivity {
+ let protocol: FileTransferProtocol = params.protocol.clone();
+ FileTransferActivity {
+ disconnected: false,
+ quit: false,
+ context: None,
+ params: params,
+ client: match protocol {
+ FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new()),
+ FileTransferProtocol::Ftp => panic!("FTP is not supported YET!"), // FIXME: FTP
+ },
+ local: FileExplorer::new(),
+ remote: FileExplorer::new(),
+ tab: FileExplorerTab::Local,
+ log_index: 0,
+ log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
+ log_size: 256, // Must match with capacity
+ input_mode: InputMode::Explorer,
+ input_field: InputField::Explorer,
+ input_txt: String::new(),
+ choice_opt: DialogYesNoOption::Yes,
+ transfer_progress: 0.0,
+ transfer_started: Instant::now(),
+ }
+ }
+
+}
+
+/**
+ * Activity Trait
+ * Keep it clean :)
+ * Use methods instead!
+ */
+
+impl Activity for FileTransferActivity {
+ /// ### on_create
+ ///
+ /// `on_create` is the function which must be called to initialize the activity.
+ /// `on_create` must initialize all the data structures used by the activity
+ fn on_create(&mut self, context: Context) {
+ // Set context
+ self.context = Some(context);
+ // Clear terminal
+ let _ = self.context.as_mut().unwrap().terminal.clear();
+ // Put raw mode on enabled
+ let _ = enable_raw_mode();
+ // Get files at current wd
+ self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
+ }
+
+ /// ### on_draw
+ ///
+ /// `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) {
+ let mut redraw: bool = false; // Should ui actually be redrawned?
+ // Context must be something
+ if self.context.is_none() {
+ return;
+ }
+ let is_explorer_mode: bool = match self.input_mode {
+ InputMode::Explorer => true,
+ _ => false,
+ };
+ // Check if connected
+ if !self.client.is_connected() && is_explorer_mode {
+ // Set init state to connecting popup
+ self.input_mode = InputMode::Popup(PopupType::Wait(format!(
+ "Connecting to {}:{}...",
+ self.params.address, self.params.port
+ )));
+ // Force ui draw
+ self.draw();
+ // Connect to remote
+ self.connect();
+ // Redraw
+ redraw = true;
+ }
+ // Handle input events
+ if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
+ // Iterate over input events
+ if let Some(event) = event {
+ // Handle event
+ self.handle_input_event(&event);
+ // Set redraw to true
+ redraw = true;
+ }
+ }
+ // @! draw interface
+ if redraw {
+ self.draw();
+ }
+ }
+
+ /// ### on_destroy
+ ///
+ /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
+ /// This function must be called once before terminating the activity.
+ fn on_destroy(&mut self) -> Option {
+ // Disable raw mode
+ let _ = disable_raw_mode();
+ // Disconnect client
+ if self.client.is_connected() {
+ let _ = self.client.disconnect();
+ }
+ // Clear terminal and return
+ match self.context.take() {
+ Some(mut ctx) => {
+ let _ = ctx.terminal.clear();
+ Some(ctx)
+ }
+ None => None,
+ }
+ }
+}
diff --git a/src/ui/activities/filetransfer_activity/session.rs b/src/ui/activities/filetransfer_activity/session.rs
new file mode 100644
index 0000000..d1f6d73
--- /dev/null
+++ b/src/ui/activities/filetransfer_activity/session.rs
@@ -0,0 +1,560 @@
+/*
+*
+* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
+*
+* This file is part of "TermSCP"
+*
+* TermSCP is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* TermSCP is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with TermSCP. If not, see .
+*
+*/
+
+use super::{FileTransferActivity, FsEntry, InputMode, LogLevel, PopupType};
+
+use std::io::{Read, Seek, Write};
+use std::path::{Path, PathBuf};
+use std::time::Instant;
+use tui::style::Color;
+
+impl FileTransferActivity {
+ /// ### connect
+ ///
+ /// Connect to remote
+ pub(super) fn connect(&mut self) {
+ // Connect to remote
+ match self.client.connect(
+ self.params.address.clone(),
+ self.params.port,
+ self.params.username.clone(),
+ self.params.password.clone(),
+ ) {
+ Ok(_) => {
+ // Set state to explorer
+ self.input_mode = InputMode::Explorer;
+ self.reload_remote_dir();
+ }
+ Err(err) => {
+ // Set popup fatal error
+ self.input_mode = InputMode::Popup(PopupType::Fatal(format!("{}", err)));
+ }
+ }
+ }
+
+ /// ### disconnect
+ ///
+ /// disconnect from remote
+ pub(super) fn disconnect(&mut self) {
+ // Show popup disconnecting
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ String::from("Disconnecting from remote..."),
+ ));
+ // Disconnect
+ let _ = self.client.disconnect();
+ // Quit
+ self.disconnected = true;
+ }
+
+ /// ### reload_remote_dir
+ ///
+ /// Reload remote directory entries
+ pub(super) fn reload_remote_dir(&mut self) {
+ // Get current entries
+ if let Ok(pwd) = self.client.pwd() {
+ self.remote_scan(pwd.as_path());
+ }
+ }
+
+ /// ### filetransfer_send
+ ///
+ /// Send fs entry to remote.
+ /// If dst_name is Some, entry will be saved with a different name.
+ /// If entry is a directory, this applies to directory only
+ pub(super) fn filetransfer_send(
+ &mut self,
+ entry: &FsEntry,
+ curr_remote_path: &Path,
+ dst_name: Option,
+ ) {
+ // Write popup
+ let file_name: String = match entry {
+ FsEntry::Directory(dir) => dir.name.clone(),
+ FsEntry::File(file) => file.name.clone(),
+ };
+ self.input_mode = InputMode::Popup(PopupType::Wait(format!("Uploading \"{}\"", file_name)));
+ // Draw
+ self.draw();
+ // Get remote path
+ let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
+ let remote_file_name: PathBuf = match dst_name {
+ Some(s) => PathBuf::from(s.as_str()),
+ None => PathBuf::from(file_name.as_str()),
+ };
+ remote_path.push(remote_file_name);
+ // Match entry
+ match entry {
+ FsEntry::File(file) => {
+ // Upload file
+ // Try to open local file
+ match self
+ .context
+ .as_ref()
+ .unwrap()
+ .local
+ .open_file_read(file.abs_path.as_path())
+ {
+ Ok(mut fhnd) => match self.client.send_file(remote_path.as_path()) {
+ Ok(mut rhnd) => {
+ // Write file
+ let file_size: usize =
+ fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
+ // rewind
+ if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) {
+ self.log(
+ LogLevel::Error,
+ format!("Could not rewind local file: {}", err).as_ref(),
+ );
+ }
+ // Write remote file
+ let mut total_bytes_written: usize = 0;
+ // Set input state to popup progress
+ self.input_mode = InputMode::Popup(PopupType::Progress(format!(
+ "Uploading \"{}\"",
+ file_name
+ )));
+ // Set started time
+ self.transfer_started = Instant::now();
+ let mut last_progress_val: f64 = 0.0;
+ loop {
+ // Read till you can
+ let mut buffer: [u8; 8192] = [0; 8192];
+ match fhnd.read(&mut buffer) {
+ Ok(bytes_read) => {
+ total_bytes_written += bytes_read;
+ if bytes_read == 0 {
+ break;
+ } else {
+ // Write bytes
+ if let Err(err) = rhnd.write(&buffer[0..bytes_read]) {
+ self.log(
+ LogLevel::Error,
+ format!("Could not write remote file: {}", err)
+ .as_ref(),
+ );
+ }
+ }
+ }
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!("Could not read local file: {}", err).as_ref(),
+ );
+ }
+ }
+ // Increase progress
+ self.set_progress(total_bytes_written, file_size);
+ // Draw only if a significant progress has been made (performance improvement)
+ if last_progress_val + 0.5 >= self.transfer_progress {
+ // Draw
+ self.draw();
+ last_progress_val = self.transfer_progress;
+ }
+ }
+ self.log(
+ LogLevel::Info,
+ format!(
+ "Saved file \"{}\" to \"{}\"",
+ file.abs_path.display(),
+ remote_path.display()
+ )
+ .as_ref(),
+ );
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!(
+ "Failed to upload file \"{}\": {}",
+ file.abs_path.display(),
+ err
+ )
+ .as_ref(),
+ ),
+ },
+ Err(err) => {
+ // Report error
+ self.log(
+ LogLevel::Error,
+ format!(
+ "Failed to open file \"{}\": {}",
+ file.abs_path.display(),
+ err
+ )
+ .as_ref(),
+ );
+ }
+ }
+ }
+ FsEntry::Directory(dir) => {
+ // Create directory on remote
+ match self.client.mkdir(remote_path.as_path()) {
+ Ok(_) => {
+ self.log(
+ LogLevel::Info,
+ format!("Created directory \"{}\"", remote_path.display()).as_ref(),
+ );
+ // Get files in dir
+ match self
+ .context
+ .as_ref()
+ .unwrap()
+ .local
+ .scan_dir(dir.abs_path.as_path())
+ {
+ Ok(entries) => {
+ // Iterate over files
+ for entry in entries.iter() {
+ // Send entry; name is always None after first call
+ self.filetransfer_send(&entry, remote_path.as_path(), None);
+ }
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!(
+ "Could not scan directory \"{}\": {}",
+ dir.abs_path.display(),
+ err
+ )
+ .as_ref(),
+ ),
+ }
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!(
+ "Failed to create directory \"{}\": {}",
+ remote_path.display(),
+ err
+ )
+ .as_ref(),
+ ),
+ }
+ }
+ }
+ // Scan dir on remote
+ if let Ok(path) = self.client.pwd() {
+ self.remote_scan(path.as_path());
+ }
+ // Eventually, Reset input mode to explorer
+ self.input_mode = InputMode::Explorer;
+ }
+
+ /// ### filetransfer_recv
+ ///
+ /// Recv fs entry from remote.
+ /// If dst_name is Some, entry will be saved with a different name.
+ /// If entry is a directory, this applies to directory only
+ pub(super) fn filetransfer_recv(
+ &mut self,
+ entry: &FsEntry,
+ local_path: &Path,
+ dst_name: Option,
+ ) {
+ // Write popup
+ let file_name: String = match entry {
+ FsEntry::Directory(dir) => dir.name.clone(),
+ FsEntry::File(file) => file.name.clone(),
+ };
+ self.input_mode =
+ InputMode::Popup(PopupType::Wait(format!("Downloading \"{}\"...", file_name)));
+ // Draw
+ self.draw();
+ // Match entry
+ match entry {
+ FsEntry::File(file) => {
+ // Get local file
+ let mut local_file_path: PathBuf = PathBuf::from(local_path);
+ let local_file_name: String = match dst_name {
+ Some(n) => n.clone(),
+ None => file.name.clone(),
+ };
+ local_file_path.push(local_file_name.as_str());
+ // Try to open local file
+ match self
+ .context
+ .as_ref()
+ .unwrap()
+ .local
+ .open_file_write(local_file_path.as_path())
+ {
+ Ok(mut local_file) => {
+ // Download file from remote
+ match self.client.recv_file(file.abs_path.as_path()) {
+ Ok((mut rhnd, file_size)) => {
+ // Set popup progress
+ self.input_mode = InputMode::Popup(PopupType::Progress(format!(
+ "Downloading \"{}\"...",
+ file_name
+ )));
+ let mut total_bytes_written: usize = 0;
+ // Set started time
+ self.transfer_started = Instant::now();
+ // Write local file
+ let mut last_progress_val: f64 = 0.0;
+ loop {
+ // Read till you can
+ let mut buffer: [u8; 8192] = [0; 8192];
+ match rhnd.read(&mut buffer) {
+ Ok(bytes_read) => {
+ total_bytes_written += bytes_read;
+ if bytes_read == 0 {
+ break;
+ } else {
+ // Write bytes
+ if let Err(err) =
+ local_file.write(&buffer[0..bytes_read])
+ {
+ self.log(
+ LogLevel::Error,
+ format!(
+ "Could not write local file: {}",
+ err
+ )
+ .as_ref(),
+ );
+ }
+ }
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!("Could not read remote file: {}", err).as_ref(),
+ ),
+ }
+ // Set progress
+ self.set_progress(total_bytes_written, file_size);
+ // Draw only if a significant progress has been made (performance improvement)
+ if last_progress_val + 0.5 >= self.transfer_progress {
+ // Draw
+ self.draw();
+ last_progress_val = self.transfer_progress;
+ }
+ }
+ // Log
+ self.log(
+ LogLevel::Info,
+ format!(
+ "Saved file \"{}\" to \"{}\"",
+ file.abs_path.display(),
+ local_file_path.display()
+ )
+ .as_ref(),
+ );
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!(
+ "Failed to download file \"{}\": {}",
+ file.abs_path.display(),
+ err
+ )
+ .as_ref(),
+ ),
+ }
+ }
+ Err(err) => {
+ // Report error
+ self.log(
+ LogLevel::Error,
+ format!(
+ "Failed to open local file for write \"{}\": {}",
+ local_file_path.display(),
+ err
+ )
+ .as_ref(),
+ );
+ }
+ }
+ }
+ FsEntry::Directory(dir) => {
+ // Get dir name
+ let mut local_dir_path: PathBuf = PathBuf::from(local_path);
+ match dst_name {
+ Some(name) => local_dir_path.push(name),
+ None => local_dir_path.push(dir.name.as_str()),
+ }
+ // Create directory on local
+ match self
+ .context
+ .as_mut()
+ .unwrap()
+ .local
+ .mkdir_ex(local_dir_path.as_path(), true)
+ {
+ Ok(_) => {
+ self.log(
+ LogLevel::Info,
+ format!("Created directory \"{}\"", local_dir_path.display()).as_ref(),
+ );
+ // Get files in dir
+ match self.client.list_dir(dir.abs_path.as_path()) {
+ Ok(entries) => {
+ // Iterate over files
+ for entry in entries.iter() {
+ // Receive entry; name is always None after first call
+ // Local path becomes local_dir_path
+ self.filetransfer_recv(&entry, local_dir_path.as_path(), None);
+ }
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!(
+ "Could not scan directory \"{}\": {}",
+ dir.abs_path.display(),
+ err
+ )
+ .as_ref(),
+ ),
+ }
+ }
+ Err(err) => self.log(
+ LogLevel::Error,
+ format!(
+ "Failed to create directory \"{}\": {}",
+ local_dir_path.display(),
+ err
+ )
+ .as_ref(),
+ ),
+ }
+ }
+ }
+ // Reload directory on local
+ self.local_scan(local_path);
+ // Eventually, Reset input mode to explorer
+ self.input_mode = InputMode::Explorer;
+ }
+
+ /// ### local_scan
+ ///
+ /// Scan current local directory
+ pub(super) fn local_scan(&mut self, path: &Path) {
+ match self.context.as_ref().unwrap().local.scan_dir(path) {
+ Ok(files) => {
+ // Reset index
+ self.local.index = 0;
+ self.local.files = files;
+ // Sort files
+ self.local.sort_files_by_name();
+ }
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!("Could not scan current directory: {}", err).as_str(),
+ );
+ }
+ }
+ }
+
+ /// ### remote_scan
+ ///
+ /// Scan current remote directory
+ pub(super) fn remote_scan(&mut self, path: &Path) {
+ match self.client.list_dir(path) {
+ Ok(files) => {
+ // Reset index
+ self.remote.index = 0;
+ self.remote.files = files;
+ // Sort files
+ self.remote.sort_files_by_name();
+ }
+ Err(err) => {
+ self.log(
+ LogLevel::Error,
+ format!("Could not scan current directory: {}", err).as_str(),
+ );
+ }
+ }
+ }
+
+ /// ### local_changedir
+ ///
+ /// Change directory for local
+ pub(super) fn local_changedir(&mut self, path: &Path, push: bool) {
+ // Get current directory
+ let prev_dir: PathBuf = self.context.as_ref().unwrap().local.pwd();
+ // Change directory
+ match self
+ .context
+ .as_mut()
+ .unwrap()
+ .local
+ .change_wrkdir(PathBuf::from(path))
+ {
+ Ok(_) => {
+ self.log(
+ LogLevel::Info,
+ format!("Changed directory on local: {}", path.display()).as_str(),
+ );
+ // Reload files
+ self.local_scan(path);
+ // Push prev_dir to stack
+ if push {
+ self.local.pushd(prev_dir.as_path())
+ }
+ }
+ Err(err) => {
+ // Report err
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not change working directory: {}", err),
+ ));
+ }
+ }
+ }
+
+ pub(super) fn remote_changedir(&mut self, path: &Path, push: bool) {
+ // Get current directory
+ match self.client.pwd() {
+ Ok(prev_dir) => {
+ // Change directory
+ match self.client.change_dir(path) {
+ Ok(_) => {
+ self.log(
+ LogLevel::Info,
+ format!("Changed directory on remote: {}", path.display()).as_str(),
+ );
+ // Update files
+ self.remote_scan(path);
+ // Push prev_dir to stack
+ if push {
+ self.remote.pushd(prev_dir.as_path())
+ }
+ }
+ Err(err) => {
+ // Report err
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not change working directory: {}", err),
+ ));
+ }
+ }
+ }
+ Err(err) => {
+ // Report err
+ self.input_mode = InputMode::Popup(PopupType::Alert(
+ Color::Red,
+ format!("Could not change working directory: {}", err),
+ ));
+ }
+ }
+ }
+}