File transfer activity refactoring

This commit is contained in:
veeso 2021-03-20 21:06:12 +01:00
parent 2f3c1e7f7f
commit cd31cc1fc9
12 changed files with 2250 additions and 2402 deletions

View file

@ -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::<Vec<_>>();
match filtered.get(idx) {
None => None,
Some(file) => Some(file),
}
}
// Formatting

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<FsEntry> = 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<FsEntry> = 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<usize> {
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<usize> {
match self.view.get_value(super::COMPONENT_EXPLORER_REMOTE) {
Some(Payload::Unsigned(idx)) => Some(idx),
_ => None,
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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());
}
}
}
}
}
}
}
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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 <https://github.com/rust-lang/rust/issues/59159>
// NOTE: Do you want my opinion about that issue? It's a bs and doesn't make any sense.
let popup: Option<Popup> = 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(), // <TAB> switch tab
KeyCode::Right => self.tab = FileExplorerTab::Remote, // <RIGHT> 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<FsEntry> = 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(), // <TAB> switch tab
KeyCode::Left => self.tab = FileExplorerTab::Local, // <LEFT> 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<FsEntry> = 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(), // <TAB> 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 <LEFT> 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 <RIGHT> 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
});
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<ListItem> = 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<ListItem> = 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<ListItem> = 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<Spans> = 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<ListItem> = 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<ListItem> = 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<Spans> = 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<ListItem> = 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<Spans> = 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<ListItem> = 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<u32> = fsentry.get_user();
let group: Option<u32> = fsentry.get_group();
let real_path: Option<PathBuf> = {
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<ListItem> = vec![
ListItem::new(Spans::from(vec![
Span::styled(
"<ESC>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Disconnect"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<TAB>",
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(
"<BACKSPACE>",
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(
"<RIGHT/LEFT>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Change explorer tab"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<UP/DOWN>",
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(
"<PGUP/PGDOWN>",
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(
"<ENTER>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Enter directory"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<SPACE>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Upload/download file"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<DEL>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Delete file"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<A>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Toggle hidden files"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<B>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Change file sorting mode"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<C>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Copy"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<D>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Make directory"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<E>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Same as <DEL>"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<G>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Goto path"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<H>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Show help"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<I>",
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(
"<L>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Reload directory content"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<N>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("New file"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<O>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Open text file"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<Q>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Quit TermSCP"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<R>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Rename file"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<U>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Go to parent directory"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+C>",
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
}
}
}
}

View file

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

View file

@ -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<PathBuf>,
}
/// ### 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>, // Context holder
view: View, // View
params: FileTransferParams, // FT connection params
client: Box<dyn FileTransfer>, // 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<LogRecord>, // Log records
log_size: usize, // Log records size (max)
popup: Option<Popup>, // 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();
}
}

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<FsEntry> = 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<FsEntry> = 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<TextSpan> = 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<TextSpan> = 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
}
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<PathBuf> = {
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("<ESC>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Disconnect"))
.add_row()
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(
" Switch between explorer and logs",
))
.add_row()
.add_col(
TextSpanBuilder::new("<BACKSPACE>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Go to previous directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change explorer tab"))
.add_row()
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Move up/down in list"))
.add_row()
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Enter directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<SPACE>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Upload/Download file"))
.add_row()
.add_col(
TextSpanBuilder::new("<A>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Toggle hidden files"))
.add_row()
.add_col(
TextSpanBuilder::new("<B>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change file sorting mode"))
.add_row()
.add_col(
TextSpanBuilder::new("<C>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Copy"))
.add_row()
.add_col(
TextSpanBuilder::new("<D>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Make directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<G>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Go to path"))
.add_row()
.add_col(
TextSpanBuilder::new("<H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Show help"))
.add_row()
.add_col(
TextSpanBuilder::new("<I>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Show info about selected file"))
.add_row()
.add_col(
TextSpanBuilder::new("<L>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Reload directory content"))
.add_row()
.add_col(
TextSpanBuilder::new("<N>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Create new file"))
.add_row()
.add_col(
TextSpanBuilder::new("<O>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Open text file"))
.add_row()
.add_col(
TextSpanBuilder::new("<Q>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Quit termscp"))
.add_row()
.add_col(
TextSpanBuilder::new("<R>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Rename file"))
.add_row()
.add_col(
TextSpanBuilder::new("<S>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Save file as"))
.add_row()
.add_col(
TextSpanBuilder::new("<U>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Go to parent directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Delete selected file"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+C>")
.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);
}
}

View file

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

View file

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