mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
File transfer activity refactoring
This commit is contained in:
@@ -171,10 +171,27 @@ impl FileExplorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// ### get
|
/// ### get
|
||||||
///
|
///
|
||||||
/// Get file at index
|
/// Get file at relative index
|
||||||
pub fn get(&self, idx: usize) -> Option<&FsEntry> {
|
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
|
// Formatting
|
||||||
|
|||||||
488
src/ui/activities/filetransfer_activity/actions.rs
Normal file
488
src/ui/activities/filetransfer_activity/actions.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Locals
|
// 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::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
@@ -43,52 +43,20 @@ impl FileTransferActivity {
|
|||||||
self.log_records.push_front(record);
|
self.log_records.push_front(record);
|
||||||
// Set log index
|
// Set log index
|
||||||
self.log_index = 0;
|
self.log_index = 0;
|
||||||
|
// Update log
|
||||||
|
let msg = self.update_logbox();
|
||||||
|
self.update(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### log_and_alert
|
/// ### log_and_alert
|
||||||
///
|
///
|
||||||
/// Add message to log events and also display it as an 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) {
|
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.log(level, msg.as_str());
|
||||||
self.popup = Some(Popup::Alert(color, msg));
|
self.mount_error(msg.as_str());
|
||||||
}
|
// Update log
|
||||||
|
let msg = self.update_logbox();
|
||||||
/// ### create_quit_popup
|
self.update(msg);
|
||||||
///
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### init_config_client
|
/// ### init_config_client
|
||||||
@@ -152,4 +120,21 @@ impl FileTransferActivity {
|
|||||||
env::set_var("EDITOR", config_cli.get_text_editor());
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// This module is split into files, cause it's just too big
|
// This module is split into files, cause it's just too big
|
||||||
mod callbacks;
|
mod actions;
|
||||||
mod input;
|
|
||||||
mod layout;
|
|
||||||
mod misc;
|
mod misc;
|
||||||
mod session;
|
mod session;
|
||||||
|
mod update;
|
||||||
|
mod view;
|
||||||
|
|
||||||
// Dependencies
|
// Dependencies
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
@@ -46,19 +46,41 @@ use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
|||||||
use crate::fs::explorer::FileExplorer;
|
use crate::fs::explorer::FileExplorer;
|
||||||
use crate::fs::FsEntry;
|
use crate::fs::FsEntry;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
|
use crate::ui::layout::view::View;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use crossterm::event::Event as InputEvent;
|
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tui::style::Color;
|
|
||||||
|
|
||||||
// Types
|
// -- Storage keys
|
||||||
type DialogCallback = fn(&mut FileTransferActivity);
|
|
||||||
type OnInputSubmitCallback = fn(&mut FileTransferActivity, String);
|
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
|
/// ### FileTransferParams
|
||||||
///
|
///
|
||||||
@@ -72,40 +94,6 @@ pub struct FileTransferParams {
|
|||||||
pub entry_directory: Option<PathBuf>,
|
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
|
/// ## FileExplorerTab
|
||||||
///
|
///
|
||||||
/// File explorer tab
|
/// File explorer tab
|
||||||
@@ -227,6 +215,7 @@ pub struct FileTransferActivity {
|
|||||||
pub disconnected: bool, // Has disconnected from remote?
|
pub disconnected: bool, // Has disconnected from remote?
|
||||||
pub quit: bool, // Has quit term scp?
|
pub quit: bool, // Has quit term scp?
|
||||||
context: Option<Context>, // Context holder
|
context: Option<Context>, // Context holder
|
||||||
|
view: View, // View
|
||||||
params: FileTransferParams, // FT connection params
|
params: FileTransferParams, // FT connection params
|
||||||
client: Box<dyn FileTransfer>, // File transfer client
|
client: Box<dyn FileTransfer>, // File transfer client
|
||||||
local: FileExplorer, // Local File explorer state
|
local: FileExplorer, // Local File explorer state
|
||||||
@@ -235,10 +224,6 @@ pub struct FileTransferActivity {
|
|||||||
log_index: usize, // Current log index entry selected
|
log_index: usize, // Current log index entry selected
|
||||||
log_records: VecDeque<LogRecord>, // Log records
|
log_records: VecDeque<LogRecord>, // Log records
|
||||||
log_size: usize, // Log records size (max)
|
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
|
transfer: TransferStates, // Transfer states
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +239,7 @@ impl FileTransferActivity {
|
|||||||
disconnected: false,
|
disconnected: false,
|
||||||
quit: false,
|
quit: false,
|
||||||
context: None,
|
context: None,
|
||||||
|
view: View::init(),
|
||||||
client: match protocol {
|
client: match protocol {
|
||||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
||||||
Self::make_ssh_storage(config_client.as_ref()),
|
Self::make_ssh_storage(config_client.as_ref()),
|
||||||
@@ -270,10 +256,6 @@ impl FileTransferActivity {
|
|||||||
log_index: 0,
|
log_index: 0,
|
||||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||||
log_size: 256, // Must match with capacity
|
log_size: 256, // Must match with capacity
|
||||||
popup: None,
|
|
||||||
input_field: InputField::Explorer,
|
|
||||||
input_txt: String::new(),
|
|
||||||
choice_opt: DialogYesNoOption::Yes,
|
|
||||||
transfer: TransferStates::default(),
|
transfer: TransferStates::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,9 +288,11 @@ impl Activity for FileTransferActivity {
|
|||||||
self.local.index_at_first();
|
self.local.index_at_first();
|
||||||
// Configure text editor
|
// Configure text editor
|
||||||
self.setup_text_editor();
|
self.setup_text_editor();
|
||||||
|
// init view
|
||||||
|
self.init();
|
||||||
// Verify error state from context
|
// Verify error state from context
|
||||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
|
// 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
|
// Set init state to connecting popup
|
||||||
self.popup = Some(Popup::Wait(format!(
|
self.mount_wait(format!(
|
||||||
"Connecting to {}:{}...",
|
"Connecting to {}:{}...",
|
||||||
self.params.address, self.params.port
|
self.params.address, self.params.port
|
||||||
)));
|
).as_str());
|
||||||
// Force ui draw
|
// Force ui draw
|
||||||
self.draw();
|
self.view();
|
||||||
// Connect to remote
|
// Connect to remote
|
||||||
self.connect();
|
self.connect();
|
||||||
// Redraw
|
// Redraw
|
||||||
@@ -341,7 +325,7 @@ impl Activity for FileTransferActivity {
|
|||||||
redraw |= self.read_input_event();
|
redraw |= self.read_input_event();
|
||||||
// @! draw interface
|
// @! draw interface
|
||||||
if redraw {
|
if redraw {
|
||||||
self.draw();
|
self.view();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ extern crate crossterm;
|
|||||||
extern crate tempfile;
|
extern crate tempfile;
|
||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
use super::{FileTransferActivity, LogLevel, Popup};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
use crate::fs::{FsEntry, FsFile};
|
use crate::fs::{FsEntry, FsFile};
|
||||||
use crate::utils::fmt::fmt_millis;
|
use crate::utils::fmt::fmt_millis;
|
||||||
|
|
||||||
@@ -41,7 +41,6 @@ use std::fs::OpenOptions;
|
|||||||
use std::io::{Read, Seek, Write};
|
use std::io::{Read, Seek, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{Instant, SystemTime};
|
use std::time::{Instant, SystemTime};
|
||||||
use tui::style::Color;
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// ### connect
|
/// ### connect
|
||||||
@@ -76,12 +75,15 @@ impl FileTransferActivity {
|
|||||||
self.remote_changedir(entry_directory.as_path(), false);
|
self.remote_changedir(entry_directory.as_path(), false);
|
||||||
}
|
}
|
||||||
// Set state to explorer
|
// Set state to explorer
|
||||||
self.popup = None;
|
self.umount_wait();
|
||||||
self.reload_remote_dir();
|
self.reload_remote_dir();
|
||||||
|
// Update file lists
|
||||||
|
self.update_local_filelist();
|
||||||
|
self.update_remote_filelist();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Set popup fatal error
|
// 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
|
/// disconnect from remote
|
||||||
pub(super) fn disconnect(&mut self) {
|
pub(super) fn disconnect(&mut self) {
|
||||||
// Show popup disconnecting
|
// Show popup disconnecting
|
||||||
self.popup = Some(Popup::Alert(
|
self.mount_wait(format!("Disconnecting from {}...", self.params.address).as_str());
|
||||||
Color::Red,
|
|
||||||
String::from("Disconnecting from remote..."),
|
|
||||||
));
|
|
||||||
// Disconnect
|
// Disconnect
|
||||||
let _ = self.client.disconnect();
|
let _ = self.client.disconnect();
|
||||||
// Quit
|
// Quit
|
||||||
@@ -139,9 +138,6 @@ impl FileTransferActivity {
|
|||||||
FsEntry::Directory(dir) => dir.name.clone(),
|
FsEntry::Directory(dir) => dir.name.clone(),
|
||||||
FsEntry::File(file) => file.name.clone(),
|
FsEntry::File(file) => file.name.clone(),
|
||||||
};
|
};
|
||||||
self.popup = Some(Popup::Wait(format!("Uploading \"{}\"", file_name)));
|
|
||||||
// Draw
|
|
||||||
self.draw();
|
|
||||||
// Get remote path
|
// Get remote path
|
||||||
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
||||||
let remote_file_name: PathBuf = match dst_name {
|
let remote_file_name: PathBuf = match dst_name {
|
||||||
@@ -152,7 +148,7 @@ impl FileTransferActivity {
|
|||||||
// Match entry
|
// Match entry
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::File(file) => {
|
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) => {
|
FsEntry::Directory(dir) => {
|
||||||
// Create directory on remote
|
// Create directory on remote
|
||||||
@@ -220,12 +216,8 @@ impl FileTransferActivity {
|
|||||||
self.transfer.aborted = false;
|
self.transfer.aborted = false;
|
||||||
} else {
|
} else {
|
||||||
// @! Successful
|
// @! Successful
|
||||||
// Eventually, Reset input mode to explorer (if input mode is wait or progress)
|
// Eventually, Remove progress bar
|
||||||
if let Some(ptype) = &self.popup {
|
self.umount_progress_bar();
|
||||||
if matches!(ptype, Popup::Wait(_) | Popup::Progress(_)) {
|
|
||||||
self.popup = None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,9 +237,6 @@ impl FileTransferActivity {
|
|||||||
FsEntry::Directory(dir) => dir.name.clone(),
|
FsEntry::Directory(dir) => dir.name.clone(),
|
||||||
FsEntry::File(file) => file.name.clone(),
|
FsEntry::File(file) => file.name.clone(),
|
||||||
};
|
};
|
||||||
self.popup = Some(Popup::Wait(format!("Downloading \"{}\"...", file_name)));
|
|
||||||
// Draw
|
|
||||||
self.draw();
|
|
||||||
// Match entry
|
// Match entry
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
@@ -259,7 +248,9 @@ impl FileTransferActivity {
|
|||||||
};
|
};
|
||||||
local_file_path.push(local_file_name.as_str());
|
local_file_path.push(local_file_name.as_str());
|
||||||
// Download file
|
// 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);
|
self.log_and_alert(LogLevel::Error, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,14 +352,19 @@ impl FileTransferActivity {
|
|||||||
self.transfer.aborted = false;
|
self.transfer.aborted = false;
|
||||||
} else {
|
} else {
|
||||||
// Eventually, Reset input mode to explorer
|
// Eventually, Reset input mode to explorer
|
||||||
self.popup = None;
|
self.umount_progress_bar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### filetransfer_send_file
|
/// ### filetransfer_send_file
|
||||||
///
|
///
|
||||||
/// Send local file and write it to remote path
|
/// 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
|
// Upload file
|
||||||
// Try to open local file
|
// Try to open local file
|
||||||
match self
|
match self
|
||||||
@@ -389,12 +385,12 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
// Write remote file
|
// Write remote file
|
||||||
let mut total_bytes_written: usize = 0;
|
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
|
// Reset transfer states
|
||||||
self.transfer.reset();
|
self.transfer.reset();
|
||||||
let mut last_progress_val: f64 = 0.0;
|
let mut last_progress_val: f64 = 0.0;
|
||||||
let mut last_input_event_fetch: Instant = Instant::now();
|
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,
|
// While the entire file hasn't been completely written,
|
||||||
// Or filetransfer has been aborted
|
// Or filetransfer has been aborted
|
||||||
while total_bytes_written < file_size && !self.transfer.aborted {
|
while total_bytes_written < file_size && !self.transfer.aborted {
|
||||||
@@ -421,26 +417,33 @@ impl FileTransferActivity {
|
|||||||
buf_start += bytes;
|
buf_start += bytes;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
self.umount_progress_bar();
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Could not write remote file: {}",
|
"Could not write remote file: {}",
|
||||||
err
|
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
|
// Increase progress
|
||||||
self.transfer.set_progress(total_bytes_written, file_size);
|
self.transfer.set_progress(total_bytes_written, file_size);
|
||||||
// Draw only if a significant progress has been made (performance improvement)
|
// Draw only if a significant progress has been made (performance improvement)
|
||||||
if last_progress_val < self.transfer.progress - 1.0 {
|
if last_progress_val < self.transfer.progress - 1.0 {
|
||||||
// Draw
|
// Draw
|
||||||
self.draw();
|
self.update_progress_bar(format!("Uploading \"{}\"...", file_name));
|
||||||
|
self.view();
|
||||||
last_progress_val = self.transfer.progress;
|
last_progress_val = self.transfer.progress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Umount progress bar
|
||||||
|
self.umount_progress_bar();
|
||||||
// Finalize stream
|
// Finalize stream
|
||||||
if let Err(err) = self.client.on_sent(rhnd) {
|
if let Err(err) = self.client.on_sent(rhnd) {
|
||||||
self.log(
|
self.log(
|
||||||
@@ -482,24 +485,26 @@ impl FileTransferActivity {
|
|||||||
/// ### filetransfer_recv_file
|
/// ### filetransfer_recv_file
|
||||||
///
|
///
|
||||||
/// Receive file from remote and write it to local path
|
/// 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
|
// Try to open local file
|
||||||
match self.context.as_ref().unwrap().local.open_file_write(local) {
|
match self.context.as_ref().unwrap().local.open_file_write(local) {
|
||||||
Ok(mut local_file) => {
|
Ok(mut local_file) => {
|
||||||
// Download file from remote
|
// Download file from remote
|
||||||
match self.client.recv_file(remote) {
|
match self.client.recv_file(remote) {
|
||||||
Ok(mut rhnd) => {
|
Ok(mut rhnd) => {
|
||||||
// Set popup progress
|
|
||||||
self.popup = Some(Popup::Progress(format!(
|
|
||||||
"Downloading \"{}\"...",
|
|
||||||
remote.name,
|
|
||||||
)));
|
|
||||||
let mut total_bytes_written: usize = 0;
|
let mut total_bytes_written: usize = 0;
|
||||||
// Reset transfer states
|
// Reset transfer states
|
||||||
self.transfer.reset();
|
self.transfer.reset();
|
||||||
// Write local file
|
// Write local file
|
||||||
let mut last_progress_val: f64 = 0.0;
|
let mut last_progress_val: f64 = 0.0;
|
||||||
let mut last_input_event_fetch: Instant = Instant::now();
|
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,
|
// While the entire file hasn't been completely read,
|
||||||
// Or filetransfer has been aborted
|
// Or filetransfer has been aborted
|
||||||
while total_bytes_written < remote.size && !self.transfer.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]) {
|
match local_file.write(&buffer[buf_start..bytes_read]) {
|
||||||
Ok(bytes) => buf_start += bytes,
|
Ok(bytes) => buf_start += bytes,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
self.umount_progress_bar();
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Could not write local file: {}",
|
"Could not write local file: {}",
|
||||||
err
|
err
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(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
|
// Set progress
|
||||||
@@ -542,10 +549,13 @@ impl FileTransferActivity {
|
|||||||
// Draw only if a significant progress has been made (performance improvement)
|
// Draw only if a significant progress has been made (performance improvement)
|
||||||
if last_progress_val < self.transfer.progress - 1.0 {
|
if last_progress_val < self.transfer.progress - 1.0 {
|
||||||
// Draw
|
// Draw
|
||||||
self.draw();
|
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
||||||
|
self.view();
|
||||||
last_progress_val = self.transfer.progress;
|
last_progress_val = self.transfer.progress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Umount progress bar
|
||||||
|
self.umount_progress_bar();
|
||||||
// Finalize stream
|
// Finalize stream
|
||||||
if let Err(err) = self.client.on_recv(rhnd) {
|
if let Err(err) = self.client.on_recv(rhnd) {
|
||||||
self.log(
|
self.log(
|
||||||
@@ -793,7 +803,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Download file
|
// 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);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Get current file modification time
|
// Get current file modification time
|
||||||
@@ -853,9 +863,11 @@ impl FileTransferActivity {
|
|||||||
FsEntry::File(f) => f,
|
FsEntry::File(f) => f,
|
||||||
};
|
};
|
||||||
// Send file
|
// Send file
|
||||||
if let Err(err) =
|
if let Err(err) = self.filetransfer_send_file(
|
||||||
self.filetransfer_send_file(tmpfile_entry, file.abs_path.as_path())
|
tmpfile_entry,
|
||||||
{
|
file.abs_path.as_path(),
|
||||||
|
file.name.clone(),
|
||||||
|
) {
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
721
src/ui/activities/filetransfer_activity/update.rs
Normal file
721
src/ui/activities/filetransfer_activity/update.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
897
src/ui/activities/filetransfer_activity/view.rs
Normal file
897
src/ui/activities/filetransfer_activity/view.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,10 @@ pub const MSG_KEY_DEL: Msg = Msg::OnKey(KeyEvent {
|
|||||||
code: KeyCode::Delete,
|
code: KeyCode::Delete,
|
||||||
modifiers: KeyModifiers::NONE,
|
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 {
|
pub const MSG_KEY_DOWN: Msg = Msg::OnKey(KeyEvent {
|
||||||
code: KeyCode::Down,
|
code: KeyCode::Down,
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ impl Component for FileList {
|
|||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
let (fg, bg): (Color, Color) = match self.states.focus {
|
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),
|
false => (self.props.foreground, Color::Reset),
|
||||||
};
|
};
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
let title: String = match self.props.texts.title.as_ref() {
|
||||||
|
|||||||
Reference in New Issue
Block a user