diff --git a/src/fs/explorer/mod.rs b/src/fs/explorer/mod.rs index 66f380a..0664c53 100644 --- a/src/fs/explorer/mod.rs +++ b/src/fs/explorer/mod.rs @@ -171,10 +171,27 @@ impl FileExplorer { } /// ### get - /// - /// Get file at index + /// + /// Get file at relative index pub fn get(&self, idx: usize) -> Option<&FsEntry> { - self.files.get(idx) + let opts: ExplorerOpts = self.opts; + let filtered = self + .files + .iter() + .filter(move |x| { + // If true, element IS NOT filtered + let mut pass: bool = true; + // If hidden files SHOULDN'T be shown, AND pass with not hidden + if !opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) { + pass &= !x.is_hidden(); + } + pass + }) + .collect::>(); + match filtered.get(idx) { + None => None, + Some(file) => Some(file), + } } // Formatting diff --git a/src/ui/activities/filetransfer_activity/actions.rs b/src/ui/activities/filetransfer_activity/actions.rs new file mode 100644 index 0000000..af36ad3 --- /dev/null +++ b/src/ui/activities/filetransfer_activity/actions.rs @@ -0,0 +1,488 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/* +* +* Copyright (C) 2020-2021 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 . +* +*/ + +// locals +use super::{FileTransferActivity, FsEntry, LogLevel}; +use crate::ui::layout::Payload; +// externals +use std::path::PathBuf; + +impl FileTransferActivity { + /// ### action_change_local_dir + /// + /// Change local directory reading value from input + pub(super) fn action_change_local_dir(&mut self, input: String) { + let dir_path: PathBuf = PathBuf::from(input.as_str()); + let abs_dir_path: PathBuf = match dir_path.is_relative() { + true => { + let mut d: PathBuf = self.local.wrkdir.clone(); + d.push(dir_path); + d + } + false => dir_path, + }; + self.local_changedir(abs_dir_path.as_path(), true); + } + + /// ### action_change_remote_dir + /// + /// Change remote directory reading value from input + pub(super) fn action_change_remote_dir(&mut self, input: String) { + let dir_path: PathBuf = PathBuf::from(input.as_str()); + let abs_dir_path: PathBuf = match dir_path.is_relative() { + true => { + let mut wrkdir: PathBuf = self.remote.wrkdir.clone(); + wrkdir.push(dir_path); + wrkdir + } + false => dir_path, + }; + self.remote_changedir(abs_dir_path.as_path(), true); + } + + /// ### action_local_copy + /// + /// Copy file on local + pub(super) fn action_local_copy(&mut self, input: String) { + if let Some(idx) = self.get_local_file_idx() { + let dest_path: PathBuf = PathBuf::from(input); + let entry: FsEntry = self.local.get(idx).unwrap().clone(); + if let Some(ctx) = self.context.as_mut() { + match ctx.local.copy(&entry, dest_path.as_path()) { + Ok(_) => { + self.log( + LogLevel::Info, + format!( + "Copied \"{}\" to \"{}\"", + entry.get_abs_path().display(), + dest_path.display() + ) + .as_str(), + ); + // Reload entries + let wrkdir: PathBuf = self.local.wrkdir.clone(); + self.local_scan(wrkdir.as_path()); + } + Err(err) => self.log_and_alert( + LogLevel::Error, + format!( + "Could not copy \"{}\" to \"{}\": {}", + entry.get_abs_path().display(), + dest_path.display(), + err + ), + ), + } + } + } + } + + /// ### action_remote_copy + /// + /// Copy file on remote + pub(super) fn action_remote_copy(&mut self, input: String) { + if let Some(idx) = self.get_remote_file_idx() { + let dest_path: PathBuf = PathBuf::from(input); + let entry: FsEntry = self.remote.get(idx).unwrap().clone(); + match self.client.as_mut().copy(&entry, dest_path.as_path()) { + Ok(_) => { + self.log( + LogLevel::Info, + format!( + "Copied \"{}\" to \"{}\"", + entry.get_abs_path().display(), + dest_path.display() + ) + .as_str(), + ); + self.reload_remote_dir(); + } + Err(err) => self.log_and_alert( + LogLevel::Error, + format!( + "Could not copy \"{}\" to \"{}\": {}", + entry.get_abs_path().display(), + dest_path.display(), + err + ), + ), + } + } + } + + pub(super) fn action_local_mkdir(&mut self, input: String) { + 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.local.wrkdir.clone(); + self.local_scan(wrkdir.as_path()); + } + Err(err) => { + // Report err + self.log_and_alert( + LogLevel::Error, + format!("Could not create directory \"{}\": {}", input, err), + ); + } + } + } + pub(super) fn action_remote_mkdir(&mut self, input: String) { + match self + .client + .as_mut() + .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_and_alert( + LogLevel::Error, + format!("Could not create directory \"{}\": {}", input, err), + ); + } + } + } + + pub(super) fn action_local_rename(&mut self, input: String) { + let entry: Option = match self.get_local_file_entry() { + Some(f) => Some(f.clone()), + None => None, + }; + if let Some(entry) = entry { + 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.local.wrkdir.clone(); + wrkdir.push(dst_path); + dst_path = wrkdir; + } + let full_path: PathBuf = entry.get_abs_path(); + // 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 + let path: PathBuf = self.local.wrkdir.clone(); + self.local_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_and_alert( + LogLevel::Error, + format!("Could not rename file \"{}\": {}", full_path.display(), err), + ); + } + } + } + } + + pub(super) fn action_remote_rename(&mut self, input: String) { + if let Some(idx) = self.get_remote_file_idx() { + if let Some(entry) = self.remote.get(idx) { + let dst_path: PathBuf = PathBuf::from(input); + let full_path: PathBuf = entry.get_abs_path(); + // Rename file or directory and report status as popup + match self.client.as_mut().rename(entry, dst_path.as_path()) { + Ok(_) => { + // Reload files + let path: PathBuf = self.remote.wrkdir.clone(); + 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_and_alert( + LogLevel::Error, + format!("Could not rename file \"{}\": {}", full_path.display(), err), + ); + } + } + } + } + } + + pub(super) fn action_local_delete(&mut self) { + let entry: Option = match self.get_local_file_entry() { + Some(f) => Some(f.clone()), + None => None, + }; + if let Some(entry) = entry { + let full_path: PathBuf = entry.get_abs_path(); + // Delete file or directory and report status as popup + match self.context.as_mut().unwrap().local.remove(&entry) { + Ok(_) => { + // Reload files + let p: PathBuf = self.local.wrkdir.clone(); + self.local_scan(p.as_path()); + // Log + self.log( + LogLevel::Info, + format!("Removed file \"{}\"", full_path.display()).as_ref(), + ); + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not delete file \"{}\": {}", full_path.display(), err), + ); + } + } + } + } + + pub(super) fn action_remote_delete(&mut self) { + if let Some(idx) = self.get_remote_file_idx() { + // Check if file entry exists + if let Some(entry) = self.remote.get(idx) { + let full_path: PathBuf = entry.get_abs_path(); + // 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_and_alert( + LogLevel::Error, + format!("Could not delete file \"{}\": {}", full_path.display(), err), + ); + } + } + } + } + } + + pub(super) fn action_local_saveas(&mut self, input: String) { + if let Some(idx) = self.get_local_file_idx() { + // Get pwd + let wrkdir: PathBuf = self.remote.wrkdir.clone(); + if self.local.get(idx).is_some() { + let file: FsEntry = self.local.get(idx).unwrap().clone(); + // Call upload; pass realfile, keep link name + self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(input)); + } + } + } + + pub(super) fn action_remote_saveas(&mut self, input: String) { + if let Some(idx) = self.get_remote_file_idx() { + // Get pwd + let wrkdir: PathBuf = self.remote.wrkdir.clone(); + if self.remote.get(idx).is_some() { + let file: FsEntry = self.remote.get(idx).unwrap().clone(); + // Call upload; pass realfile, keep link name + self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input)); + } + } + } + + pub(super) fn action_local_newfile(&mut self, input: String) { + // Check if file exists + let mut file_exists: bool = false; + for file in self.local.iter_files_all() { + if input == file.get_name() { + file_exists = true; + } + } + if file_exists { + self.log_and_alert( + LogLevel::Warn, + format!("File \"{}\" already exists", input,), + ); + return; + } + // Create file + let file_path: PathBuf = PathBuf::from(input.as_str()); + if let Some(ctx) = self.context.as_mut() { + if let Err(err) = ctx.local.open_file_write(file_path.as_path()) { + self.log_and_alert( + LogLevel::Error, + format!("Could not create file \"{}\": {}", file_path.display(), err), + ); + } + self.log( + LogLevel::Info, + format!("Created file \"{}\"", file_path.display()).as_str(), + ); + // Reload files + let path: PathBuf = self.local.wrkdir.clone(); + self.local_scan(path.as_path()); + } + } + + pub(super) fn action_remote_newfile(&mut self, input: String) { + // Check if file exists + let mut file_exists: bool = false; + for file in self.remote.iter_files_all() { + if input == file.get_name() { + file_exists = true; + } + } + if file_exists { + self.log_and_alert( + LogLevel::Warn, + format!("File \"{}\" already exists", input,), + ); + return; + } + // Get path on remote + let file_path: PathBuf = PathBuf::from(input.as_str()); + // Create file (on local) + match tempfile::NamedTempFile::new() { + Err(err) => self.log_and_alert( + LogLevel::Error, + format!("Could not create tempfile: {}", err), + ), + Ok(tfile) => { + // Stat tempfile + if let Some(ctx) = self.context.as_mut() { + let local_file: FsEntry = match ctx.local.stat(tfile.path()) { + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not stat tempfile: {}", err), + ); + return; + } + Ok(f) => f, + }; + if let FsEntry::File(local_file) = local_file { + // Create file + match self.client.send_file(&local_file, file_path.as_path()) { + Err(err) => self.log_and_alert( + LogLevel::Error, + format!( + "Could not create file \"{}\": {}", + file_path.display(), + err + ), + ), + Ok(writer) => { + // Finalize write + if let Err(err) = self.client.on_sent(writer) { + self.log_and_alert( + LogLevel::Warn, + format!("Could not finalize file: {}", err), + ); + } + self.log( + LogLevel::Info, + format!("Created file \"{}\"", file_path.display()).as_str(), + ); + // Reload files + let path: PathBuf = self.remote.wrkdir.clone(); + self.remote_scan(path.as_path()); + } + } + } + } + } + } + } + + /// ### get_local_file_entry + /// + /// Get local file entry + pub(super) fn get_local_file_entry(&self) -> Option<&FsEntry> { + match self.get_local_file_idx() { + None => None, + Some(idx) => self.local.get(idx), + } + } + + /// ### get_remote_file_entry + /// + /// Get remote file entry + pub(super) fn get_remote_file_entry(&self) -> Option<&FsEntry> { + match self.get_remote_file_idx() { + None => None, + Some(idx) => self.remote.get(idx), + } + } + + // -- private + + /// ### get_local_file_idx + /// + /// Get index of selected file in the local tab + fn get_local_file_idx(&self) -> Option { + match self.view.get_value(super::COMPONENT_EXPLORER_LOCAL) { + Some(Payload::Unsigned(idx)) => Some(idx), + _ => None, + } + } + + /// ### get_remote_file_idx + /// + /// Get index of selected file in the remote file + fn get_remote_file_idx(&self) -> Option { + match self.view.get_value(super::COMPONENT_EXPLORER_REMOTE) { + Some(Payload::Unsigned(idx)) => Some(idx), + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer_activity/callbacks.rs b/src/ui/activities/filetransfer_activity/callbacks.rs deleted file mode 100644 index 7dbf8e6..0000000 --- a/src/ui/activities/filetransfer_activity/callbacks.rs +++ /dev/null @@ -1,490 +0,0 @@ -//! ## FileTransferActivity -//! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall - -/* -* -* Copyright (C) 2020-2021 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 . -* -*/ - -// Locals -use super::{FileExplorerTab, FileTransferActivity, FsEntry, LogLevel}; -// Ext -use std::path::PathBuf; - -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.local.wrkdir.clone(); - 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 => { - let mut wrkdir: PathBuf = self.remote.wrkdir.clone(); - wrkdir.push(dir_path); - wrkdir - } - false => dir_path, - }; - self.remote_changedir(abs_dir_path.as_path(), true); - } - } - } - - /// ### callback_copy - /// - /// Callback for COPY command (both from local and remote) - pub(super) fn callback_copy(&mut self, input: String) { - let dest_path: PathBuf = PathBuf::from(input); - match self.tab { - FileExplorerTab::Local => { - // Get selected entry - if self.local.get_current_file().is_some() { - let entry: FsEntry = self.local.get_current_file().unwrap().clone(); - if let Some(ctx) = self.context.as_mut() { - match ctx.local.copy(&entry, dest_path.as_path()) { - Ok(_) => { - self.log( - LogLevel::Info, - format!( - "Copied \"{}\" to \"{}\"", - entry.get_abs_path().display(), - dest_path.display() - ) - .as_str(), - ); - // Reload entries - let wrkdir: PathBuf = self.local.wrkdir.clone(); - self.local_scan(wrkdir.as_path()); - } - Err(err) => self.log_and_alert( - LogLevel::Error, - format!( - "Could not copy \"{}\" to \"{}\": {}", - entry.get_abs_path().display(), - dest_path.display(), - err - ), - ), - } - } - } - } - FileExplorerTab::Remote => { - // Get selected entry - if self.remote.get_current_file().is_some() { - let entry: FsEntry = self.remote.get_current_file().unwrap().clone(); - match self.client.as_mut().copy(&entry, dest_path.as_path()) { - Ok(_) => { - self.log( - LogLevel::Info, - format!( - "Copied \"{}\" to \"{}\"", - entry.get_abs_path().display(), - dest_path.display() - ) - .as_str(), - ); - self.reload_remote_dir(); - } - Err(err) => self.log_and_alert( - LogLevel::Error, - format!( - "Could not copy \"{}\" to \"{}\": {}", - entry.get_abs_path().display(), - dest_path.display(), - err - ), - ), - } - } - } - } - } - - /// ### 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.local.wrkdir.clone(); - self.local_scan(wrkdir.as_path()); - } - Err(err) => { - // Report err - self.log_and_alert( - LogLevel::Error, - format!("Could not create directory \"{}\": {}", input, err), - ); - } - } - } - FileExplorerTab::Remote => { - match self - .client - .as_mut() - .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_and_alert( - LogLevel::Error, - 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.local.wrkdir.clone(); - wrkdir.push(dst_path); - dst_path = wrkdir; - } - // Check if file entry exists - if let Some(entry) = self.local.get_current_file() { - let full_path: PathBuf = entry.get_abs_path(); - // 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 - let path: PathBuf = self.local.wrkdir.clone(); - self.local_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_and_alert( - LogLevel::Error, - format!( - "Could not rename file \"{}\": {}", - full_path.display(), - err - ), - ); - } - } - } - } - FileExplorerTab::Remote => { - // Check if file entry exists - if let Some(entry) = self.remote.get_current_file() { - let full_path: PathBuf = entry.get_abs_path(); - // Rename file or directory and report status as popup - let dst_path: PathBuf = PathBuf::from(input); - match self.client.as_mut().rename(entry, dst_path.as_path()) { - Ok(_) => { - // Reload files - let path: PathBuf = self.remote.wrkdir.clone(); - 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_and_alert( - LogLevel::Error, - format!( - "Could not rename file \"{}\": {}", - full_path.display(), - 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.get_current_file() { - let full_path: PathBuf = entry.get_abs_path(); - // Delete file or directory and report status as popup - match self.context.as_mut().unwrap().local.remove(entry) { - Ok(_) => { - // Reload files - let p: PathBuf = self.local.wrkdir.clone(); - self.local_scan(p.as_path()); - // Log - self.log( - LogLevel::Info, - format!("Removed file \"{}\"", full_path.display()).as_ref(), - ); - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Could not delete file \"{}\": {}", - full_path.display(), - err - ), - ); - } - } - } - } - FileExplorerTab::Remote => { - // Check if file entry exists - if let Some(entry) = self.remote.get_current_file() { - let full_path: PathBuf = entry.get_abs_path(); - // 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_and_alert( - LogLevel::Error, - format!( - "Could not delete file \"{}\": {}", - full_path.display(), - 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 = self.remote.wrkdir.clone(); - // Get file and clone (due to mutable / immutable stuff...) - if self.local.get_current_file().is_some() { - let file: FsEntry = self.local.get_current_file().unwrap().clone(); - // Call upload; pass realfile, keep link name - self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(input)); - } - } - FileExplorerTab::Remote => { - // Get file and clone (due to mutable / immutable stuff...) - if self.remote.get_current_file().is_some() { - let file: FsEntry = self.remote.get_current_file().unwrap().clone(); - // Call upload; pass realfile, keep link name - let wrkdir: PathBuf = self.local.wrkdir.clone(); - self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input)); - } - } - } - } - - /// ### callback_new_file - /// - /// Create a new file in current directory with `input` as name - pub(super) fn callback_new_file(&mut self, input: String) { - match self.tab { - FileExplorerTab::Local => { - // Check if file exists - let mut file_exists: bool = false; - for file in self.local.iter_files_all() { - if input == file.get_name() { - file_exists = true; - } - } - if file_exists { - self.log_and_alert( - LogLevel::Warn, - format!("File \"{}\" already exists", input,), - ); - return; - } - // Create file - let file_path: PathBuf = PathBuf::from(input.as_str()); - if let Some(ctx) = self.context.as_mut() { - if let Err(err) = ctx.local.open_file_write(file_path.as_path()) { - self.log_and_alert( - LogLevel::Error, - format!("Could not create file \"{}\": {}", file_path.display(), err), - ); - } - self.log( - LogLevel::Info, - format!("Created file \"{}\"", file_path.display()).as_str(), - ); - // Reload files - let path: PathBuf = self.local.wrkdir.clone(); - self.local_scan(path.as_path()); - } - } - FileExplorerTab::Remote => { - // Check if file exists - let mut file_exists: bool = false; - for file in self.remote.iter_files_all() { - if input == file.get_name() { - file_exists = true; - } - } - if file_exists { - self.log_and_alert( - LogLevel::Warn, - format!("File \"{}\" already exists", input,), - ); - return; - } - // Get path on remote - let file_path: PathBuf = PathBuf::from(input.as_str()); - // Create file (on local) - match tempfile::NamedTempFile::new() { - Err(err) => self.log_and_alert( - LogLevel::Error, - format!("Could not create tempfile: {}", err), - ), - Ok(tfile) => { - // Stat tempfile - if let Some(ctx) = self.context.as_mut() { - let local_file: FsEntry = match ctx.local.stat(tfile.path()) { - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!("Could not stat tempfile: {}", err), - ); - return; - } - Ok(f) => f, - }; - if let FsEntry::File(local_file) = local_file { - // Create file - match self.client.send_file(&local_file, file_path.as_path()) { - Err(err) => self.log_and_alert( - LogLevel::Error, - format!( - "Could not create file \"{}\": {}", - file_path.display(), - err - ), - ), - Ok(writer) => { - // Finalize write - if let Err(err) = self.client.on_sent(writer) { - self.log_and_alert( - LogLevel::Warn, - format!("Could not finalize file: {}", err), - ); - } - self.log( - LogLevel::Info, - format!("Created file \"{}\"", file_path.display()) - .as_str(), - ); - // Reload files - let path: PathBuf = self.remote.wrkdir.clone(); - self.remote_scan(path.as_path()); - } - } - } - } - } - } - } - } - } -} diff --git a/src/ui/activities/filetransfer_activity/input.rs b/src/ui/activities/filetransfer_activity/input.rs deleted file mode 100644 index 5aa4e9a..0000000 --- a/src/ui/activities/filetransfer_activity/input.rs +++ /dev/null @@ -1,811 +0,0 @@ -//! ## FileTransferActivity -//! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall - -/* -* -* Copyright (C) 2020-2021 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 . -* -*/ - -// Deps -extern crate tempfile; -// Local -use super::{ - DialogCallback, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputEvent, - InputField, LogLevel, OnInputSubmitCallback, Popup, -}; -use crate::fs::explorer::{FileExplorer, FileSorting}; -// Ext -use crossterm::event::{KeyCode, KeyModifiers}; -use std::path::PathBuf; - -impl FileTransferActivity { - /// ### read_input_event - /// - /// Read one event. - /// Returns whether at least one event has been handled - pub(super) fn read_input_event(&mut self) -> bool { - if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() { - // Handle event - self.handle_input_event(&event); - // Return true - true - } else { - // Error - false - } - } - - /// ### 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.popup { - Some(ptype) => Some(ptype.clone()), - _ => None, - }; - match &self.popup { - None => self.handle_input_event_mode_explorer(ev), - Some(_) => { - 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 - if let InputEvent::Key(key) = ev { - match key.code { - KeyCode::Esc => { - // Handle quit event - // Create quit prompt dialog - self.popup = Some(self.create_disconnect_popup()); - } - KeyCode::Tab => self.switch_input_field(), // switch tab - KeyCode::Right => self.tab = FileExplorerTab::Remote, // switch to right tab - KeyCode::Up => { - // Decrement index - self.local.decr_index(); - } - KeyCode::Down => { - // Increment index - self.local.incr_index(); - } - KeyCode::PageUp => { - // Decrement index by 8 - self.local.decr_index_by(8); - } - KeyCode::PageDown => { - // Increment index by 8 - self.local.incr_index_by(8); - } - KeyCode::Enter => { - // Match selected file - let mut entry: Option = None; - if let Some(e) = self.local.get_current_file() { - entry = Some(e.clone()); - } - if let Some(entry) = entry { - // 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(symlink_entry) = &file.symlink { - // If symlink entry is a directory, go to directory - if let FsEntry::Directory(dir) = &**symlink_entry { - self.local_changedir(dir.abs_path.as_path(), true) - } - } - } - } - } - } - 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.get_current_file() { - // Get file name - let file_name: String = match entry { - FsEntry::Directory(dir) => dir.name.clone(), - FsEntry::File(file) => file.name.clone(), - }; - // Default choice to NO for delete! - self.choice_opt = DialogYesNoOption::No; - // Show delete prompt - self.popup = Some(Popup::YesNo( - format!("Delete file \"{}\"", file_name), - FileTransferActivity::callback_delete_fsentry, - FileTransferActivity::callback_nothing_to_do, - )) - } - } - KeyCode::Char(ch) => match ch { - 'a' | 'A' => { - // Toggle hidden files - self.local.toggle_hidden_files(); - } - 'b' | 'B' => { - // Choose file sorting type - self.popup = Some(Popup::FileSortingDialog); - } - 'c' | 'C' => { - // Copy - self.popup = Some(Popup::Input( - String::from("Insert destination name"), - FileTransferActivity::callback_copy, - )); - } - 'd' | 'D' => { - // Make directory - self.popup = Some(Popup::Input( - String::from("Insert directory name"), - FileTransferActivity::callback_mkdir, - )); - } - 'e' | 'E' => { - // Get file at index - if let Some(entry) = self.local.get_current_file() { - // Get file name - let file_name: String = match entry { - FsEntry::Directory(dir) => dir.name.clone(), - FsEntry::File(file) => file.name.clone(), - }; - // Default choice to NO for delete! - self.choice_opt = DialogYesNoOption::No; - // Show delete prompt - self.popup = Some(Popup::YesNo( - format!("Delete file \"{}\"", file_name), - FileTransferActivity::callback_delete_fsentry, - FileTransferActivity::callback_nothing_to_do, - )) - } - } - 'g' | 'G' => { - // Goto - // Show input popup - self.popup = Some(Popup::Input( - String::from("Change working directory"), - FileTransferActivity::callback_change_directory, - )); - } - 'h' | 'H' => { - // Show help - self.popup = Some(Popup::Help); - } - 'i' | 'I' => { - // Show file info - self.popup = Some(Popup::FileInfo); - } - 'l' | 'L' => { - // Reload file entries - let pwd: PathBuf = self.local.wrkdir.clone(); - self.local_scan(pwd.as_path()); - } - 'n' | 'N' => { - // New file - self.popup = Some(Popup::Input( - String::from("New file"), - Self::callback_new_file, - )); - } - 'o' | 'O' => { - // Edit local file - if self.local.get_current_file().is_some() { - // Clone entry due to mutable stuff... - let fsentry: FsEntry = self.local.get_current_file().unwrap().clone(); - // Check if file - if fsentry.is_file() { - self.log( - LogLevel::Info, - format!( - "Opening file \"{}\"...", - fsentry.get_abs_path().display() - ) - .as_str(), - ); - // Edit file - match self.edit_local_file(fsentry.get_abs_path().as_path()) { - Ok(_) => { - // Reload directory - let pwd: PathBuf = self.local.wrkdir.clone(); - self.local_scan(pwd.as_path()); - } - Err(err) => self.log_and_alert(LogLevel::Error, err), - } - } - } - } - 'q' | 'Q' => { - // Create quit prompt dialog - self.popup = Some(self.create_quit_popup()); - } - 'r' | 'R' => { - // Rename - self.popup = Some(Popup::Input( - String::from("Insert new name"), - FileTransferActivity::callback_rename, - )); - } - 's' | 'S' => { - // Save as... - // Ask for input - self.popup = Some(Popup::Input( - String::from("Save as..."), - FileTransferActivity::callback_save_as, - )); - } - 'u' | 'U' => { - // Go to parent directory - // Get pwd - let path: PathBuf = self.local.wrkdir.clone(); - if let Some(parent) = path.as_path().parent() { - self.local_changedir(parent, true); - } - } - ' ' => { - // Get pwd - let wrkdir: PathBuf = self.remote.wrkdir.clone(); - // Get file and clone (due to mutable / immutable stuff...) - if self.local.get_current_file().is_some() { - let file: FsEntry = self.local.get_current_file().unwrap().clone(); - let name: String = file.get_name().to_string(); - // Call upload; pass realfile, keep link name - self.filetransfer_send( - &file.get_realfile(), - wrkdir.as_path(), - Some(name), - ); - } - } - _ => { /* 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 - if let InputEvent::Key(key) = ev { - match key.code { - KeyCode::Esc => { - // Handle quit event - // Create quit prompt dialog - self.popup = Some(self.create_disconnect_popup()); - } - KeyCode::Tab => self.switch_input_field(), // switch tab - KeyCode::Left => self.tab = FileExplorerTab::Local, // switch to local tab - KeyCode::Up => { - // Decrement index - self.remote.decr_index(); - } - KeyCode::Down => { - // Increment index - self.remote.incr_index(); - } - KeyCode::PageUp => { - // Decrement index by 8 - self.remote.decr_index_by(8); - } - KeyCode::PageDown => { - // Increment index by 8 - self.remote.incr_index_by(8); - } - KeyCode::Enter => { - // Match selected file - let mut entry: Option = None; - if let Some(e) = self.remote.get_current_file() { - entry = Some(e.clone()); - } - if let Some(entry) = entry { - // 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(symlink_entry) = &file.symlink { - // If symlink entry is a directory, go to directory - if let FsEntry::Directory(dir) = &**symlink_entry { - self.remote_changedir(dir.abs_path.as_path(), true) - } - } - } - } - } - } - 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.get_current_file() { - // Get file name - let file_name: String = match entry { - FsEntry::Directory(dir) => dir.name.clone(), - FsEntry::File(file) => file.name.clone(), - }; - // Default choice to NO for delete! - self.choice_opt = DialogYesNoOption::No; - // Show delete prompt - self.popup = Some(Popup::YesNo( - format!("Delete file \"{}\"", file_name), - FileTransferActivity::callback_delete_fsentry, - FileTransferActivity::callback_nothing_to_do, - )) - } - } - KeyCode::Char(ch) => match ch { - 'a' | 'A' => { - // Toggle hidden files - self.remote.toggle_hidden_files(); - } - 'b' | 'B' => { - // Choose file sorting type - self.popup = Some(Popup::FileSortingDialog); - } - 'c' | 'C' => { - // Copy - self.popup = Some(Popup::Input( - String::from("Insert destination name"), - FileTransferActivity::callback_copy, - )); - } - 'd' | 'D' => { - // Make directory - self.popup = Some(Popup::Input( - String::from("Insert directory name"), - FileTransferActivity::callback_mkdir, - )); - } - 'e' | 'E' => { - // Get file at index - if let Some(entry) = self.remote.get_current_file() { - // Get file name - let file_name: String = match entry { - FsEntry::Directory(dir) => dir.name.clone(), - FsEntry::File(file) => file.name.clone(), - }; - // Default choice to NO for delete! - self.choice_opt = DialogYesNoOption::No; - // Show delete prompt - self.popup = Some(Popup::YesNo( - format!("Delete file \"{}\"", file_name), - FileTransferActivity::callback_delete_fsentry, - FileTransferActivity::callback_nothing_to_do, - )) - } - } - 'g' | 'G' => { - // Goto - // Show input popup - self.popup = Some(Popup::Input( - String::from("Change working directory"), - FileTransferActivity::callback_change_directory, - )); - } - 'h' | 'H' => { - // Show help - self.popup = Some(Popup::Help); - } - 'i' | 'I' => { - // Show file info - self.popup = Some(Popup::FileInfo); - } - 'l' | 'L' => { - // Reload file entries - self.reload_remote_dir(); - } - 'n' | 'N' => { - // New file - self.popup = Some(Popup::Input( - String::from("New file"), - Self::callback_new_file, - )); - } - 'o' | 'O' => { - // Edit remote file - if self.remote.get_current_file().is_some() { - // Clone entry due to mutable stuff... - let fsentry: FsEntry = self.remote.get_current_file().unwrap().clone(); - // Check if file - if let FsEntry::File(file) = fsentry { - self.log( - LogLevel::Info, - format!("Opening file \"{}\"...", file.abs_path.display()) - .as_str(), - ); - // Edit file - match self.edit_remote_file(&file) { - Ok(_) => { - // Reload directory - let pwd: PathBuf = self.remote.wrkdir.clone(); - self.remote_scan(pwd.as_path()); - } - Err(err) => self.log_and_alert(LogLevel::Error, err), - } - // Put input mode back to normal - self.popup = None; - } - } - } - 'q' | 'Q' => { - // Create quit prompt dialog - self.popup = Some(self.create_quit_popup()); - } - 'r' | 'R' => { - // Rename - self.popup = Some(Popup::Input( - String::from("Insert new name"), - FileTransferActivity::callback_rename, - )); - } - 's' | 'S' => { - // Save as... - // Ask for input - self.popup = Some(Popup::Input( - String::from("Save as..."), - FileTransferActivity::callback_save_as, - )); - } - 'u' | 'U' => { - // Get pwd - let path: PathBuf = self.remote.wrkdir.clone(); - // Go to parent directory - if let Some(parent) = path.as_path().parent() { - self.remote_changedir(parent, true); - } - } - ' ' => { - // Get file and clone (due to mutable / immutable stuff...) - if self.remote.get_current_file().is_some() { - let file: FsEntry = self.remote.get_current_file().unwrap().clone(); - let name: String = file.get_name().to_string(); - // Call upload; pass realfile, keep link name - let wrkdir: PathBuf = self.local.wrkdir.clone(); - self.filetransfer_recv( - &file.get_realfile(), - wrkdir.as_path(), - Some(name), - ); - } - } - _ => { /* 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; - if let InputEvent::Key(key) = ev { - match key.code { - KeyCode::Esc => { - // Handle quit event - // Create quit prompt dialog - self.popup = Some(self.create_disconnect_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 -= 1; - } - } - KeyCode::Up => { - // NOTE: Twisted logic - // Increase log index - if self.log_index + 1 < self.log_records.len() { - self.log_index += 1; - } - } - KeyCode::PageDown => { - // NOTE: Twisted logic - // Fast decreasing of log index - if self.log_index >= records_block { - 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 += records_block; // Increase by `records_block` - } - } - KeyCode::Char(ch) => match ch { - 'q' | 'Q' => { - // Create quit prompt dialog - self.popup = Some(self.create_quit_popup()); - } - _ => { /* 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: Popup) { - match popup { - Popup::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev), - Popup::FileInfo => self.handle_input_event_mode_popup_fileinfo(ev), - Popup::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev), - Popup::FileSortingDialog => self.handle_input_event_mode_popup_file_sorting(ev), - Popup::Help => self.handle_input_event_mode_popup_help(ev), - Popup::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb), - Popup::Progress(_) => self.handle_input_event_mode_popup_progress(ev), - Popup::Wait(_) => self.handle_input_event_mode_popup_wait(ev), - Popup::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 - if let InputEvent::Key(key) = ev { - if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { - // Set input mode back to explorer - self.popup = None; - } - } - } - - /// ### handle_input_event_mode_popup_fileinfo - /// - /// Input event handler for popup fileinfo - fn handle_input_event_mode_popup_fileinfo(&mut self, ev: &InputEvent) { - // If enter, close popup - if let InputEvent::Key(key) = ev { - if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { - // Set input mode back to explorer - self.popup = None; - } - } - } - - /// ### 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 - if let InputEvent::Key(key) = ev { - if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { - // Set quit to true; since a fatal error happened - self.disconnect(); - } - } - } - - /// ### handle_input_event_mode_popup_file_sorting - /// - /// Handle input event for file sorting dialog popup - fn handle_input_event_mode_popup_file_sorting(&mut self, ev: &InputEvent) { - // Match key code - if let InputEvent::Key(key) = ev { - match key.code { - KeyCode::Esc | KeyCode::Enter => { - // Exit - self.popup = None; - } - KeyCode::Right => { - // Update sorting mode - match self.tab { - FileExplorerTab::Local => { - Self::move_sorting_mode_opt_right(&mut self.local); - } - FileExplorerTab::Remote => { - Self::move_sorting_mode_opt_right(&mut self.remote); - } - } - } - KeyCode::Left => { - // Update sorting mode - match self.tab { - FileExplorerTab::Local => { - Self::move_sorting_mode_opt_left(&mut self.local); - } - FileExplorerTab::Remote => { - Self::move_sorting_mode_opt_left(&mut self.remote); - } - } - } - _ => { /* 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 - if let InputEvent::Key(key) = ev { - if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { - // Set input mode back to explorer - self.popup = None; - } - } - } - - /// ### 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 - if let InputEvent::Key(key) = ev { - match key.code { - KeyCode::Esc => { - // Abort input - // Clear current input text - self.input_txt.clear(); - // Set mode back to explorer - self.popup = None; - } - 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.popup = None; - // Call cb - cb(self, input_text); - } - KeyCode::Char(ch) => self.input_txt.push(ch), - KeyCode::Backspace => { - let _ = self.input_txt.pop(); - } - _ => { /* Nothing to do */ } - } - } - } - - /// ### handle_input_event_mode_popup_progress - /// - /// Input event handler for popup alert - fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) { - if let InputEvent::Key(key) = ev { - if let KeyCode::Char(ch) = key.code { - // If is 'C' and CTRL - if matches!(ch, 'c' | 'C') && key.modifiers.intersects(KeyModifiers::CONTROL) { - // Abort transfer - self.transfer.aborted = true; - } - } - } - } - - /// ### handle_input_event_mode_popup_wait - /// - /// 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 - } - - /// ### handle_input_event_mode_popup_yesno - /// - /// 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 - if let InputEvent::Key(key) = ev { - match key.code { - KeyCode::Enter => { - // @! Set input mode to Explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh? - self.popup = None; - // 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 */ } - } - } - } - - /// ### move_sorting_mode_opt_left - /// - /// Perform on file sorting dialog - fn move_sorting_mode_opt_left(explorer: &mut FileExplorer) { - let curr_sorting: FileSorting = explorer.get_file_sorting(); - explorer.sort_by(match curr_sorting { - FileSorting::BySize => FileSorting::ByCreationTime, - FileSorting::ByCreationTime => FileSorting::ByModifyTime, - FileSorting::ByModifyTime => FileSorting::ByName, - FileSorting::ByName => FileSorting::BySize, // Wrap - }); - } - - /// ### move_sorting_mode_opt_left - /// - /// Perform on file sorting dialog - fn move_sorting_mode_opt_right(explorer: &mut FileExplorer) { - let curr_sorting: FileSorting = explorer.get_file_sorting(); - explorer.sort_by(match curr_sorting { - FileSorting::ByName => FileSorting::ByModifyTime, - FileSorting::ByModifyTime => FileSorting::ByCreationTime, - FileSorting::ByCreationTime => FileSorting::BySize, - FileSorting::BySize => FileSorting::ByName, // Wrap - }); - } -} diff --git a/src/ui/activities/filetransfer_activity/layout.rs b/src/ui/activities/filetransfer_activity/layout.rs deleted file mode 100644 index ae9b3d2..0000000 --- a/src/ui/activities/filetransfer_activity/layout.rs +++ /dev/null @@ -1,959 +0,0 @@ -//! ## FileTransferActivity -//! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall - -/* -* -* Copyright (C) 2020-2021 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 . -* -*/ - -// Deps -extern crate bytesize; -extern crate hostname; -#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] -extern crate users; -// Local -use super::{ - Context, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputField, - LogLevel, LogRecord, Popup, -}; -use crate::fs::explorer::{FileExplorer, FileSorting}; -use crate::utils::fmt::{align_text_center, fmt_time}; -// Ext -use bytesize::ByteSize; -use std::path::{Path, PathBuf}; -use tui::{ - layout::{Constraint, Corner, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Span, Spans}, - widgets::{ - Block, BorderType, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, Tabs, - }, -}; -use unicode_width::UnicodeWidthStr; -#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] -use users::{get_group_by_gid, get_user_by_uid}; - -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(1) - .constraints( - [ - Constraint::Percentage(70), // Explorer - Constraint::Percentage(30), // 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[0]); - // Set localhost state - let mut localhost_state: ListState = ListState::default(); - localhost_state.select(Some(self.local.get_relative_index())); - // Set remote state - let mut remote_state: ListState = ListState::default(); - remote_state.select(Some(self.remote.get_relative_index())); - // Draw tabs - f.render_stateful_widget( - self.draw_local_explorer(tabs_chunks[0].width), - tabs_chunks[0], - &mut localhost_state, - ); - f.render_stateful_widget( - self.draw_remote_explorer(tabs_chunks[1].width), - 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[1].width), - chunks[1], - &mut log_state, - ); - // Draw popup - if let Some(popup) = &self.popup { - // Calculate popup size - let (width, height): (u16, u16) = match popup { - Popup::Alert(_, _) => (50, 10), - Popup::Fatal(_) => (50, 10), - Popup::FileInfo => (50, 50), - Popup::FileSortingDialog => (50, 10), - Popup::Help => (50, 80), - Popup::Input(_, _) => (40, 10), - Popup::Progress(_) => (40, 10), - Popup::Wait(_) => (50, 10), - Popup::YesNo(_, _, _) => (30, 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 { - Popup::Alert(color, txt) => f.render_widget( - self.draw_popup_alert(*color, txt.clone(), popup_area.width), - popup_area, - ), - Popup::Fatal(txt) => f.render_widget( - self.draw_popup_fatal(txt.clone(), popup_area.width), - popup_area, - ), - Popup::FileInfo => f.render_widget(self.draw_popup_fileinfo(), popup_area), - Popup::FileSortingDialog => { - f.render_widget(self.draw_popup_file_sorting_dialog(), popup_area) - } - Popup::Help => f.render_widget(self.draw_popup_help(), popup_area), - Popup::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, - ) - } - Popup::Progress(txt) => { - f.render_widget(self.draw_popup_progress(txt.clone()), popup_area) - } - Popup::Wait(txt) => f.render_widget( - self.draw_popup_wait(txt.clone(), popup_area.width), - popup_area, - ), - Popup::YesNo(txt, _, _) => { - f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area) - } - } - } - }); - self.context = Some(ctx); - } - - /// ### draw_local_explorer - /// - /// Draw local explorer list - pub(super) fn draw_local_explorer(&self, width: u16) -> List { - let hostname: String = match hostname::get() { - Ok(h) => { - let hostname: String = h.as_os_str().to_string_lossy().to_string(); - let tokens: Vec<&str> = hostname.split('.').collect(); - String::from(*tokens.get(0).unwrap_or(&"localhost")) - } - Err(_) => String::from("localhost"), - }; - let files: Vec = self - .local - .iter_files() - .map(|entry: &FsEntry| ListItem::new(Span::from(self.local.fmt_file(entry)))) - .collect(); - // Get colors to use; highlight element inverting fg/bg only when tab is active - let (fg, bg): (Color, Color) = match self.tab { - FileExplorerTab::Local => (Color::Black, Color::LightYellow), - _ => (Color::LightYellow, Color::Reset), - }; - 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::LightYellow), - _ => Style::default(), - }, - _ => Style::default(), - }) - .title(format!( - "{}:{} ", - hostname, - FileTransferActivity::elide_wrkdir_path( - self.local.wrkdir.as_path(), - hostname.as_str(), - width - ) - .display() - )), - ) - .start_corner(Corner::TopLeft) - .highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD)) - } - - /// ### draw_remote_explorer - /// - /// Draw remote explorer list - pub(super) fn draw_remote_explorer(&self, width: u16) -> List { - let files: Vec = self - .remote - .iter_files() - .map(|entry: &FsEntry| ListItem::new(Span::from(self.remote.fmt_file(entry)))) - .collect(); - // Get colors to use; highlight element inverting fg/bg only when tab is active - let (fg, bg): (Color, Color) = match self.tab { - FileExplorerTab::Remote => (Color::Black, Color::LightBlue), - _ => (Color::LightBlue, Color::Reset), - }; - 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(format!( - "{}:{} ", - self.params.address, - FileTransferActivity::elide_wrkdir_path( - self.remote.wrkdir.as_path(), - self.params.address.as_str(), - width - ) - .display() - )), - ) - .start_corner(Corner::TopLeft) - .highlight_style(Style::default().bg(bg).fg(fg).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, width: u16) -> List { - // Wraps texts - let message_rows = textwrap::wrap(text.as_str(), width as usize); - let mut lines: Vec = Vec::new(); - for msg in message_rows.iter() { - lines.push(ListItem::new(Spans::from(align_text_center(msg, width)))); - } - List::new(lines) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(color)) - .border_type(BorderType::Rounded) - .title("Alert"), - ) - .start_corner(Corner::TopLeft) - .style(Style::default().fg(color)) - } - - /// ### draw_popup_fatal - /// - /// Draw fatal error popup - pub(super) fn draw_popup_fatal(&self, text: String, width: u16) -> List { - // Wraps texts - let message_rows = textwrap::wrap(text.as_str(), width as usize); - let mut lines: Vec = Vec::new(); - for msg in message_rows.iter() { - lines.push(ListItem::new(Spans::from(align_text_center(msg, width)))); - } - List::new(lines) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Red)) - .border_type(BorderType::Rounded) - .title("Fatal error"), - ) - .start_corner(Corner::TopLeft) - .style(Style::default().fg(Color::Red)) - } - - /// ### draw_popup_file_sorting_dialog - /// - /// Draw FileSorting mode select popup - pub(super) fn draw_popup_file_sorting_dialog(&self) -> Tabs { - let choices: Vec = vec![ - Spans::from("Name"), - Spans::from("Modify time"), - Spans::from("Creation time"), - Spans::from("Size"), - ]; - let explorer: &FileExplorer = match self.tab { - FileExplorerTab::Local => &self.local, - FileExplorerTab::Remote => &self.remote, - }; - let index: usize = match explorer.get_file_sorting() { - FileSorting::ByCreationTime => 2, - FileSorting::ByModifyTime => 1, - FileSorting::ByName => 0, - FileSorting::BySize => 3, - }; - Tabs::new(choices) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title("Sort files by"), - ) - .select(index) - .style(Style::default()) - .highlight_style( - Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::LightMagenta) - .fg(Color::DarkGray), - ) - } - - /// ### 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) - .border_type(BorderType::Rounded) - .title(text), - ) - } - - /// ### draw_popup_progress - /// - /// Draw progress popup - pub(super) fn draw_popup_progress(&self, text: String) -> Gauge { - // Calculate ETA - let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs(); - let eta: String = match self.transfer.progress as u64 { - 0 => String::from("--:--"), // NOTE: would divide by 0 :D - _ => { - let eta: u64 = - ((elapsed_secs * 100) / (self.transfer.progress as u64)) - elapsed_secs; - format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2) - } - }; - // Calculate bytes/s - let label = format!( - "{:.2}% - ETA {} ({}/s)", - self.transfer.progress, - eta, - ByteSize(self.transfer.bytes_per_second()) - ); - 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, width: u16) -> List { - // Wraps texts - let message_rows = textwrap::wrap(text.as_str(), width as usize); - let mut lines: Vec = Vec::new(); - for msg in message_rows.iter() { - lines.push(ListItem::new(Spans::from(align_text_center(msg, width)))); - } - List::new(lines) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::White)) - .border_type(BorderType::Rounded) - .title("Please wait"), - ) - .start_corner(Corner::TopLeft) - .style(Style::default().add_modifier(Modifier::BOLD)) - } - - /// ### 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) - .border_type(BorderType::Rounded) - .title(text), - ) - .select(index) - .style(Style::default()) - .highlight_style( - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Yellow), - ) - } - - /// ### draw_popup_fileinfo - /// - /// Draw popup containing info about selected fsentry - pub(super) fn draw_popup_fileinfo(&self) -> List { - let mut info: Vec = Vec::new(); - // Get current fsentry - let fsentry: Option<&FsEntry> = match self.tab { - FileExplorerTab::Local => { - // Get selected file - match self.local.get_current_file() { - Some(entry) => Some(entry), - None => None, - } - } - FileExplorerTab::Remote => match self.remote.get_current_file() { - Some(entry) => Some(entry), - None => None, - }, - }; - // Get file_name and fill info list - let file_name: String = match fsentry { - Some(fsentry) => { - // Get name and path - let abs_path: PathBuf = fsentry.get_abs_path(); - let name: String = fsentry.get_name().to_string(); - let ctime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M:%S"); - let atime: String = fmt_time(fsentry.get_last_access_time(), "%b %d %Y %H:%M:%S"); - let mtime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M:%S"); - let (bsize, size): (ByteSize, usize) = - (ByteSize(fsentry.get_size() as u64), fsentry.get_size()); - let user: Option = fsentry.get_user(); - let group: Option = fsentry.get_group(); - let real_path: Option = { - let real_file: FsEntry = fsentry.get_realfile(); - match real_file.get_abs_path() != abs_path { - true => Some(real_file.get_abs_path()), - false => None, - } - }; - // Push path - info.push(ListItem::new(Spans::from(vec![ - Span::styled("Path: ", Style::default()), - Span::styled( - match real_path { - Some(symlink) => { - format!("{} => {}", abs_path.display(), symlink.display()) - } - None => abs_path.to_string_lossy().to_string(), - }, - Style::default() - .fg(Color::LightYellow) - .add_modifier(Modifier::BOLD), - ), - ]))); - // Push file type - if let Some(ftype) = fsentry.get_ftype() { - info.push(ListItem::new(Spans::from(vec![ - Span::styled("File type: ", Style::default()), - Span::styled( - ftype, - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ), - ]))); - } - // Push size - info.push(ListItem::new(Spans::from(vec![ - Span::styled("Size: ", Style::default()), - Span::styled( - format!("{} ({})", bsize, size), - Style::default() - .fg(Color::LightBlue) - .add_modifier(Modifier::BOLD), - ), - ]))); - // Push creation time - info.push(ListItem::new(Spans::from(vec![ - Span::styled("Creation time: ", Style::default()), - Span::styled( - ctime, - Style::default() - .fg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ), - ]))); - // Push Last change - info.push(ListItem::new(Spans::from(vec![ - Span::styled("Last change time: ", Style::default()), - Span::styled( - mtime, - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - ), - ]))); - // Push Last access - info.push(ListItem::new(Spans::from(vec![ - Span::styled("Last access time: ", Style::default()), - Span::styled( - atime, - Style::default() - .fg(Color::LightMagenta) - .add_modifier(Modifier::BOLD), - ), - ]))); - // User - #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] - let username: String = match user { - Some(uid) => match get_user_by_uid(uid) { - Some(user) => user.name().to_string_lossy().to_string(), - None => uid.to_string(), - }, - None => String::from("0"), - }; - #[cfg(target_os = "windows")] - let username: String = format!("{}", user.unwrap_or(0)); - info.push(ListItem::new(Spans::from(vec![ - Span::styled("User: ", Style::default()), - Span::styled( - username, - Style::default() - .fg(Color::LightRed) - .add_modifier(Modifier::BOLD), - ), - ]))); - // Group - #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] - let group: String = match group { - Some(gid) => match get_group_by_gid(gid) { - Some(group) => group.name().to_string_lossy().to_string(), - None => gid.to_string(), - }, - None => String::from("0"), - }; - #[cfg(target_os = "windows")] - let group: String = format!("{}", group.unwrap_or(0)); - info.push(ListItem::new(Spans::from(vec![ - Span::styled("Group: ", Style::default()), - Span::styled( - group, - Style::default() - .fg(Color::LightBlue) - .add_modifier(Modifier::BOLD), - ), - ]))); - // Finally return file name - name - } - None => String::from(""), - }; - List::new(info) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default()) - .border_type(BorderType::Rounded) - .title(file_name), - ) - .start_corner(Corner::TopLeft) - } - - /// ### 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() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Disconnect"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Switch between log tab and explorer"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Go to previous directory in stack"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Change explorer tab"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Move up/down in list"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Scroll up/down in list quickly"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Enter directory"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Upload/download file"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Delete file"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Toggle hidden files"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Change file sorting mode"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Copy"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Make directory"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Same as "), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Goto path"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Show help"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Show info about the selected file or directory"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Reload directory content"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("New file"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Open text file"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Quit TermSCP"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Rename file"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Go to parent directory"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Abort current file transfer"), - ])), - ]; - List::new(cmds) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default()) - .border_type(BorderType::Rounded) - .title("Help"), - ) - .start_corner(Corner::TopLeft) - } - - /// ### elide_wrkdir_path - /// - /// Elide working directory path if longer than width + host.len - /// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME} - fn elide_wrkdir_path(wrkdir: &Path, host: &str, width: u16) -> PathBuf { - let fmt_path: String = format!("{}", wrkdir.display()); - // NOTE: +5 is const - match fmt_path.len() + host.len() + 5 > width as usize { - false => PathBuf::from(wrkdir), - true => { - // Elide - let ancestors_len: usize = wrkdir.ancestors().count(); - let mut ancestors = wrkdir.ancestors(); - let mut elided_path: PathBuf = PathBuf::new(); - // If ancestors_len's size is bigger than 2, push count - 2 - if ancestors_len > 2 { - elided_path.push(ancestors.nth(ancestors_len - 2).unwrap()); - } - // If ancestors_len is bigger than 3, push '...' and parent too - if ancestors_len > 3 { - elided_path.push("..."); - if let Some(parent) = wrkdir.ancestors().nth(1) { - elided_path.push(parent.file_name().unwrap()); - } - } - // Push file_name - if let Some(name) = wrkdir.file_name() { - elided_path.push(name); - } - elided_path - } - } - } -} diff --git a/src/ui/activities/filetransfer_activity/misc.rs b/src/ui/activities/filetransfer_activity/misc.rs index 3c68e80..6712ffa 100644 --- a/src/ui/activities/filetransfer_activity/misc.rs +++ b/src/ui/activities/filetransfer_activity/misc.rs @@ -20,7 +20,7 @@ */ // Locals -use super::{Color, ConfigClient, FileTransferActivity, InputField, LogLevel, LogRecord, Popup}; +use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord}; use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs}; use crate::system::environment; use crate::system::sshkey_storage::SshKeyStorage; @@ -43,52 +43,20 @@ impl FileTransferActivity { self.log_records.push_front(record); // Set log index self.log_index = 0; + // Update log + let msg = self.update_logbox(); + self.update(msg); } /// ### log_and_alert /// /// Add message to log events and also display it as an alert pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) { - // Set input mode - let color: Color = match level { - LogLevel::Error => Color::Red, - LogLevel::Info => Color::Green, - LogLevel::Warn => Color::Yellow, - }; self.log(level, msg.as_str()); - self.popup = Some(Popup::Alert(color, msg)); - } - - /// ### create_quit_popup - /// - /// Create quit popup input mode (since must be shared between different input handlers) - pub(super) fn create_disconnect_popup(&mut self) -> Popup { - Popup::YesNo( - String::from("Are you sure you want to disconnect?"), - FileTransferActivity::disconnect, - FileTransferActivity::callback_nothing_to_do, - ) - } - - /// ### create_quit_popup - /// - /// Create quit popup input mode (since must be shared between different input handlers) - pub(super) fn create_quit_popup(&mut self) -> Popup { - Popup::YesNo( - String::from("Are you sure you want to quit?"), - FileTransferActivity::disconnect_and_quit, - 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, - } + self.mount_error(msg.as_str()); + // Update log + let msg = self.update_logbox(); + self.update(msg); } /// ### init_config_client @@ -152,4 +120,21 @@ impl FileTransferActivity { env::set_var("EDITOR", config_cli.get_text_editor()); } } + + /// ### read_input_event + /// + /// Read one event. + /// Returns whether at least one event has been handled + pub(super) fn read_input_event(&mut self) -> bool { + if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() { + // Handle event + let msg = self.view.on(event); + self.update(msg); + // Return true + true + } else { + // Error + false + } + } } diff --git a/src/ui/activities/filetransfer_activity/mod.rs b/src/ui/activities/filetransfer_activity/mod.rs index 66f9a35..0a5f5ef 100644 --- a/src/ui/activities/filetransfer_activity/mod.rs +++ b/src/ui/activities/filetransfer_activity/mod.rs @@ -24,11 +24,11 @@ */ // This module is split into files, cause it's just too big -mod callbacks; -mod input; -mod layout; +mod actions; mod misc; mod session; +mod update; +mod view; // Dependencies extern crate chrono; @@ -46,19 +46,41 @@ use crate::filetransfer::{FileTransfer, FileTransferProtocol}; use crate::fs::explorer::FileExplorer; use crate::fs::FsEntry; use crate::system::config_client::ConfigClient; +use crate::ui::layout::view::View; // 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::PathBuf; use std::time::Instant; -use tui::style::Color; -// Types -type DialogCallback = fn(&mut FileTransferActivity); -type OnInputSubmitCallback = fn(&mut FileTransferActivity, String); +// -- Storage keys + +const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH"; +const STORAGE_LOGBOX_WIDTH: &str = "LOGBOX_WIDTH"; + +// -- components + +const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL"; +const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE"; +const COMPONENT_LOG_BOX: &str = "LOG_BOX"; +const COMPONENT_PROGRESS_BAR: &str = "PROGRESS_BAR"; +const COMPONENT_TEXT_HELP: &str = "TEXT_HELP"; +const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR"; +const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT"; +const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL"; +const COMPONENT_INPUT_COPY: &str = "INPUT_COPY"; +const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR"; +const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO"; +const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS"; +const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE"; +const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME"; +const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT"; +const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT"; +const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING"; +const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE"; +const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO"; /// ### FileTransferParams /// @@ -72,40 +94,6 @@ pub struct FileTransferParams { pub entry_directory: 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, -} - -/// ## Popup -/// -/// Popup describes the type of popup -#[derive(Clone)] -enum Popup { - Alert(Color, String), // Block color; Block text - Fatal(String), // Must quit after being hidden - FileInfo, // Show info about current file - FileSortingDialog, // Dialog for choosing file sorting type - 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 -} - /// ## FileExplorerTab /// /// File explorer tab @@ -227,6 +215,7 @@ pub struct FileTransferActivity { pub disconnected: bool, // Has disconnected from remote? pub quit: bool, // Has quit term scp? context: Option, // Context holder + view: View, // View params: FileTransferParams, // FT connection params client: Box, // File transfer client local: FileExplorer, // Local File explorer state @@ -235,10 +224,6 @@ pub struct FileTransferActivity { log_index: usize, // Current log index entry selected log_records: VecDeque, // Log records log_size: usize, // Log records size (max) - popup: Option, // Current input mode - input_field: InputField, // Current selected input mode - input_txt: String, // Input text - choice_opt: DialogYesNoOption, // Dialog popup selected option transfer: TransferStates, // Transfer states } @@ -254,6 +239,7 @@ impl FileTransferActivity { disconnected: false, quit: false, context: None, + view: View::init(), client: match protocol { FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new( Self::make_ssh_storage(config_client.as_ref()), @@ -270,10 +256,6 @@ impl FileTransferActivity { log_index: 0, log_records: VecDeque::with_capacity(256), // 256 events is enough I guess log_size: 256, // Must match with capacity - popup: None, - input_field: InputField::Explorer, - input_txt: String::new(), - choice_opt: DialogYesNoOption::Yes, transfer: TransferStates::default(), } } @@ -306,9 +288,11 @@ impl Activity for FileTransferActivity { self.local.index_at_first(); // Configure text editor self.setup_text_editor(); + // init view + self.init(); // Verify error state from context if let Some(err) = self.context.as_mut().unwrap().get_error() { - self.popup = Some(Popup::Fatal(err)); + self.mount_fatal(&err); } } @@ -324,14 +308,14 @@ impl Activity for FileTransferActivity { return; } // Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error) - if !self.client.is_connected() && self.popup.is_none() { + if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() { // Set init state to connecting popup - self.popup = Some(Popup::Wait(format!( + self.mount_wait(format!( "Connecting to {}:{}...", self.params.address, self.params.port - ))); + ).as_str()); // Force ui draw - self.draw(); + self.view(); // Connect to remote self.connect(); // Redraw @@ -341,7 +325,7 @@ impl Activity for FileTransferActivity { redraw |= self.read_input_event(); // @! draw interface if redraw { - self.draw(); + self.view(); } } diff --git a/src/ui/activities/filetransfer_activity/session.rs b/src/ui/activities/filetransfer_activity/session.rs index ff508e4..02a22a5 100644 --- a/src/ui/activities/filetransfer_activity/session.rs +++ b/src/ui/activities/filetransfer_activity/session.rs @@ -30,7 +30,7 @@ extern crate crossterm; extern crate tempfile; // Locals -use super::{FileTransferActivity, LogLevel, Popup}; +use super::{FileTransferActivity, LogLevel}; use crate::fs::{FsEntry, FsFile}; use crate::utils::fmt::fmt_millis; @@ -41,7 +41,6 @@ use std::fs::OpenOptions; use std::io::{Read, Seek, Write}; use std::path::{Path, PathBuf}; use std::time::{Instant, SystemTime}; -use tui::style::Color; impl FileTransferActivity { /// ### connect @@ -76,12 +75,15 @@ impl FileTransferActivity { self.remote_changedir(entry_directory.as_path(), false); } // Set state to explorer - self.popup = None; + self.umount_wait(); self.reload_remote_dir(); + // Update file lists + self.update_local_filelist(); + self.update_remote_filelist(); } Err(err) => { // Set popup fatal error - self.popup = Some(Popup::Fatal(format!("{}", err))); + self.mount_fatal(&err.to_string()); } } } @@ -91,10 +93,7 @@ impl FileTransferActivity { /// disconnect from remote pub(super) fn disconnect(&mut self) { // Show popup disconnecting - self.popup = Some(Popup::Alert( - Color::Red, - String::from("Disconnecting from remote..."), - )); + self.mount_wait(format!("Disconnecting from {}...", self.params.address).as_str()); // Disconnect let _ = self.client.disconnect(); // Quit @@ -139,9 +138,6 @@ impl FileTransferActivity { FsEntry::Directory(dir) => dir.name.clone(), FsEntry::File(file) => file.name.clone(), }; - self.popup = Some(Popup::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 { @@ -152,7 +148,7 @@ impl FileTransferActivity { // Match entry match entry { FsEntry::File(file) => { - let _ = self.filetransfer_send_file(file, remote_path.as_path()); + let _ = self.filetransfer_send_file(file, remote_path.as_path(), file_name); } FsEntry::Directory(dir) => { // Create directory on remote @@ -220,12 +216,8 @@ impl FileTransferActivity { self.transfer.aborted = false; } else { // @! Successful - // Eventually, Reset input mode to explorer (if input mode is wait or progress) - if let Some(ptype) = &self.popup { - if matches!(ptype, Popup::Wait(_) | Popup::Progress(_)) { - self.popup = None - } - } + // Eventually, Remove progress bar + self.umount_progress_bar(); } } @@ -245,9 +237,6 @@ impl FileTransferActivity { FsEntry::Directory(dir) => dir.name.clone(), FsEntry::File(file) => file.name.clone(), }; - self.popup = Some(Popup::Wait(format!("Downloading \"{}\"...", file_name))); - // Draw - self.draw(); // Match entry match entry { FsEntry::File(file) => { @@ -259,7 +248,9 @@ impl FileTransferActivity { }; local_file_path.push(local_file_name.as_str()); // Download file - if let Err(err) = self.filetransfer_recv_file(local_file_path.as_path(), file) { + if let Err(err) = + self.filetransfer_recv_file(local_file_path.as_path(), file, file_name) + { self.log_and_alert(LogLevel::Error, err); } } @@ -361,14 +352,19 @@ impl FileTransferActivity { self.transfer.aborted = false; } else { // Eventually, Reset input mode to explorer - self.popup = None; + self.umount_progress_bar(); } } /// ### filetransfer_send_file /// /// Send local file and write it to remote path - fn filetransfer_send_file(&mut self, local: &FsFile, remote: &Path) -> Result<(), String> { + fn filetransfer_send_file( + &mut self, + local: &FsFile, + remote: &Path, + file_name: String, + ) -> Result<(), String> { // Upload file // Try to open local file match self @@ -389,12 +385,12 @@ impl FileTransferActivity { } // Write remote file let mut total_bytes_written: usize = 0; - // Set input state to popup progress - self.popup = Some(Popup::Progress(format!("Uploading \"{}\"", local.name))); // Reset transfer states self.transfer.reset(); let mut last_progress_val: f64 = 0.0; let mut last_input_event_fetch: Instant = Instant::now(); + // Mount progress bar + self.mount_progress_bar(); // While the entire file hasn't been completely written, // Or filetransfer has been aborted while total_bytes_written < file_size && !self.transfer.aborted { @@ -421,26 +417,33 @@ impl FileTransferActivity { buf_start += bytes; } Err(err) => { + self.umount_progress_bar(); return Err(format!( "Could not write remote file: {}", err - )) + )); } } } } } - Err(err) => return Err(format!("Could not read local file: {}", err)), + Err(err) => { + self.umount_progress_bar(); + return Err(format!("Could not read local file: {}", err)); + } } // Increase progress self.transfer.set_progress(total_bytes_written, file_size); // Draw only if a significant progress has been made (performance improvement) if last_progress_val < self.transfer.progress - 1.0 { // Draw - self.draw(); + self.update_progress_bar(format!("Uploading \"{}\"...", file_name)); + self.view(); last_progress_val = self.transfer.progress; } } + // Umount progress bar + self.umount_progress_bar(); // Finalize stream if let Err(err) = self.client.on_sent(rhnd) { self.log( @@ -482,24 +485,26 @@ impl FileTransferActivity { /// ### filetransfer_recv_file /// /// Receive file from remote and write it to local path - fn filetransfer_recv_file(&mut self, local: &Path, remote: &FsFile) -> Result<(), String> { + fn filetransfer_recv_file( + &mut self, + local: &Path, + remote: &FsFile, + file_name: String, + ) -> Result<(), String> { // Try to open local file match self.context.as_ref().unwrap().local.open_file_write(local) { Ok(mut local_file) => { // Download file from remote match self.client.recv_file(remote) { Ok(mut rhnd) => { - // Set popup progress - self.popup = Some(Popup::Progress(format!( - "Downloading \"{}\"...", - remote.name, - ))); let mut total_bytes_written: usize = 0; // Reset transfer states self.transfer.reset(); // Write local file let mut last_progress_val: f64 = 0.0; let mut last_input_event_fetch: Instant = Instant::now(); + // Mount progress bar + self.mount_progress_bar(); // While the entire file hasn't been completely read, // Or filetransfer has been aborted while total_bytes_written < remote.size && !self.transfer.aborted { @@ -524,17 +529,19 @@ impl FileTransferActivity { match local_file.write(&buffer[buf_start..bytes_read]) { Ok(bytes) => buf_start += bytes, Err(err) => { + self.umount_progress_bar(); return Err(format!( "Could not write local file: {}", err - )) + )); } } } } } Err(err) => { - return Err(format!("Could not read remote file: {}", err)) + self.umount_progress_bar(); + return Err(format!("Could not read remote file: {}", err)); } } // Set progress @@ -542,10 +549,13 @@ impl FileTransferActivity { // Draw only if a significant progress has been made (performance improvement) if last_progress_val < self.transfer.progress - 1.0 { // Draw - self.draw(); + self.update_progress_bar(format!("Downloading \"{}\"", file_name)); + self.view(); last_progress_val = self.transfer.progress; } } + // Umount progress bar + self.umount_progress_bar(); // Finalize stream if let Err(err) = self.client.on_recv(rhnd) { self.log( @@ -793,7 +803,7 @@ impl FileTransferActivity { } }; // Download file - if let Err(err) = self.filetransfer_recv_file(tmpfile.path(), file) { + if let Err(err) = self.filetransfer_recv_file(tmpfile.path(), file, file.name.clone()) { return Err(err); } // Get current file modification time @@ -853,9 +863,11 @@ impl FileTransferActivity { FsEntry::File(f) => f, }; // Send file - if let Err(err) = - self.filetransfer_send_file(tmpfile_entry, file.abs_path.as_path()) - { + if let Err(err) = self.filetransfer_send_file( + tmpfile_entry, + file.abs_path.as_path(), + file.name.clone(), + ) { return Err(err); } } diff --git a/src/ui/activities/filetransfer_activity/update.rs b/src/ui/activities/filetransfer_activity/update.rs new file mode 100644 index 0000000..01878f1 --- /dev/null +++ b/src/ui/activities/filetransfer_activity/update.rs @@ -0,0 +1,721 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/* +* +* Copyright (C) 2020-2021 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 . +* +*/ + +// deps +extern crate bytesize; +// locals +use super::{ + FileExplorerTab, FileTransferActivity, LogLevel, COMPONENT_EXPLORER_LOCAL, + COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR, + COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, + COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR, COMPONENT_RADIO_DELETE, + COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING, + COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP, +}; +use crate::fs::explorer::FileSorting; +use crate::fs::FsEntry; +use crate::ui::activities::keymap::*; +use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan, TextSpanBuilder}; +use crate::ui::layout::{Msg, Payload}; +// externals +use bytesize::ByteSize; +use std::path::{Path, PathBuf}; +use tui::style::Color; + +impl FileTransferActivity { + // -- update + + /// ### update + /// + /// Update auth activity model based on msg + /// The function exits when returns None + pub(super) fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> { + let ref_msg: Option<(&str, &Msg)> = match msg.as_ref() { + None => None, + Some((s, msg)) => Some((s, msg)), + }; + // Match msg + match ref_msg { + None => None, // Exit after None + Some(msg) => match msg { + // -- local tab + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_RIGHT) => { + // Change tab + self.view.active(COMPONENT_EXPLORER_REMOTE); + self.tab = FileExplorerTab::Remote; + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_BACKSPACE) => { + // Go to previous directory + if let Some(d) = self.local.popd() { + self.local_changedir(d.as_path(), false); + } + // Reload file list component + self.update_local_filelist() + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => { + // Toggle hidden files + self.local.toggle_hidden_files(); + // Reload file list component + self.update_local_filelist() + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => { + let file: Option = match self.get_local_file_entry() { + Some(f) => Some(f.clone()), + None => None, + }; + if let Some(file) = file { + self.mount_file_info(&file); + } + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_L) => { + // Reload directory + let pwd: PathBuf = self.local.wrkdir.clone(); + self.local_scan(pwd.as_path()); + // Reload file list component + self.update_local_filelist() + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_O) => { + // Clone entry due to mutable stuff... + if self.get_local_file_entry().is_some() { + let fsentry: FsEntry = self.get_local_file_entry().unwrap().clone(); + // Check if file + if fsentry.is_file() { + self.log( + LogLevel::Info, + format!("Opening file \"{}\"...", fsentry.get_abs_path().display()) + .as_str(), + ); + // Edit file + match self.edit_local_file(fsentry.get_abs_path().as_path()) { + Ok(_) => { + // Reload directory + let pwd: PathBuf = self.local.wrkdir.clone(); + self.local_scan(pwd.as_path()); + } + Err(err) => self.log_and_alert(LogLevel::Error, err), + } + } + } + // Reload file list component + self.update_local_filelist() + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_U) => { + // Get pwd + let path: PathBuf = self.local.wrkdir.clone(); + // Go to parent directory + if let Some(parent) = path.as_path().parent() { + self.local_changedir(parent, true); + // Reload file list component + } + self.update_local_filelist() + } + // -- remote tab + (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_LEFT) => { + // Change tab + self.view.active(COMPONENT_EXPLORER_LOCAL); + self.tab = FileExplorerTab::Local; + None + } + (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => { + // Go to previous directory + if let Some(d) = self.remote.popd() { + self.remote_changedir(d.as_path(), false); + } + // Reload file list component + self.update_remote_filelist() + } + (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_A) => { + // Toggle hidden files + self.remote.toggle_hidden_files(); + // Reload file list component + self.update_remote_filelist() + } + (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => { + let file: Option = match self.get_remote_file_entry() { + Some(f) => Some(f.clone()), + None => None, + }; + if let Some(file) = file { + self.mount_file_info(&file); + } + None + } + (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_L) => { + // Reload directory + let pwd: PathBuf = self.remote.wrkdir.clone(); + self.remote_scan(pwd.as_path()); + // Reload file list component + self.update_remote_filelist() + } + (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_O) => { + // Clone entry due to mutable stuff... + if self.get_remote_file_entry().is_some() { + let fsentry: FsEntry = self.get_remote_file_entry().unwrap().clone(); + // Check if file + if let FsEntry::File(file) = fsentry.clone() { + self.log( + LogLevel::Info, + format!("Opening file \"{}\"...", fsentry.get_abs_path().display()) + .as_str(), + ); + // Edit file + match self.edit_remote_file(&file) { + Ok(_) => { + // Reload directory + let pwd: PathBuf = self.remote.wrkdir.clone(); + self.remote_scan(pwd.as_path()); + } + Err(err) => self.log_and_alert(LogLevel::Error, err), + } + } + } + // Reload file list component + self.update_remote_filelist() + } + (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_U) => { + // Get pwd + let path: PathBuf = self.remote.wrkdir.clone(); + // Go to parent directory + if let Some(parent) = path.as_path().parent() { + self.remote_changedir(parent, true); + } + // Reload file list component + self.update_remote_filelist() + } + // -- common explorer keys + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_B) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_B) => { + // Show sorting file + self.mount_file_sorting(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_C) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_C) => { + self.mount_copy(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_D) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_D) => { + self.mount_mkdir(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_G) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_G) => { + self.mount_goto(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_H) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_H) => { + self.mount_help(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_N) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_N) => { + self.mount_newfile(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_Q) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_Q) + | (COMPONENT_LOG_BOX, &MSG_KEY_CHAR_Q) => { + self.mount_quit(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_R) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_R) => { + // Mount rename + self.mount_rename(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_S) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_S) => { + // Mount rename + self.mount_saveas(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_ESC) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_ESC) + | (COMPONENT_LOG_BOX, &MSG_KEY_ESC) => { + self.mount_disconnect(); + None + } + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_DEL) + | (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_E) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_DEL) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_E) => { + self.mount_radio_delete(); + None + } + // -- switch to log + (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_TAB) + | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_TAB) => { + self.view.active(COMPONENT_LOG_BOX); // Active log box + None + } + // -- Log box + (COMPONENT_LOG_BOX, &MSG_KEY_TAB) => { + self.view.blur(); // Blur log box + None + } + // -- copy popup + (COMPONENT_INPUT_COPY, &MSG_KEY_ESC) => { + self.umount_copy(); + None + } + (COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::Text(input))) => { + // Copy file + match self.tab { + FileExplorerTab::Local => self.action_local_copy(input.to_string()), + FileExplorerTab::Remote => self.action_remote_copy(input.to_string()), + } + self.umount_copy(); + // Reload files + match self.tab { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + } + } + // -- goto popup + (COMPONENT_INPUT_GOTO, &MSG_KEY_ESC) => { + self.umount_goto(); + None + } + (COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::Text(input))) => { + match self.tab { + FileExplorerTab::Local => self.action_change_local_dir(input.to_string()), + FileExplorerTab::Remote => self.action_change_remote_dir(input.to_string()), + } + // Umount + self.umount_goto(); + // Reload files + match self.tab { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + } + } + // -- make directory + (COMPONENT_INPUT_MKDIR, &MSG_KEY_ESC) => { + self.umount_mkdir(); + None + } + (COMPONENT_INPUT_MKDIR, Msg::OnSubmit(Payload::Text(input))) => { + match self.tab { + FileExplorerTab::Local => self.action_local_mkdir(input.to_string()), + FileExplorerTab::Remote => self.action_remote_mkdir(input.to_string()), + } + self.umount_mkdir(); + // Reload files + match self.tab { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + } + } + // -- new file + (COMPONENT_INPUT_NEWFILE, &MSG_KEY_ESC) => { + self.umount_newfile(); + None + } + (COMPONENT_INPUT_NEWFILE, Msg::OnSubmit(Payload::Text(input))) => { + match self.tab { + FileExplorerTab::Local => self.action_local_newfile(input.to_string()), + FileExplorerTab::Remote => self.action_remote_newfile(input.to_string()), + } + self.umount_newfile(); + // Reload files + match self.tab { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + } + } + // -- rename + (COMPONENT_INPUT_RENAME, &MSG_KEY_ESC) => { + self.umount_rename(); + None + } + (COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::Text(input))) => { + match self.tab { + FileExplorerTab::Local => self.action_local_rename(input.to_string()), + FileExplorerTab::Remote => self.action_remote_rename(input.to_string()), + } + self.umount_rename(); + // Reload files + match self.tab { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + } + } + // -- save as + (COMPONENT_INPUT_SAVEAS, &MSG_KEY_ESC) => { + self.umount_saveas(); + None + } + (COMPONENT_INPUT_SAVEAS, Msg::OnSubmit(Payload::Text(input))) => { + match self.tab { + FileExplorerTab::Local => self.action_local_saveas(input.to_string()), + FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()), + } + self.umount_saveas(); + // Reload files + match self.tab { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + } + } + // -- fileinfo + (COMPONENT_LIST_FILEINFO, &MSG_KEY_ENTER) + | (COMPONENT_LIST_FILEINFO, &MSG_KEY_ESC) => { + self.umount_file_info(); + None + } + // -- delete + (COMPONENT_RADIO_DELETE, &MSG_KEY_ESC) + | (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::Unsigned(1))) => { + self.umount_radio_delete(); + None + } + (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::Unsigned(0))) => { + // Choice is 'YES' + match self.tab { + FileExplorerTab::Local => self.action_local_delete(), + FileExplorerTab::Remote => self.action_remote_delete(), + } + self.umount_radio_delete(); + // Reload files + match self.tab { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + } + } + // -- disconnect + (COMPONENT_RADIO_DISCONNECT, &MSG_KEY_ESC) + | (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::Unsigned(1))) => { + self.umount_disconnect(); + None + } + (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::Unsigned(0))) => { + self.disconnect(); + self.umount_disconnect(); + None + } + // -- quit + (COMPONENT_RADIO_QUIT, &MSG_KEY_ESC) + | (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(1))) => { + self.umount_quit(); + None + } + (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(0))) => { + self.disconnect_and_quit(); + self.umount_quit(); + None + } + (COMPONENT_RADIO_SORTING, &MSG_KEY_ESC) => { + self.umount_file_sorting(); + None + } + (COMPONENT_RADIO_SORTING, Msg::OnSubmit(Payload::Unsigned(mode))) => { + // Get sorting mode + let sorting: FileSorting = match mode { + 1 => FileSorting::ByModifyTime, + 2 => FileSorting::ByCreationTime, + 3 => FileSorting::BySize, + _ => FileSorting::ByName, + }; + match self.tab { + FileExplorerTab::Local => self.local.sort_by(sorting), + FileExplorerTab::Remote => self.remote.sort_by(sorting), + } + self.umount_file_sorting(); + // Reload files + match self.tab { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + } + } + // -- error + (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) => { + self.umount_error(); + None + } + // -- fatal + (COMPONENT_TEXT_FATAL, &MSG_KEY_ESC) | (COMPONENT_TEXT_FATAL, &MSG_KEY_ENTER) => { + self.disconnected = true; + None + } + // -- help + (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) | (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) => { + self.umount_help(); + None + } + // -- fallback + (_, _) => None, // Nothing to do + }, + } + } + + /// ### update_local_filelist + /// + /// Update local file list + pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> { + match self + .view + .get_props(super::COMPONENT_EXPLORER_LOCAL) + .as_mut() + { + Some(props) => { + // Get width + let width: usize = match self + .context + .as_ref() + .unwrap() + .store + .get_unsigned(super::STORAGE_EXPLORER_WIDTH) + { + Some(val) => val, + None => 256, // Default + }; + let hostname: String = match hostname::get() { + Ok(h) => { + let hostname: String = h.as_os_str().to_string_lossy().to_string(); + let tokens: Vec<&str> = hostname.split('.').collect(); + String::from(*tokens.get(0).unwrap_or(&"localhost")) + } + Err(_) => String::from("localhost"), + }; + let hostname: String = format!( + "{}:{} ", + hostname, + FileTransferActivity::elide_wrkdir_path( + self.local.wrkdir.as_path(), + hostname.as_str(), + width + ) + .display() + ); + let files: Vec = self + .local + .iter_files() + .map(|x: &FsEntry| TextSpan::from(self.local.fmt_file(x))) + .collect(); + // Update + let props = props + .with_texts(TextParts::new(Some(hostname), Some(files))) + .build(); + // Update + self.view.update(super::COMPONENT_EXPLORER_LOCAL, props) + } + None => None, + } + } + + /// ### update_remote_filelist + /// + /// Update remote file list + pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> { + match self + .view + .get_props(super::COMPONENT_EXPLORER_REMOTE) + .as_mut() + { + Some(props) => { + // Get width + let width: usize = match self + .context + .as_ref() + .unwrap() + .store + .get_unsigned(super::STORAGE_EXPLORER_WIDTH) + { + Some(val) => val, + None => 256, // Default + }; + let hostname: String = format!( + "{}:{} ", + self.params.address, + FileTransferActivity::elide_wrkdir_path( + self.remote.wrkdir.as_path(), + self.params.address.as_str(), + width + ) + .display() + ); + let files: Vec = self + .remote + .iter_files() + .map(|x: &FsEntry| TextSpan::from(self.remote.fmt_file(x))) + .collect(); + // Update + let props = props + .with_texts(TextParts::new(Some(hostname), Some(files))) + .build(); + self.view.update(super::COMPONENT_EXPLORER_REMOTE, props) + } + None => None, + } + } + + /// ### update_logbox + /// + /// Update log box + pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> { + match self.view.get_props(super::COMPONENT_LOG_BOX).as_mut() { + Some(props) => { + // Get width + let width: usize = match self + .context + .as_ref() + .unwrap() + .store + .get_unsigned(super::STORAGE_LOGBOX_WIDTH) + { + Some(val) => val, + None => 256, // Default + }; + // Make log entries + let mut table: TableBuilder = TableBuilder::default(); + for (idx, record) in self.log_records.iter().enumerate() { + let record_rows = textwrap::wrap(record.msg.as_str(), (width as usize) - 35); // -35 'cause log prefix + // Add row if not first row + if idx > 0 { + table.add_row(); + } + let fg = match record.level { + LogLevel::Error => Color::Red, + LogLevel::Warn => Color::Yellow, + LogLevel::Info => Color::Green, + }; + for (idx, row) in record_rows.iter().enumerate() { + match idx { + 0 => { + // First row + table + .add_col(TextSpan::from(format!( + "{}", + record.time.format("%Y-%m-%dT%H:%M:%S%Z") + ))) + .add_col(TextSpan::from(" [")) + .add_col( + TextSpanBuilder::new( + format!( + "{:5}", + match record.level { + LogLevel::Error => "ERROR", + LogLevel::Warn => "WARN", + LogLevel::Info => "INFO", + } + ) + .as_str(), + ) + .with_foreground(fg) + .build(), + ) + .add_col(TextSpan::from("]: ")) + .add_col(TextSpan::from(row.as_ref())); + } + _ => { + table.add_col(TextSpan::from(textwrap::indent( + row.as_ref(), + " ", + ))); + } + } + } + } + let table = table.build(); + let props = props + .with_texts(TextParts::table(Some(String::from("Log")), table)) + .build(); + self.view.update(super::COMPONENT_LOG_BOX, props) + } + None => None, + } + } + + pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> { + match self.view.get_props(COMPONENT_PROGRESS_BAR).as_mut() { + Some(props) => { + // Calculate ETA + let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs(); + let eta: String = match self.transfer.progress as u64 { + 0 => String::from("--:--"), // NOTE: would divide by 0 :D + _ => { + let eta: u64 = + ((elapsed_secs * 100) / (self.transfer.progress as u64)) - elapsed_secs; + format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2) + } + }; + // Calculate bytes/s + let label = format!( + "{:.2}% - ETA {} ({}/s)", + self.transfer.progress, + eta, + ByteSize(self.transfer.bytes_per_second()) + ); + let props = props + .with_texts(TextParts::new( + Some(text), + Some(vec![TextSpan::from(label)]), + )) + .build(); + self.view.update(COMPONENT_PROGRESS_BAR, props) + } + None => None, + } + } + + /// ### elide_wrkdir_path + /// + /// Elide working directory path if longer than width + host.len + /// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME} + fn elide_wrkdir_path(wrkdir: &Path, host: &str, width: usize) -> PathBuf { + let fmt_path: String = format!("{}", wrkdir.display()); + // NOTE: +5 is const + match fmt_path.len() + host.len() + 5 > width { + false => PathBuf::from(wrkdir), + true => { + // Elide + let ancestors_len: usize = wrkdir.ancestors().count(); + let mut ancestors = wrkdir.ancestors(); + let mut elided_path: PathBuf = PathBuf::new(); + // If ancestors_len's size is bigger than 2, push count - 2 + if ancestors_len > 2 { + elided_path.push(ancestors.nth(ancestors_len - 2).unwrap()); + } + // If ancestors_len is bigger than 3, push '...' and parent too + if ancestors_len > 3 { + elided_path.push("..."); + if let Some(parent) = wrkdir.ancestors().nth(1) { + elided_path.push(parent.file_name().unwrap()); + } + } + // Push file_name + if let Some(name) = wrkdir.file_name() { + elided_path.push(name); + } + elided_path + } + } + } +} diff --git a/src/ui/activities/filetransfer_activity/view.rs b/src/ui/activities/filetransfer_activity/view.rs new file mode 100644 index 0000000..5754a5a --- /dev/null +++ b/src/ui/activities/filetransfer_activity/view.rs @@ -0,0 +1,897 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/* +* +* Copyright (C) 2020-2021 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 . +* +*/ + +// Deps +extern crate bytesize; +extern crate hostname; +#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] +extern crate users; +// locals +use super::{Context, FileExplorerTab, FileTransferActivity}; +use crate::fs::explorer::FileSorting; +use crate::fs::FsEntry; +use crate::ui::layout::components::{ + ctext::CText, file_list::FileList, input::Input, logbox::LogBox, progress_bar::ProgressBar, + radio_group::RadioGroup, table::Table, +}; +use crate::ui::layout::props::{ + PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder, +}; +use crate::ui::layout::utils::draw_area_in; +use crate::ui::store::Store; +use crate::utils::fmt::fmt_time; +// Ext +use bytesize::ByteSize; +use std::path::PathBuf; +use tui::{ + layout::{Constraint, Direction, Layout}, + style::Color, + widgets::Clear, +}; +#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] +use users::{get_group_by_gid, get_user_by_uid}; + +impl FileTransferActivity { + // -- init + + /// ### init + /// + /// Initialize file transfer activity's view + pub(super) fn init(&mut self) { + // Mount local file explorer + self.view.mount( + super::COMPONENT_EXPLORER_LOCAL, + Box::new(FileList::new( + PropsBuilder::default() + .with_background(Color::Yellow) + .with_foreground(Color::Yellow) + .build(), + )), + ); + // Mount remote file explorer + self.view.mount( + super::COMPONENT_EXPLORER_REMOTE, + Box::new(FileList::new( + PropsBuilder::default() + .with_background(Color::LightBlue) + .with_foreground(Color::LightBlue) + .build(), + )), + ); + // Mount log box + self.view.mount( + super::COMPONENT_LOG_BOX, + Box::new(LogBox::new( + PropsBuilder::default() + .with_foreground(Color::LightGreen) + .build(), + )), + ); + // Update components + let _ = self.update_local_filelist(); + let _ = self.update_remote_filelist(); + // Give focus to local explorer + self.view.active(super::COMPONENT_EXPLORER_LOCAL); + } + + // -- view + + /// ### view + /// + /// View gui + pub(super) fn view(&mut self) { + let mut context: Context = self.context.take().unwrap(); + let store: &mut Store = &mut context.store; + let _ = context.terminal.draw(|f| { + // Prepare chunks + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(70), // Explorer + Constraint::Percentage(30), // 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[0]); + // If width is unset in the storage, set width + if !store.isset(super::STORAGE_EXPLORER_WIDTH) { + store.set_unsigned(super::STORAGE_EXPLORER_WIDTH, tabs_chunks[0].width as usize); + } + if !store.isset(super::STORAGE_LOGBOX_WIDTH) { + store.set_unsigned(super::STORAGE_LOGBOX_WIDTH, chunks[1].width as usize); + } + // Draw explorers + self.view + .render(super::COMPONENT_EXPLORER_LOCAL, f, tabs_chunks[0]); + self.view + .render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]); + // Draw log box + self.view.render(super::COMPONENT_LOG_BOX, f, chunks[1]); + // Draw popups + if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_COPY) { + if props.build().visible { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_INPUT_COPY, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) { + if props.build().visible { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_INPUT_GOTO, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) { + if props.build().visible { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_INPUT_MKDIR, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) { + if props.build().visible { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_INPUT_NEWFILE, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) { + if props.build().visible { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_INPUT_RENAME, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) { + if props.build().visible { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_INPUT_SAVEAS, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) { + if props.build().visible { + let popup = draw_area_in(f.size(), 50, 50); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_LIST_FILEINFO, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR) { + if props.build().visible { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_PROGRESS_BAR, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) { + if props.build().visible { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_RADIO_DELETE, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) { + if props.build().visible { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.view + .render(super::COMPONENT_RADIO_DISCONNECT, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) { + if props.build().visible { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_RADIO_QUIT, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) { + if props.build().visible { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_RADIO_SORTING, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { + if props.build().visible { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_TEXT_ERROR, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_FATAL) { + if props.build().visible { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_TEXT_FATAL, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) { + if props.build().visible { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_TEXT_WAIT, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) { + if props.build().visible { + let popup = draw_area_in(f.size(), 50, 80); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_TEXT_HELP, f, popup); + } + } + }); + // Re-give context + self.context = Some(context); + } + + // -- partials + + /// ### mount_error + /// + /// Mount error box + pub(super) fn mount_error(&mut self, text: &str) { + // Mount + self.view.mount( + super::COMPONENT_TEXT_ERROR, + Box::new(CText::new( + PropsBuilder::default() + .with_foreground(Color::Red) + .bold() + .with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)]))) + .build(), + )), + ); + // Give focus to error + self.view.active(super::COMPONENT_TEXT_ERROR); + } + + /// ### umount_error + /// + /// Umount error message + pub(super) fn umount_error(&mut self) { + self.view.umount(super::COMPONENT_TEXT_ERROR); + } + + pub(super) fn mount_fatal(&mut self, text: &str) { + // Mount + self.view.mount( + super::COMPONENT_TEXT_FATAL, + Box::new(CText::new( + PropsBuilder::default() + .with_foreground(Color::Red) + .bold() + .with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)]))) + .build(), + )), + ); + // Give focus to error + self.view.active(super::COMPONENT_TEXT_FATAL); + } + + pub(super) fn mount_wait(&mut self, text: &str) { + // Mount + self.view.mount( + super::COMPONENT_TEXT_WAIT, + Box::new(CText::new( + PropsBuilder::default() + .with_foreground(Color::White) + .bold() + .with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)]))) + .build(), + )), + ); + // Give focus to info + self.view.active(super::COMPONENT_TEXT_WAIT); + } + + pub(super) fn umount_wait(&mut self) { + self.view.umount(super::COMPONENT_TEXT_WAIT); + } + + /// ### mount_quit + /// + /// Mount quit popup + pub(super) fn mount_quit(&mut self) { + // Protocol + self.view.mount( + super::COMPONENT_RADIO_QUIT, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::Yellow) + .with_background(Color::Black) + .with_texts(TextParts::new( + Some(String::from("Are you sure you want to quit?")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .build(), + )), + ); + self.view.active(super::COMPONENT_RADIO_QUIT); + } + + /// ### umount_quit + /// + /// Umount quit popup + pub(super) fn umount_quit(&mut self) { + self.view.umount(super::COMPONENT_RADIO_QUIT); + } + + /// ### mount_disconnect + /// + /// Mount disconnect popup + pub(super) fn mount_disconnect(&mut self) { + // Protocol + self.view.mount( + super::COMPONENT_RADIO_DISCONNECT, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::Yellow) + .with_background(Color::Black) + .with_texts(TextParts::new( + Some(String::from("Are you sure you want to disconnect?")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .build(), + )), + ); + self.view.active(super::COMPONENT_RADIO_DISCONNECT); + } + + /// ### umount_disconnect + /// + /// Umount disconnect popup + pub(super) fn umount_disconnect(&mut self) { + self.view.umount(super::COMPONENT_RADIO_DISCONNECT); + } + + pub(super) fn mount_copy(&mut self) { + self.view.mount( + super::COMPONENT_INPUT_COPY, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new( + Some(String::from("Insert destination name")), + None, + )) + .build(), + )), + ); + self.view.active(super::COMPONENT_INPUT_COPY); + } + + pub(super) fn umount_copy(&mut self) { + self.view.umount(super::COMPONENT_INPUT_COPY); + } + + pub(super) fn mount_goto(&mut self) { + self.view.mount( + super::COMPONENT_INPUT_GOTO, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new( + Some(String::from("Change working directory")), + None, + )) + .build(), + )), + ); + self.view.active(super::COMPONENT_INPUT_GOTO); + } + + pub(super) fn umount_goto(&mut self) { + self.view.umount(super::COMPONENT_INPUT_GOTO); + } + + pub(super) fn mount_mkdir(&mut self) { + self.view.mount( + super::COMPONENT_INPUT_MKDIR, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new( + Some(String::from("Insert directory name")), + None, + )) + .build(), + )), + ); + self.view.active(super::COMPONENT_INPUT_MKDIR); + } + + pub(super) fn umount_mkdir(&mut self) { + self.view.umount(super::COMPONENT_INPUT_MKDIR); + } + + pub(super) fn mount_newfile(&mut self) { + self.view.mount( + super::COMPONENT_INPUT_NEWFILE, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new(Some(String::from("New file name")), None)) + .build(), + )), + ); + self.view.active(super::COMPONENT_INPUT_NEWFILE); + } + + pub(super) fn umount_newfile(&mut self) { + self.view.umount(super::COMPONENT_INPUT_NEWFILE); + } + + pub(super) fn mount_rename(&mut self) { + self.view.mount( + super::COMPONENT_INPUT_RENAME, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new(Some(String::from("Insert new name")), None)) + .build(), + )), + ); + self.view.active(super::COMPONENT_INPUT_RENAME); + } + + pub(super) fn umount_rename(&mut self) { + self.view.umount(super::COMPONENT_INPUT_RENAME); + } + + pub(super) fn mount_saveas(&mut self) { + self.view.mount( + super::COMPONENT_INPUT_SAVEAS, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new(Some(String::from("Save as...")), None)) + .build(), + )), + ); + self.view.active(super::COMPONENT_INPUT_SAVEAS); + } + + pub(super) fn umount_saveas(&mut self) { + self.view.umount(super::COMPONENT_INPUT_SAVEAS); + } + + pub(super) fn mount_progress_bar(&mut self) { + self.view.mount( + super::COMPONENT_PROGRESS_BAR, + Box::new(ProgressBar::new( + PropsBuilder::default() + .with_foreground(Color::Black) + .with_background(Color::LightGreen) + .with_texts(TextParts::new(Some(String::from("Please wait")), None)) + .build(), + )), + ); + self.view.active(super::COMPONENT_PROGRESS_BAR); + } + + pub(super) fn umount_progress_bar(&mut self) { + self.view.umount(super::COMPONENT_PROGRESS_BAR); + } + + pub(super) fn mount_file_sorting(&mut self) { + let sorting: FileSorting = match self.tab { + FileExplorerTab::Local => self.local.get_file_sorting(), + FileExplorerTab::Remote => self.remote.get_file_sorting(), + }; + let index: usize = match sorting { + FileSorting::ByCreationTime => 2, + FileSorting::ByModifyTime => 1, + FileSorting::ByName => 0, + FileSorting::BySize => 3, + }; + self.view.mount( + super::COMPONENT_RADIO_SORTING, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightMagenta) + .with_background(Color::Black) + .with_texts(TextParts::new( + Some(String::from("Sort files by")), + Some(vec![ + TextSpan::from("Name"), + TextSpan::from("Modify time"), + TextSpan::from("Creation time"), + TextSpan::from("Size"), + ]), + )) + .with_value(PropValue::Unsigned(index)) + .build(), + )), + ); + self.view.active(super::COMPONENT_RADIO_SORTING); + } + + pub(super) fn umount_file_sorting(&mut self) { + self.view.umount(super::COMPONENT_RADIO_SORTING); + } + + pub(super) fn mount_radio_delete(&mut self) { + self.view.mount( + super::COMPONENT_RADIO_DELETE, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::Red) + .with_background(Color::Black) + .with_texts(TextParts::new( + Some(String::from("Delete file")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .with_value(PropValue::Unsigned(1)) + .build(), + )), + ); + self.view.active(super::COMPONENT_RADIO_DELETE); + } + + pub(super) fn umount_radio_delete(&mut self) { + self.view.umount(super::COMPONENT_RADIO_DELETE); + } + + pub(super) fn mount_file_info(&mut self, file: &FsEntry) { + let mut texts: TableBuilder = TableBuilder::default(); + // Abs path + let real_path: Option = { + let real_file: FsEntry = file.get_realfile(); + match real_file.get_abs_path() != file.get_abs_path() { + true => Some(real_file.get_abs_path()), + false => None, + } + }; + let path: String = match real_path { + Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()), + None => format!("{}", file.get_abs_path().display()), + }; + // Make texts + texts.add_col(TextSpan::from("Path: ")).add_col( + TextSpanBuilder::new(path.as_str()) + .with_foreground(Color::Yellow) + .build(), + ); + if let Some(filetype) = file.get_ftype() { + texts + .add_row() + .add_col(TextSpan::from("File type: ")) + .add_col( + TextSpanBuilder::new(filetype.as_str()) + .with_foreground(Color::LightGreen) + .build(), + ); + } + let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size()); + texts.add_row().add_col(TextSpan::from("Size: ")).add_col( + TextSpanBuilder::new(format!("{} ({})", bsize, size).as_str()) + .with_foreground(Color::Cyan) + .build(), + ); + let ctime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); + let atime: String = fmt_time(file.get_last_access_time(), "%b %d %Y %H:%M:%S"); + let mtime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); + texts + .add_row() + .add_col(TextSpan::from("Creation time: ")) + .add_col( + TextSpanBuilder::new(ctime.as_str()) + .with_foreground(Color::LightGreen) + .build(), + ); + texts + .add_row() + .add_col(TextSpan::from("Last modified time: ")) + .add_col( + TextSpanBuilder::new(mtime.as_str()) + .with_foreground(Color::LightBlue) + .build(), + ); + texts + .add_row() + .add_col(TextSpan::from("Last access time: ")) + .add_col( + TextSpanBuilder::new(atime.as_str()) + .with_foreground(Color::LightRed) + .build(), + ); + // User + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + let username: String = match file.get_user() { + Some(uid) => match get_user_by_uid(uid) { + Some(user) => user.name().to_string_lossy().to_string(), + None => uid.to_string(), + }, + None => String::from("0"), + }; + #[cfg(target_os = "windows")] + let username: String = format!("{}", file.get_user().unwrap_or(0)); + // Group + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + let group: String = match file.get_group() { + Some(gid) => match get_group_by_gid(gid) { + Some(group) => group.name().to_string_lossy().to_string(), + None => gid.to_string(), + }, + None => String::from("0"), + }; + #[cfg(target_os = "windows")] + let group: String = format!("{}", file.get_group().unwrap_or(0)); + texts.add_row().add_col(TextSpan::from("User: ")).add_col( + TextSpanBuilder::new(username.as_str()) + .with_foreground(Color::LightYellow) + .build(), + ); + texts.add_row().add_col(TextSpan::from("Group: ")).add_col( + TextSpanBuilder::new(group.as_str()) + .with_foreground(Color::Blue) + .build(), + ); + self.view.mount( + super::COMPONENT_LIST_FILEINFO, + Box::new(Table::new( + PropsBuilder::default() + .with_texts(TextParts::table( + Some(file.get_name().to_string()), + texts.build(), + )) + .build(), + )), + ); + self.view.active(super::COMPONENT_LIST_FILEINFO); + } + + pub(super) fn umount_file_info(&mut self) { + self.view.umount(super::COMPONENT_LIST_FILEINFO); + } + + /// ### mount_help + /// + /// Mount help + pub(super) fn mount_help(&mut self) { + self.view.mount( + super::COMPONENT_TEXT_HELP, + Box::new(Table::new( + PropsBuilder::default() + .with_texts(TextParts::table( + Some(String::from("Help")), + TableBuilder::default() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Disconnect")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from( + " Switch between explorer and logs", + )) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Go to previous directory")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Change explorer tab")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Move up/down in list")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Enter directory")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Upload/Download file")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Toggle hidden files")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Change file sorting mode")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Copy")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Make directory")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Go to path")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Show help")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Show info about selected file")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Reload directory content")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Create new file")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Open text file")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Quit termscp")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Rename file")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Save file as")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Go to parent directory")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Delete selected file")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Interrupt file transfer")) + .build(), + )) + .build(), + )), + ); + // Active help + self.view.active(super::COMPONENT_TEXT_HELP); + } + + pub(super) fn umount_help(&mut self) { + self.view.umount(super::COMPONENT_TEXT_HELP); + } +} diff --git a/src/ui/activities/keymap.rs b/src/ui/activities/keymap.rs index 37aa88e..5993092 100644 --- a/src/ui/activities/keymap.rs +++ b/src/ui/activities/keymap.rs @@ -44,6 +44,10 @@ pub const MSG_KEY_DEL: Msg = Msg::OnKey(KeyEvent { code: KeyCode::Delete, modifiers: KeyModifiers::NONE, }); +pub const MSG_KEY_BACKSPACE: Msg = Msg::OnKey(KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, +}); pub const MSG_KEY_DOWN: Msg = Msg::OnKey(KeyEvent { code: KeyCode::Down, modifiers: KeyModifiers::NONE, diff --git a/src/ui/layout/components/file_list.rs b/src/ui/layout/components/file_list.rs index 48f3531..67483b8 100644 --- a/src/ui/layout/components/file_list.rs +++ b/src/ui/layout/components/file_list.rs @@ -143,7 +143,7 @@ impl Component for FileList { .collect(), }; let (fg, bg): (Color, Color) = match self.states.focus { - true => (Color::Reset, self.props.background), + true => (Color::Black, self.props.background), false => (self.props.foreground, Color::Reset), }; let title: String = match self.props.texts.title.as_ref() {