Merge pull request #30 from veeso/issue-8-synchronized-browsing-of-local-and-remote-directories

Synchronized browsing of local and remote directories
This commit is contained in:
Christian Visintin
2021-05-04 09:13:42 +02:00
committed by GitHub
10 changed files with 345 additions and 115 deletions

View File

@@ -21,14 +21,20 @@
Released on FIXME: ??
- **Synchronized browsing**:
- Added the possibility to enabled the synchronized brower navigation
- when you enter a directory, the same directory will be entered on the other tab
- Enable sync browser with `<Y>`
- **Remote and Local hosts file formatter**:
- Added the possibility to set different formatters for local and remote hosts
- Enhancements
- Added a status bar in the file explorer showing whether the sync browser is enabled and which file sorting mode is selected
- Bugfix:
- Fixed wrong text wrap in log box
- Fixed error message not being shown after an upload failure
- [Issue 23](https://github.com/veeso/termscp/issues/23): Remove created file if transfer failed or was abrupted
- Dependencies:
- Added `tui-realm 0.2.1`
- Added `tui-realm 0.2.2`
- Removed `tui` (as direct dependency)
- Updated `regex` to `1.5.3`

41
Cargo.lock generated
View File

@@ -219,22 +219,6 @@ dependencies = [
"debug-helper",
]
[[package]]
name = "crossterm"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e86d73f2a0b407b5768d10a8c720cf5d2df49a9efc10ca09176d201ead4b7fb"
dependencies = [
"bitflags",
"crossterm_winapi 0.6.2",
"lazy_static",
"libc",
"mio",
"parking_lot 0.11.1",
"signal-hook",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.19.0"
@@ -242,7 +226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c"
dependencies = [
"bitflags",
"crossterm_winapi 0.7.0",
"crossterm_winapi",
"lazy_static",
"libc",
"mio",
@@ -251,15 +235,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2265c3f8e080075d9b6417aa72293fc71662f34b4af2612d8d1b074d29510db"
dependencies = [
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.7.0"
@@ -1275,7 +1250,7 @@ dependencies = [
"bytesize",
"chrono",
"content_inspector",
"crossterm 0.19.0",
"crossterm",
"dirs",
"edit",
"ftp4",
@@ -1379,24 +1354,24 @@ dependencies = [
[[package]]
name = "tui"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9"
checksum = "861d8f3ad314ede6219bcb2ab844054b1de279ee37a9bc38e3d606f9d3fb2a71"
dependencies = [
"bitflags",
"cassowary",
"crossterm 0.18.2",
"crossterm",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "tuirealm"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cade7d98a40066164d9c8e52bf01f86ecaa4fa810e854855a5a4ec3deca83c2"
checksum = "963c590368d3ab9be44ee0fccb7ad86f3611dfb4536b02be0e25cbc5685d51bb"
dependencies = [
"crossterm 0.19.0",
"crossterm",
"textwrap",
"tui",
"unicode-width",

View File

@@ -46,7 +46,7 @@ tempfile = "3.1.0"
textwrap = "0.13.4"
thiserror = "^1.0.0"
toml = "0.5.8"
tuirealm = { version = "0.2.1", features = [ "with-components" ] }
tuirealm = { version = "0.2.2", features = [ "with-components" ] }
whoami = "1.1.1"
wildmatch = "2.0.0"

View File

@@ -108,6 +108,7 @@ Password can be basically provided through 3 ways when address argument is provi
| `<S>` | Save file as... | Save |
| `<U>` | Go to parent directory | Upper |
| `<X>` | Execute a command | eXecute |
| `<Y>` | Toggle synchronized browsing | sYnc |
| `<DEL>` | Delete file | |
| `<CTRL+C>` | Abort file transfer process | |

View File

@@ -32,36 +32,156 @@ use tuirealm::{Payload, Value};
use std::path::PathBuf;
impl FileTransferActivity {
/// ### action_enter_local_dir
///
/// Enter a directory on local host from entry
/// Return true whether the directory changed
pub(super) fn action_enter_local_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
match entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true);
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name, true);
}
true
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name.clone(), true);
}
true
}
_ => false,
}
}
None => false,
}
}
}
}
/// ### action_enter_remote_dir
///
/// Enter a directory on local host from entry
/// Return true whether the directory changed
pub(super) fn action_enter_remote_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
match entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true);
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name, true);
}
true
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name.clone(), true);
}
true
}
_ => false,
}
}
None => false,
}
}
}
}
/// ### 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);
pub(super) fn action_change_local_dir(&mut self, input: String, block_sync: bool) {
let dir_path: PathBuf = self.local_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.local_changedir(dir_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(input, 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
pub(super) fn action_change_remote_dir(&mut self, input: String, block_sync: bool) {
let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.remote_changedir(dir_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(input, true);
}
}
/// ### action_go_to_previous_local_dir
///
/// Go to previous directory from localhost
pub(super) fn action_go_to_previous_local_dir(&mut self, block_sync: bool) {
if let Some(d) = self.local.popd() {
self.local_changedir(d.as_path(), false);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_go_to_previous_remote_dir(true);
}
false => dir_path,
};
self.remote_changedir(abs_dir_path.as_path(), true);
}
}
/// ### action_go_to_previous_remote_dir
///
/// Go to previous directory from remote host
pub(super) fn action_go_to_previous_remote_dir(&mut self, block_sync: bool) {
if let Some(d) = self.remote.popd() {
self.remote_changedir(d.as_path(), false);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_go_to_previous_local_dir(true);
}
}
}
/// ### action_go_to_local_upper_dir
///
/// Go to upper directory on local host
pub(super) fn action_go_to_local_upper_dir(&mut self, block_sync: bool) {
// 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);
// If sync is enabled update remote too
if self.browser.sync_browsing && !block_sync {
self.action_go_to_remote_upper_dir(true);
}
}
}
/// #### action_go_to_remote_upper_dir
///
/// Go to upper directory on remote host
pub(super) fn action_go_to_remote_upper_dir(&mut self, block_sync: bool) {
// 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);
// If sync is enabled update local too
if self.browser.sync_browsing && !block_sync {
self.action_go_to_local_upper_dir(true);
}
}
}
/// ### action_local_copy

