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), + )); + } + } + } +}