File transfer activity refactoring
This commit is contained in:
parent
2f3c1e7f7f
commit
cd31cc1fc9
|
@ -171,10 +171,27 @@ impl FileExplorer {
|
|||
}
|
||||
|
||||
/// ### get
|
||||
///
|
||||
/// Get file at index
|
||||
///
|
||||
/// Get file at relative index
|
||||
pub fn get(&self, idx: usize) -> Option<&FsEntry> {
|
||||
self.files.get(idx)
|
||||
let opts: ExplorerOpts = self.opts;
|
||||
let filtered = self
|
||||
.files
|
||||
.iter()
|
||||
.filter(move |x| {
|
||||
// If true, element IS NOT filtered
|
||||
let mut pass: bool = true;
|
||||
// If hidden files SHOULDN'T be shown, AND pass with not hidden
|
||||
if !opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
|
||||
pass &= !x.is_hidden();
|
||||
}
|
||||
pass
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
match filtered.get(idx) {
|
||||
None => None,
|
||||
Some(file) => Some(file),
|
||||
}
|
||||
}
|
||||
|
||||
// Formatting
|
||||
|
|
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
|
||||
use super::{Color, ConfigClient, FileTransferActivity, InputField, LogLevel, LogRecord, Popup};
|
||||
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
|
||||
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
|
||||
use crate::system::environment;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
|
@ -43,52 +43,20 @@ impl FileTransferActivity {
|
|||
self.log_records.push_front(record);
|
||||
// Set log index
|
||||
self.log_index = 0;
|
||||
// Update log
|
||||
let msg = self.update_logbox();
|
||||
self.update(msg);
|
||||
}
|
||||
|
||||
/// ### log_and_alert
|
||||
///
|
||||
/// Add message to log events and also display it as an alert
|
||||
pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) {
|
||||
// Set input mode
|
||||
let color: Color = match level {
|
||||
LogLevel::Error => Color::Red,
|
||||
LogLevel::Info => Color::Green,
|
||||
LogLevel::Warn => Color::Yellow,
|
||||
};
|
||||
self.log(level, msg.as_str());
|
||||
self.popup = Some(Popup::Alert(color, msg));
|
||||
}
|
||||
|
||||
/// ### create_quit_popup
|
||||
///
|
||||
/// Create quit popup input mode (since must be shared between different input handlers)
|
||||
pub(super) fn create_disconnect_popup(&mut self) -> Popup {
|
||||
Popup::YesNo(
|
||||
String::from("Are you sure you want to disconnect?"),
|
||||
FileTransferActivity::disconnect,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
)
|
||||
}
|
||||
|
||||
/// ### create_quit_popup
|
||||
///
|
||||
/// Create quit popup input mode (since must be shared between different input handlers)
|
||||
pub(super) fn create_quit_popup(&mut self) -> Popup {
|
||||
Popup::YesNo(
|
||||
String::from("Are you sure you want to quit?"),
|
||||
FileTransferActivity::disconnect_and_quit,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
)
|
||||
}
|
||||
|
||||
/// ### switch_input_field
|
||||
///
|
||||
/// Switch input field based on current input field
|
||||
pub(super) fn switch_input_field(&mut self) {
|
||||
self.input_field = match self.input_field {
|
||||
InputField::Explorer => InputField::Logs,
|
||||
InputField::Logs => InputField::Explorer,
|
||||
}
|
||||
self.mount_error(msg.as_str());
|
||||
// Update log
|
||||
let msg = self.update_logbox();
|
||||
self.update(msg);
|
||||
}
|
||||
|
||||
/// ### init_config_client
|
||||
|
@ -152,4 +120,21 @@ impl FileTransferActivity {
|
|||
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||
}
|
||||
}
|
||||
|
||||
/// ### read_input_event
|
||||
///
|
||||
/// Read one event.
|
||||
/// Returns whether at least one event has been handled
|
||||
pub(super) fn read_input_event(&mut self) -> bool {
|
||||
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
// Handle event
|
||||
let msg = self.view.on(event);
|
||||
self.update(msg);
|
||||
// Return true
|
||||
true
|
||||
} else {
|
||||
// Error
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,11 +24,11 @@
|
|||
*/
|
||||
|
||||
// This module is split into files, cause it's just too big
|
||||
mod callbacks;
|
||||
mod input;
|
||||
mod layout;
|
||||
mod actions;
|
||||
mod misc;
|
||||
mod session;
|
||||
mod update;
|
||||
mod view;
|
||||
|
||||
// Dependencies
|
||||
extern crate chrono;
|
||||
|
@ -46,19 +46,41 @@ use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
|||
use crate::fs::explorer::FileExplorer;
|
||||
use crate::fs::FsEntry;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::ui::layout::view::View;
|
||||
|
||||
// Includes
|
||||
use chrono::{DateTime, Local};
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use tui::style::Color;
|
||||
|
||||
// Types
|
||||
type DialogCallback = fn(&mut FileTransferActivity);
|
||||
type OnInputSubmitCallback = fn(&mut FileTransferActivity, String);
|
||||
// -- Storage keys
|
||||
|
||||
const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH";
|
||||
const STORAGE_LOGBOX_WIDTH: &str = "LOGBOX_WIDTH";
|
||||
|
||||
// -- components
|
||||
|
||||
const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL";
|
||||
const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE";
|
||||
const COMPONENT_LOG_BOX: &str = "LOG_BOX";
|
||||
const COMPONENT_PROGRESS_BAR: &str = "PROGRESS_BAR";
|
||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
|
||||
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
|
||||
const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL";
|
||||
const COMPONENT_INPUT_COPY: &str = "INPUT_COPY";
|
||||
const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR";
|
||||
const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO";
|
||||
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
|
||||
const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE";
|
||||
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
|
||||
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
|
||||
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
|
||||
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
|
||||
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
|
||||
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
|
||||
|
||||
/// ### FileTransferParams
|
||||
///
|
||||
|
@ -72,40 +94,6 @@ pub struct FileTransferParams {
|
|||
pub entry_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// ### InputField
|
||||
///
|
||||
/// Input field selected
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum InputField {
|
||||
Explorer,
|
||||
Logs,
|
||||
}
|
||||
|
||||
/// ### DialogYesNoOption
|
||||
///
|
||||
/// Current yes/no dialog option
|
||||
#[derive(std::cmp::PartialEq, Clone)]
|
||||
enum DialogYesNoOption {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// ## Popup
|
||||
///
|
||||
/// Popup describes the type of popup
|
||||
#[derive(Clone)]
|
||||
enum Popup {
|
||||
Alert(Color, String), // Block color; Block text
|
||||
Fatal(String), // Must quit after being hidden
|
||||
FileInfo, // Show info about current file
|
||||
FileSortingDialog, // Dialog for choosing file sorting type
|
||||
Help, // Show Help
|
||||
Input(String, OnInputSubmitCallback), // Input description; Callback for submit
|
||||
Progress(String), // Progress block text
|
||||
Wait(String), // Wait block text
|
||||
YesNo(String, DialogCallback, DialogCallback), // Yes, no callback
|
||||
}
|
||||
|
||||
/// ## FileExplorerTab
|
||||
///
|
||||
/// File explorer tab
|
||||
|
@ -227,6 +215,7 @@ pub struct FileTransferActivity {
|
|||
pub disconnected: bool, // Has disconnected from remote?
|
||||
pub quit: bool, // Has quit term scp?
|
||||
context: Option<Context>, // Context holder
|
||||
view: View, // View
|
||||
params: FileTransferParams, // FT connection params
|
||||
client: Box<dyn FileTransfer>, // File transfer client
|
||||
local: FileExplorer, // Local File explorer state
|
||||
|
@ -235,10 +224,6 @@ pub struct FileTransferActivity {
|
|||
log_index: usize, // Current log index entry selected
|
||||
log_records: VecDeque<LogRecord>, // Log records
|
||||
log_size: usize, // Log records size (max)
|
||||
popup: Option<Popup>, // Current input mode
|
||||
input_field: InputField, // Current selected input mode
|
||||
input_txt: String, // Input text
|
||||
choice_opt: DialogYesNoOption, // Dialog popup selected option
|
||||
transfer: TransferStates, // Transfer states
|
||||
}
|
||||
|
||||
|
@ -254,6 +239,7 @@ impl FileTransferActivity {
|
|||
disconnected: false,
|
||||
quit: false,
|
||||
context: None,
|
||||
view: View::init(),
|
||||
client: match protocol {
|
||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
||||
Self::make_ssh_storage(config_client.as_ref()),
|
||||
|
@ -270,10 +256,6 @@ impl FileTransferActivity {
|
|||
log_index: 0,
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
log_size: 256, // Must match with capacity
|
||||
popup: None,
|
||||
input_field: InputField::Explorer,
|
||||
input_txt: String::new(),
|
||||
choice_opt: DialogYesNoOption::Yes,
|
||||
transfer: TransferStates::default(),
|
||||
}
|
||||
}
|
||||
|
@ -306,9 +288,11 @@ impl Activity for FileTransferActivity {
|
|||
self.local.index_at_first();
|
||||
// Configure text editor
|
||||
self.setup_text_editor();
|
||||
// init view
|
||||
self.init();
|
||||
// Verify error state from context
|
||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||
self.popup = Some(Popup::Fatal(err));
|
||||
self.mount_fatal(&err);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,14 +308,14 @@ impl Activity for FileTransferActivity {
|
|||
return;
|
||||
}
|
||||
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||
if !self.client.is_connected() && self.popup.is_none() {
|
||||
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
|
||||
// Set init state to connecting popup
|
||||
self.popup = Some(Popup::Wait(format!(
|
||||
self.mount_wait(format!(
|
||||
"Connecting to {}:{}...",
|
||||
self.params.address, self.params.port
|
||||
)));
|
||||
).as_str());
|
||||
// Force ui draw
|
||||
self.draw();
|
||||
self.view();
|
||||
// Connect to remote
|
||||
self.connect();
|
||||
// Redraw
|
||||
|
@ -341,7 +325,7 @@ impl Activity for FileTransferActivity {
|
|||
redraw |= self.read_input_event();
|
||||
// @! draw interface
|
||||
if redraw {
|
||||
self.draw();
|
||||
self.view();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ extern crate crossterm;
|
|||
extern crate tempfile;
|
||||
|
||||
// Locals
|
||||
use super::{FileTransferActivity, LogLevel, Popup};
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
use crate::fs::{FsEntry, FsFile};
|
||||
use crate::utils::fmt::fmt_millis;
|
||||
|
||||
|
@ -41,7 +41,6 @@ use std::fs::OpenOptions;
|
|||
use std::io::{Read, Seek, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Instant, SystemTime};
|
||||
use tui::style::Color;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### connect
|
||||
|
@ -76,12 +75,15 @@ impl FileTransferActivity {
|
|||
self.remote_changedir(entry_directory.as_path(), false);
|
||||
}
|
||||
// Set state to explorer
|
||||
self.popup = None;
|
||||
self.umount_wait();
|
||||
self.reload_remote_dir();
|
||||
// Update file lists
|
||||
self.update_local_filelist();
|
||||
self.update_remote_filelist();
|
||||
}
|
||||
Err(err) => {
|
||||
// Set popup fatal error
|
||||
self.popup = Some(Popup::Fatal(format!("{}", err)));
|
||||
self.mount_fatal(&err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,10 +93,7 @@ impl FileTransferActivity {
|
|||
/// disconnect from remote
|
||||
pub(super) fn disconnect(&mut self) {
|
||||
// Show popup disconnecting
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Disconnecting from remote..."),
|
||||
));
|
||||
self.mount_wait(format!("Disconnecting from {}...", self.params.address).as_str());
|
||||
// Disconnect
|
||||
let _ = self.client.disconnect();
|
||||
// Quit
|
||||
|
@ -139,9 +138,6 @@ impl FileTransferActivity {
|
|||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
self.popup = Some(Popup::Wait(format!("Uploading \"{}\"", file_name)));
|
||||
// Draw
|
||||
self.draw();
|
||||
// Get remote path
|
||||
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
||||
let remote_file_name: PathBuf = match dst_name {
|
||||
|
@ -152,7 +148,7 @@ impl FileTransferActivity {
|
|||
// Match entry
|
||||
match entry {
|
||||
FsEntry::File(file) => {
|
||||
let _ = self.filetransfer_send_file(file, remote_path.as_path());
|
||||
let _ = self.filetransfer_send_file(file, remote_path.as_path(), file_name);
|
||||
}
|
||||
FsEntry::Directory(dir) => {
|
||||
// Create directory on remote
|
||||
|
@ -220,12 +216,8 @@ impl FileTransferActivity {
|
|||
self.transfer.aborted = false;
|
||||
} else {
|
||||
// @! Successful
|
||||
// Eventually, Reset input mode to explorer (if input mode is wait or progress)
|
||||
if let Some(ptype) = &self.popup {
|
||||
if matches!(ptype, Popup::Wait(_) | Popup::Progress(_)) {
|
||||
self.popup = None
|
||||
}
|
||||
}
|
||||
// Eventually, Remove progress bar
|
||||
self.umount_progress_bar();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,9 +237,6 @@ impl FileTransferActivity {
|
|||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
self.popup = Some(Popup::Wait(format!("Downloading \"{}\"...", file_name)));
|
||||
// Draw
|
||||
self.draw();
|
||||
// Match entry
|
||||
match entry {
|
||||
FsEntry::File(file) => {
|
||||
|
@ -259,7 +248,9 @@ impl FileTransferActivity {
|
|||
};
|
||||
local_file_path.push(local_file_name.as_str());
|
||||
// Download file
|
||||
if let Err(err) = self.filetransfer_recv_file(local_file_path.as_path(), file) {
|
||||
if let Err(err) =
|
||||
self.filetransfer_recv_file(local_file_path.as_path(), file, file_name)
|
||||
{
|
||||
self.log_and_alert(LogLevel::Error, err);
|
||||
}
|
||||
}
|
||||
|
@ -361,14 +352,19 @@ impl FileTransferActivity {
|
|||
self.transfer.aborted = false;
|
||||
} else {
|
||||
// Eventually, Reset input mode to explorer
|
||||
self.popup = None;
|
||||
self.umount_progress_bar();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### filetransfer_send_file
|
||||
///
|
||||
/// Send local file and write it to remote path
|
||||
fn filetransfer_send_file(&mut self, local: &FsFile, remote: &Path) -> Result<(), String> {
|
||||
fn filetransfer_send_file(
|
||||
&mut self,
|
||||
local: &FsFile,
|
||||
remote: &Path,
|
||||
file_name: String,
|
||||
) -> Result<(), String> {
|
||||
// Upload file
|
||||
// Try to open local file
|
||||
match self
|
||||
|
@ -389,12 +385,12 @@ impl FileTransferActivity {
|
|||
}
|
||||
// Write remote file
|
||||
let mut total_bytes_written: usize = 0;
|
||||
// Set input state to popup progress
|
||||
self.popup = Some(Popup::Progress(format!("Uploading \"{}\"", local.name)));
|
||||
// Reset transfer states
|
||||
self.transfer.reset();
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
let mut last_input_event_fetch: Instant = Instant::now();
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar();
|
||||
// While the entire file hasn't been completely written,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < file_size && !self.transfer.aborted {
|
||||
|
@ -421,26 +417,33 @@ impl FileTransferActivity {
|
|||
buf_start += bytes;
|
||||
}
|
||||
Err(err) => {
|
||||
self.umount_progress_bar();
|
||||
return Err(format!(
|
||||
"Could not write remote file: {}",
|
||||
err
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(format!("Could not read local file: {}", err)),
|
||||
Err(err) => {
|
||||
self.umount_progress_bar();
|
||||
return Err(format!("Could not read local file: {}", err));
|
||||
}
|
||||
}
|
||||
// Increase progress
|
||||
self.transfer.set_progress(total_bytes_written, file_size);
|
||||
// Draw only if a significant progress has been made (performance improvement)
|
||||
if last_progress_val < self.transfer.progress - 1.0 {
|
||||
// Draw
|
||||
self.draw();
|
||||
self.update_progress_bar(format!("Uploading \"{}\"...", file_name));
|
||||
self.view();
|
||||
last_progress_val = self.transfer.progress;
|
||||
}
|
||||
}
|
||||
// Umount progress bar
|
||||
self.umount_progress_bar();
|
||||
// Finalize stream
|
||||
if let Err(err) = self.client.on_sent(rhnd) {
|
||||
self.log(
|
||||
|
@ -482,24 +485,26 @@ impl FileTransferActivity {
|
|||
/// ### filetransfer_recv_file
|
||||
///
|
||||
/// Receive file from remote and write it to local path
|
||||
fn filetransfer_recv_file(&mut self, local: &Path, remote: &FsFile) -> Result<(), String> {
|
||||
fn filetransfer_recv_file(
|
||||
&mut self,
|
||||
local: &Path,
|
||||
remote: &FsFile,
|
||||
file_name: String,
|
||||
) -> Result<(), String> {
|
||||
// Try to open local file
|
||||
match self.context.as_ref().unwrap().local.open_file_write(local) {
|
||||
Ok(mut local_file) => {
|
||||
// Download file from remote
|
||||
match self.client.recv_file(remote) {
|
||||
Ok(mut rhnd) => {
|
||||
// Set popup progress
|
||||
self.popup = Some(Popup::Progress(format!(
|
||||
"Downloading \"{}\"...",
|
||||
remote.name,
|
||||
)));
|
||||
let mut total_bytes_written: usize = 0;
|
||||
// Reset transfer states
|
||||
self.transfer.reset();
|
||||
// Write local file
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
let mut last_input_event_fetch: Instant = Instant::now();
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar();
|
||||
// While the entire file hasn't been completely read,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < remote.size && !self.transfer.aborted {
|
||||
|
@ -524,17 +529,19 @@ impl FileTransferActivity {
|
|||
match local_file.write(&buffer[buf_start..bytes_read]) {
|
||||
Ok(bytes) => buf_start += bytes,
|
||||
Err(err) => {
|
||||
self.umount_progress_bar();
|
||||
return Err(format!(
|
||||
"Could not write local file: {}",
|
||||
err
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(format!("Could not read remote file: {}", err))
|
||||
self.umount_progress_bar();
|
||||
return Err(format!("Could not read remote file: {}", err));
|
||||
}
|
||||
}
|
||||
// Set progress
|
||||
|
@ -542,10 +549,13 @@ impl FileTransferActivity {
|
|||
// Draw only if a significant progress has been made (performance improvement)
|
||||
if last_progress_val < self.transfer.progress - 1.0 {
|
||||
// Draw
|
||||
self.draw();
|
||||
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
||||
self.view();
|
||||
last_progress_val = self.transfer.progress;
|
||||
}
|
||||
}
|
||||
// Umount progress bar
|
||||
self.umount_progress_bar();
|
||||
// Finalize stream
|
||||
if let Err(err) = self.client.on_recv(rhnd) {
|
||||
self.log(
|
||||
|
@ -793,7 +803,7 @@ impl FileTransferActivity {
|
|||
}
|
||||
};
|
||||
// Download file
|
||||
if let Err(err) = self.filetransfer_recv_file(tmpfile.path(), file) {
|
||||
if let Err(err) = self.filetransfer_recv_file(tmpfile.path(), file, file.name.clone()) {
|
||||
return Err(err);
|
||||
}
|
||||
// Get current file modification time
|
||||
|
@ -853,9 +863,11 @@ impl FileTransferActivity {
|
|||
FsEntry::File(f) => f,
|
||||
};
|
||||
// Send file
|
||||
if let Err(err) =
|
||||
self.filetransfer_send_file(tmpfile_entry, file.abs_path.as_path())
|
||||
{
|
||||
if let Err(err) = self.filetransfer_send_file(
|
||||
tmpfile_entry,
|
||||
file.abs_path.as_path(),
|
||||
file.name.clone(),
|
||||
) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
|
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,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
pub const MSG_KEY_BACKSPACE: Msg = Msg::OnKey(KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
pub const MSG_KEY_DOWN: Msg = Msg::OnKey(KeyEvent {
|
||||
code: KeyCode::Down,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
|
|
@ -143,7 +143,7 @@ impl Component for FileList {
|
|||
.collect(),
|
||||
};
|
||||
let (fg, bg): (Color, Color) = match self.states.focus {
|
||||
true => (Color::Reset, self.props.background),
|
||||
true => (Color::Black, self.props.background),
|
||||
false => (self.props.foreground, Color::Reset),
|
||||
};
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
|
|
Loading…
Reference in a new issue