View File

@@ -28,7 +28,7 @@ use crate::system::environment;
use crate::system::sshkey_storage::SshKeyStorage;
// Ext
use std::env;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
impl FileTransferActivity {
/// ### log
@@ -175,4 +175,32 @@ impl FileTransferActivity {
false
}
}
/// ### local_to_abs_path
///
/// Convert a path to absolute according to local explorer
pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf {
match path.is_relative() {
true => {
let mut d: PathBuf = self.local.wrkdir.clone();
d.push(path);
d
}
false => path.to_path_buf(),
}
}
/// ### remote_to_abs_path
///
/// Convert a path to absolute according to remote explorer
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
match path.is_relative() {
true => {
let mut wrkdir: PathBuf = self.remote.wrkdir.clone();
wrkdir.push(path);
wrkdir
}
false => path.to_path_buf(),
}
}
}

View File

@@ -84,6 +84,7 @@ const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
const COMPONENT_SPAN_STATUS_BAR: &str = "STATUS_BAR";
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
/// ## FileExplorerTab
@@ -202,6 +203,30 @@ impl Default for TransferStates {
}
}
/// ## Browser
///
/// Browser contains the browser options
struct Browser {
pub sync_browsing: bool,
}
impl Default for Browser {
fn default() -> Self {
Self {
sync_browsing: false,
}
}
}
impl Browser {
/// ### toggle_sync_browsing
///
/// Invert the current state for the sync browsing
pub fn toggle_sync_browsing(&mut self) {
self.sync_browsing = !self.sync_browsing;
}
}
/// ## FileTransferActivity
///
/// FileTransferActivity is the data holder for the file transfer activity
@@ -217,6 +242,7 @@ pub struct FileTransferActivity {
log_records: VecDeque<LogRecord>, // Log records
log_size: usize, // Log records size (max)
transfer: TransferStates, // Transfer states
browser: Browser, // Browser states
}
impl FileTransferActivity {
@@ -246,6 +272,7 @@ impl FileTransferActivity {
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
log_size: 256, // Must match with capacity
transfer: TransferStates::default(),
browser: Browser::default(),
}
}
}

View File

@@ -73,8 +73,9 @@ impl FileTransferActivity {
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_BACKSPACE) => {
// Go to previous directory
if let Some(d) = self.local.popd() {
self.local_changedir(d.as_path(), false);
self.action_go_to_previous_local_dir(false);
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
// Reload file list component
self.update_local_filelist()
@@ -86,25 +87,14 @@ impl FileTransferActivity {
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);
self.update_local_filelist()
}
FsEntry::File(file) => {
// Check if symlink
match &file.symlink {
Some(pointer) => match &**pointer {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true);
self.update_local_filelist()
}
_ => None,
},
None => None,
}
if self.action_enter_local_dir(entry, false) {
// Update file list if sync
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
self.update_local_filelist()
} else {
None
}
} else {
None
@@ -170,13 +160,11 @@ impl FileTransferActivity {
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.action_go_to_local_upper_dir(false);
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
// Reload file list component
self.update_local_filelist()
}
// -- remote tab
@@ -193,27 +181,14 @@ impl FileTransferActivity {
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);
self.update_remote_filelist()
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true);
self.update_remote_filelist()
}
_ => None,
}
}
None => None,
}
if self.action_enter_remote_dir(entry, false) {
// Update file list if sync
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
self.update_remote_filelist()
} else {
None
}
} else {
None
@@ -234,8 +209,10 @@ impl FileTransferActivity {
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => {
// Go to previous directory
if let Some(d) = self.remote.popd() {
self.remote_changedir(d.as_path(), false);
self.action_go_to_previous_remote_dir(false);
// If sync is enabled update local too
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
// Reload file list component
self.update_remote_filelist()
@@ -286,11 +263,9 @@ impl FileTransferActivity {
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);
self.action_go_to_remote_upper_dir(false);
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
// Reload file list component
self.update_remote_filelist()
@@ -357,6 +332,14 @@ impl FileTransferActivity {
self.mount_exec();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_Y)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_Y) => {
// Toggle browser sync
self.browser.toggle_sync_browsing();
// Update status bar
self.refresh_status_bar();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_ESC)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_ESC)
| (COMPONENT_LOG_BOX, &MSG_KEY_ESC) => {
@@ -505,12 +488,24 @@ impl FileTransferActivity {
}
(COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.tab {
FileExplorerTab::Local => self.action_change_local_dir(input.to_string()),
FileExplorerTab::Remote => self.action_change_remote_dir(input.to_string()),
FileExplorerTab::Local => {
self.action_change_local_dir(input.to_string(), false)
}
FileExplorerTab::Remote => {
self.action_change_remote_dir(input.to_string(), false)
}
_ => panic!("Found tab doesn't support GOTO"),
}
// Umount
self.umount_goto();
// Reload files if sync
if self.browser.sync_browsing {
match self.tab {
FileExplorerTab::Remote => self.update_local_filelist(),
FileExplorerTab::Local => self.update_remote_filelist(),
_ => None,
};
}
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
@@ -682,6 +677,8 @@ impl FileTransferActivity {
FileExplorerTab::Remote => self.remote.sort_by(sorting),
_ => panic!("Found result doesn't support SORTING"),
}
// Update status bar
self.refresh_status_bar();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),

View File

@@ -49,6 +49,7 @@ use tuirealm::components::{
input::{Input, InputPropsBuilder},
progress_bar::{ProgressBar, ProgressBarPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
table::{Table, TablePropsBuilder},
};
use tuirealm::props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder};
@@ -98,6 +99,13 @@ impl FileTransferActivity {
.build(),
)),
);
// Mount status bar
self.view.mount(
super::COMPONENT_SPAN_STATUS_BAR,
Box::new(Span::new(SpanPropsBuilder::default().build())),
);
// Load process bar
self.refresh_status_bar();
// Update components
let _ = self.update_local_filelist();
let _ = self.update_remote_filelist();
@@ -131,6 +139,11 @@ impl FileTransferActivity {
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.direction(Direction::Horizontal)
.split(chunks[0]);
// Create log box chunks
let bottom_chunks = Layout::default()
.constraints([Constraint::Length(1), Constraint::Length(10)].as_ref())
.direction(Direction::Vertical)
.split(chunks[1]);
// 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);
@@ -159,8 +172,11 @@ impl FileTransferActivity {
.view
.render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]),
}
// Draw log box
self.view.render(super::COMPONENT_LOG_BOX, f, chunks[1]);
// Draw log box and status bar
self.view
.render(super::COMPONENT_LOG_BOX, f, bottom_chunks[1]);
self.view
.render(super::COMPONENT_SPAN_STATUS_BAR, f, bottom_chunks[0]);
// @! Draw popups
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_COPY) {
if props.visible {
@@ -793,6 +809,41 @@ impl FileTransferActivity {
self.view.umount(super::COMPONENT_LIST_FILEINFO);
}
pub(super) fn refresh_status_bar(&mut self) {
let bar_spans: Vec<TextSpan> = vec![
TextSpanBuilder::new("Synchronized Browsing: ")
.with_foreground(Color::LightGreen)
.build(),
TextSpanBuilder::new(match self.browser.sync_browsing {
true => "ON ",
false => "OFF",
})
.with_foreground(Color::LightGreen)
.reversed()
.build(),
TextSpanBuilder::new(" Localhost file sorting: ")
.with_foreground(Color::LightYellow)
.build(),
TextSpanBuilder::new(Self::get_file_sorting_str(self.local.get_file_sorting()))
.with_foreground(Color::LightYellow)
.reversed()
.build(),
TextSpanBuilder::new(" Remote host file sorting: ")
.with_foreground(Color::LightBlue)
.build(),
TextSpanBuilder::new(Self::get_file_sorting_str(self.remote.get_file_sorting()))
.with_foreground(Color::LightBlue)
.reversed()
.build(),
];
if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR) {
self.view.update(
super::COMPONENT_SPAN_STATUS_BAR,
SpanPropsBuilder::from(props).with_spans(bar_spans).build(),
);
}
}
/// ### mount_help
///
/// Mount help
@@ -975,6 +1026,22 @@ impl FileTransferActivity {
)
.add_col(TextSpan::from(" Go to parent directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<X>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Execute shell command"))
.add_row()
.add_col(
TextSpanBuilder::new("<Y>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Toggle synchronized browsing"))
.add_row()
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
@@ -1002,4 +1069,13 @@ impl FileTransferActivity {
pub(super) fn umount_help(&mut self) {
self.view.umount(super::COMPONENT_TEXT_HELP);
}
fn get_file_sorting_str(mode: FileSorting) -> &'static str {
match mode {
FileSorting::ByName => "By name",
FileSorting::ByCreationTime => "By creation time",
FileSorting::ByModifyTime => "By modify time",
FileSorting::BySize => "By size",
}
}
}

View File

@@ -179,11 +179,11 @@ pub const MSG_KEY_CHAR_X: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('x'),
modifiers: KeyModifiers::NONE,
});
/*
pub const MSG_KEY_CHAR_Y: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('y'),
modifiers: KeyModifiers::NONE,
});
/*
pub const MSG_KEY_CHAR_Z: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('z'),
modifiers: KeyModifiers::NONE,