From 5f53c654de41ddf848ae288d63cc53092f7de254 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Nov 2021 12:05:48 +0100 Subject: [PATCH 01/45] 0.8.0 setup --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- docs/de/README.md | 2 +- docs/es/README.md | 2 +- docs/fr/README.md | 2 +- docs/it/README.md | 2 +- docs/zh-CN/README.md | 2 +- install.sh | 2 +- 10 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5284d7..f27d084a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - [Changelog](#changelog) + - [0.8.0](#080) - [0.7.0](#070) - [0.6.1](#061) - [0.6.0](#060) @@ -21,6 +22,12 @@ --- +## 0.8.0 + +Released on FIXME: + +> ❄️ Winter update 2022 ⛄ + ## 0.7.0 Released on 12/10/2021 diff --git a/Cargo.lock b/Cargo.lock index 321d4d4c..70125ae6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,7 +2176,7 @@ dependencies = [ [[package]] name = "termscp" -version = "0.7.0" +version = "0.8.0" dependencies = [ "argh", "bitflags 1.3.2", diff --git a/Cargo.toml b/Cargo.toml index 4cf49029..92d61603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" name = "termscp" readme = "README.md" repository = "https://github.com/veeso/termscp" -version = "0.7.0" +version = "0.8.0" [package.metadata.rpm] package = "termscp" diff --git a/README.md b/README.md index ef27600e..03145842 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@

Developed by @veeso

-

Current version: 0.7.0 (12/10/2021)

+

Current version: 0.8.0 (12/10/2021)

Entwickelt von @veeso

-

Aktuelle Version: 0.7.0 (12/10/2021)

+

Aktuelle Version: 0.8.0 (12/10/2021)

Desarrollado por @veeso

-

Versión actual: 0.7.0 (12/10/2021)

+

Versión actual: 0.8.0 (12/10/2021)

Développé par @veeso

-

Version actuelle: 0.7.0 (12/10/2021)

+

Version actuelle: 0.8.0 (12/10/2021)

Sviluppato da @veeso

-

Versione corrente: 0.7.0 (12/10/2021)

+

Versione corrente: 0.8.0 (12/10/2021)

@veeso 开发

-

当前版本: 0.7.0 (12/10/2021)

+

当前版本: 0.8.0 (12/10/2021)

Date: Tue, 9 Nov 2021 12:10:21 +0100 Subject: [PATCH 02/45] Rust 2021 --- Cargo.toml | 2 +- src/ui/activities/filetransfer/actions/find.rs | 4 ---- src/ui/activities/filetransfer/actions/save.rs | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92d61603..8260943a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Christian Visintin"] categories = ["command-line-utilities"] description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3" documentation = "https://docs.rs/termscp" -edition = "2018" +edition = "2021" homepage = "https://veeso.github.io/termscp/" include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] keywords = ["scp-client", "sftp-client", "ftp-client", "winscp", "command-line-utility"] diff --git a/src/ui/activities/filetransfer/actions/find.rs b/src/ui/activities/filetransfer/actions/find.rs index e2eda451..26e83269 100644 --- a/src/ui/activities/filetransfer/actions/find.rs +++ b/src/ui/activities/filetransfer/actions/find.rs @@ -97,7 +97,6 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not upload file: {}", err), ); - return; } } FileExplorerTab::FindRemote | FileExplorerTab::Remote => { @@ -119,7 +118,6 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not download file: {}", err), ); - return; } } }, @@ -162,7 +160,6 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not upload file: {}", err), ); - return; } } } @@ -196,7 +193,6 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not download file: {}", err), ); - return; } } } diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index 495c9279..bffcbc3f 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -113,7 +113,6 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not upload file: {}", err), ); - return; } } } @@ -154,7 +153,6 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not upload file: {}", err), ); - return; } } } @@ -185,7 +183,6 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not download file: {}", err), ); - return; } } } @@ -226,7 +223,6 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not download file: {}", err), ); - return; } } } From 6f2b469a01d230ed9292ac69dc70d51bc738b7c7 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Nov 2021 12:11:04 +0100 Subject: [PATCH 03/45] -Dwarnings for clippy on windows/macos ci --- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 18661b2e..169cc6f7 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -16,4 +16,4 @@ jobs: - name: Run tests run: cargo test --verbose --lib --features github-actions -- --test-threads 1 - name: Clippy - run: cargo clippy + run: cargo clippy -- -Dwarnings diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b162f672..cd348d45 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -16,4 +16,4 @@ jobs: - name: Run tests run: cargo test --verbose --lib --features github-actions -- --test-threads 1 - name: Clippy - run: cargo clippy + run: cargo clippy -- -Dwarnings From c7971378b2472c48277723df93b5f60abfeb4629 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Nov 2021 12:13:27 +0100 Subject: [PATCH 04/45] Removed lib.rs --- .github/actions-rs/grcov.yml | 1 - .github/workflows/coverage.yml | 2 +- .github/workflows/freebsd.yml | 2 +- .github/workflows/linux.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- src/lib.rs | 77 ---------------------------------- src/main.rs | 1 - 8 files changed, 5 insertions(+), 84 deletions(-) delete mode 100644 src/lib.rs diff --git a/.github/actions-rs/grcov.yml b/.github/actions-rs/grcov.yml index 2f72d6e2..4fea5aab 100644 --- a/.github/actions-rs/grcov.yml +++ b/.github/actions-rs/grcov.yml @@ -7,7 +7,6 @@ ignore: - "C:/*" - "../*" - src/main.rs - - src/lib.rs - src/activity_manager.rs - src/filetransfer/transfer/s3/mod.rs - src/support.rs diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 70087391..8c8753ed 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,7 +24,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --lib --no-default-features --features github-actions --features with-containers --no-fail-fast + args: --no-default-features --features github-actions --features with-containers --no-fail-fast env: CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 2ef0c910..7ddbe799 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -19,4 +19,4 @@ jobs: /tmp/rustup.sh -y . $HOME/.cargo/env cargo build --no-default-features - cargo test --no-default-features --verbose --lib --features github-actions -- --test-threads 1 + cargo test --no-default-features --verbose --features github-actions -- --test-threads 1 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2f8c99e6..de3d1c47 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -24,7 +24,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --lib --no-default-features --features github-actions --features with-containers --no-fail-fast + args: --no-default-features --features github-actions --features with-containers --no-fail-fast - name: Format run: cargo fmt --all -- --check - name: Clippy diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 169cc6f7..7c90d2f8 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -14,6 +14,6 @@ jobs: - name: Build run: cargo build - name: Run tests - run: cargo test --verbose --lib --features github-actions -- --test-threads 1 + run: cargo test --verbose --features github-actions -- --test-threads 1 - name: Clippy run: cargo clippy -- -Dwarnings diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index cd348d45..2879439d 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -14,6 +14,6 @@ jobs: - name: Build run: cargo build - name: Run tests - run: cargo test --verbose --lib --features github-actions -- --test-threads 1 + run: cargo test --verbose --features github-actions -- --test-threads 1 - name: Clippy run: cargo clippy -- -Dwarnings diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index e6f99b6e..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![doc(html_playground_url = "https://play.rust-lang.org")] -#![doc( - html_favicon_url = "https://raw.githubusercontent.com/veeso/termscp/main/assets/images/termscp-128.png" -)] -#![doc( - html_logo_url = "https://raw.githubusercontent.com/veeso/termscp/main/assets/images/termscp-512.png" -)] - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -#[macro_use] -extern crate bitflags; -extern crate bytesize; -extern crate chrono; -extern crate content_inspector; -extern crate crossterm; -extern crate dirs; -extern crate edit; -extern crate hostname; -#[cfg(feature = "with-keyring")] -extern crate keyring; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate log; -#[macro_use] -extern crate magic_crypt; -extern crate notify_rust; -extern crate open; -#[cfg(target_os = "windows")] -extern crate path_slash; -extern crate rand; -extern crate regex; -extern crate s3; -extern crate self_update; -extern crate ssh2; -extern crate suppaftp; -extern crate tempfile; -extern crate textwrap; -extern crate tui_realm_stdlib; -extern crate tuirealm; -#[cfg(target_family = "unix")] -extern crate users; -extern crate whoami; -extern crate wildmatch; - -pub mod activity_manager; -pub mod config; -pub mod filetransfer; -pub mod fs; -pub mod host; -pub mod support; -pub mod system; -pub mod ui; -pub mod utils; diff --git a/src/main.rs b/src/main.rs index c1e37bb5..8d770cd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,6 @@ extern crate lazy_static; extern crate log; #[macro_use] extern crate magic_crypt; -extern crate rpassword; // External libs use argh::FromArgs; From 4f5d8c9224769c04d40efc6850ea5e13957a5196 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Nov 2021 12:29:58 +0100 Subject: [PATCH 05/45] A "wait popup" will now be displayed while searching files --- CHANGELOG.md | 4 ++++ src/ui/activities/filetransfer/update.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f27d084a..b22f9910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ Released on FIXME: > ❄️ Winter update 2022 ⛄ +- **Enhancements**: + - Find feature: + - A "wait popup" will now be displayed while searching files + ## 0.7.0 Released on 12/10/2021 diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 77169e37..e3315f29 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -433,12 +433,16 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_FIND, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { self.umount_find_input(); + // Mount wait + self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, input).as_str()); // Find let res: Result, String> = match self.browser.tab() { FileExplorerTab::Local => self.action_local_find(input.to_string()), FileExplorerTab::Remote => self.action_remote_find(input.to_string()), _ => panic!("Trying to search for files, while already in a find result"), }; + // Umount wait + self.umount_wait(); // Match result match res { Err(err) => { From 9a56ebaa572b067a646f5d5ed0e2bf6e7e7f310f Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Nov 2021 12:36:57 +0100 Subject: [PATCH 06/45] If find command doesn't return any result show an info dialog and not an empty explorer --- CHANGELOG.md | 1 + src/ui/activities/filetransfer/update.rs | 6 ++++++ src/ui/activities/filetransfer/view.rs | 9 +++++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b22f9910..dc1d06c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Released on FIXME: - **Enhancements**: - Find feature: - A "wait popup" will now be displayed while searching files + - If find command doesn't return any result show an info dialog and not an empty explorer ## 0.7.0 diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index e3315f29..c34e274b 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -449,6 +449,12 @@ impl Update for FileTransferActivity { // Mount error self.mount_error(err.as_str()); } + Ok(files) if files.is_empty() => { + // If no file has been found notify user + self.mount_info( + format!(r#"Could not find any file matching "{}""#, input).as_str(), + ); + } Ok(files) => { // Create explorer and load files self.browser.set_found(files); diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index c277067d..f6ab723e 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -400,6 +400,15 @@ impl FileTransferActivity { // -- partials + /// ### mount_info + /// + /// Mount info box + pub(super) fn mount_info(&mut self, text: &str) { + // Mount + let info_color = self.theme().misc_info_dialog; + self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, info_color); + } + /// ### mount_error /// /// Mount error box From 9b9786bfbf6962b9e638c92ca165523d8ff05100 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Nov 2021 14:53:25 +0100 Subject: [PATCH 07/45] It is now possible to keep navigating on the other explorer while 'found tab' is open --- CHANGELOG.md | 2 + src/ui/activities/filetransfer/lib/browser.rs | 32 +++++++--- src/ui/activities/filetransfer/update.rs | 64 ++++++++++++++++--- src/ui/activities/filetransfer/view.rs | 33 +++++----- 4 files changed, 96 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc1d06c9..6f7c5b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ Released on FIXME: - Find feature: - A "wait popup" will now be displayed while searching files - If find command doesn't return any result show an info dialog and not an empty explorer + - It is now possible to keep navigating on the other explorer while "found tab" is open + - ❗ It is not possible though to have the "found tab" on both explorers (otherwise you wouldn't be able to tell whether you're transferring files) ## 0.7.0 diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index 501ab92f..92413adc 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -40,14 +40,23 @@ pub enum FileExplorerTab { FindRemote, // Find result tab } +/// ## FoundExplorerTab +/// +/// Describes the explorer tab type +#[derive(Copy, Clone, Debug)] +pub enum FoundExplorerTab { + Local, + Remote, +} + /// ## Browser /// /// Browser contains the browser options pub struct Browser { - local: FileExplorer, // Local File explorer state - remote: FileExplorer, // Remote File explorer state - found: Option, // File explorer for find result - tab: FileExplorerTab, // Current selected tab + local: FileExplorer, // Local File explorer state + remote: FileExplorer, // Remote File explorer state + found: Option<(FoundExplorerTab, FileExplorer)>, // File explorer for find result + tab: FileExplorerTab, // Current selected tab pub sync_browsing: bool, } @@ -82,23 +91,30 @@ impl Browser { } pub fn found(&self) -> Option<&FileExplorer> { - self.found.as_ref() + self.found.as_ref().map(|x| &x.1) } pub fn found_mut(&mut self) -> Option<&mut FileExplorer> { - self.found.as_mut() + self.found.as_mut().map(|x| &mut x.1) } - pub fn set_found(&mut self, files: Vec) { + pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec) { let mut explorer = Self::build_found_explorer(); explorer.set_files(files); - self.found = Some(explorer); + self.found = Some((tab, explorer)); } pub fn del_found(&mut self) { self.found = None; } + /// ### found_tab + /// + /// Returns found tab if any + pub fn found_tab(&self) -> Option { + self.found.as_ref().map(|x| x.0) + } + pub fn tab(&self) -> FileExplorerTab { self.tab } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index c34e274b..2a035b5d 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -27,15 +27,17 @@ */ // locals use super::{ - actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel, TransferOpts, - COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, - COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, - COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH, - COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, - COMPONENT_LIST_REPLACING_FILES, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL, - COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT, - COMPONENT_RADIO_QUIT, COMPONENT_RADIO_REPLACE, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, - COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP, + actions::SelectedEntry, + browser::{FileExplorerTab, FoundExplorerTab}, + FileTransferActivity, LogLevel, TransferOpts, COMPONENT_EXPLORER_FIND, + COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY, + COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR, + COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH, COMPONENT_INPUT_RENAME, + COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LIST_REPLACING_FILES, + COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, + COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, + COMPONENT_RADIO_REPLACE, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, + COMPONENT_TEXT_HELP, }; use crate::fs::explorer::FileSorting; use crate::fs::FsEntry; @@ -64,6 +66,15 @@ impl Update for FileTransferActivity { None => None, // Exit after None Some(msg) => match msg { // -- local tab + (COMPONENT_EXPLORER_LOCAL, key) + if key == &MSG_KEY_RIGHT + && matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) => + { + // Go to find explorer + self.view.active(COMPONENT_EXPLORER_FIND); + self.browser.change_tab(FileExplorerTab::FindRemote); + None + } (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_RIGHT => { // Change tab self.view.active(COMPONENT_EXPLORER_REMOTE); @@ -137,6 +148,15 @@ impl Update for FileTransferActivity { self.update_local_filelist() } // -- remote tab + (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_LEFT + && matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) => + { + // Go to find explorer + self.view.active(COMPONENT_EXPLORER_FIND); + self.browser.change_tab(FileExplorerTab::FindLocal); + None + } (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_LEFT => { // Change tab self.view.active(COMPONENT_EXPLORER_LOCAL); @@ -336,6 +356,24 @@ impl Update for FileTransferActivity { None } // -- find result explorer + (COMPONENT_EXPLORER_FIND, key) + if key == &MSG_KEY_RIGHT + && matches!(self.browser.tab(), FileExplorerTab::FindLocal) => + { + // Active remote explorer + self.view.active(COMPONENT_EXPLORER_REMOTE); + self.browser.change_tab(FileExplorerTab::Remote); + None + } + (COMPONENT_EXPLORER_FIND, key) + if key == &MSG_KEY_LEFT + && matches!(self.browser.tab(), FileExplorerTab::FindRemote) => + { + // Active local explorer + self.view.active(COMPONENT_EXPLORER_LOCAL); + self.browser.change_tab(FileExplorerTab::Local); + None + } (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_ESC => { // Umount find self.umount_find(); @@ -457,7 +495,13 @@ impl Update for FileTransferActivity { } Ok(files) => { // Create explorer and load files - self.browser.set_found(files); + self.browser.set_found( + match self.browser.tab() { + FileExplorerTab::Local => FoundExplorerTab::Local, + _ => FoundExplorerTab::Remote, + }, + files, + ); // Mount result widget self.mount_find(input); self.update_find_list(); diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index f6ab723e..5cf74ad8 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -26,7 +26,10 @@ * SOFTWARE. */ // locals -use super::{browser::FileExplorerTab, Context, FileTransferActivity}; +use super::{ + browser::{FileExplorerTab, FoundExplorerTab}, + Context, FileTransferActivity, +}; use crate::fs::explorer::FileSorting; use crate::fs::FsEntry; use crate::ui::components::{ @@ -165,24 +168,20 @@ impl FileTransferActivity { } // Draw explorers // @! Local explorer (Find or default) - match self.browser.tab() { - FileExplorerTab::FindLocal => { - self.view - .render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[0]) - } - _ => self - .view - .render(super::COMPONENT_EXPLORER_LOCAL, f, tabs_chunks[0]), + if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) { + self.view + .render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[0]); + } else { + self.view + .render(super::COMPONENT_EXPLORER_LOCAL, f, tabs_chunks[0]); } // @! Remote explorer (Find or default) - match self.browser.tab() { - FileExplorerTab::FindRemote => { - self.view - .render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[1]) - } - _ => self - .view - .render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]), + if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) { + self.view + .render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[1]); + } else { + self.view + .render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]); } // Draw log box self.view From d9abb5545ef2d3cc3a8657a1d7a30292fac84a36 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Nov 2021 14:58:57 +0100 Subject: [PATCH 08/45] Windows opts --- src/host/mod.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/host/mod.rs b/src/host/mod.rs index 10610790..481bcba0 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -543,10 +543,9 @@ impl Localhost { }), false => { // Is File - let extension: Option = match path.extension() { - Some(s) => Some(String::from(s.to_str().unwrap_or(""))), - None => None, - }; + let extension: Option = path + .extension() + .map(|s| String::from(s.to_str().unwrap_or(""))); FsEntry::File(FsFile { name: file_name, abs_path: path.clone(), From d9c01c55e84353c0ca1eed5c24eee3f45b894710 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Nov 2021 16:40:35 +0100 Subject: [PATCH 09/45] Files found from search are now displayed with their absolute path --- CHANGELOG.md | 1 + docs/de/man.md | 1 + docs/es/man.md | 3 +- docs/fr/man.md | 1 + docs/it/man.md | 3 +- docs/man.md | 3 +- docs/zh-CN/man.md | 1 + src/fs/explorer/formatter.rs | 57 +++++++++++++++++++ src/ui/activities/filetransfer/lib/browser.rs | 2 +- 9 files changed, 68 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7c5b53..3d9cf70b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Released on FIXME: - If find command doesn't return any result show an info dialog and not an empty explorer - It is now possible to keep navigating on the other explorer while "found tab" is open - ❗ It is not possible though to have the "found tab" on both explorers (otherwise you wouldn't be able to tell whether you're transferring files) + - Files found from search are now displayed with their absolute path ## 0.7.0 diff --git a/docs/de/man.md b/docs/de/man.md index c0a00a53..53f047ce 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -339,6 +339,7 @@ These are the keys supported by the formatter: - `GROUP`: Owner group - `MTIME`: Last change time (with syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{MTIME:8:%H:%M}`) - `NAME`: File name (Elided if longer than LENGTH) +- `PATH`: File absolute path (Elided if longer than LENGHT) - `PEX`: File permissions (UNIX format) - `SIZE`: File size (omitted for directories) - `SYMLINK`: Symlink (if any `-> {FILE_PATH}`) diff --git a/docs/es/man.md b/docs/es/man.md index 70cf88c5..91beaaea 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -337,7 +337,8 @@ Estas son las claves admitidas por el formateador: - `CTIME`: Hora de creación (con sintaxis`%b %d %Y %H:%M`); Se puede proporcionar un extra como sintaxis de tiempo (p. Ej., `{CTIME:8:%H:%M}`) - `GROUP`: Grupo propietario - `MTIME`: Hora del último cambio (con sintaxis`%b %d %Y %H:%M`); Se puede proporcionar extra como sintaxis de tiempo (p. Ej., `{MTIME: 8:% H:% M}`) -- `NAME`: nombre de archivo (se omite si es más largo que LENGTH) +- `NAME`: nombre de archivo (Las carpetas entre la raíz y los primeros antepasados ​​se eliminan si es más largo que LENGTH) +- `PATH`: Percorso completo de archivo (Las carpetas entre la raíz y los primeros antepasados ​​se eliminan si es màs largo que LENGHT) - `PEX`: permisos de archivo (formato UNIX) - `SIZE`: Tamaño del archivo (se omite para directorios) - `SYMLINK`: Symlink (si existe` -> {FILE_PATH} `) diff --git a/docs/fr/man.md b/docs/fr/man.md index c77f3adf..ee26fa6a 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -336,6 +336,7 @@ Voici les clés prises en charge par le formateur : - `GROUP`: Groupe de propriétaires - `MTIME`: Heure du dernier changement (avec la syntaxe `%b %d %Y %H:%M`); Un supplément peut être fourni comme syntaxe de l'heure (par exemple, `{MTIME:8:%H:%M}`) - `NAME`: Nom du fichier (élidé si plus long que LENGTH) +- `PATH`: Chemin absolu du fichier (les dossiers entre la racine et les premiers ancêtres sont éludés s'ils sont plus longs que LENGTH) - `PEX`: Autorisations de fichiers (format UNIX) - `SIZE`: Taille du fichier (omis pour les répertoires) - `SYMLINK`: Lien symbolique (le cas échéant `-> {FILE_PATH}`) diff --git a/docs/it/man.md b/docs/it/man.md index 1c219c2d..90521da6 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -333,7 +333,8 @@ These are the keys supported by the formatter: - `CTIME`: Creation time (con sintassi di default `%b %d %Y %H:%M`); Extra definisce il formato data (e.g. `{CTIME:8:%H:%M}`) - `GROUP`: Owner group - `MTIME`: Last change time (con sintassi di default `%b %d %Y %H:%M`); Extra definisce il formato data (e.g. `{MTIME:8:%H:%M}`) -- `NAME`: Nome file (Elided if longer than LENGTH) +- `NAME`: Nome file (Le cartelle comprese tra la root ed il genitore del file sono omessi se la lunghezza è maggiore di LENGTH) +- `PATH`: Percorso assoluto del file (Le cartelle comprese tra la root ed il genitore del file sono omessi se la lunghezza è maggiore di LENGHT) - `PEX`: Permessi utente (formato UNIX) - `SIZE`: Dimensione file (omesso per le directory) - `SYMLINK`: Link simbolico (se presente `-> {FILE_PATH}`) diff --git a/docs/man.md b/docs/man.md index ed77ddf5..89003917 100644 --- a/docs/man.md +++ b/docs/man.md @@ -336,7 +336,8 @@ These are the keys supported by the formatter: - `CTIME`: Creation time (with syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{CTIME:8:%H:%M}`) - `GROUP`: Owner group - `MTIME`: Last change time (with syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{MTIME:8:%H:%M}`) -- `NAME`: File name (Elided if longer than LENGTH) +- `NAME`: File name (Folders between root and first ancestors are elided if longer than LENGTH) +- `PATH`: File absolute path (Folders between root and first ancestors are elided if longer than LENGHT) - `PEX`: File permissions (UNIX format) - `SIZE`: File size (omitted for directories) - `SYMLINK`: Symlink (if any `-> {FILE_PATH}`) diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index 6489fdb1..13c274df 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -330,6 +330,7 @@ termscp和书签一样,只需要保证这些路径是可访问的: - `GROUP`: 所属组 - `MTIME`: 最后修改时间(语法为`%b %d %Y %H:%M`);Extra参数可以指定时间显示语法(例如:`{MTIME:8:%H:%M}`) - `NAME`: 文件名(超过 LENGTH 个字符的部分会被省略) +- `PATH`:文件绝对路径(如果长于 LENGTH,则根目录和第一个祖先之间的文件夹将被排除) - `PEX`: 文件权限(UNIX格式) - `SIZE`: 文件大小(目录不显示) - `SYMLINK`: 超链接(如果存在的话`-> {FILE_PATH}`)。 diff --git a/src/fs/explorer/formatter.rs b/src/fs/explorer/formatter.rs index 006da849..d3774563 100644 --- a/src/fs/explorer/formatter.rs +++ b/src/fs/explorer/formatter.rs @@ -43,6 +43,7 @@ const FMT_KEY_CTIME: &str = "CTIME"; const FMT_KEY_GROUP: &str = "GROUP"; const FMT_KEY_MTIME: &str = "MTIME"; const FMT_KEY_NAME: &str = "NAME"; +const FMT_KEY_PATH: &str = "PATH"; const FMT_KEY_PEX: &str = "PEX"; const FMT_KEY_SIZE: &str = "SIZE"; const FMT_KEY_SYMLINK: &str = "SYMLINK"; @@ -68,10 +69,15 @@ lazy_static! { /// a chain of function is made using the Formatters method. /// This method provides an extremely fast way to format fs entries struct CallChainBlock { + /// The function to call to format current item func: FmtCallback, + /// All the content which is between two `{KEY}` items prefix: String, + /// The fmt len, specied for key as `{KEY:LEN}` fmt_len: Option, + /// The extra argument for formatting, specified for key as `{KEY:LEN:EXTRA}` fmt_extra: Option, + /// The next block to format next_block: Option>, } @@ -331,6 +337,28 @@ impl Formatter { format!("{}{}{:0width$}", cur_str, prefix, name, width = file_len) } + /// ### fmt_path + /// + /// Format path + fn fmt_path( + &self, + fsentry: &FsEntry, + cur_str: &str, + prefix: &str, + fmt_len: Option<&usize>, + _fmt_extra: Option<&String>, + ) -> String { + format!( + "{}{}{}", + cur_str, + prefix, + match fmt_len { + None => fsentry.get_abs_path().display().to_string(), + Some(len) => fmt_path_elide(fsentry.get_abs_path().as_path(), *len), + } + ) + } + /// ### fmt_pex /// /// Format file permissions @@ -490,6 +518,7 @@ impl Formatter { FMT_KEY_GROUP => Self::fmt_group, FMT_KEY_MTIME => Self::fmt_mtime, FMT_KEY_NAME => Self::fmt_name, + FMT_KEY_PATH => Self::fmt_path, FMT_KEY_PEX => Self::fmt_pex, FMT_KEY_SIZE => Self::fmt_size, FMT_KEY_SYMLINK => Self::fmt_symlink, @@ -880,6 +909,34 @@ mod tests { )); } + #[test] + fn should_fmt_path() { + let t: SystemTime = SystemTime::now(); + let entry: FsEntry = FsEntry::File(FsFile { + name: String::from("bar.txt"), + abs_path: PathBuf::from("/tmp/a/b/c/bar.txt"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + }); + let formatter: Formatter = Formatter::new("File path: {PATH}"); + assert_eq!( + formatter.fmt(&entry).as_str(), + "File path: /tmp/a/b/c/bar.txt" + ); + let formatter: Formatter = Formatter::new("File path: {PATH:8}"); + assert_eq!( + formatter.fmt(&entry).as_str(), + "File path: /tmp/…/c/bar.txt" + ); + } + /// ### dummy_fmt /// /// Dummy formatter, just yelds an 'A' at the end of the current string diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index 92413adc..932f3c9b 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -174,7 +174,7 @@ impl Browser { .with_group_dirs(Some(GroupDirs::First)) .with_hidden_files(true) .with_stack_size(0) - .with_formatter(Some("{NAME:32} {SYMLINK}")) + .with_formatter(Some("{PATH:36} {SYMLINK}")) .build() } } From 18c30e5814cb84d6deb6a18198a13329013eafef Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 20 Nov 2021 09:40:06 +0100 Subject: [PATCH 10/45] Files found from search are now displayed with their relative path from working directory --- CHANGELOG.md | 4 ++-- src/fs/explorer/formatter.rs | 18 +++++++++++++++--- src/ui/activities/filetransfer/lib/browser.rs | 12 ++++++++---- src/ui/activities/filetransfer/update.rs | 6 ++++++ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d9cf70b..4c0c1280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,12 +29,12 @@ Released on FIXME: > ❄️ Winter update 2022 ⛄ - **Enhancements**: - - Find feature: + - **Find** feature: - A "wait popup" will now be displayed while searching files - If find command doesn't return any result show an info dialog and not an empty explorer - It is now possible to keep navigating on the other explorer while "found tab" is open - ❗ It is not possible though to have the "found tab" on both explorers (otherwise you wouldn't be able to tell whether you're transferring files) - - Files found from search are now displayed with their absolute path + - Files found from search are now displayed with their relative path from working directory ## 0.7.0 diff --git a/src/fs/explorer/formatter.rs b/src/fs/explorer/formatter.rs index d3774563..4acaf6c9 100644 --- a/src/fs/explorer/formatter.rs +++ b/src/fs/explorer/formatter.rs @@ -28,9 +28,11 @@ // Locals use super::FsEntry; use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time}; +use crate::utils::path::diff_paths; // Ext use bytesize::ByteSize; use regex::Regex; +use std::path::PathBuf; #[cfg(target_family = "unix")] use users::{get_group_by_gid, get_user_by_uid}; // Types @@ -346,15 +348,23 @@ impl Formatter { cur_str: &str, prefix: &str, fmt_len: Option<&usize>, - _fmt_extra: Option<&String>, + fmt_extra: Option<&String>, ) -> String { + let p = match fmt_extra { + None => fsentry.get_abs_path(), + Some(rel) => diff_paths( + fsentry.get_abs_path().as_path(), + PathBuf::from(rel.as_str()).as_path(), + ) + .unwrap_or_else(|| fsentry.get_abs_path()), + }; format!( "{}{}{}", cur_str, prefix, match fmt_len { - None => fsentry.get_abs_path().display().to_string(), - Some(len) => fmt_path_elide(fsentry.get_abs_path().as_path(), *len), + None => p.display().to_string(), + Some(len) => fmt_path_elide(p.as_path(), *len), } ) } @@ -935,6 +945,8 @@ mod tests { formatter.fmt(&entry).as_str(), "File path: /tmp/…/c/bar.txt" ); + let formatter: Formatter = Formatter::new("File path: {PATH:128:/tmp/a/b}"); + assert_eq!(formatter.fmt(&entry).as_str(), "File path: c/bar.txt"); } /// ### dummy_fmt diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index 932f3c9b..396f3712 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -29,6 +29,8 @@ use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSortin use crate::fs::FsEntry; use crate::system::config_client::ConfigClient; +use std::path::Path; + /// ## FileExplorerTab /// /// File explorer tab @@ -98,8 +100,8 @@ impl Browser { self.found.as_mut().map(|x| &mut x.1) } - pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec) { - let mut explorer = Self::build_found_explorer(); + pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec, wrkdir: &Path) { + let mut explorer = Self::build_found_explorer(wrkdir); explorer.set_files(files); self.found = Some((tab, explorer)); } @@ -168,13 +170,15 @@ impl Browser { /// ### build_found_explorer /// /// Build explorer reading from `ConfigClient`, for found result (has some differences) - fn build_found_explorer() -> FileExplorer { + fn build_found_explorer(wrkdir: &Path) -> FileExplorer { FileExplorerBuilder::new() .with_file_sorting(FileSorting::Name) .with_group_dirs(Some(GroupDirs::First)) .with_hidden_files(true) .with_stack_size(0) - .with_formatter(Some("{PATH:36} {SYMLINK}")) + .with_formatter(Some( + format!("{{PATH:36:{}}} {{SYMLINK}}", wrkdir.display()).as_str(), + )) .build() } } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 2a035b5d..da003bf2 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -494,6 +494,11 @@ impl Update for FileTransferActivity { ); } Ok(files) => { + // Get wrkdir + let wrkdir = match self.browser.tab() { + FileExplorerTab::Local => self.local().wrkdir.clone(), + _ => self.remote().wrkdir.clone(), + }; // Create explorer and load files self.browser.set_found( match self.browser.tab() { @@ -501,6 +506,7 @@ impl Update for FileTransferActivity { _ => FoundExplorerTab::Remote, }, files, + wrkdir.as_path(), ); // Mount result widget self.mount_find(input); From 17a4efe80d349178d8c2af735ff014f8aceec584 Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 20 Nov 2021 09:42:13 +0100 Subject: [PATCH 11/45] serial test for env paths --- .github/workflows/freebsd.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- Cargo.lock | 23 +++++++++++++++++++++++ Cargo.toml | 1 + src/system/environment.rs | 7 +++++++ 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 7ddbe799..b7b98ba8 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -19,4 +19,4 @@ jobs: /tmp/rustup.sh -y . $HOME/.cargo/env cargo build --no-default-features - cargo test --no-default-features --verbose --features github-actions -- --test-threads 1 + cargo test --no-default-features --verbose --features github-actions diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7c90d2f8..f493fa5d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -14,6 +14,6 @@ jobs: - name: Build run: cargo build - name: Run tests - run: cargo test --verbose --features github-actions -- --test-threads 1 + run: cargo test --verbose --features github-actions - name: Clippy run: cargo clippy -- -Dwarnings diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 2879439d..e2a23a66 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -14,6 +14,6 @@ jobs: - name: Build run: cargo build - name: Run tests - run: cargo test --verbose --features github-actions -- --test-threads 1 + run: cargo test --verbose --features github-actions - name: Clippy run: cargo clippy -- -Dwarnings diff --git a/Cargo.lock b/Cargo.lock index 70125ae6..7dddc944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1970,6 +1970,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" +dependencies = [ + "lazy_static", + "parking_lot 0.11.2", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.76", +] + [[package]] name = "sha2" version = "0.9.8" @@ -2201,6 +2223,7 @@ dependencies = [ "rust-s3", "self_update", "serde", + "serial_test", "simplelog", "ssh2", "suppaftp", diff --git a/Cargo.toml b/Cargo.toml index 8260943a..e1d214a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ wildmatch = "2.0.0" [dev-dependencies] pretty_assertions = "0.7.2" +serial_test = "^0.5.1" [features] default = [ "with-keyring" ] diff --git a/src/system/environment.rs b/src/system/environment.rs index ea943d0e..ca396922 100644 --- a/src/system/environment.rs +++ b/src/system/environment.rs @@ -110,10 +110,12 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + use serial_test::serial; use std::fs::{File, OpenOptions}; use std::io::Write; #[test] + #[serial] fn test_system_environment_get_config_dir() { // Create and get conf_dir let conf_dir: PathBuf = init_config_dir().ok().unwrap().unwrap(); @@ -122,6 +124,7 @@ mod tests { } #[test] + #[serial] fn test_system_environment_get_config_dir_err() { let mut conf_dir: PathBuf = std::env::temp_dir(); conf_dir.push("termscp"); @@ -143,6 +146,7 @@ mod tests { } #[test] + #[serial] fn test_system_environment_get_bookmarks_paths() { assert_eq!( get_bookmarks_paths(&Path::new("/home/omar/.config/termscp/")), @@ -151,6 +155,7 @@ mod tests { } #[test] + #[serial] fn test_system_environment_get_config_paths() { assert_eq!( get_config_paths(&Path::new("/home/omar/.config/termscp/")), @@ -162,6 +167,7 @@ mod tests { } #[test] + #[serial] fn test_system_environment_get_log_paths() { assert_eq!( get_log_paths(&Path::new("/home/omar/.config/termscp/")), @@ -170,6 +176,7 @@ mod tests { } #[test] + #[serial] fn test_system_environment_get_theme_path() { assert_eq!( get_theme_path(&Path::new("/home/omar/.config/termscp/")), From c317bba40b66e50a449e8faabe92cd6b691ed174 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 21 Nov 2021 10:02:03 +0100 Subject: [PATCH 12/45] Migrated termscp to tui-realm 1.x --- CHANGELOG.md | 10 + Cargo.lock | 27 +- Cargo.toml | 5 +- src/activity_manager.rs | 20 +- src/fs/explorer/formatter.rs | 1 + src/system/auto_update.rs | 10 +- src/ui/activities/auth/bookmarks.rs | 60 +- .../activities/auth/components/bookmarks.rs | 445 +++++ src/ui/activities/auth/components/form.rs | 694 +++++++ src/ui/activities/auth/components/mod.rs | 91 + src/ui/activities/auth/components/popup.rs | 452 +++++ src/ui/activities/auth/components/text.rs | 132 ++ src/ui/activities/auth/misc.rs | 38 +- src/ui/activities/auth/mod.rs | 219 ++- src/ui/activities/auth/update.rs | 601 ++---- src/ui/activities/auth/view.rs | 1087 +++++------ .../activities/filetransfer/actions/edit.rs | 26 +- src/ui/activities/filetransfer/actions/mod.rs | 20 +- .../activities/filetransfer/actions/open.rs | 4 +- .../activities/filetransfer/components/log.rs | 296 +++ .../activities/filetransfer/components/mod.rs | 72 + .../filetransfer/components/popups.rs | 1689 +++++++++++++++++ .../components/transfer/file_list.rs | 400 ++++ .../filetransfer/components/transfer/mod.rs | 494 +++++ src/ui/activities/filetransfer/lib/browser.rs | 2 +- src/ui/activities/filetransfer/misc.rs | 298 ++- src/ui/activities/filetransfer/mod.rs | 228 ++- src/ui/activities/filetransfer/session.rs | 4 +- src/ui/activities/filetransfer/update.rs | 1349 ++++--------- src/ui/activities/filetransfer/view.rs | 1415 ++++++-------- src/ui/activities/setup/actions.rs | 242 ++- src/ui/activities/setup/components/commons.rs | 334 ++++ src/ui/activities/setup/components/config.rs | 489 +++++ src/ui/activities/setup/components/mod.rs | 86 + src/ui/activities/setup/components/ssh.rs | 339 ++++ src/ui/activities/setup/components/theme.rs | 910 +++++++++ src/ui/activities/setup/config.rs | 21 +- src/ui/activities/setup/mod.rs | 331 +++- src/ui/activities/setup/update.rs | 1121 ++++------- src/ui/activities/setup/view/mod.rs | 369 ++-- src/ui/activities/setup/view/setup.rs | 420 ++-- src/ui/activities/setup/view/ssh_keys.rs | 230 +-- src/ui/activities/setup/view/theme.rs | 795 ++++---- src/ui/components/bookmark_list.rs | 456 ----- src/ui/components/bytes.rs | 310 --- src/ui/components/color_picker.rs | 301 --- src/ui/components/file_list.rs | 765 -------- src/ui/components/logbox.rs | 433 ----- src/ui/components/mod.rs | 33 - src/ui/context.rs | 96 +- src/ui/input.rs | 102 - src/ui/keymap.rs | 215 --- src/ui/mod.rs | 3 - src/utils/parser.rs | 5 +- 54 files changed, 10949 insertions(+), 7646 deletions(-) create mode 100644 src/ui/activities/auth/components/bookmarks.rs create mode 100644 src/ui/activities/auth/components/form.rs create mode 100644 src/ui/activities/auth/components/mod.rs create mode 100644 src/ui/activities/auth/components/popup.rs create mode 100644 src/ui/activities/auth/components/text.rs create mode 100644 src/ui/activities/filetransfer/components/log.rs create mode 100644 src/ui/activities/filetransfer/components/mod.rs create mode 100644 src/ui/activities/filetransfer/components/popups.rs create mode 100644 src/ui/activities/filetransfer/components/transfer/file_list.rs create mode 100644 src/ui/activities/filetransfer/components/transfer/mod.rs create mode 100644 src/ui/activities/setup/components/commons.rs create mode 100644 src/ui/activities/setup/components/config.rs create mode 100644 src/ui/activities/setup/components/mod.rs create mode 100644 src/ui/activities/setup/components/ssh.rs create mode 100644 src/ui/activities/setup/components/theme.rs delete mode 100644 src/ui/components/bookmark_list.rs delete mode 100644 src/ui/components/bytes.rs delete mode 100644 src/ui/components/color_picker.rs delete mode 100644 src/ui/components/file_list.rs delete mode 100644 src/ui/components/logbox.rs delete mode 100644 src/ui/components/mod.rs delete mode 100644 src/ui/input.rs delete mode 100644 src/ui/keymap.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0c1280..5c5db355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,16 @@ Released on FIXME: - It is now possible to keep navigating on the other explorer while "found tab" is open - ❗ It is not possible though to have the "found tab" on both explorers (otherwise you wouldn't be able to tell whether you're transferring files) - Files found from search are now displayed with their relative path from working directory + - **Ui**: + - Transfer abortion is now more responsive + - Selected files will now be rendered with **Reversed, underlined and italic** text modifiers instead of being prepended with `*`. + - **Tui-realm migration**: + - migrated application to tui-realm 1.x + - Improved application performance +- Dependencies: + - Updated `tui-realm` to `1.3.0` + - Updated `tui-realm-stdlib` to `1.1.4` + - Removed `crossterm` (since bridged by tui-realm) ## 0.7.0 diff --git a/Cargo.lock b/Cargo.lock index 7dddc944..43a3e13d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1977,7 +1977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", - "parking_lot 0.11.2", + "parking_lot 0.10.2", "serial_test_derive", ] @@ -2205,7 +2205,6 @@ dependencies = [ "bytesize", "chrono", "content_inspector", - "crossterm", "dirs 4.0.0", "edit", "hostname", @@ -2413,9 +2412,9 @@ dependencies = [ [[package]] name = "tui-realm-stdlib" -version = "0.6.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f159d383b40dec75e0541530bc3416318f5e0a8b6999db9df9b5efa6b122380e" +checksum = "b6444ac3cf88c6cbee4267b6999775aa65ef4ddf556587d2154631d74b5d65fc" dependencies = [ "textwrap", "tuirealm", @@ -2424,12 +2423,28 @@ dependencies = [ [[package]] name = "tuirealm" -version = "0.6.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634ad8e6a4b80ef032d31356b55964a995da5d05a9cf3a1bd134bae1ba7c197a" +checksum = "69e5c7137a0bd92feadea98033a1849fe51c83d23f7761b866e8700a3d6f1de7" dependencies = [ + "bitflags 1.3.2", "crossterm", + "lazy_static", + "regex", + "thiserror", "tui", + "tuirealm_derive", +] + +[[package]] +name = "tuirealm_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0adcdaf59881626555558eae08f8a53003c8a1961723b4d7a10c51599abbc81" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.76", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e1d214a5..2c556090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,6 @@ bitflags = "1.3.2" bytesize = "1.1.0" chrono = "0.4.19" content_inspector = "0.2.4" -crossterm = "0.20" dirs = "4.0.0" edit = "0.1.3" hostname = "0.3.1" @@ -60,8 +59,8 @@ tempfile = "3.1.0" textwrap = "0.14.2" thiserror = "^1.0.0" toml = "0.5.8" -tui-realm-stdlib = "0.6.3" -tuirealm = "0.6.0" +tui-realm-stdlib = "^1.1.0" +tuirealm = "^1.2.0" whoami = "1.1.1" wildmatch = "2.0.0" diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 49885249..1171b39d 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -39,7 +39,6 @@ use crate::ui::context::Context; // Namespaces use std::path::{Path, PathBuf}; -use std::thread::sleep; use std::time::Duration; /// ### NextActivity @@ -56,7 +55,7 @@ pub enum NextActivity { /// The activity manager takes care of running activities and handling them until the application has ended pub struct ActivityManager { context: Option, - interval: Duration, + ticks: Duration, local_dir: PathBuf, } @@ -64,7 +63,7 @@ impl ActivityManager { /// ### new /// /// Initializes a new Activity Manager - pub fn new(local_dir: &Path, interval: Duration) -> Result { + pub fn new(local_dir: &Path, ticks: Duration) -> Result { // Prepare Context // Initialize configuration client let (config_client, error): (ConfigClient, Option) = @@ -80,7 +79,7 @@ impl ActivityManager { Ok(ActivityManager { context: Some(ctx), local_dir: local_dir.to_path_buf(), - interval, + ticks, }) } @@ -123,7 +122,7 @@ impl ActivityManager { fn run_authentication(&mut self) -> Option { info!("Starting AuthActivity..."); // Prepare activity - let mut activity: AuthActivity = AuthActivity::default(); + let mut activity: AuthActivity = AuthActivity::new(self.ticks); // Prepare result let result: Option; // Get context @@ -162,8 +161,6 @@ impl ActivityManager { _ => { /* Nothing to do */ } } } - // Sleep for ticks - sleep(self.interval); } // Destroy activity self.context = activity.on_destroy(); @@ -205,7 +202,8 @@ impl ActivityManager { return None; } }; - let mut activity: FileTransferActivity = FileTransferActivity::new(host, protocol); + let mut activity: FileTransferActivity = + FileTransferActivity::new(host, protocol, self.ticks); // Prepare result let result: Option; // Create activity @@ -230,8 +228,6 @@ impl ActivityManager { _ => { /* Nothing to do */ } } } - // Sleep for ticks - sleep(self.interval); } // Destroy activity self.context = activity.on_destroy(); @@ -245,7 +241,7 @@ impl ActivityManager { /// Returns the next activity to run fn run_setup(&mut self) -> Option { // Prepare activity - let mut activity: SetupActivity = SetupActivity::default(); + let mut activity: SetupActivity = SetupActivity::new(self.ticks); // Get context let ctx: Context = match self.context.take() { Some(ctx) => ctx, @@ -264,8 +260,6 @@ impl ActivityManager { info!("SetupActivity terminated due to 'Quit'"); break; } - // Sleep for ticks - sleep(self.interval); } // Destroy activity self.context = activity.on_destroy(); diff --git a/src/fs/explorer/formatter.rs b/src/fs/explorer/formatter.rs index 4acaf6c9..440e89a8 100644 --- a/src/fs/explorer/formatter.rs +++ b/src/fs/explorer/formatter.rs @@ -920,6 +920,7 @@ mod tests { } #[test] + #[cfg(target_family = "unix")] fn should_fmt_path() { let t: SystemTime = SystemTime::now(); let entry: FsEntry = FsEntry::File(FsFile { diff --git a/src/system/auto_update.rs b/src/system/auto_update.rs index 0194d4d8..6f551713 100644 --- a/src/system/auto_update.rs +++ b/src/system/auto_update.rs @@ -187,7 +187,10 @@ mod test { } #[test] - #[cfg(not(all(target_os = "macos", feature = "github-actions")))] + #[cfg(not(all( + any(target_os = "macos", target_os = "freebsd"), + feature = "github-actions" + )))] fn auto_update() { // Wno version assert_eq!( @@ -201,7 +204,10 @@ mod test { } #[test] - #[cfg(not(all(target_os = "macos", feature = "github-actions")))] + #[cfg(not(all( + any(target_os = "macos", target_os = "freebsd"), + feature = "github-actions" + )))] fn check_for_updates() { println!("{:?}", Update::is_new_version_available()); assert!(Update::is_new_version_available().is_ok()); diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index 699f04c3..f1634b3c 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -33,8 +33,6 @@ use crate::system::environment; // Ext use std::path::PathBuf; -use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder}; -use tuirealm::PropsBuilder; impl AuthActivity { /// ### del_bookmark @@ -234,12 +232,8 @@ impl AuthActivity { /// Load bookmark data into the gui components fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) { // Load parameters into components - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) { - let props = RadioPropsBuilder::from(props) - .with_value(Self::protocol_enum_to_opt(bookmark.protocol)) - .build(); - self.view.update(super::COMPONENT_RADIO_PROTOCOL, props); - } + self.protocol = bookmark.protocol; + self.mount_protocol(bookmark.protocol); match bookmark.params { ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params), ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params), @@ -247,51 +241,15 @@ impl AuthActivity { } fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) { - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) { - let props = InputPropsBuilder::from(props) - .with_value(params.address.clone()) - .build(); - self.view.update(super::COMPONENT_INPUT_ADDR, props); - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) { - let props = InputPropsBuilder::from(props) - .with_value(params.port.to_string()) - .build(); - self.view.update(super::COMPONENT_INPUT_PORT, props); - } - - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) { - let props = InputPropsBuilder::from(props) - .with_value(params.username.as_deref().unwrap_or_default().to_string()) - .build(); - self.view.update(super::COMPONENT_INPUT_USERNAME, props); - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) { - let props = InputPropsBuilder::from(props) - .with_value(params.password.as_deref().unwrap_or_default().to_string()) - .build(); - self.view.update(super::COMPONENT_INPUT_PASSWORD, props); - } + self.mount_address(params.address.as_str()); + self.mount_port(params.port); + self.mount_username(params.username.as_deref().unwrap_or("")); + self.mount_password(params.password.as_deref().unwrap_or("")); } fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) { - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_BUCKET) { - let props = InputPropsBuilder::from(props) - .with_value(params.bucket_name.clone()) - .build(); - self.view.update(super::COMPONENT_INPUT_S3_BUCKET, props); - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_REGION) { - let props = InputPropsBuilder::from(props) - .with_value(params.region.clone()) - .build(); - self.view.update(super::COMPONENT_INPUT_S3_REGION, props); - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_PROFILE) { - let props = InputPropsBuilder::from(props) - .with_value(params.profile.as_deref().unwrap_or_default().to_string()) - .build(); - self.view.update(super::COMPONENT_INPUT_S3_PROFILE, props); - } + self.mount_s3_bucket(params.bucket_name.as_str()); + self.mount_s3_region(params.region.as_str()); + self.mount_s3_profile(params.profile.as_deref().unwrap_or("")); } } diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs new file mode 100644 index 00000000..51acfa9f --- /dev/null +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -0,0 +1,445 @@ +//! ## Bookmarks +//! +//! auth activity bookmarks components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::Msg; + +use tui_realm_stdlib::{Input, List, Radio}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputType, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +// -- bookmark list + +#[derive(MockComponent)] +pub struct BookmarksList { + component: List, +} + +impl BookmarksList { + pub fn new(bookmarks: &[String], color: Color) -> Self { + Self { + component: List::default() + .borders(Borders::default().color(color).modifiers(BorderType::Plain)) + .highlighted_color(color) + .rewind(true) + .scroll(true) + .step(4) + .title("Bookmarks", Alignment::Left) + .rows( + bookmarks + .iter() + .map(|x| vec![TextSpan::from(x.as_str())]) + .collect(), + ), + } + } +} + +impl Component for BookmarksList { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::Usize(choice)) => Some(Msg::LoadBookmark(choice)), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => Some(Msg::BookmarksListBlur), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::BookmarksTabBlur), + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => Some(Msg::ShowDeleteBookmarkPopup), + _ => None, + } + } +} + +// -- recents list + +#[derive(MockComponent)] +pub struct RecentsList { + component: List, +} + +impl RecentsList { + pub fn new(bookmarks: &[String], color: Color) -> Self { + Self { + component: List::default() + .borders(Borders::default().color(color).modifiers(BorderType::Plain)) + .highlighted_color(color) + .rewind(true) + .scroll(true) + .step(4) + .title("Recent connections", Alignment::Left) + .rows( + bookmarks + .iter() + .map(|x| vec![TextSpan::from(x.as_str())]) + .collect(), + ), + } + } +} + +impl Component for RecentsList { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::Usize(choice)) => Some(Msg::LoadRecent(choice)), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => Some(Msg::RececentsListBlur), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::BookmarksTabBlur), + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => Some(Msg::ShowDeleteRecentPopup), + _ => None, + } + } +} + +// -- delete bookmark + +#[derive(MockComponent)] +pub struct DeleteBookmarkPopup { + component: Radio, +} + +impl DeleteBookmarkPopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .choices(&["Yes", "No"]) + .value(1) + .rewind(true) + .foreground(color) + .title("Delete selected bookmark?", Alignment::Center), + } + } +} + +impl Component for DeleteBookmarkPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseDeleteBookmark), + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::DeleteBookmark) + } else { + Some(Msg::CloseDeleteBookmark) + } + } + _ => None, + } + } +} + +// -- delete recent + +#[derive(MockComponent)] +pub struct DeleteRecentPopup { + component: Radio, +} + +impl DeleteRecentPopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .choices(&["Yes", "No"]) + .value(1) + .rewind(true) + .foreground(color) + .title("Delete selected recent host?", Alignment::Center), + } + } +} + +impl Component for DeleteRecentPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseDeleteRecent), + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::DeleteRecent) + } else { + Some(Msg::CloseDeleteRecent) + } + } + _ => None, + } + } +} + +// -- bookmark name + +// -- save password + +#[derive(MockComponent)] +pub struct BookmarkSavePassword { + component: Radio, +} + +impl BookmarkSavePassword { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::Reset) + .sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT) + .modifiers(BorderType::Rounded), + ) + .choices(&["Yes", "No"]) + .value(0) + .rewind(true) + .foreground(color) + .title("Save password?", Alignment::Center), + } + } +} + +impl Component for BookmarkSavePassword { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseSaveBookmark), + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::SaveBookmark), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::SaveBookmarkPasswordBlur), + _ => None, + } + } +} + +// -- new bookmark name + +#[derive(MockComponent)] +pub struct BookmarkName { + component: Input, +} + +impl BookmarkName { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(Color::Reset) + .sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Bookmark name", Alignment::Left) + .input_type(InputType::Text), + } + } +} + +impl Component for BookmarkName { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::SaveBookmark), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::BookmarkNameBlur), + _ => None, + } + } +} diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs new file mode 100644 index 00000000..29a5c72e --- /dev/null +++ b/src/ui/activities/auth/components/form.rs @@ -0,0 +1,694 @@ +//! ## Form +//! +//! auth activity components for file transfer params form + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{FileTransferProtocol, Msg}; + +use tui_realm_stdlib::{Input, Radio}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +// -- protocol + +#[derive(MockComponent)] +pub struct ProtocolRadio { + component: Radio, +} + +impl ProtocolRadio { + pub fn new(default_protocol: FileTransferProtocol, color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .choices(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"]) + .foreground(color) + .rewind(true) + .title("Protocol", Alignment::Left) + .value(Self::protocol_enum_to_opt(default_protocol)), + } + } + + /// ### protocol_opt_to_enum + /// + /// Convert radio index for protocol into a `FileTransferProtocol` + fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol { + match protocol { + 1 => FileTransferProtocol::Scp, + 2 => FileTransferProtocol::Ftp(false), + 3 => FileTransferProtocol::Ftp(true), + 4 => FileTransferProtocol::AwsS3, + _ => FileTransferProtocol::Sftp, + } + } + + /// ### protocol_enum_to_opt + /// + /// Convert `FileTransferProtocol` enum into radio group index + fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize { + match protocol { + FileTransferProtocol::Sftp => 0, + FileTransferProtocol::Scp => 1, + FileTransferProtocol::Ftp(false) => 2, + FileTransferProtocol::Ftp(true) => 3, + FileTransferProtocol::AwsS3 => 4, + } + } +} + +impl Component for ProtocolRadio { + fn on(&mut self, ev: Event) -> Option { + let result = match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => self.perform(Cmd::Move(Direction::Left)), + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => self.perform(Cmd::Move(Direction::Right)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => return Some(Msg::Connect), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => return Some(Msg::ProtocolBlurDown), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => return Some(Msg::ProtocolBlurUp), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => return Some(Msg::ParamsFormBlur), + _ => return None, + }; + match result { + CmdResult::Changed(State::One(StateValue::Usize(choice))) => { + Some(Msg::ProtocolChanged(Self::protocol_opt_to_enum(choice))) + } + _ => Some(Msg::None), + } + } +} + +// -- address + +#[derive(MockComponent)] +pub struct InputAddress { + component: Input, +} + +impl InputAddress { + pub fn new(host: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("127.0.0.1", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Remote host", Alignment::Left) + .input_type(InputType::Text) + .value(host), + } + } +} + +impl Component for InputAddress { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Connect), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::AddressBlurDown), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::AddressBlurUp), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + _ => None, + } + } +} + +// -- port number + +#[derive(MockComponent)] +pub struct InputPort { + component: Input, +} + +impl InputPort { + pub fn new(port: u16, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("22", Style::default().fg(Color::Rgb(128, 128, 128))) + .input_type(InputType::UnsignedInteger) + .input_len(5) + .title("Port number", Alignment::Left) + .value(port.to_string()), + } + } +} + +impl Component for InputPort { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Connect), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::PortBlurDown), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PortBlurUp), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + _ => None, + } + } +} + +// -- username + +#[derive(MockComponent)] +pub struct InputUsername { + component: Input, +} + +impl InputUsername { + pub fn new(username: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Username", Alignment::Left) + .input_type(InputType::Text) + .value(username), + } + } +} + +impl Component for InputUsername { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Connect), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::UsernameBlurDown), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::UsernameBlurUp), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + _ => None, + } + } +} + +// -- password + +#[derive(MockComponent)] +pub struct InputPassword { + component: Input, +} + +impl InputPassword { + pub fn new(password: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Password", Alignment::Left) + .input_type(InputType::Password('*')) + .value(password), + } + } +} + +impl Component for InputPassword { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Connect), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::PasswordBlurDown), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PasswordBlurUp), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + _ => None, + } + } +} + +// -- s3 bucket + +#[derive(MockComponent)] +pub struct InputS3Bucket { + component: Input, +} + +impl InputS3Bucket { + pub fn new(bucket: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("my-bucket", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Bucket name", Alignment::Left) + .input_type(InputType::Text) + .value(bucket), + } + } +} + +impl Component for InputS3Bucket { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Connect), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::S3BucketBlurDown), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3BucketBlurUp), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + _ => None, + } + } +} + +// -- s3 bucket + +#[derive(MockComponent)] +pub struct InputS3Region { + component: Input, +} + +impl InputS3Region { + pub fn new(region: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("eu-west-1", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Region", Alignment::Left) + .input_type(InputType::Text) + .value(region), + } + } +} + +impl Component for InputS3Region { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Connect), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::S3RegionBlurDown), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3RegionBlurUp), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + _ => None, + } + } +} + +// -- s3 bucket + +#[derive(MockComponent)] +pub struct InputS3Profile { + component: Input, +} + +impl InputS3Profile { + pub fn new(profile: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("default", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Profile", Alignment::Left) + .input_type(InputType::Text) + .value(profile), + } + } +} + +impl Component for InputS3Profile { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Connect), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::S3ProfileBlurDown), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3ProfileBlurUp), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + _ => None, + } + } +} diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs new file mode 100644 index 00000000..908cd361 --- /dev/null +++ b/src/ui/activities/auth/components/mod.rs @@ -0,0 +1,91 @@ +//! ## Components +//! +//! auth activity components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{FileTransferProtocol, Msg}; + +mod bookmarks; +mod form; +mod popup; +mod text; + +pub use bookmarks::{ + BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup, + RecentsList, +}; +pub use form::{ + InputAddress, InputPassword, InputPort, InputS3Bucket, InputS3Profile, InputS3Region, + InputUsername, ProtocolRadio, +}; +pub use popup::{ + ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup, + WindowSizeError, +}; +pub use text::{HelpText, NewVersionDisclaimer, Subtitle, Title}; + +use tui_realm_stdlib::Phantom; +use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; +use tuirealm::{Component, MockComponent}; + +// -- global listener + +#[derive(MockComponent)] +pub struct GlobalListener { + component: Phantom, +} + +impl Default for GlobalListener { + fn default() -> Self { + Self { + component: Phantom::default(), + } + } +} + +impl Component for GlobalListener { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::ShowQuitPopup), + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL, + }) => Some(Msg::EnterSetup), + Event::Keyboard(KeyEvent { + code: Key::Char('h'), + modifiers: KeyModifiers::CONTROL, + }) => Some(Msg::ShowKeybindingsPopup), + Event::Keyboard(KeyEvent { + code: Key::Char('r'), + modifiers: KeyModifiers::CONTROL, + }) => Some(Msg::ShowReleaseNotes), + Event::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::CONTROL, + }) => Some(Msg::ShowSaveBookmarkPopup), + _ => None, + } + } +} diff --git a/src/ui/activities/auth/components/popup.rs b/src/ui/activities/auth/components/popup.rs new file mode 100644 index 00000000..3613ee87 --- /dev/null +++ b/src/ui/activities/auth/components/popup.rs @@ -0,0 +1,452 @@ +//! ## Popup +//! +//! auth activity popups + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::Msg; + +use tui_realm_stdlib::{List, Paragraph, Radio, Textarea}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +// -- error popup + +#[derive(MockComponent)] +pub struct ErrorPopup { + component: Paragraph, +} + +impl ErrorPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text(&[TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for ErrorPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::CloseErrorPopup), + _ => None, + } + } +} + +// -- info popup + +#[derive(MockComponent)] +pub struct InfoPopup { + component: Paragraph, +} + +impl InfoPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text(&[TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for InfoPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::CloseInfoPopup), + _ => None, + } + } +} + +// -- wait popup + +#[derive(MockComponent)] +pub struct WaitPopup { + component: Paragraph, +} + +impl WaitPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text(&[TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for WaitPopup { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +// -- window size error + +#[derive(MockComponent)] +pub struct WindowSizeError { + component: Paragraph, +} + +impl WindowSizeError { + pub fn new(color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text(&[TextSpan::from( + "termscp requires at least 24 lines of height to run", + )]) + .wrap(true), + } + } +} + +impl Component for WindowSizeError { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +// -- quit popup + +#[derive(MockComponent)] +pub struct QuitPopup { + component: Radio, +} + +impl QuitPopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Quit termscp?", Alignment::Center) + .rewind(true) + .choices(&["Yes", "No"]), + } + } +} + +impl Component for QuitPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseQuitPopup), + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Quit) + } else { + Some(Msg::CloseQuitPopup) + } + } + _ => None, + } + } +} + +// -- install update popup + +#[derive(MockComponent)] +pub struct InstallUpdatePopup { + component: Radio, +} + +impl InstallUpdatePopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Install update?", Alignment::Center) + .rewind(true) + .choices(&["Yes", "No"]), + } + } +} + +impl Component for InstallUpdatePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseInstallUpdatePopup), + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::InstallUpdate) + } else { + Some(Msg::CloseInstallUpdatePopup) + } + } + _ => None, + } + } +} + +// -- release notes popup + +#[derive(MockComponent)] +pub struct ReleaseNotes { + component: Textarea, +} + +impl ReleaseNotes { + pub fn new(notes: &str, color: Color) -> Self { + Self { + component: Textarea::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Release notes", Alignment::Center) + .text_rows( + notes + .lines() + .map(TextSpan::from) + .collect::>() + .as_slice(), + ), + } + } +} + +impl Component for ReleaseNotes { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::CloseInstallUpdatePopup), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + _ => None, + } + } +} + +// -- keybindings popup + +#[derive(MockComponent)] +pub struct Keybindings { + component: List, +} + +impl Keybindings { + pub fn new(color: Color) -> Self { + Self { + component: List::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .highlighted_str("? ") + .title("Keybindings", Alignment::Center) + .scroll(true) + .step(4) + .rows( + TableBuilder::default() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Quit termscp")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Switch from form and bookmarks")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Switch bookmark tab")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Move up/down in current tab")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Connect/Load bookmark")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Delete selected bookmark")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Enter setup")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Save bookmark")) + .build(), + ), + } + } +} + +impl Component for Keybindings { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::CloseKeybindingsPopup), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + _ => None, + } + } +} diff --git a/src/ui/activities/auth/components/text.rs b/src/ui/activities/auth/components/text.rs new file mode 100644 index 00000000..c2f7fa74 --- /dev/null +++ b/src/ui/activities/auth/components/text.rs @@ -0,0 +1,132 @@ +//! ## Text +//! +//! auth activity texts + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::Msg; + +use tui_realm_stdlib::{Label, Span}; +use tuirealm::props::{Color, TextModifiers, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +// -- Title + +#[derive(MockComponent)] +pub struct Title { + component: Label, +} + +impl Default for Title { + fn default() -> Self { + Self { + component: Label::default() + .modifiers(TextModifiers::BOLD | TextModifiers::ITALIC) + .text("$ termscp"), + } + } +} + +impl Component for Title { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +// -- subtitle + +#[derive(MockComponent)] +pub struct Subtitle { + component: Label, +} + +impl Default for Subtitle { + fn default() -> Self { + Self { + component: Label::default() + .modifiers(TextModifiers::BOLD | TextModifiers::ITALIC) + .text(format!("$ version {}", env!("CARGO_PKG_VERSION"))), + } + } +} + +impl Component for Subtitle { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +// -- new version disclaimer + +#[derive(MockComponent)] +pub struct NewVersionDisclaimer { + component: Span, +} + +impl NewVersionDisclaimer { + pub fn new(new_version: &str, color: Color) -> Self { + Self { + component: Span::default().foreground(color).spans(&[ + TextSpan::from("termscp "), + TextSpan::new(new_version).underlined().bold(), + TextSpan::from( + " is NOW available! Install update and view release notes with ", + ), + ]), + } + } +} + +impl Component for NewVersionDisclaimer { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +// -- HelpText + +#[derive(MockComponent)] +pub struct HelpText { + component: Span, +} + +impl HelpText { + pub fn new(key_color: Color) -> Self { + Self { + component: Span::default().spans(&[ + TextSpan::new("Press ").bold(), + TextSpan::new("").bold().fg(key_color), + TextSpan::new(" to show keybindings; ").bold(), + TextSpan::new("").bold().fg(key_color), + TextSpan::new(" to enter setup").bold(), + ]), + } + } +} + +impl Component for HelpText { + fn on(&mut self, _ev: Event) -> Option { + None + } +} diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index f85c09e8..5e831180 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -31,32 +31,6 @@ use crate::system::auto_update::{Release, Update, UpdateStatus}; use crate::system::notifications::Notification; impl AuthActivity { - /// ### protocol_opt_to_enum - /// - /// Convert radio index for protocol into a `FileTransferProtocol` - pub(super) fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol { - match protocol { - 1 => FileTransferProtocol::Scp, - 2 => FileTransferProtocol::Ftp(false), - 3 => FileTransferProtocol::Ftp(true), - 4 => FileTransferProtocol::AwsS3, - _ => FileTransferProtocol::Sftp, - } - } - - /// ### protocol_enum_to_opt - /// - /// Convert `FileTransferProtocol` enum into radio group index - pub(super) fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize { - match protocol { - FileTransferProtocol::Sftp => 0, - FileTransferProtocol::Scp => 1, - FileTransferProtocol::Ftp(false) => 2, - FileTransferProtocol::Ftp(true) => 3, - FileTransferProtocol::AwsS3 => 4, - } - } - /// ### get_default_port_for_protocol /// /// Get the default port for protocol @@ -91,9 +65,8 @@ impl AuthActivity { /// /// Collect host params as `FileTransferParams` pub(super) fn collect_host_params(&self) -> Result { - let protocol: FileTransferProtocol = self.get_protocol(); - match protocol { - FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol), + match self.protocol { + FileTransferProtocol::AwsS3 => self.collect_s3_host_params(), protocol => self.collect_generic_host_params(protocol), } } @@ -135,10 +108,7 @@ impl AuthActivity { /// ### collect_s3_host_params /// /// Get input values from fields or return an error if fields are invalid to work as aws s3 - pub(super) fn collect_s3_host_params( - &self, - protocol: FileTransferProtocol, - ) -> Result { + pub(super) fn collect_s3_host_params(&self) -> Result { let (bucket, region, profile): (String, String, Option) = self.get_s3_params_input(); if bucket.is_empty() { @@ -148,7 +118,7 @@ impl AuthActivity { return Err("Invalid region"); } Ok(FileTransferParams { - protocol, + protocol: FileTransferProtocol::AwsS3, params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)), entry_directory: None, }) diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index a5682354..2d9da506 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -27,6 +27,7 @@ */ // Sub modules mod bookmarks; +mod components; mod misc; mod update; mod view; @@ -39,37 +40,101 @@ use crate::system::bookmarks_client::BookmarksClient; use crate::system::config_client::ConfigClient; // Includes -use crossterm::event::Event; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; -use tuirealm::{Update, View}; +use std::time::Duration; +use tuirealm::listener::EventListenerCfg; +use tuirealm::{application::PollStrategy, Application, NoUserEvent, Update}; // -- components -const COMPONENT_TEXT_H1: &str = "TEXT_H1"; -const COMPONENT_TEXT_H2: &str = "TEXT_H2"; -const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION"; -const COMPONENT_TEXT_NEW_VERSION_NOTES: &str = "TEXTAREA_NEW_VERSION"; -const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER"; -const COMPONENT_TEXT_HELP: &str = "TEXT_HELP"; -const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR"; -const COMPONENT_TEXT_INFO: &str = "TEXT_INFO"; -const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT"; -const COMPONENT_TEXT_SIZE_ERR: &str = "TEXT_SIZE_ERR"; -const COMPONENT_INPUT_ADDR: &str = "INPUT_ADDRESS"; -const COMPONENT_INPUT_PORT: &str = "INPUT_PORT"; -const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME"; -const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD"; -const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME"; -const COMPONENT_INPUT_S3_BUCKET: &str = "INPUT_S3_BUCKET"; -const COMPONENT_INPUT_S3_REGION: &str = "INPUT_S3_REGION"; -const COMPONENT_INPUT_S3_PROFILE: &str = "INPUT_S3_PROFILE"; -const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL"; -const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT"; -const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK"; -const COMPONENT_RADIO_BOOKMARK_DEL_RECENT: &str = "RADIO_DELETE_RECENT"; -const COMPONENT_RADIO_BOOKMARK_SAVE_PWD: &str = "RADIO_SAVE_PASSWORD"; -const COMPONENT_RADIO_INSTALL_UPDATE: &str = "RADIO_INSTALL_UPDATE"; -const COMPONENT_BOOKMARKS_LIST: &str = "BOOKMARKS_LIST"; -const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST"; +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum Id { + Address, + BookmarkName, + BookmarkSavePassword, + BookmarksList, + DeleteBookmarkPopup, + DeleteRecentPopup, + ErrorPopup, + GlobalListener, + HelpText, + InfoPopup, + InstallUpdatePopup, + Keybindings, + NewVersionChangelog, + NewVersionDisclaimer, + Password, + Port, + Protocol, + QuitPopup, + RecentsList, + S3Bucket, + S3Profile, + S3Region, + Subtitle, + Title, + Username, + WaitPopup, + WindowSizeError, +} + +#[derive(Debug, PartialEq)] +pub enum Msg { + AddressBlurDown, + AddressBlurUp, + BookmarksListBlur, + BookmarksTabBlur, + CloseDeleteBookmark, + CloseDeleteRecent, + CloseErrorPopup, + CloseInfoPopup, + CloseInstallUpdatePopup, + CloseKeybindingsPopup, + CloseQuitPopup, + CloseSaveBookmark, + Connect, + DeleteBookmark, + DeleteRecent, + EnterSetup, + InstallUpdate, + LoadBookmark(usize), + LoadRecent(usize), + ParamsFormBlur, + PasswordBlurDown, + PasswordBlurUp, + PortBlurDown, + PortBlurUp, + ProtocolBlurDown, + ProtocolBlurUp, + ProtocolChanged(FileTransferProtocol), + Quit, + RececentsListBlur, + S3BucketBlurDown, + S3BucketBlurUp, + S3ProfileBlurDown, + S3ProfileBlurUp, + S3RegionBlurDown, + S3RegionBlurUp, + SaveBookmark, + BookmarkNameBlur, + SaveBookmarkPasswordBlur, + ShowDeleteBookmarkPopup, + ShowDeleteRecentPopup, + ShowKeybindingsPopup, + ShowQuitPopup, + ShowReleaseNotes, + ShowSaveBookmarkPopup, + UsernameBlurDown, + UsernameBlurUp, + None, +} + +/// ## InputMask +/// +/// Auth form input mask +#[derive(Eq, PartialEq)] +enum InputMask { + Generic, + AwsS3, +} // Store keys const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION"; @@ -79,34 +144,39 @@ const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES"; /// /// AuthActivity is the data holder for the authentication activity pub struct AuthActivity { + app: Application, + bookmarks_client: Option, + /// List of bookmarks + bookmarks_list: Vec, + /// List of recent hosts + recents_list: Vec, + /// Exit reason exit_reason: Option, + /// Should redraw ui + redraw: bool, + /// Protocol + protocol: FileTransferProtocol, context: Option, - view: View, - bookmarks_client: Option, - redraw: bool, // Should ui actually be redrawned? - bookmarks_list: Vec, // List of bookmarks - recents_list: Vec, // list of recents -} - -impl Default for AuthActivity { - fn default() -> Self { - Self::new() - } } impl AuthActivity { /// ### new /// /// Instantiates a new AuthActivity - pub fn new() -> AuthActivity { + pub fn new(ticks: Duration) -> AuthActivity { AuthActivity { - exit_reason: None, + app: Application::init( + EventListenerCfg::default() + .default_input_listener(ticks) + .poll_timeout(ticks), + ), context: None, - view: View::init(), bookmarks_client: None, - redraw: true, // True at startup bookmarks_list: Vec::new(), + exit_reason: None, recents_list: Vec::new(), + redraw: true, + protocol: FileTransferProtocol::Sftp, } } @@ -142,9 +212,11 @@ impl AuthActivity { /// /// Get current input mask to show fn input_mask(&self) -> InputMask { - match self.get_protocol() { + match self.protocol { FileTransferProtocol::AwsS3 => InputMask::AwsS3, - _ => InputMask::Generic, + FileTransferProtocol::Ftp(_) + | FileTransferProtocol::Scp + | FileTransferProtocol::Sftp => InputMask::Generic, } } } @@ -162,9 +234,11 @@ impl Activity for AuthActivity { // Set context self.context = Some(context); // Clear terminal - self.context_mut().clear_screen(); + if let Err(err) = self.context_mut().terminal().clear_screen() { + error!("Failed to clear screen: {}", err); + } // Put raw mode on enabled - if let Err(err) = enable_raw_mode() { + if let Err(err) = self.context_mut().terminal().enable_raw_mode() { error!("Failed to enter raw mode: {}", err); } // If check for updates is enabled, check for updates @@ -194,24 +268,23 @@ impl Activity for AuthActivity { if self.context.is_none() { return; } - // Read one event - if let Ok(Some(event)) = self.context().input_hnd().read_event() { - // Set redraw to true - self.redraw = true; - // Handle on resize - if let Event::Resize(_, h) = event { - self.check_minimum_window_size(h); + // Tick + match self.app.tick(PollStrategy::UpTo(3)) { + Ok(messages) => { + for msg in messages.into_iter() { + let mut msg = Some(msg); + while msg.is_some() { + msg = self.update(msg); + } + } + } + Err(err) => { + self.mount_error(format!("Application error: {}", err)); } - // Handle event on view and update - let msg = self.view.on(event); - self.update(msg); } - // Redraw if necessary + // View if self.redraw { - // View self.view(); - // Set redraw to false - self.redraw = false; } } @@ -231,26 +304,12 @@ impl Activity for AuthActivity { /// This function finally releases the context fn on_destroy(&mut self) -> Option { // Disable raw mode - if let Err(err) = disable_raw_mode() { + if let Err(err) = self.context_mut().terminal().disable_raw_mode() { error!("Failed to disable raw mode: {}", err); } - self.context.as_ref()?; - // Clear terminal and return - match self.context.take() { - Some(mut ctx) => { - ctx.clear_screen(); - Some(ctx) - } - None => None, + if let Err(err) = self.context_mut().terminal().clear_screen() { + error!("Failed to clear screen: {}", err); } + self.context.take() } } - -/// ## InputMask -/// -/// Auth form input mask -#[derive(Eq, PartialEq)] -enum InputMask { - Generic, - AwsS3, -} diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index de764198..2457bee6 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -1,6 +1,6 @@ -//! ## AuthActivity +//! ## Update //! -//! `auth_activity` is the module which implements the authentication activity +//! Update impl /** * MIT License @@ -25,415 +25,222 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -// locals -use super::{ - AuthActivity, FileTransferProtocol, InputMask, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR, - COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT, - COMPONENT_INPUT_S3_BUCKET, COMPONENT_INPUT_S3_PROFILE, COMPONENT_INPUT_S3_REGION, - COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, - COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD, - COMPONENT_RADIO_INSTALL_UPDATE, COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, - COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP, COMPONENT_TEXT_INFO, - COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR, COMPONENT_TEXT_WAIT, -}; -use crate::ui::keymap::*; -use tui_realm_stdlib::InputPropsBuilder; -use tuirealm::{Msg, Payload, PropsBuilder, Update, Value}; +use super::{AuthActivity, ExitReason, Id, InputMask, Msg, Update}; -// -- update +use tuirealm::{State, StateValue}; -impl Update for AuthActivity { - /// ### update - /// - /// Update auth activity model based on msg - /// The function exits when returns None - fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> { - let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg)); - // Match msg - match ref_msg { - None => None, // Exit after None - Some(msg) => match msg { - // Focus ( DOWN ) - (COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => { - // Give focus based on current mask - match self.input_mask() { - InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR), - InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET), - }; - None - } - // -- generic mask (DOWN) - (COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_PORT); - None - } - (COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_USERNAME); - None - } - (COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_PASSWORD); - None - } - (COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_PROTOCOL); - None - } - // -- s3 mask (DOWN) - (COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_S3_REGION); - None - } - (COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_S3_PROFILE); - None - } - (COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_PROTOCOL); - None - } - // Focus ( UP ) - // -- generic (UP) - (COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_USERNAME); - None - } - (COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_PORT); - None - } - (COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_ADDR); - None - } - (COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_PROTOCOL); - None - } - // -- s3 (UP) - (COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_PROTOCOL); - None - } - (COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_S3_BUCKET); - None - } - (COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_S3_REGION); - None - } - (COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => { - // Give focus based on current mask - match self.input_mask() { - InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD), - InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE), - }; - None - } - // Protocol - On Change - (COMPONENT_RADIO_PROTOCOL, Msg::OnChange(Payload::One(Value::Usize(protocol)))) => { - // If port is standard, update the current port with default for selected protocol - let protocol: FileTransferProtocol = Self::protocol_opt_to_enum(*protocol); - // Get port - let port: u16 = self.get_input_port(); - match Self::is_port_standard(port) { - false => None, // Return None - true => { - self.update_input_port(Self::get_default_port_for_protocol(protocol)) - } - } - } - // Bookmarks commands - // / - (COMPONENT_BOOKMARKS_LIST, key) if key == &MSG_KEY_RIGHT => { - // Give focus to recents - self.view.active(COMPONENT_RECENTS_LIST); - None - } - (COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_LEFT => { - // Give focus to bookmarks - self.view.active(COMPONENT_BOOKMARKS_LIST); - None - } - // - (COMPONENT_BOOKMARKS_LIST, key) - if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E => - { - // Show delete popup - self.mount_bookmark_del_dialog(); - None - } - (COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E => { - // Show delete popup - self.mount_recent_del_dialog(); - None - } - // Enter - (COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { - self.load_bookmark(*idx); - // Give focus to input password (or to protocol if not generic) - self.view.active(match self.input_mask() { - InputMask::Generic => COMPONENT_INPUT_PASSWORD, - InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET, - }); - None - } - (COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { - self.load_recent(*idx); - // Give focus to input password - self.view.active(match self.input_mask() { - InputMask::Generic => COMPONENT_INPUT_PASSWORD, - InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET, - }); - None - } - // Bookmark radio - // Del bookmarks - ( - COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, - Msg::OnSubmit(Payload::One(Value::Usize(index))), - ) => { - // hide bookmark delete - self.umount_bookmark_del_dialog(); - // Index must be 0 => YES - match *index { - 0 => { - // Get selected bookmark - match self.view.get_state(COMPONENT_BOOKMARKS_LIST) { - Some(Payload::One(Value::Usize(index))) => { - // Delete bookmark - self.del_bookmark(index); - // Update bookmarks - self.view_bookmarks() - } - _ => None, - } - } - _ => None, +impl Update for AuthActivity { + fn update(&mut self, msg: Option) -> Option { + self.redraw = true; + match msg.unwrap_or(Msg::None) { + Msg::AddressBlurDown => { + assert!(self.app.active(&Id::Port).is_ok()); + } + Msg::AddressBlurUp => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + Msg::BookmarksListBlur => { + assert!(self.app.active(&Id::RecentsList).is_ok()); + } + Msg::BookmarkNameBlur => { + assert!(self.app.active(&Id::BookmarkSavePassword).is_ok()); + } + Msg::BookmarksTabBlur => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + Msg::CloseDeleteBookmark => { + assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok()); + } + Msg::CloseDeleteRecent => { + assert!(self.app.umount(&Id::DeleteRecentPopup).is_ok()); + } + Msg::CloseErrorPopup => { + self.umount_error(); + } + Msg::CloseInfoPopup => { + self.umount_info(); + } + Msg::CloseInstallUpdatePopup => { + assert!(self.app.umount(&Id::NewVersionChangelog).is_ok()); + assert!(self.app.umount(&Id::InstallUpdatePopup).is_ok()); + } + Msg::CloseKeybindingsPopup => { + self.umount_help(); + } + Msg::CloseQuitPopup => self.umount_quit(), + Msg::CloseSaveBookmark => { + assert!(self.app.umount(&Id::BookmarkName).is_ok()); + assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok()); + } + Msg::Connect => { + match self.collect_host_params() { + Err(err) => { + // mount error + self.mount_error(err); } - } - ( - COMPONENT_RADIO_BOOKMARK_DEL_RECENT, - Msg::OnSubmit(Payload::One(Value::Usize(index))), - ) => { - // hide bookmark delete - self.umount_recent_del_dialog(); - // Index must be 0 => YES - match *index { - 0 => { - // Get selected bookmark - match self.view.get_state(COMPONENT_RECENTS_LIST) { - Some(Payload::One(Value::Usize(index))) => { - // Delete recent - self.del_recent(index); - // Update bookmarks - self.view_recent_connections() - } - _ => None, - } - } - _ => None, + Ok(params) => { + self.save_recent(); + // Set file transfer params to context + self.context_mut().set_ftparams(params); + // Set exit reason + self.exit_reason = Some(super::ExitReason::Connect); } } - // hide tab - (COMPONENT_RADIO_BOOKMARK_DEL_RECENT, key) if key == &MSG_KEY_ESC => { - self.umount_recent_del_dialog(); - None - } - (COMPONENT_RADIO_BOOKMARK_DEL_RECENT, _) => None, - (COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, key) if key == &MSG_KEY_ESC => { + } + Msg::DeleteBookmark => { + if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) { + // Umount dialog self.umount_bookmark_del_dialog(); - None - } - (COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, _) => None, - // Error message - (COMPONENT_TEXT_ERROR, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => { - // Umount text error - self.umount_error(); - None - } - // -- Text info - (COMPONENT_TEXT_INFO, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => { - // Umount text info - self.umount_info(); - None - } - (COMPONENT_TEXT_ERROR, _) | (COMPONENT_TEXT_INFO, _) => None, - // -- Text wait - (COMPONENT_TEXT_WAIT, _) => None, - // -- Release notes - (COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_ESC => { - // Umount release notes - self.umount_release_notes(); - None - } - (COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_TAB => { - // Focus to radio update - self.view.active(COMPONENT_RADIO_INSTALL_UPDATE); - None - } - (COMPONENT_TEXT_NEW_VERSION_NOTES, _) => None, - // -- Install update radio - (COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Install update - self.install_update(); - None - } - (COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { - // Umount - self.umount_release_notes(); - None - } - (COMPONENT_RADIO_INSTALL_UPDATE, key) if key == &MSG_KEY_TAB => { - // Focus to changelog - self.view.active(COMPONENT_TEXT_NEW_VERSION_NOTES); - None - } - (COMPONENT_RADIO_INSTALL_UPDATE, _) => None, - // Help - (_, key) if key == &MSG_KEY_CTRL_H => { - // Show help - self.mount_help(); - None - } - // Release notes - (_, key) if key == &MSG_KEY_CTRL_R => { - // Show release notes - self.mount_release_notes(); - None - } - (COMPONENT_TEXT_HELP, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => { - // Hide text help - self.umount_help(); - None - } - (COMPONENT_TEXT_HELP, _) => None, - // Enter setup - (_, key) if key == &MSG_KEY_CTRL_C => { - self.exit_reason = Some(super::ExitReason::EnterSetup); - None - } - // Save bookmark; show popup - (_, key) if key == &MSG_KEY_CTRL_S => { - // Show popup - self.mount_bookmark_save_dialog(); - // Give focus to bookmark name - self.view.active(COMPONENT_INPUT_BOOKMARK_NAME); - None - } - (COMPONENT_INPUT_BOOKMARK_NAME, key) if key == &MSG_KEY_DOWN => { - // Give focus to pwd - self.view.active(COMPONENT_RADIO_BOOKMARK_SAVE_PWD); - None - } - (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key) if key == &MSG_KEY_UP => { - // Give focus to pwd - self.view.active(COMPONENT_INPUT_BOOKMARK_NAME); - None - } - // Save bookmark - (COMPONENT_INPUT_BOOKMARK_NAME, Msg::OnSubmit(_)) - | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => { - // Get values - let bookmark_name: String = - match self.view.get_state(COMPONENT_INPUT_BOOKMARK_NAME) { - Some(Payload::One(Value::Str(s))) => s, - _ => String::new(), - }; - let save_pwd: bool = matches!( - self.view.get_state(COMPONENT_RADIO_BOOKMARK_SAVE_PWD), - Some(Payload::One(Value::Usize(0))) - ); - // Save bookmark - if !bookmark_name.is_empty() { - self.save_bookmark(bookmark_name, save_pwd); - } - // Umount popup - self.umount_bookmark_save_dialog(); - // Reload bookmarks + // Delete bookmark + self.del_bookmark(idx); + // Update bookmarks self.view_bookmarks() } - // Hide save bookmark - (COMPONENT_INPUT_BOOKMARK_NAME, key) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key) - if key == &MSG_KEY_ESC => - { - // Umount popup - self.umount_bookmark_save_dialog(); - None - } - (COMPONENT_INPUT_BOOKMARK_NAME, _) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, _) => None, - // Quit dialog - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(choice)))) => { - // If choice is 0, quit termscp - if *choice == 0 { - self.exit_reason = Some(super::ExitReason::Quit); - } - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, key) if key == &MSG_KEY_ESC => { - self.umount_quit(); - None - } - // -- text size error; block everything - (COMPONENT_TEXT_SIZE_ERR, _) => None, - // bookmarks - (COMPONENT_BOOKMARKS_LIST, key) | (COMPONENT_RECENTS_LIST, key) - if key == &MSG_KEY_TAB => - { - // Give focus to address - self.view.active(COMPONENT_RADIO_PROTOCOL); - None + } + Msg::DeleteRecent => { + if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::RecentsList) { + // Umount dialog + self.umount_recent_del_dialog(); + // Delete recent + self.del_recent(idx); + // Update recents + self.view_recent_connections(); } - // Any , go to bookmarks - (_, key) if key == &MSG_KEY_TAB => { - self.view.active(COMPONENT_BOOKMARKS_LIST); - None + } + Msg::EnterSetup => { + self.exit_reason = Some(ExitReason::EnterSetup); + } + Msg::InstallUpdate => { + self.install_update(); + } + Msg::LoadBookmark(i) => { + self.load_bookmark(i); + // Give focus to input password (or to protocol if not generic) + assert!(self + .app + .active(match self.input_mask() { + InputMask::Generic => &Id::Password, + InputMask::AwsS3 => &Id::S3Bucket, + }) + .is_ok()); + } + Msg::LoadRecent(i) => { + self.load_recent(i); + // Give focus to input password (or to protocol if not generic) + assert!(self + .app + .active(match self.input_mask() { + InputMask::Generic => &Id::Password, + InputMask::AwsS3 => &Id::S3Bucket, + }) + .is_ok()); + } + Msg::ParamsFormBlur => { + assert!(self.app.active(&Id::BookmarksList).is_ok()); + } + Msg::PasswordBlurDown => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + Msg::PasswordBlurUp => { + assert!(self.app.active(&Id::Username).is_ok()); + } + Msg::PortBlurDown => { + assert!(self.app.active(&Id::Username).is_ok()); + } + Msg::PortBlurUp => { + assert!(self.app.active(&Id::Address).is_ok()); + } + Msg::ProtocolBlurDown => { + assert!(self + .app + .active(match self.input_mask() { + InputMask::Generic => &Id::Address, + InputMask::AwsS3 => &Id::S3Bucket, + }) + .is_ok()); + } + Msg::ProtocolBlurUp => { + assert!(self + .app + .active(match self.input_mask() { + InputMask::Generic => &Id::Password, + InputMask::AwsS3 => &Id::S3Profile, + }) + .is_ok()); + } + Msg::ProtocolChanged(protocol) => { + self.protocol = protocol; + // Update port + let port: u16 = self.get_input_port(); + if Self::is_port_standard(port) { + self.mount_port(Self::get_default_port_for_protocol(protocol)); } - // On submit on any unhandled (connect) - (_, Msg::OnSubmit(_)) => self.on_unhandled_submit(), - (_, key) if key == &MSG_KEY_ENTER => self.on_unhandled_submit(), - // => Quit - (_, key) if key == &MSG_KEY_ESC => { - self.mount_quit(); - None + } + Msg::Quit => { + self.exit_reason = Some(ExitReason::Quit); + } + Msg::RececentsListBlur => { + assert!(self.app.active(&Id::BookmarksList).is_ok()); + } + Msg::S3BucketBlurDown => { + assert!(self.app.active(&Id::S3Region).is_ok()); + } + Msg::S3BucketBlurUp => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + Msg::S3RegionBlurDown => { + assert!(self.app.active(&Id::S3Profile).is_ok()); + } + Msg::S3RegionBlurUp => { + assert!(self.app.active(&Id::S3Bucket).is_ok()); + } + Msg::S3ProfileBlurDown => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + Msg::S3ProfileBlurUp => { + assert!(self.app.active(&Id::S3Region).is_ok()); + } + Msg::SaveBookmark => { + // get bookmark name + let (name, save_password) = self.get_new_bookmark(); + // Save bookmark + if !name.is_empty() { + self.save_bookmark(name, save_password); } - (_, _) => None, // Ignore other events - }, - } - } -} - -impl AuthActivity { - fn update_input_port(&mut self, port: u16) -> Option<(String, Msg)> { - match self.view.get_props(COMPONENT_INPUT_PORT) { - None => None, - Some(props) => { - let props = InputPropsBuilder::from(props) - .with_value(port.to_string()) - .build(); - self.view.update(COMPONENT_INPUT_PORT, props) + // Umount popup + self.umount_bookmark_save_dialog(); + // Reload bookmarks + self.view_bookmarks() } - } - } - - fn on_unhandled_submit(&mut self) -> Option<(String, Msg)> { - // Validate fields - match self.collect_host_params() { - Err(err) => { - // mount error - self.mount_error(err); - } - Ok(params) => { - self.save_recent(); - // Set file transfer params to context - self.context_mut().set_ftparams(params); - // Set exit reason - self.exit_reason = Some(super::ExitReason::Connect); + Msg::SaveBookmarkPasswordBlur => { + assert!(self.app.active(&Id::BookmarkName).is_ok()); + } + Msg::ShowDeleteBookmarkPopup => { + self.mount_bookmark_del_dialog(); + } + Msg::ShowDeleteRecentPopup => { + self.mount_recent_del_dialog(); + } + Msg::ShowKeybindingsPopup => { + self.mount_keybindings(); + } + Msg::ShowQuitPopup => { + self.mount_quit(); + } + Msg::ShowReleaseNotes => { + self.mount_release_notes(); + } + Msg::ShowSaveBookmarkPopup => { + self.mount_bookmark_save_dialog(); + } + Msg::UsernameBlurDown => { + assert!(self.app.active(&Id::Password).is_ok()); + } + Msg::UsernameBlurUp => { + assert!(self.app.active(&Id::Port).is_ok()); } + Msg::None => {} } - // Return None None } } diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 21c22b48..ec81b0b7 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -26,26 +26,15 @@ * SOFTWARE. */ // Locals -use super::{AuthActivity, Context, FileTransferProtocol, InputMask}; +use super::{components, AuthActivity, Context, FileTransferProtocol, Id, InputMask}; use crate::filetransfer::params::ProtocolParams; use crate::filetransfer::FileTransferParams; -use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder}; use crate::utils::ui::draw_area_in; -// Ext -use tui_realm_stdlib::{ - Input, InputPropsBuilder, Label, LabelPropsBuilder, List, ListPropsBuilder, Paragraph, - ParagraphPropsBuilder, Radio, RadioPropsBuilder, Span, SpanPropsBuilder, Textarea, - TextareaPropsBuilder, -}; -use tuirealm::tui::{ - layout::{Constraint, Direction, Layout}, - style::Color, - widgets::{BorderType, Borders, Clear}, -}; -use tuirealm::{ - props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan}, - Msg, Payload, Value, -}; + +use std::str::FromStr; +use tuirealm::tui::layout::{Constraint, Direction, Layout}; +use tuirealm::tui::widgets::Clear; +use tuirealm::{State, StateValue, Sub, SubClause, SubEventClause}; impl AuthActivity { /// ### init @@ -53,110 +42,40 @@ impl AuthActivity { /// Initialize view, mounting all startup components inside the view pub(super) fn init(&mut self) { let key_color = self.theme().misc_keys; - let addr_color = self.theme().auth_address; - let protocol_color = self.theme().auth_protocol; - let port_color = self.theme().auth_port; - let username_color = self.theme().auth_username; - let password_color = self.theme().auth_password; - let bookmarks_color = self.theme().auth_bookmarks; - let recents_color = self.theme().auth_recents; + let info_color = self.theme().misc_info_dialog; // Headers - self.view.mount( - super::COMPONENT_TEXT_H1, - Box::new(Label::new( - LabelPropsBuilder::default() - .bold() - .italic() - .with_text(String::from("$ termscp")) - .build(), - )), - ); - self.view.mount( - super::COMPONENT_TEXT_H2, - Box::new(Label::new( - LabelPropsBuilder::default() - .bold() - .italic() - .with_text(format!("$ version {}", env!("CARGO_PKG_VERSION"))) - .build(), - )), - ); + assert!(self + .app + .mount(Id::Title, Box::new(components::Title::default()), vec![]) + .is_ok()); + assert!(self + .app + .mount( + Id::Subtitle, + Box::new(components::Subtitle::default()), + vec![] + ) + .is_ok()); // Footer - self.view.mount( - super::COMPONENT_TEXT_FOOTER, - Box::new(Span::new( - SpanPropsBuilder::default() - .with_spans(vec![ - TextSpan::new("Press ").bold(), - TextSpan::new("").bold().fg(key_color), - TextSpan::new(" to show keybindings; ").bold(), - TextSpan::new("").bold().fg(key_color), - TextSpan::new(" to enter setup").bold(), - ]) - .build(), - )), - ); + assert!(self + .app + .mount( + Id::HelpText, + Box::new(components::HelpText::new(key_color)), + vec![] + ) + .is_ok()); // Get default protocol let default_protocol: FileTransferProtocol = self.context().config().get_default_protocol(); - // Protocol - self.mount_radio( - super::COMPONENT_RADIO_PROTOCOL, - "Protocol", - &["SFTP", "SCP", "FTP", "FTPS", "AWS S3"], - Self::protocol_enum_to_opt(default_protocol), - protocol_color, - ); - // Address - self.mount_input( - super::COMPONENT_INPUT_ADDR, - "Remote host", - addr_color, - InputType::Text, - ); - // Port - self.mount_input_ex( - super::COMPONENT_INPUT_PORT, - "Port number", - port_color, - InputType::Number, - Some(5), - Some(Self::get_default_port_for_protocol(default_protocol).to_string()), - ); - // Username - self.mount_input( - super::COMPONENT_INPUT_USERNAME, - "Username", - username_color, - InputType::Text, - ); - // Password - self.mount_input( - super::COMPONENT_INPUT_PASSWORD, - "Password", - password_color, - InputType::Password, - ); - // Bucket - self.mount_input( - super::COMPONENT_INPUT_S3_BUCKET, - "Bucket name", - addr_color, - InputType::Text, - ); - // Region - self.mount_input( - super::COMPONENT_INPUT_S3_REGION, - "Region", - port_color, - InputType::Text, - ); - // Profile - self.mount_input( - super::COMPONENT_INPUT_S3_PROFILE, - "Profile", - username_color, - InputType::Text, - ); + // Auth form + self.mount_protocol(default_protocol); + self.mount_address(""); + self.mount_port(Self::get_default_port_for_protocol(default_protocol)); + self.mount_username(""); + self.mount_password(""); + self.mount_s3_bucket(""); + self.mount_s3_profile(""); + self.mount_s3_region(""); // Version notice if let Some(version) = self .context() @@ -164,57 +83,34 @@ impl AuthActivity { .get_string(super::STORE_KEY_LATEST_VERSION) { let version: String = version.to_string(); - self.view.mount( - super::COMPONENT_TEXT_NEW_VERSION, - Box::new(Span::new( - SpanPropsBuilder::default() - .with_foreground(Color::Yellow) - .with_spans(vec![ - TextSpan::from("termscp "), - TextSpan::new(version.as_str()).underlined().bold(), - TextSpan::from(" is NOW available! Install update and view release notes with "), - ]) - .build(), - )), - ); + assert!(self + .app + .mount( + Id::NewVersionDisclaimer, + Box::new(components::NewVersionDisclaimer::new( + version.as_str(), + info_color + )), + vec![] + ) + .is_ok()); } - // Bookmarks - self.view.mount( - super::COMPONENT_BOOKMARKS_LIST, - Box::new(BookmarkList::new( - BookmarkListPropsBuilder::default() - .with_background(bookmarks_color) - .with_foreground(Color::Black) - .with_borders(Borders::ALL, BorderType::Plain, bookmarks_color) - .with_title("Bookmarks", Alignment::Left) - .build(), - )), - ); - // Recents - self.view.mount( - super::COMPONENT_RECENTS_LIST, - Box::new(BookmarkList::new( - BookmarkListPropsBuilder::default() - .with_background(recents_color) - .with_foreground(Color::Black) - .with_borders(Borders::ALL, BorderType::Plain, recents_color) - .with_title("Recent connections", Alignment::Left) - .build(), - )), - ); // Load bookmarks - let _ = self.view_bookmarks(); - let _ = self.view_recent_connections(); + self.view_bookmarks(); + self.view_recent_connections(); + // Global listener + self.init_global_listener(); // Active protocol - self.view.active(super::COMPONENT_RADIO_PROTOCOL); + assert!(self.app.active(&Id::Protocol).is_ok()); } /// ### view /// /// Display view on canvas pub(super) fn view(&mut self) { + self.redraw = false; let mut ctx: Context = self.context.take().unwrap(); - let _ = ctx.terminal().draw(|f| { + let _ = ctx.terminal().raw_mut().draw(|f| { // Check window size let height: u16 = f.size().height; self.check_minimum_window_size(height); @@ -278,159 +174,101 @@ impl AuthActivity { .split(chunks[1]); // Render // Auth chunks - self.view - .render(super::COMPONENT_TEXT_H1, f, auth_chunks[0]); - self.view - .render(super::COMPONENT_TEXT_H2, f, auth_chunks[1]); - self.view - .render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]); - self.view - .render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]); + self.app.view(&Id::Title, f, auth_chunks[0]); + self.app.view(&Id::Subtitle, f, auth_chunks[1]); + self.app.view(&Id::NewVersionDisclaimer, f, auth_chunks[2]); + self.app.view(&Id::Protocol, f, auth_chunks[3]); // Render input mask match self.input_mask() { InputMask::AwsS3 => { - self.view - .render(super::COMPONENT_INPUT_S3_BUCKET, f, input_mask[0]); - self.view - .render(super::COMPONENT_INPUT_S3_REGION, f, input_mask[1]); - self.view - .render(super::COMPONENT_INPUT_S3_PROFILE, f, input_mask[2]); + self.app.view(&Id::S3Bucket, f, input_mask[0]); + self.app.view(&Id::S3Region, f, input_mask[1]); + self.app.view(&Id::S3Profile, f, input_mask[2]); } InputMask::Generic => { - self.view - .render(super::COMPONENT_INPUT_ADDR, f, input_mask[0]); - self.view - .render(super::COMPONENT_INPUT_PORT, f, input_mask[1]); - self.view - .render(super::COMPONENT_INPUT_USERNAME, f, input_mask[2]); - self.view - .render(super::COMPONENT_INPUT_PASSWORD, f, input_mask[3]); + self.app.view(&Id::Address, f, input_mask[0]); + self.app.view(&Id::Port, f, input_mask[1]); + self.app.view(&Id::Username, f, input_mask[2]); + self.app.view(&Id::Password, f, input_mask[3]); } } - self.view - .render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[5]); + self.app.view(&Id::HelpText, f, auth_chunks[5]); // Bookmark chunks - self.view - .render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]); - self.view - .render(super::COMPONENT_RECENTS_LIST, f, bookmark_chunks[1]); + self.app.view(&Id::BookmarksList, f, bookmark_chunks[0]); + self.app.view(&Id::RecentsList, f, bookmark_chunks[1]); // Popups - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { - if props.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(props) = self.view.get_props(super::COMPONENT_TEXT_INFO) { - if props.visible { - let popup = draw_area_in(f.size(), 50, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_TEXT_INFO, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) { - if props.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(props) = self.view.get_props(super::COMPONENT_TEXT_SIZE_ERR) { - if props.visible { - let popup = draw_area_in(f.size(), 80, 20); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_TEXT_SIZE_ERR, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_RADIO_QUIT, f, popup); - } - } - if let Some(props) = self - .view - .get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK) - { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - self.view - .render(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, f, popup); - } - } - if let Some(props) = self - .view - .get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT) - { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - self.view - .render(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_NEW_VERSION_NOTES) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 90, 85); - f.render_widget(Clear, popup); - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(90), // Notes - Constraint::Length(3), // Install radio - ] - .as_ref(), - ) - .split(popup); - self.view - .render(super::COMPONENT_TEXT_NEW_VERSION_NOTES, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_RADIO_INSTALL_UPDATE, f, popup_chunks[1]); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 50, 70); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_TEXT_HELP, f, popup); - } - } - if let Some(props) = self - .view - .get_props(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD) - { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 20, 20); - f.render_widget(Clear, popup); - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(3), // Input form - Constraint::Length(2), // Yes/No - ] - .as_ref(), - ) - .split(popup); - self.view - .render(super::COMPONENT_INPUT_BOOKMARK_NAME, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD, f, popup_chunks[1]); - } + if self.app.mounted(&Id::ErrorPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::ErrorPopup, f, popup); + } else if self.app.mounted(&Id::InfoPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::InfoPopup, f, popup); + } else if self.app.mounted(&Id::WaitPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::WaitPopup, f, popup); + } else if self.app.mounted(&Id::WindowSizeError) { + let popup = draw_area_in(f.size(), 80, 20); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::WindowSizeError, f, popup); + } else if self.app.mounted(&Id::QuitPopup) { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::QuitPopup, f, popup); + } else if self.app.mounted(&Id::DeleteBookmarkPopup) { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::DeleteBookmarkPopup, f, popup); + } else if self.app.mounted(&Id::DeleteRecentPopup) { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::DeleteRecentPopup, f, popup); + } else if self.app.mounted(&Id::NewVersionChangelog) { + // make popup + let popup = draw_area_in(f.size(), 90, 85); + f.render_widget(Clear, popup); + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(90), // Notes + Constraint::Length(3), // Install radio + ] + .as_ref(), + ) + .split(popup); + self.app.view(&Id::NewVersionChangelog, f, popup_chunks[0]); + self.app.view(&Id::InstallUpdatePopup, f, popup_chunks[1]); + } else if self.app.mounted(&Id::Keybindings) { + // make popup + let popup = draw_area_in(f.size(), 50, 70); + f.render_widget(Clear, popup); + self.app.view(&Id::Keybindings, f, popup); + } else if self.app.mounted(&Id::BookmarkSavePassword) { + // make popup + let popup = draw_area_in(f.size(), 20, 20); + f.render_widget(Clear, popup); + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), // Input form + Constraint::Length(2), // Yes/No + ] + .as_ref(), + ) + .split(popup); + self.app.view(&Id::BookmarkName, f, popup_chunks[0]); + self.app.view(&Id::BookmarkSavePassword, f, popup_chunks[1]); } }); self.context = Some(ctx); @@ -441,7 +279,7 @@ impl AuthActivity { /// ### view_bookmarks /// /// Make text span from bookmarks - pub(super) fn view_bookmarks(&mut self) -> Option<(String, Msg)> { + pub(super) fn view_bookmarks(&mut self) { let bookmarks: Vec = self .bookmarks_list .iter() @@ -456,24 +294,21 @@ impl AuthActivity { ) }) .collect(); - match self.view.get_props(super::COMPONENT_BOOKMARKS_LIST) { - None => None, - Some(props) => { - let msg = self.view.update( - super::COMPONENT_BOOKMARKS_LIST, - BookmarkListPropsBuilder::from(props) - .with_bookmarks(bookmarks) - .build(), - ); - msg - } - } + let bookmarks_color = self.theme().auth_bookmarks; + assert!(self + .app + .remount( + Id::BookmarksList, + Box::new(components::BookmarksList::new(&bookmarks, bookmarks_color)), + vec![] + ) + .is_ok()); } /// ### view_recent_connections /// /// View recent connections - pub(super) fn view_recent_connections(&mut self) -> Option<(String, Msg)> { + pub(super) fn view_recent_connections(&mut self) { let bookmarks: Vec = self .recents_list .iter() @@ -487,18 +322,15 @@ impl AuthActivity { ) }) .collect(); - match self.view.get_props(super::COMPONENT_RECENTS_LIST) { - None => None, - Some(props) => { - let msg = self.view.update( - super::COMPONENT_RECENTS_LIST, - BookmarkListPropsBuilder::from(props) - .with_bookmarks(bookmarks) - .build(), - ); - msg - } - } + let recents_color = self.theme().auth_recents; + assert!(self + .app + .remount( + Id::RecentsList, + Box::new(components::RecentsList::new(&bookmarks, recents_color)), + vec![] + ) + .is_ok()); } // -- mount @@ -508,14 +340,22 @@ impl AuthActivity { /// Mount error box pub(super) fn mount_error>(&mut self, text: S) { let err_color = self.theme().misc_error_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text.as_ref(), err_color); + assert!(self + .app + .remount( + Id::ErrorPopup, + Box::new(components::ErrorPopup::new(text, err_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::ErrorPopup).is_ok()); } /// ### umount_error /// /// Umount error message pub(super) fn umount_error(&mut self) { - self.view.umount(super::COMPONENT_TEXT_ERROR); + let _ = self.app.umount(&Id::ErrorPopup); } /// ### mount_info @@ -523,14 +363,22 @@ impl AuthActivity { /// Mount info box pub(super) fn mount_info>(&mut self, text: S) { let color = self.theme().misc_info_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_INFO, text.as_ref(), color); + assert!(self + .app + .remount( + Id::InfoPopup, + Box::new(components::InfoPopup::new(text, color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::InfoPopup).is_ok()); } /// ### umount_info /// /// Umount info message pub(super) fn umount_info(&mut self) { - self.view.umount(super::COMPONENT_TEXT_INFO); + let _ = self.app.umount(&Id::InfoPopup); } /// ### mount_error @@ -538,14 +386,22 @@ impl AuthActivity { /// Mount wait box pub(super) fn mount_wait(&mut self, text: &str) { let wait_color = self.theme().misc_info_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_WAIT, text, wait_color); + assert!(self + .app + .remount( + Id::WaitPopup, + Box::new(components::WaitPopup::new(text, wait_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::WaitPopup).is_ok()); } /// ### umount_wait /// /// Umount wait message pub(super) fn umount_wait(&mut self) { - self.view.umount(super::COMPONENT_TEXT_WAIT); + let _ = self.app.umount(&Id::WaitPopup); } /// ### mount_size_err @@ -554,18 +410,22 @@ impl AuthActivity { pub(super) fn mount_size_err(&mut self) { // Mount let err_color = self.theme().misc_error_dialog; - self.mount_text_dialog( - super::COMPONENT_TEXT_SIZE_ERR, - "termscp requires at least 24 lines of height to run", - err_color, - ); + assert!(self + .app + .remount( + Id::WindowSizeError, + Box::new(components::WindowSizeError::new(err_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::WindowSizeError).is_ok()); } /// ### umount_size_err /// /// Umount error size error pub(super) fn umount_size_err(&mut self) { - self.view.umount(super::COMPONENT_TEXT_SIZE_ERR); + let _ = self.app.umount(&Id::WindowSizeError); } /// ### mount_quit @@ -574,20 +434,22 @@ impl AuthActivity { pub(super) fn mount_quit(&mut self) { // Protocol let quit_color = self.theme().misc_quit_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_QUIT, - "Quit termscp?", - &["Yes", "No"], - 0, - quit_color, - ); + assert!(self + .app + .remount( + Id::QuitPopup, + Box::new(components::QuitPopup::new(quit_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::QuitPopup).is_ok()); } /// ### umount_quit /// /// Umount quit popup pub(super) fn umount_quit(&mut self) { - self.view.umount(super::COMPONENT_RADIO_QUIT); + let _ = self.app.umount(&Id::QuitPopup); } /// ### mount_bookmark_del_dialog @@ -595,21 +457,22 @@ impl AuthActivity { /// Mount bookmark delete dialog pub(super) fn mount_bookmark_del_dialog(&mut self) { let warn_color = self.theme().misc_warn_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, - "Delete bookmark?", - &["Yes", "No"], - 1, - warn_color, - ); + assert!(self + .app + .remount( + Id::DeleteBookmarkPopup, + Box::new(components::DeleteBookmarkPopup::new(warn_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::DeleteBookmarkPopup).is_ok()); } /// ### umount_bookmark_del_dialog /// /// umount delete bookmark dialog pub(super) fn umount_bookmark_del_dialog(&mut self) { - self.view - .umount(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK); + let _ = self.app.umount(&Id::DeleteBookmarkPopup); } /// ### mount_bookmark_del_dialog @@ -617,20 +480,22 @@ impl AuthActivity { /// Mount recent delete dialog pub(super) fn mount_recent_del_dialog(&mut self) { let warn_color = self.theme().misc_warn_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, - "Delete bookmark?", - &["Yes", "No"], - 1, - warn_color, - ); + assert!(self + .app + .remount( + Id::DeleteRecentPopup, + Box::new(components::DeleteRecentPopup::new(warn_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::DeleteRecentPopup).is_ok()); } /// ### umount_recent_del_dialog /// /// umount delete recent dialog pub(super) fn umount_recent_del_dialog(&mut self) { - self.view.umount(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT); + let _ = self.app.umount(&Id::DeleteRecentPopup); } /// ### mount_bookmark_save_dialog @@ -639,102 +504,56 @@ impl AuthActivity { pub(super) fn mount_bookmark_save_dialog(&mut self) { let save_color = self.theme().misc_save_dialog; let warn_color = self.theme().misc_warn_dialog; - self.view.mount( - super::COMPONENT_INPUT_BOOKMARK_NAME, - Box::new(Input::new( - InputPropsBuilder::default() - .with_foreground(save_color) - .with_label("Save bookmark as…", Alignment::Center) - .with_borders( - Borders::TOP | Borders::RIGHT | Borders::LEFT, - BorderType::Rounded, - Color::Reset, - ) - .build(), - )), - ); - self.view.mount( - super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(warn_color) - .with_borders( - Borders::BOTTOM | Borders::RIGHT | Borders::LEFT, - BorderType::Rounded, - Color::Reset, - ) - .with_title("Save password?", Alignment::Center) - .with_options(&[String::from("Yes"), String::from("No")]) - .rewind(true) - .build(), - )), - ); + assert!(self + .app + .remount( + Id::BookmarkName, + Box::new(components::BookmarkName::new(save_color)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::BookmarkSavePassword, + Box::new(components::BookmarkSavePassword::new(warn_color)), + vec![] + ) + .is_ok()); // Give focus to input bookmark name - self.view.active(super::COMPONENT_INPUT_BOOKMARK_NAME); + assert!(self.app.active(&Id::BookmarkName).is_ok()); } /// ### umount_bookmark_save_dialog /// /// Umount bookmark save dialog pub(super) fn umount_bookmark_save_dialog(&mut self) { - self.view.umount(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD); - self.view.umount(super::COMPONENT_INPUT_BOOKMARK_NAME); + let _ = self.app.umount(&Id::BookmarkName); + let _ = self.app.umount(&Id::BookmarkSavePassword); } - /// ### mount_help + /// ### mount_keybindings /// - /// Mount help - pub(super) fn mount_help(&mut self) { + /// Mount keybindings + pub(super) fn mount_keybindings(&mut self) { let key_color = self.theme().misc_keys; - self.view.mount( - super::COMPONENT_TEXT_HELP, - Box::new(List::new( - ListPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, Color::White) - .with_highlighted_str(Some("?")) - .with_max_scroll_step(8) - .scrollable(true) - .bold() - .with_title("Help", Alignment::Center) - .with_rows( - TableBuilder::default() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Quit termscp")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Switch from form and bookmarks")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Switch bookmark tab")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Move up/down in current tab")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Connect/Load bookmark")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Delete selected bookmark")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Enter setup")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Save bookmark")) - .build(), - ) - .build(), - )), - ); + assert!(self + .app + .remount( + Id::Keybindings, + Box::new(components::Keybindings::new(key_color)), + vec![] + ) + .is_ok()); // Active help - self.view.active(super::COMPONENT_TEXT_HELP); + assert!(self.app.active(&Id::Keybindings).is_ok()); } /// ### umount_help /// /// Umount help pub(super) fn umount_help(&mut self) { - self.view.umount(super::COMPONENT_TEXT_HELP); + let _ = self.app.umount(&Id::Keybindings); } /// ### mount_release_notes @@ -744,26 +563,24 @@ impl AuthActivity { if let Some(ctx) = self.context.as_ref() { if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) { // make spans - let spans: Vec = release_notes.lines().map(TextSpan::from).collect(); let info_color = self.theme().misc_info_dialog; - self.view.mount( - super::COMPONENT_TEXT_NEW_VERSION_NOTES, - Box::new(Textarea::new( - TextareaPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, info_color) - .with_title("Release notes", Alignment::Center) - .with_texts(spans) - .build(), - )), - ); - // Mount install popup - self.mount_radio_dialog( - super::COMPONENT_RADIO_INSTALL_UPDATE, - "Install new version?", - &["Yes", "No"], - 0, - info_color, - ); + assert!(self + .app + .remount( + Id::NewVersionChangelog, + Box::new(components::ReleaseNotes::new(release_notes, info_color)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::InstallUpdatePopup, + Box::new(components::InstallUpdatePopup::new(info_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::InstallUpdatePopup).is_ok()); } } } @@ -772,17 +589,108 @@ impl AuthActivity { /// /// Umount release notes text area pub(super) fn umount_release_notes(&mut self) { - self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES); - self.view.umount(super::COMPONENT_RADIO_INSTALL_UPDATE); + let _ = self.app.umount(&Id::NewVersionChangelog); + let _ = self.app.umount(&Id::InstallUpdatePopup); } - /// ### get_protocol - /// - /// Get protocol from view - pub(super) fn get_protocol(&self) -> FileTransferProtocol { - self.get_input_protocol() + pub(super) fn mount_protocol(&mut self, protocol: FileTransferProtocol) { + let protocol_color = self.theme().auth_protocol; + assert!(self + .app + .remount( + Id::Protocol, + Box::new(components::ProtocolRadio::new(protocol, protocol_color)), + vec![] + ) + .is_ok()); + } + + pub(super) fn mount_address(&mut self, address: &str) { + let addr_color = self.theme().auth_address; + assert!(self + .app + .remount( + Id::Address, + Box::new(components::InputAddress::new(address, addr_color)), + vec![] + ) + .is_ok()); + } + + pub(super) fn mount_port(&mut self, port: u16) { + let port_color = self.theme().auth_port; + assert!(self + .app + .remount( + Id::Port, + Box::new(components::InputPort::new(port, port_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_username(&mut self, username: &str) { + let username_color = self.theme().auth_username; + assert!(self + .app + .remount( + Id::Username, + Box::new(components::InputUsername::new(username, username_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_password(&mut self, password: &str) { + let password_color = self.theme().auth_password; + assert!(self + .app + .remount( + Id::Password, + Box::new(components::InputPassword::new(password, password_color)), + vec![] + ) + .is_ok()); + } + + pub(super) fn mount_s3_bucket(&mut self, bucket: &str) { + let addr_color = self.theme().auth_address; + assert!(self + .app + .remount( + Id::S3Bucket, + Box::new(components::InputS3Bucket::new(bucket, addr_color)), + vec![] + ) + .is_ok()); } + pub(super) fn mount_s3_region(&mut self, region: &str) { + let port_color = self.theme().auth_port; + assert!(self + .app + .remount( + Id::S3Region, + Box::new(components::InputS3Region::new(region, port_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_s3_profile(&mut self, profile: &str) { + let username_color = self.theme().auth_username; + assert!(self + .app + .remount( + Id::S3Profile, + Box::new(components::InputS3Profile::new(profile, username_color)), + vec![] + ) + .is_ok()); + } + + // -- query + /// ### get_generic_params /// /// Collect input values from view @@ -805,67 +713,77 @@ impl AuthActivity { } pub(super) fn get_input_addr(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_ADDR) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::Address) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_port(&self) -> u16 { - match self.view.get_state(super::COMPONENT_INPUT_PORT) { - Some(Payload::One(Value::Usize(x))) => match x > 65535 { - true => 0, - false => x as u16, + match self.app.state(&Id::Port) { + Ok(State::One(StateValue::String(x))) => match u16::from_str(x.as_str()) { + Ok(v) => v, + _ => 0, }, _ => 0, } } - pub(super) fn get_input_protocol(&self) -> FileTransferProtocol { - match self.view.get_state(super::COMPONENT_RADIO_PROTOCOL) { - Some(Payload::One(Value::Usize(x))) => Self::protocol_opt_to_enum(x), - _ => FileTransferProtocol::Sftp, - } - } - pub(super) fn get_input_username(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_USERNAME) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::Username) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_password(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_PASSWORD) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::Password) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_s3_bucket(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_S3_BUCKET) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::S3Bucket) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_s3_region(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_S3_REGION) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::S3Region) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_s3_profile(&self) -> Option { - match self.view.get_state(super::COMPONENT_INPUT_S3_PROFILE) { - Some(Payload::One(Value::Str(x))) => match x.is_empty() { - true => None, - false => Some(x), - }, + match self.app.state(&Id::S3Profile) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } + /// ### get_new_bookmark + /// + /// Get new bookmark params + pub(super) fn get_new_bookmark(&self) -> (String, bool) { + let name = match self.app.state(&Id::BookmarkName) { + Ok(State::One(StateValue::String(name))) => name, + _ => String::default(), + }; + if matches!( + self.app.state(&Id::BookmarkSavePassword), + Ok(State::One(StateValue::Usize(0))) + ) { + (name, true) + } else { + (name, false) + } + } + + // -- len + /// ### input_mask_size /// /// Returns the input mask size based on current input mask @@ -876,6 +794,8 @@ impl AuthActivity { } } + // -- fmt + /// ### fmt_bookmark /// /// Format bookmark to display on ui @@ -913,94 +833,95 @@ impl AuthActivity { } } - // -- mount helpers - - fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) { - // Mount - self.view.mount( - id, - Box::new(Paragraph::new( - ParagraphPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Thick, color) - .with_foreground(color) - .bold() - .with_text_alignment(Alignment::Center) - .with_texts(vec![TextSpan::from(text)]) - .build(), - )), - ); - // Give focus to error - self.view.active(id); - } - - fn mount_radio_dialog( - &mut self, - id: &str, - text: &str, - opts: &[&str], - default: usize, - color: Color, - ) { - self.view.mount( - id, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(color) - .with_inverted_color(Color::Black) - .with_borders(Borders::ALL, BorderType::Rounded, color) - .with_title(text, Alignment::Center) - .with_options(opts) - .with_value(default) - .rewind(true) - .build(), - )), - ); - // Active - self.view.active(id); - } - - fn mount_radio(&mut self, id: &str, text: &str, opts: &[&str], default: usize, color: Color) { - self.view.mount( - id, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(color) - .with_inverted_color(Color::Black) - .with_borders(Borders::ALL, BorderType::Rounded, color) - .with_title(text, Alignment::Left) - .with_options(opts) - .with_value(default) - .rewind(true) - .build(), + fn init_global_listener(&mut self) { + use tuirealm::event::{Key, KeyEvent, KeyModifiers}; + assert!(self + .app + .mount( + Id::GlobalListener, + Box::new(components::GlobalListener::default()), + vec![ + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Esc, + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('h'), + modifiers: KeyModifiers::CONTROL, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('r'), + modifiers: KeyModifiers::CONTROL, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::CONTROL, + }), + Self::no_popup_mounted_clause(), + ), + ] + ) + .is_ok()); + } + + /// ### no_popup_mounted_clause + /// + /// Returns a sub clause which requires that no popup is mounted in order to be satisfied + fn no_popup_mounted_clause() -> SubClause { + SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ErrorPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::InfoPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::Keybindings, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::DeleteBookmarkPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::DeleteRecentPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::InstallUpdatePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::BookmarkSavePassword, + )))), + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::WaitPopup, + )))), + )), + )), + )), + )), + )), )), - ); - } - - fn mount_input(&mut self, id: &str, label: &str, fg: Color, typ: InputType) { - self.mount_input_ex(id, label, fg, typ, None, None); - } - - fn mount_input_ex( - &mut self, - id: &str, - label: &str, - fg: Color, - typ: InputType, - len: Option, - value: Option, - ) { - let mut props = InputPropsBuilder::default(); - props - .with_foreground(fg) - .with_borders(Borders::ALL, BorderType::Rounded, fg) - .with_label(label, Alignment::Left) - .with_input(typ); - if let Some(len) = len { - props.with_input_len(len); - } - if let Some(value) = value { - props.with_value(value); - } - self.view.mount(id, Box::new(Input::new(props.build()))); + ) } } diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index eacb24be..c6545a5a 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -29,7 +29,6 @@ use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; use crate::fs::FsFile; // ext -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::fs::OpenOptions; use std::io::Read; use std::path::{Path, PathBuf}; @@ -109,13 +108,15 @@ impl FileTransferActivity { } } // Put input mode back to normal - if let Err(err) = disable_raw_mode() { + if let Err(err) = self.context_mut().terminal().disable_raw_mode() { error!("Failed to disable raw mode: {}", err); } // Leave alternate mode - if let Some(ctx) = self.context.as_mut() { - ctx.leave_alternate_screen(); + if let Err(err) = self.context_mut().terminal().leave_alternate_screen() { + error!("Could not leave alternate screen: {}", err); } + // Lock ports + assert!(self.app.lock_ports().is_ok()); // Open editor match edit::edit_file(path) { Ok(_) => self.log( @@ -128,13 +129,20 @@ impl FileTransferActivity { Err(err) => return Err(format!("Could not open editor: {}", err)), } if let Some(ctx) = self.context.as_mut() { - // Clear screen - ctx.clear_screen(); + if let Err(err) = ctx.terminal().clear_screen() { + error!("Could not clear screen screen: {}", err); + } // Enter alternate mode - ctx.enter_alternate_screen(); + if let Err(err) = ctx.terminal().enter_alternate_screen() { + error!("Could not enter alternate screen: {}", err); + } + // Re-enable raw mode + if let Err(err) = ctx.terminal().enable_raw_mode() { + error!("Failed to enter raw mode: {}", err); + } + // Unlock ports + assert!(self.app.unlock_ports().is_ok()); } - // Re-enable raw mode - let _ = enable_raw_mode(); Ok(()) } diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index e384951d..2e4fad8d 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -26,10 +26,10 @@ * SOFTWARE. */ pub(self) use super::{ - browser::FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, TransferOpts, + browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts, TransferPayload, }; -use tuirealm::{Payload, Value}; +use tuirealm::{State, StateValue}; // actions pub(crate) mod change_dir; @@ -79,7 +79,7 @@ impl FileTransferActivity { /// /// Get local file entry pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry { - match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) { + match self.get_selected_index(&Id::ExplorerLocal) { SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)), SelectedEntryIndex::Many(files) => { let files: Vec<&FsEntry> = files @@ -97,7 +97,7 @@ impl FileTransferActivity { /// /// Get remote file entry pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry { - match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) { + match self.get_selected_index(&Id::ExplorerRemote) { SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)), SelectedEntryIndex::Many(files) => { let files: Vec<&FsEntry> = files @@ -115,7 +115,7 @@ impl FileTransferActivity { /// /// Get remote file entry pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry { - match self.get_selected_index(super::COMPONENT_EXPLORER_FIND) { + match self.get_selected_index(&Id::ExplorerFind) { SelectedEntryIndex::One(idx) => { SelectedEntry::from(self.found().as_ref().unwrap().get(idx)) } @@ -133,14 +133,14 @@ impl FileTransferActivity { // -- private - fn get_selected_index(&self, component: &str) -> SelectedEntryIndex { - match self.view.get_state(component) { - Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx), - Some(Payload::Vec(files)) => { + fn get_selected_index(&self, id: &Id) -> SelectedEntryIndex { + match self.app.state(id) { + Ok(State::One(StateValue::Usize(idx))) => SelectedEntryIndex::One(idx), + Ok(State::Vec(files)) => { let list: Vec = files .iter() .map(|x| match x { - Value::Usize(v) => *v, + StateValue::Usize(v) => *v, _ => 0, }) .collect(); diff --git a/src/ui/activities/filetransfer/actions/open.rs b/src/ui/activities/filetransfer/actions/open.rs index c3de73a3..86f056e5 100644 --- a/src/ui/activities/filetransfer/actions/open.rs +++ b/src/ui/activities/filetransfer/actions/open.rs @@ -160,7 +160,9 @@ impl FileTransferActivity { // NOTE: clear screen in order to prevent crap on stderr if let Some(ctx) = self.context.as_mut() { // Clear screen - ctx.clear_screen(); + if let Err(err) = ctx.terminal().clear_screen() { + error!("Could not clear screen screen: {}", err); + } } } } diff --git a/src/ui/activities/filetransfer/components/log.rs b/src/ui/activities/filetransfer/components/log.rs new file mode 100644 index 00000000..2ddb935c --- /dev/null +++ b/src/ui/activities/filetransfer/components/log.rs @@ -0,0 +1,296 @@ +//! ## Log +//! +//! log tab component + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{Msg, UiMsg}; + +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table}; +use tuirealm::tui::layout::Corner; +use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue}; + +pub struct Log { + props: Props, + states: OwnStates, +} + +impl Log { + pub fn new(lines: Table, fg: Color, bg: Color) -> Self { + let mut props = Props::default(); + props.set( + Attribute::Borders, + AttrValue::Borders(Borders::default().color(fg)), + ); + props.set(Attribute::Background, AttrValue::Color(bg)); + props.set(Attribute::Content, AttrValue::Table(lines)); + Self { + props, + states: OwnStates::default(), + } + } +} + +impl MockComponent for Log { + fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) { + let width: usize = area.width as usize - 4; + let focus = self + .props + .get_or(Attribute::Focus, AttrValue::Flag(false)) + .unwrap_flag(); + let fg = self + .props + .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset)) + .unwrap_color(); + let bg = self + .props + .get_or(Attribute::Background, AttrValue::Color(Color::Reset)) + .unwrap_color(); + // Make list + let list_items: Vec = self + .props + .get(Attribute::Content) + .unwrap() + .unwrap_table() + .iter() + .map(|row| ListItem::new(tui_realm_stdlib::utils::wrap_spans(row, width, &self.props))) + .collect(); + let w = TuiList::new(list_items) + .block(tui_realm_stdlib::utils::get_block( + Borders::default().color(fg), + Some(("Log".to_string(), Alignment::Left)), + focus, + None, + )) + .start_corner(Corner::BottomLeft) + .highlight_symbol(">> ") + .style(Style::default().bg(bg)) + .highlight_style(Style::default()); + let mut state: ListState = ListState::default(); + state.select(Some(self.states.get_list_index())); + frame.render_stateful_widget(w, area, &mut state); + } + + fn query(&self, attr: Attribute) -> Option { + self.props.get(attr) + } + + fn attr(&mut self, attr: Attribute, value: AttrValue) { + self.props.set(attr, value); + if matches!(attr, Attribute::Content) { + self.states.set_list_len( + match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) { + Some(spans) => spans.len(), + _ => 0, + }, + ); + self.states.reset_list_index(); + } + } + + fn state(&self) -> State { + State::One(StateValue::Usize(self.states.get_list_index())) + } + + fn perform(&mut self, cmd: Cmd) -> CmdResult { + match cmd { + Cmd::Move(Direction::Down) => { + let prev = self.states.get_list_index(); + self.states.incr_list_index(); + if prev != self.states.get_list_index() { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Move(Direction::Up) => { + let prev = self.states.get_list_index(); + self.states.decr_list_index(); + if prev != self.states.get_list_index() { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Scroll(Direction::Down) => { + let prev = self.states.get_list_index(); + (0..8).for_each(|_| self.states.incr_list_index()); + if prev != self.states.get_list_index() { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Scroll(Direction::Up) => { + let prev = self.states.get_list_index(); + (0..8).for_each(|_| self.states.decr_list_index()); + if prev != self.states.get_list_index() { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::GoTo(Position::Begin) => { + let prev = self.states.get_list_index(); + self.states.reset_list_index(); + if prev != self.states.get_list_index() { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::GoTo(Position::End) => { + let prev = self.states.get_list_index(); + self.states.list_index_at_last(); + if prev != self.states.get_list_index() { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + _ => CmdResult::None, + } + } +} + +impl Component for Log { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + // -- comp msg + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::Ui(UiMsg::LogTabbed)), + _ => None, + } + } +} + +// -- states + +/// ## OwnStates +/// +/// OwnStates contains states for this component +#[derive(Clone)] +struct OwnStates { + list_index: usize, // Index of selected element in list + list_len: usize, // Length of file list + focus: bool, // Has focus? +} + +impl Default for OwnStates { + fn default() -> Self { + OwnStates { + list_index: 0, + list_len: 0, + focus: false, + } + } +} + +impl OwnStates { + /// ### set_list_len + /// + /// Set list length + pub fn set_list_len(&mut self, len: usize) { + self.list_len = len; + } + + /// ### get_list_index + /// + /// Return current value for list index + pub fn get_list_index(&self) -> usize { + self.list_index + } + + /// ### incr_list_index + /// + /// Incremenet list index + pub fn incr_list_index(&mut self) { + // Check if index is at last element + if self.list_index + 1 < self.list_len { + self.list_index += 1; + } + } + + /// ### decr_list_index + /// + /// Decrement list index + pub fn decr_list_index(&mut self) { + // Check if index is bigger than 0 + if self.list_index > 0 { + self.list_index -= 1; + } + } + + /// ### list_index_at_last + /// + /// Set list index at last item + pub fn list_index_at_last(&mut self) { + self.list_index = match self.list_len { + 0 => 0, + len => len - 1, + }; + } + + /// ### reset_list_index + /// + /// Reset list index to last element + pub fn reset_list_index(&mut self) { + self.list_index = 0; // Last element is always 0 + } +} diff --git a/src/ui/activities/filetransfer/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs new file mode 100644 index 00000000..cd79cd72 --- /dev/null +++ b/src/ui/activities/filetransfer/components/mod.rs @@ -0,0 +1,72 @@ +//! ## Components +//! +//! file transfer activity components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{Msg, TransferMsg, UiMsg}; + +use tui_realm_stdlib::Phantom; +use tuirealm::{ + event::{Event, Key, KeyEvent, KeyModifiers}, + Component, MockComponent, NoUserEvent, +}; + +// -- export +mod log; +mod popups; +mod transfer; + +pub use self::log::Log; +pub use popups::{ + CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup, FileInfoPopup, + FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup, + ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup, + ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, WaitPopup, +}; +pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote}; + +#[derive(Default, MockComponent)] +pub struct GlobalListener { + component: Phantom, +} + +impl Component for GlobalListener { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Char('q'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowQuitPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('h'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs new file mode 100644 index 00000000..a124013a --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -0,0 +1,1689 @@ +//! ## Popups +//! +//! popups components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::super::Browser; +use super::{Msg, TransferMsg, UiMsg}; +use crate::fs::explorer::FileSorting; +use crate::fs::FsEntry; +use crate::utils::fmt::fmt_time; + +use bytesize::ByteSize; +use std::path::PathBuf; + +use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{ + Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan, +}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; +#[cfg(target_family = "unix")] +use users::{get_group_by_gid, get_user_by_uid}; + +#[derive(MockComponent)] +pub struct CopyPopup { + component: Input, +} + +impl CopyPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "destination", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Copy file(s) to…", Alignment::Center), + } + } +} + +impl Component for CopyPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::CopyFileTo(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseCopyPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct DeletePopup { + component: Radio, +} + +impl DeletePopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Yes", "No"]) + .value(1) + .title("Delete file(s)?", Alignment::Center), + } + } +} + +impl Component for DeletePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseDeletePopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Transfer(TransferMsg::DeleteFile)) + } else { + Some(Msg::Ui(UiMsg::CloseDeletePopup)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct DisconnectPopup { + component: Radio, +} + +impl DisconnectPopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Yes", "No"]) + .title("Are you sure you want to disconnect?", Alignment::Center), + } + } +} + +impl Component for DisconnectPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Ui(UiMsg::Disconnect)) + } else { + Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ErrorPopup { + component: Paragraph, +} + +impl ErrorPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text(&[TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for ErrorPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseErrorPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ExecPopup { + component: Input, +} + +impl ExecPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder("ps a", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Execute command", Alignment::Center), + } + } +} + +impl Component for ExecPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::ExecuteCmd(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseExecPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct FatalPopup { + component: Paragraph, +} + +impl FatalPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text(&[TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for FatalPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseFatalPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct FileInfoPopup { + component: List, +} + +impl FileInfoPopup { + pub fn new(file: &FsEntry) -> Self { + let mut texts: TableBuilder = TableBuilder::default(); + // Abs path + let real_path: Option = { + let real_file: FsEntry = file.get_realfile(); + match real_file.get_abs_path() != file.get_abs_path() { + true => Some(real_file.get_abs_path()), + false => None, + } + }; + let path: String = match real_path { + Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()), + None => format!("{}", file.get_abs_path().display()), + }; + // Make texts + texts + .add_col(TextSpan::from("Path: ")) + .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); + if let Some(filetype) = file.get_ftype() { + texts + .add_row() + .add_col(TextSpan::from("File type: ")) + .add_col(TextSpan::new(filetype.as_str()).fg(Color::LightGreen)); + } + let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size()); + texts + .add_row() + .add_col(TextSpan::from("Size: ")) + .add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan)); + 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(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); + texts + .add_row() + .add_col(TextSpan::from("Last modified time: ")) + .add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); + texts + .add_row() + .add_col(TextSpan::from("Last access time: ")) + .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); + // User + #[cfg(target_family = "unix")] + 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(target_family = "unix")] + 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(TextSpan::new(username.as_str()).fg(Color::LightYellow)); + texts + .add_row() + .add_col(TextSpan::from("Group: ")) + .add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); + Self { + component: List::default() + .borders(Borders::default().modifiers(BorderType::Rounded)) + .scroll(false) + .title(file.get_name(), Alignment::Left) + .rows(texts.build()), + } + } +} + +impl Component for FileInfoPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseFileInfoPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct FindPopup { + component: Input, +} + +impl FindPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "Search files by name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("*.txt", Alignment::Center), + } + } +} + +impl Component for FindPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::SearchFile(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseFindPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct GoToPopup { + component: Input, +} + +impl GoToPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Go to…", Alignment::Center), + } + } +} + +impl Component for GoToPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::GoTo(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseGotoPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct KeybindingsPopup { + component: List, +} + +impl KeybindingsPopup { + pub fn new(key_color: Color) -> Self { + Self { + component: List::default() + .borders(Borders::default().modifiers(BorderType::Rounded)) + .scroll(true) + .step(8) + .highlighted_str("? ") + .title("Keybindings", Alignment::Center) + .rows( + TableBuilder::default() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Disconnect")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Switch between explorer and logs", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to previous directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Change explorer tab")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Move up/down in list")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Enter directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Upload/Download file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Toggle hidden files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Change file sorting mode")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Copy")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Make directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Search files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to path")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Show help")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Show info about selected file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Reload directory content")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Select file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Create new file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open text file with preferred editor", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Quit termscp")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Rename file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Save file as")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to parent directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open file with default application for file type", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open file with specified application", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Execute shell command")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Toggle synchronized browsing")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Delete selected file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Select all files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Interrupt file transfer")) + .build(), + ), + } + } +} + +impl Component for KeybindingsPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseKeybindingsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct MkdirPopup { + component: Input, +} + +impl MkdirPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "New directory name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("directory-name", Alignment::Center), + } + } +} + +impl Component for MkdirPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::Mkdir(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseMkdirPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct NewfilePopup { + component: Input, +} + +impl NewfilePopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "New file name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("file.txt", Alignment::Center), + } + } +} + +impl Component for NewfilePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::NewFile(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseNewFilePopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct OpenWithPopup { + component: Input, +} + +impl OpenWithPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "Open file with…", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("vscode", Alignment::Center), + } + } +} + +impl Component for OpenWithPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::OpenFileWith(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseOpenWithPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ProgressBarFull { + component: ProgressBar, +} + +impl ProgressBarFull { + pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { + Self { + component: ProgressBar::default() + .borders( + Borders::default() + .modifiers(BorderType::Rounded) + .sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT), + ) + .foreground(color) + .label(label) + .progress(prog) + .title(title, Alignment::Center), + } + } +} + +impl Component for ProgressBarFull { + fn on(&mut self, ev: Event) -> Option { + if matches!( + ev, + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL + }) + ) { + Some(Msg::Transfer(TransferMsg::AbortTransfer)) + } else { + None + } + } +} + +#[derive(MockComponent)] +pub struct ProgressBarPartial { + component: ProgressBar, +} + +impl ProgressBarPartial { + pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { + Self { + component: ProgressBar::default() + .borders( + Borders::default() + .modifiers(BorderType::Rounded) + .sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT), + ) + .foreground(color) + .label(label) + .progress(prog) + .title(title, Alignment::Center), + } + } +} + +impl Component for ProgressBarPartial { + fn on(&mut self, ev: Event) -> Option { + if matches!( + ev, + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL + }) + ) { + Some(Msg::Transfer(TransferMsg::AbortTransfer)) + } else { + None + } + } +} + +#[derive(MockComponent)] +pub struct QuitPopup { + component: Radio, +} + +impl QuitPopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Yes", "No"]) + .title("Are you sure you want to quit termscp?", Alignment::Center), + } + } +} + +impl Component for QuitPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseQuitPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Ui(UiMsg::Quit)) + } else { + Some(Msg::Ui(UiMsg::CloseQuitPopup)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct RenamePopup { + component: Input, +} + +impl RenamePopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz.txt", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Move file(s) to…", Alignment::Center), + } + } +} + +impl Component for RenamePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::RenameFile(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseRenamePopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ReplacePopup { + component: Radio, +} + +impl ReplacePopup { + pub fn new(filename: Option<&str>, color: Color) -> Self { + let text = match filename { + Some(f) => format!(r#"File "{}" already exists. Overwrite file?"#, f), + None => "Overwrite files?".to_string(), + }; + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Yes", "No"]) + .title(text, Alignment::Center), + } + } +} + +impl Component for ReplacePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ReplacePopupTabbed)) + } + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseReplacePopups)) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Transfer(TransferMsg::TransferPendingFile)) + } else { + Some(Msg::Ui(UiMsg::CloseReplacePopups)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ReplacingFilesListPopup { + component: List, +} + +impl ReplacingFilesListPopup { + pub fn new(files: &[&str], color: Color) -> Self { + Self { + component: List::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .scroll(true) + .step(4) + .highlighted_color(color) + .highlighted_str("➤ ") + .title( + "The following files are going to be replaced", + Alignment::Center, + ) + .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), + } + } +} + +impl Component for ReplacingFilesListPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseReplacePopups)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ReplacePopupTabbed)) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct SaveAsPopup { + component: Input, +} + +impl SaveAsPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz.txt", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Save as…", Alignment::Center), + } + } +} + +impl Component for SaveAsPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::SaveFileAs(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseSaveAsPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct SortingPopup { + component: Radio, +} + +impl SortingPopup { + pub fn new(value: FileSorting, color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Name", "Modify time", "Creation time", "Size"]) + .title("Sort files by…", Alignment::Center) + .value(match value { + FileSorting::CreationTime => 2, + FileSorting::ModifyTime => 1, + FileSorting::Name => 0, + FileSorting::Size => 3, + }), + } + } +} + +impl Component for SortingPopup { + fn on(&mut self, ev: Event) -> Option { + let result = match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => self.perform(Cmd::Move(Direction::Left)), + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => self.perform(Cmd::Move(Direction::Right)), + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => return Some(Msg::Ui(UiMsg::CloseFileSortingPopup)), + _ => return None, + }; + if let CmdResult::Changed(State::One(StateValue::Usize(i))) = result { + Some(Msg::Ui(UiMsg::ChangeFileSorting(match i { + 0 => FileSorting::Name, + 1 => FileSorting::ModifyTime, + 2 => FileSorting::CreationTime, + 3 => FileSorting::Size, + _ => FileSorting::Name, + }))) + } else { + Some(Msg::None) + } + } +} + +#[derive(MockComponent)] +pub struct StatusBarLocal { + component: Span, +} + +impl StatusBarLocal { + pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self { + let file_sorting = file_sorting_label(browser.local().file_sorting); + let hidden_files = hidden_files_label(browser.local().hidden_files_visible()); + Self { + component: Span::default().spans(&[ + TextSpan::new("File sorting: ").fg(sorting_color), + TextSpan::new(file_sorting).fg(sorting_color).reversed(), + TextSpan::new(" Hidden files: ").fg(hidden_color), + TextSpan::new(hidden_files).fg(hidden_color).reversed(), + ]), + } + } +} + +impl Component for StatusBarLocal { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct StatusBarRemote { + component: Span, +} + +impl StatusBarRemote { + pub fn new( + browser: &Browser, + sorting_color: Color, + hidden_color: Color, + sync_color: Color, + ) -> Self { + let file_sorting = file_sorting_label(browser.remote().file_sorting); + let hidden_files = hidden_files_label(browser.remote().hidden_files_visible()); + let sync_browsing = match browser.sync_browsing { + true => "ON ", + false => "OFF", + }; + Self { + component: Span::default().spans(&[ + TextSpan::new("File sorting: ").fg(sorting_color), + TextSpan::new(file_sorting).fg(sorting_color).reversed(), + TextSpan::new(" Hidden files: ").fg(hidden_color), + TextSpan::new(hidden_files).fg(hidden_color).reversed(), + TextSpan::new(" Sync browsing: ").fg(sync_color), + TextSpan::new(sync_browsing).fg(sync_color).reversed(), + ]), + } + } +} + +impl Component for StatusBarRemote { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +fn file_sorting_label(sorting: FileSorting) -> &'static str { + match sorting { + FileSorting::CreationTime => "By creation time", + FileSorting::ModifyTime => "By modify time", + FileSorting::Name => "By name", + FileSorting::Size => "By size", + } +} + +fn hidden_files_label(visible: bool) -> &'static str { + match visible { + true => "Show", + false => "Hide", + } +} + +#[derive(MockComponent)] +pub struct WaitPopup { + component: Paragraph, +} + +impl WaitPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text(&[TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for WaitPopup { + fn on(&mut self, _ev: Event) -> Option { + None + } +} diff --git a/src/ui/activities/filetransfer/components/transfer/file_list.rs b/src/ui/activities/filetransfer/components/transfer/file_list.rs new file mode 100644 index 00000000..27ca9c07 --- /dev/null +++ b/src/ui/activities/filetransfer/components/transfer/file_list.rs @@ -0,0 +1,400 @@ +//! ## FileList +//! +//! `FileList` component renders a file list tab + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::props::{ + Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers, +}; +use tuirealm::tui::layout::Corner; +use tuirealm::tui::text::{Span, Spans}; +use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState}; +use tuirealm::{MockComponent, Props, State, StateValue}; + +pub const FILE_LIST_CMD_SELECT_ALL: &str = "A"; + +/// ## OwnStates +/// +/// OwnStates contains states for this component +#[derive(Clone)] +struct OwnStates { + list_index: usize, // Index of selected element in list + selected: Vec, // Selected files +} + +impl Default for OwnStates { + fn default() -> Self { + OwnStates { + list_index: 0, + selected: Vec::new(), + } + } +} + +impl OwnStates { + /// ### init_list_states + /// + /// Initialize list states + pub fn init_list_states(&mut self, len: usize) { + self.selected = Vec::with_capacity(len); + self.fix_list_index(); + } + + /// ### list_index + /// + /// Return current value for list index + pub fn list_index(&self) -> usize { + self.list_index + } + + /// ### incr_list_index + /// + /// Incremenet list index. + /// If `can_rewind` is `true` the index rewinds when boundary is reached + pub fn incr_list_index(&mut self, can_rewind: bool) { + // Check if index is at last element + if self.list_index + 1 < self.list_len() { + self.list_index += 1; + } else if can_rewind { + self.list_index = 0; + } + } + + /// ### decr_list_index + /// + /// Decrement list index + /// If `can_rewind` is `true` the index rewinds when boundary is reached + pub fn decr_list_index(&mut self, can_rewind: bool) { + // Check if index is bigger than 0 + if self.list_index > 0 { + self.list_index -= 1; + } else if self.list_len() > 0 && can_rewind { + self.list_index = self.list_len() - 1; + } + } + + pub fn list_index_at_first(&mut self) { + self.list_index = 0; + } + + pub fn list_index_at_last(&mut self) { + self.list_index = match self.list_len() { + 0 => 0, + len => len - 1, + }; + } + + /// ### list_len + /// + /// Returns the length of the file list, which is actually the capacity of the selection vector + pub fn list_len(&self) -> usize { + self.selected.capacity() + } + + /// ### is_selected + /// + /// Returns whether the file with index `entry` is selected + pub fn is_selected(&self, entry: usize) -> bool { + self.selected.contains(&entry) + } + + /// ### is_selection_empty + /// + /// Returns whether the selection is currently empty + pub fn is_selection_empty(&self) -> bool { + self.selected.is_empty() + } + + /// ### get_selection + /// + /// Returns current file selection + pub fn get_selection(&self) -> Vec { + self.selected.clone() + } + + /// ### fix_list_index + /// + /// Keep index if possible, otherwise set to lenght - 1 + fn fix_list_index(&mut self) { + if self.list_index >= self.list_len() && self.list_len() > 0 { + self.list_index = self.list_len() - 1; + } else if self.list_len() == 0 { + self.list_index = 0; + } + } + + // -- select manipulation + + /// ### toggle_file + /// + /// Select or deselect file with provided entry index + pub fn toggle_file(&mut self, entry: usize) { + match self.is_selected(entry) { + true => self.deselect(entry), + false => self.select(entry), + } + } + + /// ### select_all + /// + /// Select all files + pub fn select_all(&mut self) { + for i in 0..self.list_len() { + self.select(i); + } + } + + /// ### select + /// + /// Select provided index if not selected yet + fn select(&mut self, entry: usize) { + if !self.is_selected(entry) { + self.selected.push(entry); + } + } + + /// ### deselect + /// + /// Remove element file with associated index + fn deselect(&mut self, entry: usize) { + if self.is_selected(entry) { + self.selected.retain(|&x| x != entry); + } + } +} + +#[derive(Default)] +pub struct FileList { + props: Props, + states: OwnStates, +} + +impl FileList { + pub fn foreground(mut self, fg: Color) -> Self { + self.attr(Attribute::Foreground, AttrValue::Color(fg)); + self + } + + pub fn background(mut self, bg: Color) -> Self { + self.attr(Attribute::Background, AttrValue::Color(bg)); + self + } + + pub fn borders(mut self, b: Borders) -> Self { + self.attr(Attribute::Borders, AttrValue::Borders(b)); + self + } + + pub fn title>(mut self, t: S, a: Alignment) -> Self { + self.attr( + Attribute::Title, + AttrValue::Title((t.as_ref().to_string(), a)), + ); + self + } + + pub fn highlighted_color(mut self, c: Color) -> Self { + self.attr(Attribute::HighlightedColor, AttrValue::Color(c)); + self + } + + pub fn rows(mut self, rows: Table) -> Self { + self.attr(Attribute::Content, AttrValue::Table(rows)); + self + } +} + +impl MockComponent for FileList { + fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) { + let title = self + .props + .get_or( + Attribute::Title, + AttrValue::Title((String::default(), Alignment::Left)), + ) + .unwrap_title(); + let borders = self + .props + .get_or(Attribute::Borders, AttrValue::Borders(Borders::default())) + .unwrap_borders(); + let focus = self + .props + .get_or(Attribute::Focus, AttrValue::Flag(false)) + .unwrap_flag(); + let div = tui_realm_stdlib::utils::get_block(borders, Some(title), focus, None); + // Make list entries + let list_items: Vec = match self + .props + .get(Attribute::Content) + .map(|x| x.unwrap_table()) + { + Some(table) => table + .iter() + .enumerate() + .map(|(num, row)| { + let columns: Vec = row + .iter() + .map(|col| { + let (fg, bg, mut modifiers) = + tui_realm_stdlib::utils::use_or_default_styles(&self.props, col); + if self.states.is_selected(num) { + modifiers |= TextModifiers::REVERSED + | TextModifiers::UNDERLINED + | TextModifiers::ITALIC; + } + Span::styled( + col.content.clone(), + Style::default().add_modifier(modifiers).fg(fg).bg(bg), + ) + }) + .collect(); + ListItem::new(Spans::from(columns)) + }) + .collect(), // Make List item from TextSpan + _ => Vec::new(), + }; + let highlighted_color = self + .props + .get(Attribute::HighlightedColor) + .map(|x| x.unwrap_color()); + let modifiers = match focus { + true => TextModifiers::REVERSED, + false => TextModifiers::empty(), + }; + // Make list + let mut list = TuiList::new(list_items) + .block(div) + .start_corner(Corner::TopLeft); + if let Some(highlighted_color) = highlighted_color { + list = list.highlight_style( + Style::default() + .fg(highlighted_color) + .add_modifier(modifiers), + ); + } + let mut state: ListState = ListState::default(); + state.select(Some(self.states.list_index)); + frame.render_stateful_widget(list, area, &mut state); + } + + fn attr(&mut self, attr: Attribute, value: AttrValue) { + self.props.set(attr, value); + if matches!(attr, Attribute::Content) { + self.states.init_list_states( + match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) { + Some(spans) => spans.len(), + _ => 0, + }, + ); + self.states.fix_list_index(); + } + } + + fn query(&self, attr: Attribute) -> Option { + self.props.get(attr) + } + + fn state(&self) -> State { + match self.states.is_selection_empty() { + true => State::One(StateValue::Usize(self.states.list_index())), + false => State::Vec( + self.states + .get_selection() + .into_iter() + .map(StateValue::Usize) + .collect(), + ), + } + } + + fn perform(&mut self, cmd: Cmd) -> CmdResult { + match cmd { + Cmd::Move(Direction::Down) => { + let prev = self.states.list_index; + self.states.incr_list_index(true); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Move(Direction::Up) => { + let prev = self.states.list_index; + self.states.decr_list_index(true); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Scroll(Direction::Down) => { + let prev = self.states.list_index; + (0..8).for_each(|_| self.states.incr_list_index(false)); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Scroll(Direction::Up) => { + let prev = self.states.list_index; + (0..8).for_each(|_| self.states.decr_list_index(false)); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::GoTo(Position::Begin) => { + let prev = self.states.list_index; + self.states.list_index_at_first(); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::GoTo(Position::End) => { + let prev = self.states.list_index; + self.states.list_index_at_last(); + if prev != self.states.list_index { + CmdResult::Changed(self.state()) + } else { + CmdResult::None + } + } + Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => { + self.states.select_all(); + CmdResult::None + } + Cmd::Toggle => { + self.states.toggle_file(self.states.list_index()); + CmdResult::None + } + _ => CmdResult::None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/transfer/mod.rs b/src/ui/activities/filetransfer/components/transfer/mod.rs new file mode 100644 index 00000000..26a6ff01 --- /dev/null +++ b/src/ui/activities/filetransfer/components/transfer/mod.rs @@ -0,0 +1,494 @@ +//! ## Transfer +//! +//! file transfer components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{Msg, TransferMsg, UiMsg}; + +mod file_list; +use file_list::FileList; + +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, Borders, Color, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +#[derive(MockComponent)] +pub struct ExplorerFind { + component: FileList, +} + +impl ExplorerFind { + pub fn new>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self { + Self { + component: FileList::default() + .background(bg) + .borders(Borders::default().color(hg)) + .foreground(fg) + .highlighted_color(hg) + .title(title, Alignment::Left) + .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), + } + } +} + +impl Component for ExplorerFind { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::CONTROL, + }) => { + let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char('m'), + modifiers: KeyModifiers::NONE, + }) => { + let _ = self.perform(Cmd::Toggle); + Some(Msg::None) + } + // -- comp msg + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ExplorerTabbed)) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseFindExplorer)) + } + Event::Keyboard(KeyEvent { + code: Key::Left | Key::Right, + .. + }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Transfer(TransferMsg::EnterDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char(' '), + .. + }) => Some(Msg::Transfer(TransferMsg::TransferFile)), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)), + Event::Keyboard(KeyEvent { + code: Key::Char('b'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('e') | Key::Delete, + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('i'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('v'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('w'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ExplorerLocal { + component: FileList, +} + +impl ExplorerLocal { + pub fn new>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self { + Self { + component: FileList::default() + .background(bg) + .borders(Borders::default().color(hg)) + .foreground(fg) + .highlighted_color(hg) + .title(title, Alignment::Left) + .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), + } + } +} + +impl Component for ExplorerLocal { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::CONTROL, + }) => { + let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char('m'), + modifiers: KeyModifiers::NONE, + }) => { + let _ = self.perform(Cmd::Toggle); + Some(Msg::None) + } + // -- comp msg + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ExplorerTabbed)) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Transfer(TransferMsg::EnterDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char(' '), + .. + }) => Some(Msg::Transfer(TransferMsg::TransferFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)), + Event::Keyboard(KeyEvent { + code: Key::Char('b'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowCopyPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('d'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('e') | Key::Delete, + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('f'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFindPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('g'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowGotoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('i'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('l'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::ReloadDir)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('o'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenTextFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('r'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowRenamePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('u'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::GoToParentDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('x'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowExecPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)), + Event::Keyboard(KeyEvent { + code: Key::Char('v'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('w'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ExplorerRemote { + component: FileList, +} + +impl ExplorerRemote { + pub fn new>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self { + Self { + component: FileList::default() + .background(bg) + .borders(Borders::default().color(hg)) + .foreground(fg) + .highlighted_color(hg) + .title(title, Alignment::Left) + .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), + } + } +} + +impl Component for ExplorerRemote { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::CONTROL, + }) => { + let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char('m'), + modifiers: KeyModifiers::NONE, + }) => { + let _ = self.perform(Cmd::Toggle); + Some(Msg::None) + } + // -- comp msg + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ExplorerTabbed)) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Transfer(TransferMsg::EnterDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char(' '), + .. + }) => Some(Msg::Transfer(TransferMsg::TransferFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('a'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)), + Event::Keyboard(KeyEvent { + code: Key::Char('b'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowCopyPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('d'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('e') | Key::Delete, + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('f'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFindPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('g'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowGotoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('i'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('l'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::ReloadDir)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('o'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenTextFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('r'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowRenamePopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('u'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::GoToParentDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('x'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowExecPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)), + Event::Keyboard(KeyEvent { + code: Key::Char('v'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::OpenFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('w'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)), + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index 396f3712..ecd948ef 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -34,7 +34,7 @@ use std::path::Path; /// ## FileExplorerTab /// /// File explorer tab -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum FileExplorerTab { Local, Remote, diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 1ae4c670..808e0694 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -22,22 +22,50 @@ * SOFTWARE. */ // Locals -use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord, TransferPayload}; +use super::{ + browser::FileExplorerTab, ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord, + TransferPayload, +}; use crate::filetransfer::ProtocolParams; use crate::system::environment; use crate::system::notifications::Notification; use crate::system::sshkey_storage::SshKeyStorage; -use crate::utils::fmt::fmt_millis; +use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex}; use crate::utils::path; // Ext use bytesize::ByteSize; use std::env; use std::path::{Path, PathBuf}; -use tuirealm::Update; +use tuirealm::props::{ + Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan, +}; +use tuirealm::{PollStrategy, Update}; const LOG_CAPACITY: usize = 256; impl FileTransferActivity { + /// ### tick + /// + /// Call `Application::tick()` and process messages in `Update` + pub(super) fn tick(&mut self) { + match self.app.tick(PollStrategy::UpTo(3)) { + Ok(messages) => { + if !messages.is_empty() { + self.redraw = true; + } + for msg in messages.into_iter() { + let mut msg = Some(msg); + while msg.is_some() { + msg = self.update(msg); + } + } + } + Err(err) => { + self.mount_error(format!("Application error: {}", err)); + } + } + } + /// ### log /// /// Add message to log events @@ -57,8 +85,7 @@ impl FileTransferActivity { // Eventually push front the new record self.log_records.push_front(record); // Update log - let msg = self.update_logbox(); - self.update(msg); + self.update_logbox(); } /// ### log_and_alert @@ -68,8 +95,7 @@ impl FileTransferActivity { self.mount_error(msg.as_str()); self.log(level, msg); // Update log - let msg = self.update_logbox(); - self.update(msg); + self.update_logbox(); } /// ### init_config_client @@ -108,23 +134,6 @@ impl FileTransferActivity { env::set_var("EDITOR", self.config().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().input_hnd().read_event() { - // Handle event - let msg = self.view.on(event); - self.update(msg); - // Return true - true - } else { - // Error - false - } - } - /// ### local_to_abs_path /// /// Convert a path to absolute according to local explorer @@ -231,4 +240,245 @@ impl FileTransferActivity { } } } + + /// ### update_local_filelist + /// + /// Update local file list + pub(super) fn update_local_filelist(&mut self) { + // Get width + let width: usize = self + .context() + .store() + .get_unsigned(super::STORAGE_EXPLORER_WIDTH) + .unwrap_or(256); + 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, + fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/' + ); + let files: Vec> = self + .local() + .iter_files() + .map(|x| vec![TextSpan::from(self.local().fmt_file(x))]) + .collect(); + // Update content and title + assert!(self + .app + .attr( + &Id::ExplorerLocal, + Attribute::Content, + AttrValue::Table(files) + ) + .is_ok()); + assert!(self + .app + .attr( + &Id::ExplorerLocal, + Attribute::Title, + AttrValue::Title((hostname, Alignment::Left)) + ) + .is_ok()); + } + + /// ### update_remote_filelist + /// + /// Update remote file list + pub(super) fn update_remote_filelist(&mut self) { + let width: usize = self + .context() + .store() + .get_unsigned(super::STORAGE_EXPLORER_WIDTH) + .unwrap_or(256); + let hostname = self.get_remote_hostname(); + let hostname: String = format!( + "{}:{} ", + hostname, + fmt_path_elide_ex( + self.remote().wrkdir.as_path(), + width, + hostname.len() + 3 // 3 because of '/…/' + ) + ); + let files: Vec> = self + .remote() + .iter_files() + .map(|x| vec![TextSpan::from(self.remote().fmt_file(x))]) + .collect(); + // Update content and title + assert!(self + .app + .attr( + &Id::ExplorerRemote, + Attribute::Content, + AttrValue::Table(files) + ) + .is_ok()); + assert!(self + .app + .attr( + &Id::ExplorerRemote, + Attribute::Title, + AttrValue::Title((hostname, Alignment::Left)) + ) + .is_ok()); + } + + /// ### update_logbox + /// + /// Update log box + pub(super) fn update_logbox(&mut self) { + let mut table: TableBuilder = TableBuilder::default(); + for (idx, record) in self.log_records.iter().enumerate() { + // 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, + }; + table + .add_col(TextSpan::from(format!( + "{}", + record.time.format("%Y-%m-%dT%H:%M:%S%Z") + ))) + .add_col(TextSpan::from(" [")) + .add_col( + TextSpan::new( + format!( + "{:5}", + match record.level { + LogLevel::Error => "ERROR", + LogLevel::Warn => "WARN", + LogLevel::Info => "INFO", + } + ) + .as_str(), + ) + .fg(fg), + ) + .add_col(TextSpan::from("]: ")) + .add_col(TextSpan::from(record.msg.as_str())); + } + assert!(self + .app + .attr( + &Id::Log, + Attribute::Content, + AttrValue::Table(table.build()) + ) + .is_ok()); + } + + pub(super) fn update_progress_bar(&mut self, filename: String) { + assert!(self + .app + .attr( + &Id::ProgressBarFull, + Attribute::Text, + AttrValue::String(self.transfer.full.to_string()) + ) + .is_ok()); + assert!(self + .app + .attr( + &Id::ProgressBarFull, + Attribute::Value, + AttrValue::Payload(PropPayload::One(PropValue::F64( + self.transfer.full.calc_progress() + ))) + ) + .is_ok()); + assert!(self + .app + .attr( + &Id::ProgressBarPartial, + Attribute::Text, + AttrValue::String(self.transfer.partial.to_string()) + ) + .is_ok()); + assert!(self + .app + .attr( + &Id::ProgressBarPartial, + Attribute::Value, + AttrValue::Payload(PropPayload::One(PropValue::F64( + self.transfer.partial.calc_progress() + ))) + ) + .is_ok()); + assert!(self + .app + .attr( + &Id::ProgressBarPartial, + Attribute::Title, + AttrValue::Title((filename, Alignment::Left)) + ) + .is_ok()); + } + + /// ### finalize_find + /// + /// Finalize find process + pub(super) fn finalize_find(&mut self) { + // Set found to none + self.browser.del_found(); + // Restore tab + let new_tab = match self.browser.tab() { + FileExplorerTab::FindLocal => FileExplorerTab::Local, + FileExplorerTab::FindRemote => FileExplorerTab::Remote, + _ => FileExplorerTab::Local, + }; + // Give focus to new tab + match new_tab { + FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()), + FileExplorerTab::Remote => { + assert!(self.app.active(&Id::ExplorerRemote).is_ok()) + } + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + assert!(self.app.active(&Id::ExplorerFind).is_ok()) + } + } + self.browser.change_tab(new_tab); + } + + pub(super) fn update_find_list(&mut self) { + let files: Vec> = self + .found() + .unwrap() + .iter_files() + .map(|x| vec![TextSpan::from(self.found().unwrap().fmt_file(x))]) + .collect(); + assert!(self + .app + .attr( + &Id::ExplorerFind, + Attribute::Content, + AttrValue::Table(files) + ) + .is_ok()); + } + + pub(super) fn update_browser_file_list(&mut self) { + match self.browser.tab() { + FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_local_filelist(), + FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(), + } + } + + pub(super) fn update_browser_file_list_swapped(&mut self) { + match self.browser.tab() { + FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_remote_filelist(), + FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_local_filelist(), + } + } } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 3a9f6ab9..905da934 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -26,19 +26,20 @@ * SOFTWARE. */ // This module is split into files, cause it's just too big -pub(self) mod actions; -pub(self) mod lib; -pub(self) mod misc; -pub(self) mod session; -pub(self) mod update; -pub(self) mod view; +mod actions; +mod components; +mod lib; +mod misc; +mod session; +mod update; +mod view; // locals use super::{Activity, Context, ExitReason}; use crate::config::themes::Theme; use crate::filetransfer::{FileTransfer, FileTransferProtocol}; use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer}; -use crate::fs::explorer::FileExplorer; +use crate::fs::explorer::{FileExplorer, FileSorting}; use crate::fs::FsEntry; use crate::host::Localhost; use crate::system::config_client::ConfigClient; @@ -49,10 +50,10 @@ pub(self) use session::TransferPayload; // Includes use chrono::{DateTime, Local}; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::collections::VecDeque; +use std::time::Duration; use tempfile::TempDir; -use tuirealm::View; +use tuirealm::{Application, EventListenerCfg, NoUserEvent}; // -- Storage keys @@ -61,34 +62,115 @@ const STORAGE_PENDING_TRANSFER: &str = "FILETRANSFER_PENDING_TRANSFER"; // -- components -const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL"; -const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE"; -const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND"; -const COMPONENT_LOG_BOX: &str = "LOG_BOX"; -const COMPONENT_PROGRESS_BAR_FULL: &str = "PROGRESS_BAR_FULL"; -const COMPONENT_PROGRESS_BAR_PARTIAL: &str = "PROGRESS_BAR_PARTIAL"; -const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR"; -const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL"; -const COMPONENT_TEXT_HELP: &str = "TEXT_HELP"; -const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT"; -const COMPONENT_INPUT_COPY: &str = "INPUT_COPY"; -const COMPONENT_INPUT_EXEC: &str = "INPUT_EXEC"; -const COMPONENT_INPUT_FIND: &str = "INPUT_FIND"; -const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO"; -const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR"; -const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE"; -const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH"; -const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME"; -const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS"; -const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE"; -const COMPONENT_RADIO_REPLACE: &str = "RADIO_REPLACE"; // NOTE: used for file transfers, to choose whether to replace files -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_LOCAL: &str = "STATUS_BAR_LOCAL"; -const COMPONENT_SPAN_STATUS_BAR_REMOTE: &str = "STATUS_BAR_REMOTE"; -const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO"; -const COMPONENT_LIST_REPLACING_FILES: &str = "LIST_REPLACING_FILES"; // NOTE: used for file transfers, to list files which are going to be replaced +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +enum Id { + CopyPopup, + DeletePopup, + DisconnectPopup, + ErrorPopup, + ExecPopup, + ExplorerFind, + ExplorerLocal, + ExplorerRemote, + FatalPopup, + FileInfoPopup, + FindPopup, + GlobalListener, + GotoPopup, + KeybindingsPopup, + Log, + MkdirPopup, + NewfilePopup, + OpenWithPopup, + ProgressBarFull, + ProgressBarPartial, + QuitPopup, + RenamePopup, + ReplacePopup, + ReplacingFilesListPopup, + SaveAsPopup, + SortingPopup, + StatusBarLocal, + StatusBarRemote, + WaitPopup, +} + +#[derive(Debug, PartialEq)] +enum Msg { + Transfer(TransferMsg), + Ui(UiMsg), + None, +} + +#[derive(Debug, PartialEq)] +enum TransferMsg { + AbortTransfer, + CopyFileTo(String), + DeleteFile, + EnterDirectory, + ExecuteCmd(String), + GoTo(String), + GoToParentDirectory, + GoToPreviousDirectory, + Mkdir(String), + NewFile(String), + OpenFile, + OpenFileWith(String), + OpenTextFile, + ReloadDir, + RenameFile(String), + SaveFileAs(String), + SearchFile(String), + TransferFile, + TransferPendingFile, +} + +#[derive(Debug, PartialEq)] +enum UiMsg { + ChangeFileSorting(FileSorting), + ChangeTransferWindow, + CloseCopyPopup, + CloseDeletePopup, + CloseDisconnectPopup, + CloseErrorPopup, + CloseExecPopup, + CloseFatalPopup, + CloseFileInfoPopup, + CloseFileSortingPopup, + CloseFindExplorer, + CloseFindPopup, + CloseGotoPopup, + CloseKeybindingsPopup, + CloseMkdirPopup, + CloseNewFilePopup, + CloseOpenWithPopup, + CloseQuitPopup, + CloseReplacePopups, + CloseRenamePopup, + CloseSaveAsPopup, + Disconnect, + ExplorerTabbed, + LogTabbed, + Quit, + ReplacePopupTabbed, + ShowCopyPopup, + ShowDeletePopup, + ShowDisconnectPopup, + ShowExecPopup, + ShowFileInfoPopup, + ShowFileSortingPopup, + ShowFindPopup, + ShowGotoPopup, + ShowKeybindingsPopup, + ShowMkdirPopup, + ShowNewFilePopup, + ShowOpenWithPopup, + ShowQuitPopup, + ShowRenamePopup, + ShowSaveAsPopup, + ToggleHiddenFiles, + ToggleSyncBrowsing, +} /// ## LogLevel /// @@ -125,28 +207,43 @@ impl LogRecord { /// /// FileTransferActivity is the data holder for the file transfer activity pub struct FileTransferActivity { - exit_reason: Option, // Exit reason - context: Option, // Context holder - view: View, // View - host: Localhost, // Localhost - client: Box, // File transfer client - browser: Browser, // Browser - log_records: VecDeque, // Log records - transfer: TransferStates, // Transfer states - cache: Option, // Temporary directory where to store stuff + /// Exit reason + exit_reason: Option, + /// Context holder + context: Option, + /// Tui-realm application + app: Application, + /// Whether should redraw UI + redraw: bool, + /// Localhost bridge + host: Localhost, + /// Remote host + client: Box, + /// Browser + browser: Browser, + /// Current log lines + log_records: VecDeque, + transfer: TransferStates, + /// Temporary directory where to store temporary stuff + cache: Option, } impl FileTransferActivity { /// ### new /// /// Instantiates a new FileTransferActivity - pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity { + pub fn new(host: Localhost, protocol: FileTransferProtocol, ticks: Duration) -> Self { // Get config client let config_client: ConfigClient = Self::init_config_client(); - FileTransferActivity { + Self { exit_reason: None, context: None, - view: View::init(), + app: Application::init( + EventListenerCfg::default() + .poll_timeout(ticks) + .default_input_listener(ticks), + ), + redraw: true, host, client: match protocol { FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new( @@ -257,9 +354,11 @@ impl Activity for FileTransferActivity { // Set context self.context = Some(context); // Clear terminal - self.context_mut().clear_screen(); + if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() { + error!("Failed to clear screen: {}", err); + } // Put raw mode on enabled - if let Err(err) = enable_raw_mode() { + if let Err(err) = self.context_mut().terminal().enable_raw_mode() { error!("Failed to enter raw mode: {}", err); } // Get files at current pwd @@ -284,14 +383,12 @@ impl Activity for FileTransferActivity { /// `on_draw` is the function which draws the graphical interface. /// This function must be called at each tick to refresh the interface fn on_draw(&mut self) { - // Should ui actually be redrawned? - let mut redraw: bool = false; // Context must be something if self.context.is_none() { return; } // Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error) - if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() { + if !self.client.is_connected() && !self.app.mounted(&Id::FatalPopup) { let ftparams = self.context().ft_params().unwrap(); // print params let msg: String = Self::get_connection_msg(&ftparams.params); @@ -302,12 +399,11 @@ impl Activity for FileTransferActivity { // Connect to remote self.connect(); // Redraw - redraw = true; + self.redraw = true; } - // Handle input events (if false, becomes true; otherwise remains true) - redraw |= self.read_input_event(); - // @! draw interface - if redraw { + self.tick(); + // View + if self.redraw { self.view(); } } @@ -333,20 +429,16 @@ impl Activity for FileTransferActivity { } } // Disable raw mode - if let Err(err) = disable_raw_mode() { + if let Err(err) = self.context_mut().terminal().disable_raw_mode() { error!("Failed to disable raw mode: {}", err); } + if let Err(err) = self.context_mut().terminal().clear_screen() { + error!("Failed to clear screen: {}", err); + } // Disconnect client if self.client.is_connected() { let _ = self.client.disconnect(); } - // Clear terminal and return - match self.context.take() { - Some(mut ctx) => { - ctx.clear_screen(); - Some(ctx) - } - None => None, - } + self.context.take() } } diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 09cdd9fa..4d90943a 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -505,7 +505,7 @@ impl FileTransferActivity { >= 500 { // Read events - self.read_input_event(); + self.tick(); // Reset instant last_input_event_fetch = Some(Instant::now()); } @@ -937,7 +937,7 @@ impl FileTransferActivity { >= 500 { // Read events - self.read_input_event(); + self.tick(); // Reset instant last_input_event_fetch = Some(Instant::now()); } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index da003bf2..af5e54b4 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -29,1002 +29,459 @@ use super::{ actions::SelectedEntry, browser::{FileExplorerTab, FoundExplorerTab}, - FileTransferActivity, LogLevel, TransferOpts, COMPONENT_EXPLORER_FIND, - COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY, - COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR, - COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH, COMPONENT_INPUT_RENAME, - COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LIST_REPLACING_FILES, - COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, - COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, - COMPONENT_RADIO_REPLACE, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, - COMPONENT_TEXT_HELP, + ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg, }; -use crate::fs::explorer::FileSorting; use crate::fs::FsEntry; -use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder}; -use crate::ui::keymap::*; -use crate::utils::fmt::fmt_path_elide_ex; // externals -use tui_realm_stdlib::ProgressBarPropsBuilder; use tuirealm::{ - props::{Alignment, PropsBuilder, TableBuilder, TextSpan}, - tui::style::Color, - Msg, Payload, Update, Value, + props::{AttrValue, Attribute}, + State, StateValue, Update, }; -impl Update for FileTransferActivity { - // -- update +impl Update for FileTransferActivity { + fn update(&mut self, msg: Option) -> Option { + match msg.unwrap_or(Msg::None) { + Msg::None => None, + Msg::Transfer(msg) => self.update_transfer(msg), + Msg::Ui(msg) => self.update_ui(msg), + } + } +} - /// ### update - /// - /// Update auth activity model based on msg - /// The function exits when returns None - fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> { - let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg)); - // Match msg - match ref_msg { - None => None, // Exit after None - Some(msg) => match msg { - // -- local tab - (COMPONENT_EXPLORER_LOCAL, key) - if key == &MSG_KEY_RIGHT - && matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) => - { - // Go to find explorer - self.view.active(COMPONENT_EXPLORER_FIND); - self.browser.change_tab(FileExplorerTab::FindRemote); - None - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_RIGHT => { - // Change tab - self.view.active(COMPONENT_EXPLORER_REMOTE); - self.browser.change_tab(FileExplorerTab::Remote); - None - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_BACKSPACE => { - // Go to previous directory - 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() - } - (COMPONENT_EXPLORER_LOCAL, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { - // Match selected file - let mut entry: Option = None; - if let Some(e) = self.local().get(*idx) { - entry = Some(e.clone()); - } - if let Some(entry) = entry { - if self.action_submit_local(entry) { - // Update file list if sync - if self.browser.sync_browsing { - let _ = self.update_remote_filelist(); +impl FileTransferActivity { + fn update_transfer(&mut self, msg: TransferMsg) -> Option { + match msg { + TransferMsg::AbortTransfer => { + self.transfer.abort(); + } + TransferMsg::CopyFileTo(dest) => { + self.umount_copy(); + self.mount_blocking_wait("Copying file(s)…"); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_copy(dest), + FileExplorerTab::Remote => self.action_remote_copy(dest), + _ => panic!("Found tab doesn't support COPY"), + } + self.umount_wait(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::DeleteFile => { + self.umount_radio_delete(); + self.mount_blocking_wait("Removing file(s)…"); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_delete(), + FileExplorerTab::Remote => self.action_remote_delete(), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + // Get entry + self.action_find_delete(); + // Delete entries + match self.app.state(&Id::ExplorerFind) { + Ok(State::One(StateValue::Usize(idx))) => { + // Reload entries + self.found_mut().unwrap().del_entry(idx); } - self.update_local_filelist() - } else { - None - } - } else { - None - } - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_SPACE => { - self.action_local_send(); - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_A => { - // Toggle hidden files - self.local_mut().toggle_hidden_files(); - // Update status bar - self.refresh_local_status_bar(); - // Reload file list component - self.update_local_filelist() - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_I => { - if let SelectedEntry::One(file) = self.get_local_selected_entries() { - self.mount_file_info(&file); - } - None - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_L => { - // Reload directory - self.reload_local_dir(); - // Reload file list component - self.update_local_filelist() - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_O => { - self.action_edit_local_file(); - // Reload file list component - self.update_local_filelist() - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_U => { - 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 - (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_LEFT - && matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) => - { - // Go to find explorer - self.view.active(COMPONENT_EXPLORER_FIND); - self.browser.change_tab(FileExplorerTab::FindLocal); - None - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_LEFT => { - // Change tab - self.view.active(COMPONENT_EXPLORER_LOCAL); - self.browser.change_tab(FileExplorerTab::Local); - None - } - (COMPONENT_EXPLORER_REMOTE, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { - // Match selected file - let mut entry: Option = None; - if let Some(e) = self.remote().get(*idx) { - entry = Some(e.clone()); - } - if let Some(entry) = entry { - if self.action_submit_remote(entry) { - // Update file list if sync - if self.browser.sync_browsing { - let _ = self.update_local_filelist(); + Ok(State::Vec(values)) => { + values + .iter() + .map(|x| match x { + StateValue::Usize(v) => *v, + _ => 0, + }) + .for_each(|x| self.found_mut().unwrap().del_entry(x)); } - self.update_remote_filelist() - } else { - None + _ => {} } - } else { - None + self.update_find_list(); } } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_SPACE => { - self.action_remote_recv(); - self.update_local_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_BACKSPACE => { - // Go to previous directory - 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() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_A => { - // Toggle hidden files - self.remote_mut().toggle_hidden_files(); - // Update status bar - self.refresh_remote_status_bar(); - // Reload file list component - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_I => { - if let SelectedEntry::One(file) = self.get_remote_selected_entries() { - self.mount_file_info(&file); - } - None - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_L => { - // Reload directory - self.reload_remote_dir(); - // Reload file list component - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_O => { - // Edit file - self.action_edit_remote_file(); - // Reload file list component - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_U => { - 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() - } - // -- common explorer keys - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_B => - { - // Show sorting file - self.mount_file_sorting(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_C => - { - self.mount_copy(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_D => - { - self.mount_mkdir(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_F => - { - self.mount_find_input(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_G => - { - self.mount_goto(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_H => - { - self.mount_help(); - None + self.umount_wait(); + // Reload files + match self.browser.tab() { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + FileExplorerTab::FindLocal => self.update_local_filelist(), + FileExplorerTab::FindRemote => self.update_remote_filelist(), } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_N => - { - self.mount_newfile(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_LOG_BOX, key) - if key == &MSG_KEY_CHAR_Q => - { - self.mount_quit(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_R => - { - // Mount rename - self.mount_rename(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_CHAR_S => - { - // Mount save as - self.mount_saveas(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_CHAR_V => - { - // View - match self.browser.tab() { - FileExplorerTab::Local => self.action_open_local(), - FileExplorerTab::Remote => self.action_open_remote(), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { - self.action_find_open() + } + TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Local => { + if let SelectedEntry::One(entry) = self.get_local_selected_entries() { + if self.action_submit_local(entry) { + // Update file list if sync + if self.browser.sync_browsing { + let _ = self.update_remote_filelist(); } + self.update_local_filelist(); } - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_CHAR_W => - { - // Open with - self.mount_openwith(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_X => - { - // Mount exec - self.mount_exec(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_Y => - { - // Toggle browser sync - self.browser.toggle_sync_browsing(); - // Update status bar - self.refresh_remote_status_bar(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_LOG_BOX, key) - if key == &MSG_KEY_ESC => - { - self.mount_disconnect(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_CHAR_E || key == &MSG_KEY_DEL => - { - self.mount_radio_delete(); - None - } - // -- find result explorer - (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_RIGHT - && matches!(self.browser.tab(), FileExplorerTab::FindLocal) => - { - // Active remote explorer - self.view.active(COMPONENT_EXPLORER_REMOTE); - self.browser.change_tab(FileExplorerTab::Remote); - None - } - (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_LEFT - && matches!(self.browser.tab(), FileExplorerTab::FindRemote) => - { - // Active local explorer - self.view.active(COMPONENT_EXPLORER_LOCAL); - self.browser.change_tab(FileExplorerTab::Local); - None } - (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_ESC => { - // Umount find - self.umount_find(); - // Finalize find - self.finalize_find(); - None - } - (COMPONENT_EXPLORER_FIND, Msg::OnSubmit(_)) => { - // Find changedir - self.action_find_changedir(); - // Umount find - self.umount_find(); - // Finalize find - self.finalize_find(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_SPACE => { - // Get entry - self.action_find_transfer(TransferOpts::default()); - // Reload files - match self.browser.tab() { - // NOTE: swapped by purpose - FileExplorerTab::FindLocal => self.update_remote_filelist(), - FileExplorerTab::FindRemote => self.update_local_filelist(), - _ => None, - } - } - // -- switch to log - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_TAB => - { - self.view.active(COMPONENT_LOG_BOX); // Active log box - None - } - // -- Log box - (COMPONENT_LOG_BOX, key) if key == &MSG_KEY_TAB => { - self.view.blur(); // Blur log box - None - } - // -- copy popup - (COMPONENT_INPUT_COPY, key) if key == &MSG_KEY_ESC => { - self.umount_copy(); - None - } - (COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - // Copy file - self.umount_copy(); - self.mount_blocking_wait("Copying file(s)…"); - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_copy(input.to_string()), - FileExplorerTab::Remote => self.action_remote_copy(input.to_string()), - _ => panic!("Found tab doesn't support COPY"), - } - self.umount_wait(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_INPUT_COPY, _) => None, - // -- exec popup - (COMPONENT_INPUT_EXEC, key) if key == &MSG_KEY_ESC => { - self.umount_exec(); - None - } - (COMPONENT_INPUT_EXEC, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - // Exex command - self.umount_exec(); - self.mount_blocking_wait(format!("Executing '{}'…", input).as_str()); - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_exec(input.to_string()), - FileExplorerTab::Remote => self.action_remote_exec(input.to_string()), - _ => panic!("Found tab doesn't support EXEC"), - } - self.umount_wait(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, + } + TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => { + if let SelectedEntry::One(entry) = self.get_remote_selected_entries() { + if self.action_submit_remote(entry) { + // Update file list if sync + if self.browser.sync_browsing { + let _ = self.update_local_filelist(); + } + self.update_remote_filelist(); } } - (COMPONENT_INPUT_EXEC, _) => None, - // -- find popup - (COMPONENT_INPUT_FIND, key) if key == &MSG_KEY_ESC => { - self.umount_find_input(); - None - } - (COMPONENT_INPUT_FIND, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - self.umount_find_input(); - // Mount wait - self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, input).as_str()); - // Find - let res: Result, String> = match self.browser.tab() { - FileExplorerTab::Local => self.action_local_find(input.to_string()), - FileExplorerTab::Remote => self.action_remote_find(input.to_string()), - _ => panic!("Trying to search for files, while already in a find result"), - }; - // Umount wait - self.umount_wait(); - // Match result - match res { - Err(err) => { - // Mount error - self.mount_error(err.as_str()); - } - Ok(files) if files.is_empty() => { - // If no file has been found notify user - self.mount_info( - format!(r#"Could not find any file matching "{}""#, input).as_str(), - ); + } + TransferMsg::EnterDirectory => { + // NOTE: is find explorer + // Find changedir + self.action_find_changedir(); + // Umount find + self.umount_find(); + // Finalize find + self.finalize_find(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::ExecuteCmd(cmd) => { + // Exex command + self.umount_exec(); + self.mount_blocking_wait(format!("Executing '{}'…", cmd).as_str()); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_exec(cmd), + FileExplorerTab::Remote => self.action_remote_exec(cmd), + _ => panic!("Found tab doesn't support EXEC"), + } + self.umount_wait(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::GoTo(dir) => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_change_local_dir(dir, false), + FileExplorerTab::Remote => self.action_change_remote_dir(dir, false), + _ => panic!("Found tab doesn't support GOTO"), + } + // Umount + self.umount_goto(); + // Reload files if sync + if self.browser.sync_browsing { + self.update_browser_file_list_swapped(); + } + // Reload files + self.update_browser_file_list() + } + TransferMsg::GoToParentDirectory => { + match self.browser.tab() { + FileExplorerTab::Local => { + self.action_go_to_local_upper_dir(false); + if self.browser.sync_browsing { + let _ = self.update_remote_filelist(); } - Ok(files) => { - // Get wrkdir - let wrkdir = match self.browser.tab() { - FileExplorerTab::Local => self.local().wrkdir.clone(), - _ => self.remote().wrkdir.clone(), - }; - // Create explorer and load files - self.browser.set_found( - match self.browser.tab() { - FileExplorerTab::Local => FoundExplorerTab::Local, - _ => FoundExplorerTab::Remote, - }, - files, - wrkdir.as_path(), - ); - // Mount result widget - self.mount_find(input); - self.update_find_list(); - // Initialize tab - self.browser.change_tab(match self.browser.tab() { - FileExplorerTab::Local => FileExplorerTab::FindLocal, - FileExplorerTab::Remote => FileExplorerTab::FindRemote, - _ => FileExplorerTab::FindLocal, - }); + // Reload file list component + self.update_local_filelist() + } + FileExplorerTab::Remote => { + 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() } - None - } - // -- goto popup - (COMPONENT_INPUT_GOTO, key) if key == &MSG_KEY_ESC => { - self.umount_goto(); - None + _ => {} } - (COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => { - self.action_change_local_dir(input.to_string(), false) - } - FileExplorerTab::Remote => { - self.action_change_remote_dir(input.to_string(), false) + } + TransferMsg::GoToPreviousDirectory => { + match self.browser.tab() { + FileExplorerTab::Local => { + self.action_go_to_previous_local_dir(false); + if self.browser.sync_browsing { + let _ = self.update_remote_filelist(); } - _ => panic!("Found tab doesn't support GOTO"), - } - // Umount - self.umount_goto(); - // Reload files if sync - if self.browser.sync_browsing { - match self.browser.tab() { - FileExplorerTab::Remote => self.update_local_filelist(), - FileExplorerTab::Local => self.update_remote_filelist(), - _ => None, - }; + // Reload file list component + self.update_local_filelist() } - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, + FileExplorerTab::Remote => { + self.action_go_to_previous_remote_dir(false); + if self.browser.sync_browsing { + let _ = self.update_local_filelist(); + } + // Reload file list component + self.update_remote_filelist() } + _ => {} } - (COMPONENT_INPUT_GOTO, _) => None, - // -- make directory - (COMPONENT_INPUT_MKDIR, key) if key == &MSG_KEY_ESC => { - self.umount_mkdir(); - None - } - (COMPONENT_INPUT_MKDIR, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_mkdir(input.to_string()), - FileExplorerTab::Remote => self.action_remote_mkdir(input.to_string()), - _ => panic!("Found tab doesn't support MKDIR"), - } - self.umount_mkdir(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, + } + TransferMsg::Mkdir(dir) => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_mkdir(dir), + FileExplorerTab::Remote => self.action_remote_mkdir(dir), + _ => {} + } + self.umount_mkdir(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::NewFile(name) => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_newfile(name), + FileExplorerTab::Remote => self.action_remote_newfile(name), + _ => {} + } + self.umount_newfile(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::OpenFile => match self.browser.tab() { + FileExplorerTab::Local => self.action_open_local(), + FileExplorerTab::Remote => self.action_open_remote(), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => self.action_find_open(), + }, + TransferMsg::OpenFileWith(prog) => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_open_with(&prog), + FileExplorerTab::Remote => self.action_remote_open_with(&prog), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + self.action_find_open_with(&prog) } } - (COMPONENT_INPUT_MKDIR, _) => None, - // -- new file - (COMPONENT_INPUT_NEWFILE, key) if key == &MSG_KEY_ESC => { - self.umount_newfile(); - None + self.umount_openwith(); + } + TransferMsg::OpenTextFile => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_edit_local_file(), + FileExplorerTab::Remote => self.action_edit_remote_file(), + _ => {} } - (COMPONENT_INPUT_NEWFILE, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_newfile(input.to_string()), - FileExplorerTab::Remote => self.action_remote_newfile(input.to_string()), - _ => panic!("Found tab doesn't support NEWFILE"), - } - self.umount_newfile(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, + self.update_browser_file_list() + } + TransferMsg::ReloadDir => self.update_browser_file_list(), + TransferMsg::RenameFile(dest) => { + self.umount_rename(); + self.mount_blocking_wait("Moving file(s)…"); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_rename(dest), + FileExplorerTab::Remote => self.action_remote_rename(dest), + _ => {} + } + self.umount_wait(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::SaveFileAs(dest) => { + self.umount_saveas(); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_saveas(dest), + FileExplorerTab::Remote => self.action_remote_saveas(dest), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + // Get entry + self.action_find_transfer(TransferOpts::default().save_as(Some(dest))); + } + } + self.umount_saveas(); + // Reload files + self.update_browser_file_list_swapped(); + } + TransferMsg::SearchFile(search) => { + self.umount_find_input(); + // Mount wait + self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str()); + // Find + let res: Result, String> = match self.browser.tab() { + FileExplorerTab::Local => self.action_local_find(search.clone()), + FileExplorerTab::Remote => self.action_remote_find(search.clone()), + _ => panic!("Trying to search for files, while already in a find result"), + }; + // Umount wait + self.umount_wait(); + // Match result + match res { + Err(err) => { + // Mount error + self.mount_error(err.as_str()); + } + Ok(files) if files.is_empty() => { + // If no file has been found notify user + self.mount_info( + format!(r#"Could not find any file matching "{}""#, search).as_str(), + ); + } + Ok(files) => { + // Get wrkdir + let wrkdir = match self.browser.tab() { + FileExplorerTab::Local => self.local().wrkdir.clone(), + _ => self.remote().wrkdir.clone(), + }; + // Create explorer and load files + self.browser.set_found( + match self.browser.tab() { + FileExplorerTab::Local => FoundExplorerTab::Local, + _ => FoundExplorerTab::Remote, + }, + files, + wrkdir.as_path(), + ); + // Mount result widget + self.mount_find(&search); + self.update_find_list(); + // Initialize tab + self.browser.change_tab(match self.browser.tab() { + FileExplorerTab::Local => FileExplorerTab::FindLocal, + FileExplorerTab::Remote => FileExplorerTab::FindRemote, + _ => FileExplorerTab::FindLocal, + }); } } - (COMPONENT_INPUT_NEWFILE, _) => None, - // -- open with - (COMPONENT_INPUT_OPEN_WITH, key) if key == &MSG_KEY_ESC => { - self.umount_openwith(); - None - } - (COMPONENT_INPUT_OPEN_WITH, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_open_with(input), - FileExplorerTab::Remote => self.action_remote_open_with(input), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { - self.action_find_open_with(input) - } + } + TransferMsg::TransferFile => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_send(), + FileExplorerTab::Remote => self.action_remote_recv(), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + self.action_find_transfer(TransferOpts::default()) } - self.umount_openwith(); - None - } - (COMPONENT_INPUT_OPEN_WITH, _) => None, - // -- rename - (COMPONENT_INPUT_RENAME, key) if key == &MSG_KEY_ESC => { - self.umount_rename(); - None } - (COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - self.umount_rename(); - self.mount_blocking_wait("Moving file(s)…"); - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_rename(input.to_string()), - FileExplorerTab::Remote => self.action_remote_rename(input.to_string()), - _ => panic!("Found tab doesn't support RENAME"), + self.update_browser_file_list_swapped(); + } + TransferMsg::TransferPendingFile => { + self.umount_radio_replace(); + self.action_finalize_pending_transfer(); + } + } + // Force redraw + self.redraw = true; + None + } + + fn update_ui(&mut self, msg: UiMsg) -> Option { + match msg { + UiMsg::ChangeFileSorting(sorting) => { + match self.browser.tab() { + FileExplorerTab::Local | FileExplorerTab::FindLocal => { + self.local_mut().sort_by(sorting); + self.refresh_local_status_bar(); } - self.umount_wait(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, + FileExplorerTab::Remote | FileExplorerTab::FindRemote => { + self.remote_mut().sort_by(sorting); + self.refresh_remote_status_bar() } } - (COMPONENT_INPUT_RENAME, _) => None, - // -- save as - (COMPONENT_INPUT_SAVEAS, key) if key == &MSG_KEY_ESC => { - self.umount_saveas(); - None - } - (COMPONENT_INPUT_SAVEAS, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_saveas(input.to_string()), - FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { - // Get entry - self.action_find_transfer(TransferOpts::default().save_as(Some(input))); - } - } - self.umount_saveas(); - // Reload files - match self.browser.tab() { - // NOTE: Swapped is intentional - FileExplorerTab::Local => self.update_remote_filelist(), - FileExplorerTab::Remote => self.update_local_filelist(), - FileExplorerTab::FindLocal => self.update_remote_filelist(), - FileExplorerTab::FindRemote => self.update_local_filelist(), + self.update_browser_file_list(); + } + UiMsg::ChangeTransferWindow => { + let new_tab = match self.browser.tab() { + FileExplorerTab::Local if self.browser.found().is_some() => { + FileExplorerTab::FindRemote } - } - (COMPONENT_INPUT_SAVEAS, _) => None, - // -- fileinfo - (COMPONENT_LIST_FILEINFO, key) | (COMPONENT_LIST_FILEINFO, key) - if key == &MSG_KEY_ENTER || key == &MSG_KEY_ESC => - { - self.umount_file_info(); - None - } - (COMPONENT_LIST_FILEINFO, _) => None, - // -- delete - (COMPONENT_RADIO_DELETE, key) - if key == &MSG_KEY_ESC - || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => - { - self.umount_radio_delete(); - None - } - (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Choice is 'YES' - self.umount_radio_delete(); - self.mount_blocking_wait("Removing file(s)…"); - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_delete(), - FileExplorerTab::Remote => self.action_remote_delete(), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { - // Get entry - self.action_find_delete(); - // Delete entries - match self.view.get_state(COMPONENT_EXPLORER_FIND) { - Some(Payload::One(Value::Usize(idx))) => { - // Reload entries - self.found_mut().unwrap().del_entry(idx); - } - Some(Payload::Vec(values)) => { - values - .iter() - .map(|x| match x { - Value::Usize(v) => *v, - _ => 0, - }) - .for_each(|x| self.found_mut().unwrap().del_entry(x)); - } - _ => {} - } - self.update_find_list(); - } + FileExplorerTab::FindLocal | FileExplorerTab::Local => FileExplorerTab::Remote, + FileExplorerTab::Remote if self.browser.found().is_some() => { + FileExplorerTab::FindLocal } - self.umount_wait(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - FileExplorerTab::FindLocal => self.update_local_filelist(), - FileExplorerTab::FindRemote => self.update_remote_filelist(), + FileExplorerTab::FindRemote | FileExplorerTab::Remote => FileExplorerTab::Local, + }; + // Set focus + match new_tab { + FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()), + FileExplorerTab::Remote => { + assert!(self.app.active(&Id::ExplorerRemote).is_ok()) } - } - (COMPONENT_RADIO_DELETE, _) => None, - // -- replace - (COMPONENT_RADIO_REPLACE, key) - if key == &MSG_KEY_ESC - || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => - { - self.umount_radio_replace(); - None - } - (COMPONENT_RADIO_REPLACE, key) if key == &MSG_KEY_TAB => { - if self.is_radio_replace_extended() { - self.view.active(COMPONENT_LIST_REPLACING_FILES); + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + assert!(self.app.active(&Id::ExplorerFind).is_ok()) } - None - } - (COMPONENT_RADIO_REPLACE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Choice is 'YES' - self.umount_radio_replace(); - self.action_finalize_pending_transfer(); - None } - (COMPONENT_RADIO_REPLACE, _) => None, - (COMPONENT_LIST_REPLACING_FILES, key) if key == &MSG_KEY_TAB => { - self.view.active(COMPONENT_RADIO_REPLACE); - None - } - (COMPONENT_LIST_REPLACING_FILES, key) if key == &MSG_KEY_ESC => { - self.umount_radio_replace(); - None - } - (COMPONENT_LIST_REPLACING_FILES, _) => None, - // -- disconnect - (COMPONENT_RADIO_DISCONNECT, key) - if key == &MSG_KEY_ESC - || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => - { - self.umount_disconnect(); - None - } - (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - self.disconnect(); - self.umount_disconnect(); - None - } - (COMPONENT_RADIO_DISCONNECT, _) => None, - // -- quit - (COMPONENT_RADIO_QUIT, key) - if key == &MSG_KEY_ESC - || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => + self.browser.change_tab(new_tab); + } + UiMsg::CloseCopyPopup => self.umount_copy(), + UiMsg::CloseDeletePopup => self.umount_radio_delete(), + UiMsg::CloseDisconnectPopup => self.umount_disconnect(), + UiMsg::CloseErrorPopup => self.umount_error(), + UiMsg::CloseExecPopup => self.umount_exec(), + UiMsg::CloseFatalPopup => { + self.umount_fatal(); + self.exit_reason = Some(ExitReason::Disconnect); + } + UiMsg::CloseFileInfoPopup => self.umount_file_info(), + UiMsg::CloseFileSortingPopup => self.umount_file_sorting(), + UiMsg::CloseFindExplorer => { + self.finalize_find(); + self.umount_find(); + } + UiMsg::CloseFindPopup => self.umount_find_input(), + UiMsg::CloseGotoPopup => self.umount_goto(), + UiMsg::CloseKeybindingsPopup => self.umount_help(), + UiMsg::CloseMkdirPopup => self.umount_mkdir(), + UiMsg::CloseNewFilePopup => self.umount_newfile(), + UiMsg::CloseOpenWithPopup => self.umount_openwith(), + UiMsg::CloseQuitPopup => self.umount_quit(), + UiMsg::CloseRenamePopup => self.umount_rename(), + UiMsg::CloseReplacePopups => { + self.umount_radio_replace(); + } + UiMsg::CloseSaveAsPopup => self.umount_saveas(), + UiMsg::Disconnect => { + self.disconnect(); + self.umount_disconnect(); + } + UiMsg::ExplorerTabbed => { + assert!(self.app.active(&Id::Log).is_ok()); + } + UiMsg::LogTabbed => { + assert!(self.app.active(&Id::ExplorerLocal).is_ok()); + } + UiMsg::Quit => { + self.disconnect_and_quit(); + self.umount_quit(); + } + UiMsg::ReplacePopupTabbed => { + if let Ok(Some(AttrValue::Flag(true))) = + self.app.query(&Id::ReplacePopup, Attribute::Focus) { - self.umount_quit(); - None + assert!(self.app.active(&Id::ReplacingFilesListPopup).is_ok()); + } else { + assert!(self.app.active(&Id::ReplacePopup).is_ok()); } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - self.disconnect_and_quit(); - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, _) => None, - // -- sorting - (COMPONENT_RADIO_SORTING, key) if key == &MSG_KEY_ESC => { - self.umount_file_sorting(); - None - } - (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => { - self.umount_file_sorting(); - None - } - (COMPONENT_RADIO_SORTING, Msg::OnChange(Payload::One(Value::Usize(mode)))) => { - // Get sorting mode - let sorting: FileSorting = match mode { - 1 => FileSorting::ModifyTime, - 2 => FileSorting::CreationTime, - 3 => FileSorting::Size, - _ => FileSorting::Name, - }; - match self.browser.tab() { - FileExplorerTab::Local => self.local_mut().sort_by(sorting), - FileExplorerTab::Remote => self.remote_mut().sort_by(sorting), - _ => panic!("Found result doesn't support SORTING"), - } - // Update status bar - match self.browser.tab() { - FileExplorerTab::Local => self.refresh_local_status_bar(), - FileExplorerTab::Remote => self.refresh_remote_status_bar(), - _ => panic!("Found result doesn't support SORTING"), - }; - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } + } + UiMsg::ShowCopyPopup => self.mount_copy(), + UiMsg::ShowDeletePopup => self.mount_radio_delete(), + UiMsg::ShowDisconnectPopup => self.mount_disconnect(), + UiMsg::ShowExecPopup => self.mount_exec(), + UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Local => { + if let SelectedEntry::One(file) = self.get_local_selected_entries() { + self.mount_file_info(&file); } - (COMPONENT_RADIO_SORTING, _) => None, - // -- error - (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - self.umount_error(); - None + } + UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Remote => { + if let SelectedEntry::One(file) = self.get_remote_selected_entries() { + self.mount_file_info(&file); } - (COMPONENT_TEXT_ERROR, _) => None, - // -- fatal - (COMPONENT_TEXT_FATAL, key) | (COMPONENT_TEXT_FATAL, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - self.exit_reason = Some(super::ExitReason::Disconnect); - None + } + UiMsg::ShowFileInfoPopup => { + if let SelectedEntry::One(file) = self.get_found_selected_entries() { + self.mount_file_info(&file); } - (COMPONENT_TEXT_FATAL, _) => None, - // -- help - (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - self.umount_help(); - None + } + UiMsg::ShowFileSortingPopup => self.mount_file_sorting(), + UiMsg::ShowFindPopup => self.mount_find_input(), + UiMsg::ShowGotoPopup => self.mount_goto(), + UiMsg::ShowKeybindingsPopup => self.mount_help(), + UiMsg::ShowMkdirPopup => self.mount_mkdir(), + UiMsg::ShowNewFilePopup => self.mount_newfile(), + UiMsg::ShowOpenWithPopup => self.mount_openwith(), + UiMsg::ShowQuitPopup => self.mount_quit(), + UiMsg::ShowRenamePopup => self.mount_rename(), + UiMsg::ShowSaveAsPopup => self.mount_saveas(), + UiMsg::ToggleHiddenFiles => match self.browser.tab() { + FileExplorerTab::FindLocal | FileExplorerTab::Local => { + self.browser.local_mut().toggle_hidden_files(); + self.refresh_local_status_bar(); + self.update_browser_file_list(); } - (COMPONENT_TEXT_HELP, _) => None, - // -- progress bar - (COMPONENT_PROGRESS_BAR_PARTIAL, key) if key == &MSG_KEY_CTRL_C => { - // Set transfer aborted to True - self.transfer.abort(); - None + FileExplorerTab::FindRemote | FileExplorerTab::Remote => { + self.browser.remote_mut().toggle_hidden_files(); + self.refresh_remote_status_bar(); + self.update_browser_file_list(); } - (COMPONENT_PROGRESS_BAR_PARTIAL, _) => None, - // -- fallback - (_, _) => None, // Nothing to do }, - } - } -} - -impl FileTransferActivity { - /// ### 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) { - Some(props) => { - // Get width - let width: usize = self - .context() - .store() - .get_unsigned(super::STORAGE_EXPLORER_WIDTH) - .unwrap_or(256); - 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, - fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/' - ); - let files: Vec = self - .local() - .iter_files() - .map(|x: &FsEntry| self.local().fmt_file(x)) - .collect(); - // Update - let props = FileListPropsBuilder::from(props) - .with_files(files) - .with_title(hostname, Alignment::Left) - .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) { - Some(props) => { - // Get width - let width: usize = self - .context() - .store() - .get_unsigned(super::STORAGE_EXPLORER_WIDTH) - .unwrap_or(256); - let hostname = self.get_remote_hostname(); - let hostname: String = format!( - "{}:{} ", - hostname, - fmt_path_elide_ex( - self.remote().wrkdir.as_path(), - width, - hostname.len() + 3 // 3 because of '/…/' - ) - ); - let files: Vec = self - .remote() - .iter_files() - .map(|x: &FsEntry| self.remote().fmt_file(x)) - .collect(); - // Update - let props = FileListPropsBuilder::from(props) - .with_files(files) - .with_title(hostname, Alignment::Left) - .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) { - Some(props) => { - // Make log entries - let mut table: TableBuilder = TableBuilder::default(); - for (idx, record) in self.log_records.iter().enumerate() { - // 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, - }; - table - .add_col(TextSpan::from(format!( - "{}", - record.time.format("%Y-%m-%dT%H:%M:%S%Z") - ))) - .add_col(TextSpan::from(" [")) - .add_col( - TextSpan::new( - format!( - "{:5}", - match record.level { - LogLevel::Error => "ERROR", - LogLevel::Warn => "WARN", - LogLevel::Info => "INFO", - } - ) - .as_str(), - ) - .fg(fg), - ) - .add_col(TextSpan::from("]: ")) - .add_col(TextSpan::from(record.msg.as_ref())); - } - let table = table.build(); - let props = LogboxPropsBuilder::from(props).with_log(table).build(); - self.view.update(super::COMPONENT_LOG_BOX, props) - } - None => None, - } - } - - pub(super) fn update_progress_bar(&mut self, filename: String) -> Option<(String, Msg)> { - if let Some(props) = self.view.get_props(COMPONENT_PROGRESS_BAR_FULL) { - let props = ProgressBarPropsBuilder::from(props) - .with_label(self.transfer.full.to_string()) - .with_progress(self.transfer.full.calc_progress()) - .build(); - let _ = self.view.update(COMPONENT_PROGRESS_BAR_FULL, props); - } - match self.view.get_props(COMPONENT_PROGRESS_BAR_PARTIAL) { - Some(props) => { - let props = ProgressBarPropsBuilder::from(props) - .with_title(filename, Alignment::Center) - .with_label(self.transfer.partial.to_string()) - .with_progress(self.transfer.partial.calc_progress()) - .build(); - self.view.update(COMPONENT_PROGRESS_BAR_PARTIAL, props) - } - None => None, - } - } - - /// ### finalize_find - /// - /// Finalize find process - fn finalize_find(&mut self) { - // Set found to none - self.browser.del_found(); - // Restore tab - self.browser.change_tab(match self.browser.tab() { - FileExplorerTab::FindLocal => FileExplorerTab::Local, - FileExplorerTab::FindRemote => FileExplorerTab::Remote, - _ => FileExplorerTab::Local, - }); - } - - fn update_find_list(&mut self) -> Option<(String, Msg)> { - match self.view.get_props(COMPONENT_EXPLORER_FIND) { - None => None, - Some(props) => { - // Prepare files - let files: Vec = self - .found() - .unwrap() - .iter_files() - .map(|x: &FsEntry| self.found().unwrap().fmt_file(x)) - .collect(); - let props = FileListPropsBuilder::from(props).with_files(files).build(); - self.view.update(COMPONENT_EXPLORER_FIND, props) + UiMsg::ToggleSyncBrowsing => { + self.browser.toggle_sync_browsing(); + self.refresh_remote_status_bar(); } } + None } } diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 5cf74ad8..8a03f34c 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -28,33 +28,17 @@ // locals use super::{ browser::{FileExplorerTab, FoundExplorerTab}, - Context, FileTransferActivity, + components, Context, FileTransferActivity, Id, }; use crate::fs::explorer::FileSorting; use crate::fs::FsEntry; -use crate::ui::components::{ - file_list::{FileList, FileListPropsBuilder}, - logbox::{LogBox, LogboxPropsBuilder}, -}; use crate::ui::store::Store; -use crate::utils::fmt::fmt_time; use crate::utils::ui::draw_area_in; // Ext -use bytesize::ByteSize; -use std::path::PathBuf; -use tui_realm_stdlib::{ - Input, InputPropsBuilder, List, ListPropsBuilder, Paragraph, ParagraphPropsBuilder, - ProgressBar, ProgressBarPropsBuilder, Radio, RadioPropsBuilder, Span, SpanPropsBuilder, Table, - TablePropsBuilder, -}; -use tuirealm::props::{Alignment, PropsBuilder, TableBuilder, TextSpan}; -use tuirealm::tui::{ - layout::{Constraint, Direction, Layout}, - style::Color, - widgets::{BorderType, Borders, Clear}, -}; -#[cfg(target_family = "unix")] -use users::{get_group_by_gid, get_user_by_uid}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::tui::layout::{Constraint, Direction, Layout}; +use tuirealm::tui::widgets::Clear; +use tuirealm::{Sub, SubClause, SubEventClause}; impl FileTransferActivity { // -- init @@ -72,57 +56,52 @@ impl FileTransferActivity { let remote_explorer_highlighted = self.theme().transfer_remote_explorer_highlighted; let log_panel = self.theme().transfer_log_window; let log_background = self.theme().transfer_log_background; - self.view.mount( - super::COMPONENT_EXPLORER_LOCAL, - Box::new(FileList::new( - FileListPropsBuilder::default() - .with_highlight_color(local_explorer_highlighted) - .with_background(local_explorer_background) - .with_foreground(local_explorer_foreground) - .with_borders(Borders::ALL, BorderType::Plain, local_explorer_highlighted) - .build(), - )), - ); - // Mount remote file explorer - self.view.mount( - super::COMPONENT_EXPLORER_REMOTE, - Box::new(FileList::new( - FileListPropsBuilder::default() - .with_highlight_color(remote_explorer_highlighted) - .with_background(remote_explorer_background) - .with_foreground(remote_explorer_foreground) - .with_borders(Borders::ALL, BorderType::Plain, remote_explorer_highlighted) - .build(), - )), - ); - // Mount log box - self.view.mount( - super::COMPONENT_LOG_BOX, - Box::new(LogBox::new( - LogboxPropsBuilder::default() - .with_title("Log", Alignment::Left) - .with_background(log_background) - .with_borders(Borders::ALL, BorderType::Plain, log_panel) - .build(), - )), - ); - // Mount status bars - self.view.mount( - super::COMPONENT_SPAN_STATUS_BAR_LOCAL, - Box::new(Span::new(SpanPropsBuilder::default().build())), - ); - self.view.mount( - super::COMPONENT_SPAN_STATUS_BAR_REMOTE, - Box::new(Span::new(SpanPropsBuilder::default().build())), - ); - // Load process bar + assert!(self + .app + .mount( + Id::ExplorerLocal, + Box::new(components::ExplorerLocal::new( + "", + &[], + local_explorer_background, + local_explorer_foreground, + local_explorer_highlighted + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .mount( + Id::ExplorerRemote, + Box::new(components::ExplorerRemote::new( + "", + &[], + remote_explorer_background, + remote_explorer_foreground, + remote_explorer_highlighted + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .mount( + Id::Log, + Box::new(components::Log::new(vec![], log_panel, log_background)), + vec![] + ) + .is_ok()); + // Load status bar self.refresh_local_status_bar(); self.refresh_remote_status_bar(); // Update components let _ = self.update_local_filelist(); let _ = self.update_remote_filelist(); + // Global listener + self.mount_global_listener(); // Give focus to local explorer - self.view.active(super::COMPONENT_EXPLORER_LOCAL); + assert!(self.app.active(&Id::ExplorerLocal).is_ok()); } // -- view @@ -131,9 +110,10 @@ impl FileTransferActivity { /// /// View gui pub(super) fn view(&mut self) { + self.redraw = false; let mut context: Context = self.context.take().unwrap(); let store: &mut Store = &mut context.store; - let _ = context.terminal.draw(|f| { + let _ = context.terminal.raw_mut().draw(|f| { // Prepare chunks let chunks = Layout::default() .direction(Direction::Vertical) @@ -169,228 +149,152 @@ impl FileTransferActivity { // Draw explorers // @! Local explorer (Find or default) if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) { - self.view - .render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[0]); + self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]); } else { - self.view - .render(super::COMPONENT_EXPLORER_LOCAL, f, tabs_chunks[0]); + self.app.view(&Id::ExplorerLocal, f, tabs_chunks[0]); } // @! Remote explorer (Find or default) if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) { - self.view - .render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[1]); + self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]); } else { - self.view - .render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]); + self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]); } // Draw log box - self.view - .render(super::COMPONENT_LOG_BOX, f, bottom_chunks[1]); + self.app.view(&Id::Log, f, bottom_chunks[1]); // Draw status bar - self.view.render( - super::COMPONENT_SPAN_STATUS_BAR_LOCAL, - f, - status_bar_chunks[0], - ); - self.view.render( - super::COMPONENT_SPAN_STATUS_BAR_REMOTE, - f, - status_bar_chunks[1], - ); + self.app.view(&Id::StatusBarLocal, f, status_bar_chunks[0]); + self.app.view(&Id::StatusBarRemote, f, status_bar_chunks[1]); // @! Draw popups - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_COPY) { - if props.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(props) = self.view.get_props(super::COMPONENT_INPUT_FIND) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_FIND, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) { - if props.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(props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) { - if props.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(props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) { - if props.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(props) = self.view.get_props(super::COMPONENT_INPUT_OPEN_WITH) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_OPEN_WITH, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) { - if props.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(props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) { - if props.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(props) = self.view.get_props(super::COMPONENT_INPUT_EXEC) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_EXEC, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) { - if props.visible { + if self.app.mounted(&Id::CopyPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::CopyPopup, f, popup); + } else if self.app.mounted(&Id::FindPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::FindPopup, f, popup); + } else if self.app.mounted(&Id::GotoPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::GotoPopup, f, popup); + } else if self.app.mounted(&Id::MkdirPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::MkdirPopup, f, popup); + } else if self.app.mounted(&Id::NewfilePopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::NewfilePopup, f, popup); + } else if self.app.mounted(&Id::OpenWithPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::OpenWithPopup, f, popup); + } else if self.app.mounted(&Id::RenamePopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::RenamePopup, f, popup); + } else if self.app.mounted(&Id::SaveAsPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::SaveAsPopup, f, popup); + } else if self.app.mounted(&Id::ExecPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::ExecPopup, f, popup); + } else if self.app.mounted(&Id::FileInfoPopup) { + let popup = draw_area_in(f.size(), 50, 50); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::FileInfoPopup, f, popup); + } else if self.app.mounted(&Id::ProgressBarPartial) { + let popup = draw_area_in(f.size(), 50, 20); + f.render_widget(Clear, popup); + // make popup + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(50), // Full + Constraint::Percentage(50), // Partial + ] + .as_ref(), + ) + .split(popup); + self.app.view(&Id::ProgressBarFull, f, popup_chunks[0]); + self.app.view(&Id::ProgressBarPartial, f, popup_chunks[1]); + } else if self.app.mounted(&Id::DeletePopup) { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::DeletePopup, f, popup); + } else if self.app.mounted(&Id::ReplacePopup) { + // NOTE: handle extended / normal modes + if self.is_radio_replace_extended() { 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(props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR_PARTIAL) { - if props.visible { - let popup = draw_area_in(f.size(), 50, 20); - f.render_widget(Clear, popup); - // make popup let popup_chunks = Layout::default() .direction(Direction::Vertical) .constraints( [ - Constraint::Percentage(50), // Full - Constraint::Percentage(50), // Partial + Constraint::Percentage(85), // List + Constraint::Percentage(15), // Radio ] .as_ref(), ) .split(popup); - self.view - .render(super::COMPONENT_PROGRESS_BAR_FULL, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_PROGRESS_BAR_PARTIAL, f, popup_chunks[1]); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) { - if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_REPLACE) { - if props.visible { - // NOTE: handle extended / normal modes - if self.is_radio_replace_extended() { - let popup = draw_area_in(f.size(), 50, 50); - f.render_widget(Clear, popup); - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(85), // List - Constraint::Percentage(15), // Radio - ] - .as_ref(), - ) - .split(popup); - self.view - .render(super::COMPONENT_LIST_REPLACING_FILES, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_RADIO_REPLACE, f, popup_chunks[1]); - } else { - let popup = draw_area_in(f.size(), 50, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_RADIO_REPLACE, f, popup); - } - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) { - if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) { - if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) { - if props.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(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { - if props.visible { + self.app + .view(&Id::ReplacingFilesListPopup, f, popup_chunks[0]); + self.app.view(&Id::ReplacePopup, f, popup_chunks[1]); + } else { 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(props) = self.view.get_props(super::COMPONENT_TEXT_FATAL) { - if props.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(props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) { - if props.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(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) { - if props.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); + self.app.view(&Id::ReplacePopup, f, popup); } + } else if self.app.mounted(&Id::DisconnectPopup) { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::DisconnectPopup, f, popup); + } else if self.app.mounted(&Id::QuitPopup) { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::QuitPopup, f, popup); + } else if self.app.mounted(&Id::SortingPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::SortingPopup, f, popup); + } else if self.app.mounted(&Id::ErrorPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::ErrorPopup, f, popup); + } else if self.app.mounted(&Id::FatalPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::FatalPopup, f, popup); + } else if self.app.mounted(&Id::WaitPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::WaitPopup, f, popup); + } else if self.app.mounted(&Id::KeybindingsPopup) { + let popup = draw_area_in(f.size(), 50, 80); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::KeybindingsPopup, f, popup); } }); // Re-give context @@ -402,50 +306,85 @@ impl FileTransferActivity { /// ### mount_info /// /// Mount info box - pub(super) fn mount_info(&mut self, text: &str) { + pub(super) fn mount_info>(&mut self, text: S) { // Mount let info_color = self.theme().misc_info_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, info_color); + assert!(self + .app + .remount( + Id::ErrorPopup, + Box::new(components::ErrorPopup::new(text, info_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ErrorPopup).is_ok()); } /// ### mount_error /// /// Mount error box - pub(super) fn mount_error(&mut self, text: &str) { + pub(super) fn mount_error>(&mut self, text: S) { // Mount let error_color = self.theme().misc_error_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, error_color); + assert!(self + .app + .remount( + Id::ErrorPopup, + Box::new(components::ErrorPopup::new(text, error_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ErrorPopup).is_ok()); } /// ### umount_error /// /// Umount error message pub(super) fn umount_error(&mut self) { - self.view.umount(super::COMPONENT_TEXT_ERROR); + let _ = self.app.umount(&Id::ErrorPopup); } - pub(super) fn mount_fatal(&mut self, text: &str) { + pub(super) fn mount_fatal>(&mut self, text: S) { // Mount let error_color = self.theme().misc_error_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_FATAL, text, error_color); - } - - pub(super) fn mount_wait(&mut self, text: &str) { - self.mount_wait_ex(text); - } - - pub(super) fn mount_blocking_wait(&mut self, text: &str) { - self.mount_wait_ex(text); - self.view(); + assert!(self + .app + .remount( + Id::FatalPopup, + Box::new(components::FatalPopup::new(text, error_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::FatalPopup).is_ok()); + } + + /// ### umount_fatal + /// + /// Umount fatal error message + pub(super) fn umount_fatal(&mut self) { + let _ = self.app.umount(&Id::FatalPopup); } - fn mount_wait_ex(&mut self, text: &str) { + pub(super) fn mount_wait>(&mut self, text: S) { let color = self.theme().misc_info_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_WAIT, text, color); + assert!(self + .app + .remount( + Id::WaitPopup, + Box::new(components::WaitPopup::new(text, color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::WaitPopup).is_ok()); + } + + pub(super) fn mount_blocking_wait>(&mut self, text: S) { + self.mount_wait(text); + self.view(); } pub(super) fn umount_wait(&mut self) { - self.view.umount(super::COMPONENT_TEXT_WAIT); + let _ = self.app.umount(&Id::WaitPopup); } /// ### mount_quit @@ -454,20 +393,22 @@ impl FileTransferActivity { pub(super) fn mount_quit(&mut self) { // Protocol let quit_color = self.theme().misc_quit_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_QUIT, - "Are you sure you want to quit?", - &["Yes", "No"], - 0, - quit_color, - ); + assert!(self + .app + .remount( + Id::QuitPopup, + Box::new(components::QuitPopup::new(quit_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::QuitPopup).is_ok()); } /// ### umount_quit /// /// Umount quit popup pub(super) fn umount_quit(&mut self) { - self.view.umount(super::COMPONENT_RADIO_QUIT); + let _ = self.app.umount(&Id::QuitPopup); } /// ### mount_disconnect @@ -476,53 +417,61 @@ impl FileTransferActivity { pub(super) fn mount_disconnect(&mut self) { // Protocol let quit_color = self.theme().misc_quit_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_DISCONNECT, - "Are you sure you want to disconnect?", - &["Yes", "No"], - 0, - quit_color, - ); + assert!(self + .app + .remount( + Id::DisconnectPopup, + Box::new(components::DisconnectPopup::new(quit_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::DisconnectPopup).is_ok()); } /// ### umount_disconnect /// /// Umount disconnect popup pub(super) fn umount_disconnect(&mut self) { - self.view.umount(super::COMPONENT_RADIO_DISCONNECT); + let _ = self.app.umount(&Id::DisconnectPopup); } pub(super) fn mount_copy(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_COPY, - "Copy file(s) to…", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::CopyPopup, + Box::new(components::CopyPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::CopyPopup).is_ok()); } pub(super) fn umount_copy(&mut self) { - self.view.umount(super::COMPONENT_INPUT_COPY); + let _ = self.app.umount(&Id::CopyPopup); } pub(super) fn mount_exec(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_EXEC, - "Execute command", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::ExecPopup, + Box::new(components::ExecPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ExecPopup).is_ok()); } pub(super) fn umount_exec(&mut self) { - self.view.umount(super::COMPONENT_INPUT_EXEC); + let _ = self.app.umount(&Id::ExecPopup); } pub(super) fn mount_find(&mut self, search: &str) { // Get color - let (bg, fg, hg): (Color, Color, Color) = match self.browser.tab() { + let (bg, fg, hg) = match self.browser.tab() { FileExplorerTab::Local | FileExplorerTab::FindLocal => ( self.theme().transfer_local_explorer_background, self.theme().transfer_local_explorer_foreground, @@ -535,162 +484,182 @@ impl FileTransferActivity { ), }; // Mount component - self.view.mount( - super::COMPONENT_EXPLORER_FIND, - Box::new(FileList::new( - FileListPropsBuilder::default() - .with_title( - format!("Search results for \"{}\"", search), - Alignment::Left, - ) - .with_borders(Borders::ALL, BorderType::Plain, hg) - .with_highlight_color(hg) - .with_background(bg) - .with_foreground(fg) - .build(), - )), - ); - // Give focus to explorer findd - self.view.active(super::COMPONENT_EXPLORER_FIND); + assert!(self + .app + .remount( + Id::ExplorerFind, + Box::new(components::ExplorerFind::new( + format!(r#"Search results for "{}""#, search), + &[], + bg, + fg, + hg + )), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ExplorerFind).is_ok()); } pub(super) fn umount_find(&mut self) { - self.view.umount(super::COMPONENT_EXPLORER_FIND); + let _ = self.app.umount(&Id::ExplorerFind); } pub(super) fn mount_find_input(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_FIND, - "Search files by name", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::FindPopup, + Box::new(components::FindPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::FindPopup).is_ok()); } pub(super) fn umount_find_input(&mut self) { // Umount input find - self.view.umount(super::COMPONENT_INPUT_FIND); + let _ = self.app.umount(&Id::FindPopup); } pub(super) fn mount_goto(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_GOTO, - "Change working directory", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::GotoPopup, + Box::new(components::GoToPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::GotoPopup).is_ok()); } pub(super) fn umount_goto(&mut self) { - self.view.umount(super::COMPONENT_INPUT_GOTO); + let _ = self.app.umount(&Id::GotoPopup); } pub(super) fn mount_mkdir(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_MKDIR, - "Insert directory name", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::MkdirPopup, + Box::new(components::MkdirPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::MkdirPopup).is_ok()); } pub(super) fn umount_mkdir(&mut self) { - self.view.umount(super::COMPONENT_INPUT_MKDIR); + let _ = self.app.umount(&Id::MkdirPopup); } pub(super) fn mount_newfile(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_NEWFILE, - "New file name", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::NewfilePopup, + Box::new(components::NewfilePopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::NewfilePopup).is_ok()); } pub(super) fn umount_newfile(&mut self) { - self.view.umount(super::COMPONENT_INPUT_NEWFILE); + let _ = self.app.umount(&Id::NewfilePopup); } pub(super) fn mount_openwith(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_OPEN_WITH, - "Open file with…", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::OpenWithPopup, + Box::new(components::OpenWithPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::OpenWithPopup).is_ok()); } pub(super) fn umount_openwith(&mut self) { - self.view.umount(super::COMPONENT_INPUT_OPEN_WITH); + let _ = self.app.umount(&Id::OpenWithPopup); } pub(super) fn mount_rename(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_RENAME, - "Move file(s) to…", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::RenamePopup, + Box::new(components::RenamePopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::RenamePopup).is_ok()); } pub(super) fn umount_rename(&mut self) { - self.view.umount(super::COMPONENT_INPUT_RENAME); + let _ = self.app.umount(&Id::RenamePopup); } pub(super) fn mount_saveas(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog(super::COMPONENT_INPUT_SAVEAS, "Save as…", "", input_color); + assert!(self + .app + .remount( + Id::SaveAsPopup, + Box::new(components::SaveAsPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::SaveAsPopup).is_ok()); } pub(super) fn umount_saveas(&mut self) { - self.view.umount(super::COMPONENT_INPUT_SAVEAS); + let _ = self.app.umount(&Id::SaveAsPopup); } pub(super) fn mount_progress_bar(&mut self, root_name: String) { let prog_color_full = self.theme().transfer_progress_bar_full; let prog_color_partial = self.theme().transfer_progress_bar_partial; - self.view.mount( - super::COMPONENT_PROGRESS_BAR_FULL, - Box::new(ProgressBar::new( - ProgressBarPropsBuilder::default() - .with_progbar_color(prog_color_full) - .with_background(Color::Black) - .with_borders( - Borders::TOP | Borders::RIGHT | Borders::LEFT, - BorderType::Rounded, - Color::Reset, - ) - .with_title(root_name, Alignment::Center) - .build(), - )), - ); - self.view.mount( - super::COMPONENT_PROGRESS_BAR_PARTIAL, - Box::new(ProgressBar::new( - ProgressBarPropsBuilder::default() - .with_progbar_color(prog_color_partial) - .with_background(Color::Black) - .with_borders( - Borders::BOTTOM | Borders::RIGHT | Borders::LEFT, - BorderType::Rounded, - Color::Reset, - ) - .with_title("Please wait", Alignment::Center) - .build(), - )), - ); - self.view.active(super::COMPONENT_PROGRESS_BAR_PARTIAL); + assert!(self + .app + .remount( + Id::ProgressBarFull, + Box::new(components::ProgressBarFull::new( + 0.0, + "", + &root_name, + prog_color_full + )), + vec![], + ) + .is_ok()); + assert!(self + .app + .remount( + Id::ProgressBarPartial, + Box::new(components::ProgressBarPartial::new( + 0.0, + "", + "Please wait", + prog_color_partial + )), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ProgressBarPartial).is_ok()); } pub(super) fn umount_progress_bar(&mut self) { - self.view.umount(super::COMPONENT_PROGRESS_BAR_PARTIAL); - self.view.umount(super::COMPONENT_PROGRESS_BAR_FULL); + let _ = self.app.umount(&Id::ProgressBarPartial); + let _ = self.app.umount(&Id::ProgressBarFull); } pub(super) fn mount_file_sorting(&mut self) { @@ -698,244 +667,136 @@ impl FileTransferActivity { let sorting: FileSorting = match self.browser.tab() { FileExplorerTab::Local => self.local().get_file_sorting(), FileExplorerTab::Remote => self.remote().get_file_sorting(), - _ => panic!("You can't mount file sorting when in found result"), + _ => return, }; - let index: usize = match sorting { - FileSorting::CreationTime => 2, - FileSorting::ModifyTime => 1, - FileSorting::Name => 0, - FileSorting::Size => 3, - }; - self.mount_radio_dialog( - super::COMPONENT_RADIO_SORTING, - "Sort files by", - &["Name", "Modify time", "Creation time", "Size"], - index, - sorting_color, - ); + assert!(self + .app + .remount( + Id::SortingPopup, + Box::new(components::SortingPopup::new(sorting, sorting_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::SortingPopup).is_ok()); } pub(super) fn umount_file_sorting(&mut self) { - self.view.umount(super::COMPONENT_RADIO_SORTING); + let _ = self.app.umount(&Id::SortingPopup); } pub(super) fn mount_radio_delete(&mut self) { let warn_color = self.theme().misc_warn_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_DELETE, - "Delete file", - &["Yes", "No"], - 1, - warn_color, - ); + assert!(self + .app + .remount( + Id::DeletePopup, + Box::new(components::DeletePopup::new(warn_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::DeletePopup).is_ok()); } pub(super) fn umount_radio_delete(&mut self) { - self.view.umount(super::COMPONENT_RADIO_DELETE); + let _ = self.app.umount(&Id::DeletePopup); } pub(super) fn mount_radio_replace(&mut self, file_name: &str) { let warn_color = self.theme().misc_warn_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_REPLACE, - format!("File '{}' already exists. Overwrite file?", file_name), - &["Yes", "No"], - 0, - warn_color, - ); + assert!(self + .app + .remount( + Id::ReplacePopup, + Box::new(components::ReplacePopup::new(Some(file_name), warn_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ReplacePopup).is_ok()); } pub(super) fn mount_radio_replace_many(&mut self, files: &[&str]) { let warn_color = self.theme().misc_warn_dialog; - // Make rows - let rows = files.iter().map(|x| vec![TextSpan::new(x)]).collect(); - self.view.mount( - super::COMPONENT_LIST_REPLACING_FILES, - Box::new(List::new( - ListPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, warn_color) - .scrollable(true) - .with_highlighted_color(warn_color) - .with_highlighted_str(Some("> ")) - .with_title( - "The following files are going to be replaced", - Alignment::Center, - ) - .with_foreground(warn_color) - .with_rows(rows) - .build(), - )), - ); - self.mount_radio_dialog( - super::COMPONENT_RADIO_REPLACE, - "Overwrite files?", - &["Yes", "No"], - 0, - warn_color, - ); + assert!(self + .app + .remount( + Id::ReplacingFilesListPopup, + Box::new(components::ReplacingFilesListPopup::new(files, warn_color)), + vec![], + ) + .is_ok()); + assert!(self + .app + .remount( + Id::ReplacePopup, + Box::new(components::ReplacePopup::new(None, warn_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ReplacePopup).is_ok()); } /// ### is_radio_replace_extended /// /// Returns whether radio replace is in "extended" mode (for many files) pub(super) fn is_radio_replace_extended(&self) -> bool { - self.view - .get_state(super::COMPONENT_LIST_REPLACING_FILES) - .is_some() + self.app.mounted(&Id::ReplacingFilesListPopup) } pub(super) fn umount_radio_replace(&mut self) { - self.view.umount(super::COMPONENT_RADIO_REPLACE); - self.view.umount(super::COMPONENT_LIST_REPLACING_FILES); // NOTE: replace anyway + let _ = self.app.umount(&Id::ReplacePopup); + let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway } pub(super) fn mount_file_info(&mut self, file: &FsEntry) { - let mut texts: TableBuilder = TableBuilder::default(); - // Abs path - let real_path: Option = { - let real_file: FsEntry = file.get_realfile(); - match real_file.get_abs_path() != file.get_abs_path() { - true => Some(real_file.get_abs_path()), - false => None, - } - }; - let path: String = match real_path { - Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()), - None => format!("{}", file.get_abs_path().display()), - }; - // Make texts - texts - .add_col(TextSpan::from("Path: ")) - .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); - if let Some(filetype) = file.get_ftype() { - texts - .add_row() - .add_col(TextSpan::from("File type: ")) - .add_col(TextSpan::new(filetype.as_str()).fg(Color::LightGreen)); - } - let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size()); - texts - .add_row() - .add_col(TextSpan::from("Size: ")) - .add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan)); - 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(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); - texts - .add_row() - .add_col(TextSpan::from("Last modified time: ")) - .add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); - texts - .add_row() - .add_col(TextSpan::from("Last access time: ")) - .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); - // User - #[cfg(target_family = "unix")] - 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(target_family = "unix")] - 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(TextSpan::new(username.as_str()).fg(Color::LightYellow)); - texts - .add_row() - .add_col(TextSpan::from("Group: ")) - .add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); - self.view.mount( - super::COMPONENT_LIST_FILEINFO, - Box::new(Table::new( - TablePropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, Color::White) - .with_title(file.get_name(), Alignment::Left) - .with_table(texts.build()) - .build(), - )), - ); - self.view.active(super::COMPONENT_LIST_FILEINFO); + assert!(self + .app + .remount( + Id::FileInfoPopup, + Box::new(components::FileInfoPopup::new(file)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::FileInfoPopup).is_ok()); } pub(super) fn umount_file_info(&mut self) { - self.view.umount(super::COMPONENT_LIST_FILEINFO); + let _ = self.app.umount(&Id::FileInfoPopup); } pub(super) fn refresh_local_status_bar(&mut self) { let sorting_color = self.theme().transfer_status_sorting; let hidden_color = self.theme().transfer_status_hidden; - let local_bar_spans: Vec = vec![ - TextSpan::new("File sorting: ").fg(sorting_color), - TextSpan::new(Self::get_file_sorting_str(self.local().get_file_sorting())) - .fg(sorting_color) - .reversed(), - TextSpan::new(" Hidden files: ").fg(hidden_color), - TextSpan::new(Self::get_hidden_files_str( - self.local().hidden_files_visible(), - )) - .fg(hidden_color) - .reversed(), - ]; - if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR_LOCAL) { - self.view.update( - super::COMPONENT_SPAN_STATUS_BAR_LOCAL, - SpanPropsBuilder::from(props) - .with_spans(local_bar_spans) - .build(), - ); - } + assert!(self + .app + .remount( + Id::StatusBarLocal, + Box::new(components::StatusBarLocal::new( + &self.browser, + sorting_color, + hidden_color + )), + vec![], + ) + .is_ok()); } pub(super) fn refresh_remote_status_bar(&mut self) { let sorting_color = self.theme().transfer_status_sorting; let hidden_color = self.theme().transfer_status_hidden; let sync_color = self.theme().transfer_status_sync_browsing; - let remote_bar_spans: Vec = vec![ - TextSpan::new("File sorting: ").fg(sorting_color), - TextSpan::new(Self::get_file_sorting_str(self.remote().get_file_sorting())) - .fg(sorting_color) - .reversed(), - TextSpan::new(" Hidden files: ").fg(hidden_color), - TextSpan::new(Self::get_hidden_files_str( - self.remote().hidden_files_visible(), - )) - .fg(hidden_color) - .reversed(), - TextSpan::new(" Sync Browsing: ").fg(sync_color), - TextSpan::new(match self.browser.sync_browsing { - true => "ON ", - false => "OFF", - }) - .fg(sync_color) - .reversed(), - ]; - if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR_REMOTE) { - self.view.update( - super::COMPONENT_SPAN_STATUS_BAR_REMOTE, - SpanPropsBuilder::from(props) - .with_spans(remote_bar_spans) - .build(), - ); - } + assert!(self + .app + .remount( + Id::StatusBarRemote, + Box::new(components::StatusBarRemote::new( + &self.browser, + sorting_color, + hidden_color, + sync_color + )), + vec![], + ) + .is_ok()); } /// ### mount_help @@ -943,199 +804,165 @@ impl FileTransferActivity { /// Mount help pub(super) fn mount_help(&mut self) { let key_color = self.theme().misc_keys; - self.view.mount( - super::COMPONENT_TEXT_HELP, - Box::new(List::new( - ListPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, Color::White) - .with_highlighted_str(Some("?")) - .with_max_scroll_step(8) - .bold() - .scrollable(true) - .with_title("Help", Alignment::Center) - .with_rows( - TableBuilder::default() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Disconnect")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Switch between explorer and logs", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to previous directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change explorer tab")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Move up/down in list")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Enter directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Upload/Download file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Toggle hidden files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change file sorting mode")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Copy")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Make directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to path")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Show help")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Show info about selected file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Reload directory content")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Select file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Create new file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open text file with preferred editor", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Quit termscp")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Rename file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Save file as")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to parent directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open file with default application for file type", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open file with specified application", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Execute shell command")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Toggle synchronized browsing")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Delete selected file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Select all files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Interrupt file transfer")) - .build(), - ) - .build(), - )), - ); - // Active help - self.view.active(super::COMPONENT_TEXT_HELP); + assert!(self + .app + .remount( + Id::KeybindingsPopup, + Box::new(components::KeybindingsPopup::new(key_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::KeybindingsPopup).is_ok()); } 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::Name => "By name", - FileSorting::CreationTime => "By creation time", - FileSorting::ModifyTime => "By modify time", - FileSorting::Size => "By size", - } - } - - fn get_hidden_files_str(show: bool) -> &'static str { - match show { - true => "Show", - false => "Hide", - } - } - - // -- Mount helpers - - fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) { - // Mount - self.view.mount( - id, - Box::new(Paragraph::new( - ParagraphPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Thick, color) - .with_foreground(color) - .bold() - .with_text_alignment(Alignment::Center) - .with_texts(vec![TextSpan::from(text)]) - .build(), - )), - ); - // Give focus to error - self.view.active(id); - } - - fn mount_input_dialog(&mut self, id: &str, text: &str, val: &str, color: Color) { - self.view.mount( - id, - Box::new(Input::new( - InputPropsBuilder::default() - .with_foreground(color) - .with_label(text, Alignment::Center) - .with_borders(Borders::ALL, BorderType::Rounded, color) - .with_value(val.to_string()) - .build(), - )), - ); - self.view.active(id); - } - - fn mount_radio_dialog>( - &mut self, - id: &str, - text: S, - opts: &[&str], - default: usize, - color: Color, - ) { - self.view.mount( - id, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(color) - .with_inverted_color(Color::Black) - .with_borders(Borders::ALL, BorderType::Rounded, color) - .with_title(text.as_ref(), Alignment::Center) - .with_options(opts) - .with_value(default) - .rewind(true) - .build(), + let _ = self.app.umount(&Id::KeybindingsPopup); + } + + fn mount_global_listener(&mut self) { + assert!(self + .app + .mount( + Id::GlobalListener, + Box::new(components::GlobalListener::default()), + vec![ + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Esc, + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('h'), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('q'), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + ] + ) + .is_ok()); + } + + /// ### no_popup_mounted_clause + /// + /// Returns a sub clause which requires that no popup is mounted in order to be satisfied + fn no_popup_mounted_clause() -> SubClause { + SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::CopyPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::DeletePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::DisconnectPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ErrorPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ExecPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::FatalPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::FileInfoPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::GotoPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::KeybindingsPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::MkdirPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::NewfilePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::OpenWithPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ProgressBarFull, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ProgressBarPartial, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ExplorerFind, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::QuitPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::RenamePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ReplacePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::SaveAsPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::SortingPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::FindPopup, + )))), + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::WaitPopup, + )))), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), )), - ); - // Active - self.view.active(id); + ) } } diff --git a/src/ui/activities/setup/actions.rs b/src/ui/activities/setup/actions.rs index 6be4711b..89e3696b 100644 --- a/src/ui/activities/setup/actions.rs +++ b/src/ui/activities/setup/actions.rs @@ -27,13 +27,12 @@ * SOFTWARE. */ // Locals -use super::{SetupActivity, ViewLayout}; +use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout}; // Ext use crate::config::themes::Theme; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::env; use tuirealm::tui::style::Color; -use tuirealm::{Payload, Value}; +use tuirealm::{State, StateValue}; impl SetupActivity { /// ### action_on_esc @@ -78,7 +77,7 @@ impl SetupActivity { // Collect input values if in theme form if self.layout == ViewLayout::Theme { self.collect_styles() - .map_err(|e| format!("'{}' has an invalid color", e))?; + .map_err(|e| format!("'{:?}' has an invalid color", e))?; } // save theme self.save_theme() @@ -93,7 +92,7 @@ impl SetupActivity { ViewLayout::SetupForm => self.collect_input_values(), ViewLayout::Theme => self .collect_styles() - .map_err(|e| format!("'{}' has an invalid color", e))?, + .map_err(|e| format!("'{:?}' has an invalid color", e))?, _ => {} } // Update view @@ -133,8 +132,8 @@ impl SetupActivity { pub(super) fn action_delete_ssh_key(&mut self) { // Get key // get index - let idx: Option = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) { - Some(Payload::One(Value::Usize(idx))) => Some(idx), + let idx: Option = match self.app.state(&Id::Ssh(IdSsh::SshKeys)) { + Ok(State::One(StateValue::Usize(idx))) => Some(idx), _ => None, }; if let Some(idx) = idx { @@ -166,29 +165,27 @@ impl SetupActivity { /// Create a new ssh key pub(super) fn action_new_ssh_key(&mut self) { // get parameters - let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) { - Some(Payload::One(Value::Str(host))) => host, + let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) { + Ok(State::One(StateValue::String(host))) => host, _ => String::new(), }; - let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) { - Some(Payload::One(Value::Str(user))) => user, + let username: String = match self.app.state(&Id::Ssh(IdSsh::SshUsername)) { + Ok(State::One(StateValue::String(user))) => user, _ => String::new(), }; // Prepare text editor env::set_var("EDITOR", self.config().get_text_editor()); let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host); // Put input mode back to normal - if let Err(err) = disable_raw_mode() { - error!("Failed to disable raw mode: {}", err); + if let Err(err) = self.context_mut().terminal().disable_raw_mode() { + error!("Could not disable raw mode: {}", err); } // Leave alternate mode - if let Some(ctx) = self.context.as_mut() { - ctx.leave_alternate_screen(); - } - // Re-enable raw mode - if let Err(err) = enable_raw_mode() { - error!("Failed to enter raw mode: {}", err); + if let Err(err) = self.context_mut().terminal().leave_alternate_screen() { + error!("Could not leave alternate screen: {}", err); } + // Lock ports + assert!(self.app.lock_ports().is_ok()); // Write key to file match edit::edit(placeholder.as_bytes()) { Ok(rsa_key) => { @@ -215,101 +212,246 @@ impl SetupActivity { } // Restore terminal if let Some(ctx) = self.context.as_mut() { - // Clear screen - ctx.clear_screen(); + if let Err(err) = ctx.terminal().clear_screen() { + error!("Could not clear screen screen: {}", err); + } // Enter alternate mode - ctx.enter_alternate_screen(); + if let Err(err) = ctx.terminal().enter_alternate_screen() { + error!("Could not enter alternate screen: {}", err); + } + // Re-enable raw mode + if let Err(err) = ctx.terminal().enable_raw_mode() { + error!("Failed to enter raw mode: {}", err); + } + // Unlock ports + assert!(self.app.unlock_ports().is_ok()); } } /// ### set_color /// /// Given a component and a color, save the color into the theme - pub(super) fn action_save_color(&mut self, component: &str, color: Color) { + pub(super) fn action_save_color(&mut self, component: IdTheme, color: Color) { let theme: &mut Theme = self.theme_mut(); match component { - super::COMPONENT_COLOR_AUTH_ADDR => { + IdTheme::AuthAddress => { theme.auth_address = color; } - super::COMPONENT_COLOR_AUTH_BOOKMARKS => { + IdTheme::AuthBookmarks => { theme.auth_bookmarks = color; } - super::COMPONENT_COLOR_AUTH_PASSWORD => { + IdTheme::AuthPassword => { theme.auth_password = color; } - super::COMPONENT_COLOR_AUTH_PORT => { + IdTheme::AuthPort => { theme.auth_port = color; } - super::COMPONENT_COLOR_AUTH_PROTOCOL => { + IdTheme::AuthProtocol => { theme.auth_protocol = color; } - super::COMPONENT_COLOR_AUTH_RECENTS => { + IdTheme::AuthRecentHosts => { theme.auth_recents = color; } - super::COMPONENT_COLOR_AUTH_USERNAME => { + IdTheme::AuthUsername => { theme.auth_username = color; } - super::COMPONENT_COLOR_MISC_ERROR => { + IdTheme::MiscError => { theme.misc_error_dialog = color; } - super::COMPONENT_COLOR_MISC_INFO => { + IdTheme::MiscInfo => { theme.misc_info_dialog = color; } - super::COMPONENT_COLOR_MISC_INPUT => { + IdTheme::MiscInput => { theme.misc_input_dialog = color; } - super::COMPONENT_COLOR_MISC_KEYS => { + IdTheme::MiscKeys => { theme.misc_keys = color; } - super::COMPONENT_COLOR_MISC_QUIT => { + IdTheme::MiscQuit => { theme.misc_quit_dialog = color; } - super::COMPONENT_COLOR_MISC_SAVE => { + IdTheme::MiscSave => { theme.misc_save_dialog = color; } - super::COMPONENT_COLOR_MISC_WARN => { + IdTheme::MiscWarn => { theme.misc_warn_dialog = color; } - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG => { + IdTheme::ExplorerLocalBg => { theme.transfer_local_explorer_background = color; } - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG => { + IdTheme::ExplorerLocalFg => { theme.transfer_local_explorer_foreground = color; } - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG => { + IdTheme::ExplorerLocalHg => { theme.transfer_local_explorer_highlighted = color; } - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG => { + IdTheme::ExplorerRemoteBg => { theme.transfer_remote_explorer_background = color; } - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG => { + IdTheme::ExplorerRemoteFg => { theme.transfer_remote_explorer_foreground = color; } - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG => { + IdTheme::ExplorerRemoteHg => { theme.transfer_remote_explorer_highlighted = color; } - super::COMPONENT_COLOR_TRANSFER_LOG_BG => { + IdTheme::LogBg => { theme.transfer_log_background = color; } - super::COMPONENT_COLOR_TRANSFER_LOG_WIN => { + IdTheme::LogWindow => { theme.transfer_log_window = color; } - super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL => { + IdTheme::ProgBarFull => { theme.transfer_progress_bar_full = color; } - super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL => { + IdTheme::ProgBarPartial => { theme.transfer_progress_bar_partial = color; } - super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN => { + IdTheme::StatusHidden => { theme.transfer_status_hidden = color; } - super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING => { + IdTheme::StatusSorting => { theme.transfer_status_sorting = color; } - super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC => { + IdTheme::StatusSync => { theme.transfer_status_sync_browsing = color; } _ => {} } } + + /// ### collect_styles + /// + /// Collect values from input and put them into the theme. + /// If a component has an invalid color, returns Err(component_id) + fn collect_styles(&mut self) -> Result<(), Id> { + // auth + let auth_address = self + .get_color(&Id::Theme(IdTheme::AuthAddress)) + .map_err(|_| Id::Theme(IdTheme::AuthAddress))?; + let auth_bookmarks = self + .get_color(&Id::Theme(IdTheme::AuthBookmarks)) + .map_err(|_| Id::Theme(IdTheme::AuthBookmarks))?; + let auth_password = self + .get_color(&Id::Theme(IdTheme::AuthPassword)) + .map_err(|_| Id::Theme(IdTheme::AuthPassword))?; + let auth_port = self + .get_color(&Id::Theme(IdTheme::AuthPort)) + .map_err(|_| Id::Theme(IdTheme::AuthPort))?; + let auth_protocol = self + .get_color(&Id::Theme(IdTheme::AuthProtocol)) + .map_err(|_| Id::Theme(IdTheme::AuthProtocol))?; + let auth_recents = self + .get_color(&Id::Theme(IdTheme::AuthRecentHosts)) + .map_err(|_| Id::Theme(IdTheme::AuthRecentHosts))?; + let auth_username = self + .get_color(&Id::Theme(IdTheme::AuthUsername)) + .map_err(|_| Id::Theme(IdTheme::AuthUsername))?; + // misc + let misc_error_dialog = self + .get_color(&Id::Theme(IdTheme::MiscError)) + .map_err(|_| Id::Theme(IdTheme::MiscError))?; + let misc_info_dialog = self + .get_color(&Id::Theme(IdTheme::MiscInfo)) + .map_err(|_| Id::Theme(IdTheme::MiscInfo))?; + let misc_input_dialog = self + .get_color(&Id::Theme(IdTheme::MiscInput)) + .map_err(|_| Id::Theme(IdTheme::MiscInput))?; + let misc_keys = self + .get_color(&Id::Theme(IdTheme::MiscKeys)) + .map_err(|_| Id::Theme(IdTheme::MiscKeys))?; + let misc_quit_dialog = self + .get_color(&Id::Theme(IdTheme::MiscQuit)) + .map_err(|_| Id::Theme(IdTheme::MiscQuit))?; + let misc_save_dialog = self + .get_color(&Id::Theme(IdTheme::MiscSave)) + .map_err(|_| Id::Theme(IdTheme::MiscSave))?; + let misc_warn_dialog = self + .get_color(&Id::Theme(IdTheme::MiscWarn)) + .map_err(|_| Id::Theme(IdTheme::MiscWarn))?; + // transfer + let transfer_local_explorer_background = self + .get_color(&Id::Theme(IdTheme::ExplorerLocalBg)) + .map_err(|_| Id::Theme(IdTheme::ExplorerLocalBg))?; + let transfer_local_explorer_foreground = self + .get_color(&Id::Theme(IdTheme::ExplorerLocalFg)) + .map_err(|_| Id::Theme(IdTheme::ExplorerLocalFg))?; + let transfer_local_explorer_highlighted = self + .get_color(&Id::Theme(IdTheme::ExplorerLocalHg)) + .map_err(|_| Id::Theme(IdTheme::ExplorerLocalHg))?; + let transfer_remote_explorer_background = self + .get_color(&Id::Theme(IdTheme::ExplorerRemoteBg)) + .map_err(|_| Id::Theme(IdTheme::ExplorerRemoteBg))?; + let transfer_remote_explorer_foreground = self + .get_color(&Id::Theme(IdTheme::ExplorerRemoteFg)) + .map_err(|_| Id::Theme(IdTheme::ExplorerRemoteFg))?; + let transfer_remote_explorer_highlighted = self + .get_color(&Id::Theme(IdTheme::ExplorerRemoteHg)) + .map_err(|_| Id::Theme(IdTheme::ExplorerRemoteHg))?; + let transfer_log_background = self + .get_color(&Id::Theme(IdTheme::LogBg)) + .map_err(|_| Id::Theme(IdTheme::LogBg))?; + let transfer_log_window = self + .get_color(&Id::Theme(IdTheme::LogWindow)) + .map_err(|_| Id::Theme(IdTheme::LogWindow))?; + let transfer_progress_bar_full = self + .get_color(&Id::Theme(IdTheme::ProgBarFull)) + .map_err(|_| Id::Theme(IdTheme::ProgBarFull))?; + let transfer_progress_bar_partial = self + .get_color(&Id::Theme(IdTheme::ProgBarPartial)) + .map_err(|_| Id::Theme(IdTheme::ProgBarPartial))?; + let transfer_status_hidden = self + .get_color(&Id::Theme(IdTheme::StatusHidden)) + .map_err(|_| Id::Theme(IdTheme::StatusHidden))?; + let transfer_status_sorting = self + .get_color(&Id::Theme(IdTheme::StatusSorting)) + .map_err(|_| Id::Theme(IdTheme::StatusSorting))?; + let transfer_status_sync_browsing = self + .get_color(&Id::Theme(IdTheme::StatusSync)) + .map_err(|_| Id::Theme(IdTheme::StatusSync))?; + // Update theme + let mut theme: &mut Theme = self.theme_mut(); + theme.auth_address = auth_address; + theme.auth_bookmarks = auth_bookmarks; + theme.auth_password = auth_password; + theme.auth_port = auth_port; + theme.auth_protocol = auth_protocol; + theme.auth_recents = auth_recents; + theme.auth_username = auth_username; + theme.misc_error_dialog = misc_error_dialog; + theme.misc_info_dialog = misc_info_dialog; + theme.misc_input_dialog = misc_input_dialog; + theme.misc_keys = misc_keys; + theme.misc_quit_dialog = misc_quit_dialog; + theme.misc_save_dialog = misc_save_dialog; + theme.misc_warn_dialog = misc_warn_dialog; + theme.transfer_local_explorer_background = transfer_local_explorer_background; + theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground; + theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted; + theme.transfer_remote_explorer_background = transfer_remote_explorer_background; + theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground; + theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted; + theme.transfer_log_background = transfer_log_background; + theme.transfer_log_window = transfer_log_window; + theme.transfer_progress_bar_full = transfer_progress_bar_full; + theme.transfer_progress_bar_partial = transfer_progress_bar_partial; + theme.transfer_status_hidden = transfer_status_hidden; + theme.transfer_status_sorting = transfer_status_sorting; + theme.transfer_status_sync_browsing = transfer_status_sync_browsing; + Ok(()) + } + + /// ### get_color + /// + /// Get color from component + fn get_color(&self, component: &Id) -> Result { + match self.app.state(component) { + Ok(State::One(StateValue::String(color))) => { + match crate::utils::parser::parse_color(color.as_str()) { + Some(c) => Ok(c), + None => Err(()), + } + } + _ => Err(()), + } + } } diff --git a/src/ui/activities/setup/components/commons.rs b/src/ui/activities/setup/components/commons.rs new file mode 100644 index 00000000..3344f92a --- /dev/null +++ b/src/ui/activities/setup/components/commons.rs @@ -0,0 +1,334 @@ +//! ## Config +//! +//! config tab components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{CommonMsg, Msg, ViewLayout}; + +use tui_realm_stdlib::{List, Paragraph, Radio, Span}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, TableBuilder, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +#[derive(MockComponent)] +pub struct ErrorPopup { + component: Paragraph, +} + +impl ErrorPopup { + pub fn new>(text: S) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(Color::Red) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::Red) + .text(&[TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for ErrorPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Common(CommonMsg::CloseErrorPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct Footer { + component: Span, +} + +impl Default for Footer { + fn default() -> Self { + Self { + component: Span::default().spans(&[ + TextSpan::new("Press ").bold(), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" to show keybindings; ").bold(), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" to save parameters; ").bold(), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" to change panel").bold(), + ]), + } + } +} + +impl Component for Footer { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct Header { + component: Radio, +} + +impl Header { + pub fn new(layout: ViewLayout) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::Yellow) + .sides(BorderSides::BOTTOM), + ) + .choices(&["User interface", "SSH Keys", "Theme"]) + .foreground(Color::Yellow) + .value(match layout { + ViewLayout::SetupForm => 0, + ViewLayout::SshKeys => 1, + ViewLayout::Theme => 2, + }), + } + } +} + +impl Component for Header { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct Keybindings { + component: List, +} + +impl Default for Keybindings { + fn default() -> Self { + Self { + component: List::default() + .borders(Borders::default().modifiers(BorderType::Rounded)) + .title("Keybindings", Alignment::Center) + .scroll(true) + .highlighted_str("? ") + .rows( + TableBuilder::default() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Exit setup")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Change setup page")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Change cursor")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Change input field")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Select / Dismiss popup")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Delete SSH key")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" New SSH key")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Revert changes")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Save configuration")) + .build(), + ), + } + } +} + +impl Component for Keybindings { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Common(CommonMsg::CloseKeybindingsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct QuitPopup { + component: Radio, +} + +impl Default for QuitPopup { + fn default() -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::Red) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::Red) + .title( + "There are unsaved changes! Save changes before leaving?", + Alignment::Center, + ) + .rewind(true) + .choices(&["Save", "Don't save", "Cancel"]), + } + } +} + +impl Component for QuitPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Common(CommonMsg::CloseQuitPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.perform(Cmd::Submit) { + CmdResult::Submit(State::One(StateValue::Usize(0))) => { + Some(Msg::Common(CommonMsg::SaveAndQuit)) + } + CmdResult::Submit(State::One(StateValue::Usize(1))) => { + Some(Msg::Common(CommonMsg::Quit)) + } + _ => Some(Msg::Common(CommonMsg::CloseQuitPopup)), + }, + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct SavePopup { + component: Radio, +} + +impl Default for SavePopup { + fn default() -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::Yellow) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::Yellow) + .title("Save changes?", Alignment::Center) + .rewind(true) + .choices(&["Yes", "No"]), + } + } +} + +impl Component for SavePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Common(CommonMsg::CloseSavePopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Common(CommonMsg::SaveConfig)) + } else { + Some(Msg::Common(CommonMsg::CloseSavePopup)) + } + } + _ => None, + } + } +} diff --git a/src/ui/activities/setup/components/config.rs b/src/ui/activities/setup/components/config.rs new file mode 100644 index 00000000..09bb4b70 --- /dev/null +++ b/src/ui/activities/setup/components/config.rs @@ -0,0 +1,489 @@ +//! ## Config +//! +//! config tab components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{ConfigMsg, Msg}; +use crate::filetransfer::FileTransferProtocol; +use crate::fs::explorer::GroupDirs as GroupDirsEnum; +use crate::utils::parser::parse_bytesize; + +use tui_realm_stdlib::{Input, Radio}; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +// -- components + +#[derive(MockComponent)] +pub struct CheckUpdates { + component: Radio, +} + +impl CheckUpdates { + pub fn new(enabled: bool) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::LightYellow) + .modifiers(BorderType::Rounded), + ) + .choices(&["Yes", "No"]) + .foreground(Color::LightYellow) + .rewind(true) + .title("Check for updates?", Alignment::Left) + .value(if enabled { 0 } else { 1 }), + } + } +} + +impl Component for CheckUpdates { + fn on(&mut self, ev: Event) -> Option { + handle_radio_ev( + self, + ev, + Msg::Config(ConfigMsg::CheckUpdatesBlurDown), + Msg::Config(ConfigMsg::CheckUpdatesBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct DefaultProtocol { + component: Radio, +} + +impl DefaultProtocol { + pub fn new(protocol: FileTransferProtocol) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::Cyan) + .modifiers(BorderType::Rounded), + ) + .choices(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"]) + .foreground(Color::Cyan) + .rewind(true) + .title("Default protocol", Alignment::Left) + .value(match protocol { + FileTransferProtocol::AwsS3 => 4, + FileTransferProtocol::Ftp(true) => 3, + FileTransferProtocol::Ftp(false) => 2, + FileTransferProtocol::Scp => 1, + FileTransferProtocol::Sftp => 0, + }), + } + } +} + +impl Component for DefaultProtocol { + fn on(&mut self, ev: Event) -> Option { + handle_radio_ev( + self, + ev, + Msg::Config(ConfigMsg::DefaultProtocolBlurDown), + Msg::Config(ConfigMsg::DefaultProtocolBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct GroupDirs { + component: Radio, +} + +impl GroupDirs { + pub fn new(opt: Option) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::LightMagenta) + .modifiers(BorderType::Rounded), + ) + .choices(&["Display first", "Display last", "No"]) + .foreground(Color::LightMagenta) + .rewind(true) + .title("Group directories", Alignment::Left) + .value(match opt { + Some(GroupDirsEnum::First) => 0, + Some(GroupDirsEnum::Last) => 1, + None => 2, + }), + } + } +} + +impl Component for GroupDirs { + fn on(&mut self, ev: Event) -> Option { + handle_radio_ev( + self, + ev, + Msg::Config(ConfigMsg::GroupDirsBlurDown), + Msg::Config(ConfigMsg::GroupDirsBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct HiddenFiles { + component: Radio, +} + +impl HiddenFiles { + pub fn new(enabled: bool) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::LightRed) + .modifiers(BorderType::Rounded), + ) + .choices(&["Yes", "No"]) + .foreground(Color::LightRed) + .rewind(true) + .title("Show hidden files? (by default)", Alignment::Left) + .value(if enabled { 0 } else { 1 }), + } + } +} + +impl Component for HiddenFiles { + fn on(&mut self, ev: Event) -> Option { + handle_radio_ev( + self, + ev, + Msg::Config(ConfigMsg::HiddenFilesBlurDown), + Msg::Config(ConfigMsg::HiddenFilesBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct NotificationsEnabled { + component: Radio, +} + +impl NotificationsEnabled { + pub fn new(enabled: bool) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::LightRed) + .modifiers(BorderType::Rounded), + ) + .choices(&["Yes", "No"]) + .foreground(Color::LightRed) + .rewind(true) + .title("Enable notifications?", Alignment::Left) + .value(if enabled { 0 } else { 1 }), + } + } +} + +impl Component for NotificationsEnabled { + fn on(&mut self, ev: Event) -> Option { + handle_radio_ev( + self, + ev, + Msg::Config(ConfigMsg::NotificationsEnabledBlurDown), + Msg::Config(ConfigMsg::NotificationsEnabledBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct PromptOnFileReplace { + component: Radio, +} + +impl PromptOnFileReplace { + pub fn new(enabled: bool) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::LightBlue) + .modifiers(BorderType::Rounded), + ) + .choices(&["Yes", "No"]) + .foreground(Color::LightBlue) + .rewind(true) + .title("Prompt when replacing existing files?", Alignment::Left) + .value(if enabled { 0 } else { 1 }), + } + } +} + +impl Component for PromptOnFileReplace { + fn on(&mut self, ev: Event) -> Option { + handle_radio_ev( + self, + ev, + Msg::Config(ConfigMsg::PromptOnFileReplaceBlurDown), + Msg::Config(ConfigMsg::PromptOnFileReplaceBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct LocalFileFmt { + component: Input, +} + +impl LocalFileFmt { + pub fn new(value: &str) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(Color::LightGreen) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::LightGreen) + .input_type(InputType::Text) + .placeholder( + "{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("File formatter syntax (local)", Alignment::Left) + .value(value), + } + } +} + +impl Component for LocalFileFmt { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Config(ConfigMsg::LocalFileFmtBlurDown), + Msg::Config(ConfigMsg::LocalFileFmtBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct NotificationsThreshold { + component: Input, +} + +impl NotificationsThreshold { + pub fn new(value: &str) -> Self { + // -- validators + fn validate(bytes: &str) -> bool { + parse_bytesize(bytes).is_some() + } + fn char_valid(_input: &str, incoming: char) -> bool { + incoming.is_digit(10) || ['B', 'K', 'M', 'G', 'T', 'P'].contains(&incoming) + } + Self { + component: Input::default() + .borders( + Borders::default() + .color(Color::LightYellow) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::LightYellow) + .invalid_style(Style::default().fg(Color::Red)) + .input_type(InputType::Custom(validate, char_valid)) + .placeholder("64 MB", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Notifications: minimum transfer size", Alignment::Left) + .value(value), + } + } +} + +impl Component for NotificationsThreshold { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Config(ConfigMsg::NotificationsThresholdBlurDown), + Msg::Config(ConfigMsg::NotificationsThresholdBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct RemoteFileFmt { + component: Input, +} + +impl RemoteFileFmt { + pub fn new(value: &str) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(Color::Cyan) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::Cyan) + .input_type(InputType::Text) + .placeholder( + "{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("File formatter syntax (remote)", Alignment::Left) + .value(value), + } + } +} + +impl Component for RemoteFileFmt { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Config(ConfigMsg::RemoteFileFmtBlurDown), + Msg::Config(ConfigMsg::RemoteFileFmtBlurUp), + ) + } +} + +#[derive(MockComponent)] +pub struct TextEditor { + component: Input, +} + +impl TextEditor { + pub fn new(value: &str) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(Color::LightGreen) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::LightGreen) + .input_type(InputType::Text) + .placeholder("vim", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Text editor", Alignment::Left) + .value(value), + } + } +} + +impl Component for TextEditor { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Config(ConfigMsg::TextEditorBlurDown), + Msg::Config(ConfigMsg::TextEditorBlurUp), + ) + } +} + +// -- event handler + +fn handle_input_ev( + component: &mut dyn Component, + ev: Event, + on_key_down: Msg, + on_key_up: Msg, +) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + component.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + component.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + component.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + component.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + component.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + component.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + component.perform(Cmd::Type(ch)); + Some(Msg::Config(ConfigMsg::ConfigChanged)) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(on_key_down), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up), + _ => None, + } +} + +fn handle_radio_ev( + component: &mut dyn Component, + ev: Event, + on_key_down: Msg, + on_key_up: Msg, +) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + component.perform(Cmd::Move(Direction::Left)); + Some(Msg::Config(ConfigMsg::ConfigChanged)) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + component.perform(Cmd::Move(Direction::Right)); + Some(Msg::Config(ConfigMsg::ConfigChanged)) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(on_key_down), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up), + _ => None, + } +} diff --git a/src/ui/activities/setup/components/mod.rs b/src/ui/activities/setup/components/mod.rs new file mode 100644 index 00000000..b1e3794d --- /dev/null +++ b/src/ui/activities/setup/components/mod.rs @@ -0,0 +1,86 @@ +//! ## Components +//! +//! setup activity components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{CommonMsg, ConfigMsg, Msg, SshMsg, ThemeMsg, ViewLayout}; + +mod commons; +mod config; +mod ssh; +mod theme; + +pub(super) use commons::{ErrorPopup, Footer, Header, Keybindings, QuitPopup, SavePopup}; +pub(super) use config::{ + CheckUpdates, DefaultProtocol, GroupDirs, HiddenFiles, LocalFileFmt, NotificationsEnabled, + NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, TextEditor, +}; +pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername}; +pub(super) use theme::*; + +use tui_realm_stdlib::Phantom; +use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; +use tuirealm::{Component, MockComponent}; + +// -- global listener + +#[derive(MockComponent)] +pub struct GlobalListener { + component: Phantom, +} + +impl Default for GlobalListener { + fn default() -> Self { + Self { + component: Phantom::default(), + } + } +} + +impl Component for GlobalListener { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Common(CommonMsg::ShowQuitPopup)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Common(CommonMsg::ChangeLayout)) + } + Event::Keyboard(KeyEvent { + code: Key::Char('h'), + modifiers: KeyModifiers::CONTROL, + }) => Some(Msg::Common(CommonMsg::ShowKeybindings)), + Event::Keyboard(KeyEvent { + code: Key::Char('r'), + modifiers: KeyModifiers::CONTROL, + }) => Some(Msg::Common(CommonMsg::RevertChanges)), + Event::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::CONTROL, + }) => Some(Msg::Common(CommonMsg::ShowSavePopup)), + _ => None, + } + } +} diff --git a/src/ui/activities/setup/components/ssh.rs b/src/ui/activities/setup/components/ssh.rs new file mode 100644 index 00000000..cbd43d54 --- /dev/null +++ b/src/ui/activities/setup/components/ssh.rs @@ -0,0 +1,339 @@ +//! ## Ssh +//! +//! ssh components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{Msg, SshMsg}; + +use tui_realm_stdlib::{Input, List, Radio}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{ + Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TextSpan, +}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +/* DelSshKeyPopup, +SshHost, +SshKeys, +SshUsername, */ + +#[derive(MockComponent)] +pub struct DelSshKeyPopup { + component: Radio, +} + +impl Default for DelSshKeyPopup { + fn default() -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(Color::Red) + .modifiers(BorderType::Rounded), + ) + .choices(&["Yes", "No"]) + .foreground(Color::Red) + .rewind(true) + .title("Delete key?", Alignment::Center) + .value(1), + } + } +} + +impl Component for DelSshKeyPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Ssh(SshMsg::DeleteSshKey)) + } else { + Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct SshKeys { + component: List, +} + +impl SshKeys { + pub fn new(keys: &[String]) -> Self { + Self { + component: List::default() + .borders( + Borders::default() + .color(Color::LightGreen) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::LightGreen) + .highlighted_color(Color::LightGreen) + .rewind(true) + .rows(keys.iter().map(|x| vec![TextSpan::from(x)]).collect()) + .step(4) + .scroll(true) + .title("SSH Keys", Alignment::Left), + } + } +} + +impl Component for SshKeys { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::Usize(choice)) => Some(Msg::Ssh(SshMsg::EditSshKey(choice))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => Some(Msg::Ssh(SshMsg::ShowDelSshKeyPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::CONTROL, + }) => Some(Msg::Ssh(SshMsg::ShowNewSshKeyPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct SshHost { + component: Input, +} + +impl Default for SshHost { + fn default() -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .sides(BorderSides::TOP | BorderSides::RIGHT | BorderSides::LEFT), + ) + .input_type(InputType::Text) + .placeholder( + "192.168.1.2", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Hostname or address", Alignment::Center), + } + } +} + +impl Component for SshHost { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Ssh(SshMsg::SaveSshKey)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ssh(SshMsg::SshHostBlur)), + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ssh(SshMsg::CloseNewSshKeyPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct SshUsername { + component: Input, +} + +impl Default for SshUsername { + fn default() -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .sides(BorderSides::BOTTOM | BorderSides::RIGHT | BorderSides::LEFT), + ) + .input_type(InputType::Text) + .placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Username", Alignment::Center), + } + } +} + +impl Component for SshUsername { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Ssh(SshMsg::SaveSshKey)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ssh(SshMsg::SshUsernameBlur)) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ssh(SshMsg::CloseNewSshKeyPopup)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/setup/components/theme.rs b/src/ui/activities/setup/components/theme.rs new file mode 100644 index 00000000..3b937247 --- /dev/null +++ b/src/ui/activities/setup/components/theme.rs @@ -0,0 +1,910 @@ +//! ## Theme +//! +//! theme tab components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{Msg, ThemeMsg}; +use crate::ui::activities::setup::IdTheme; + +use tui_realm_stdlib::{Input, Label}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style, TextModifiers}; +use tuirealm::{ + AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent, State, StateValue, +}; + +// -- components + +#[derive(MockComponent)] +pub struct AuthTitle { + component: Label, +} + +impl Default for AuthTitle { + fn default() -> Self { + Self { + component: Label::default() + .modifiers(TextModifiers::BOLD) + .text("Authentication styles"), + } + } +} + +impl Component for AuthTitle { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct MiscTitle { + component: Label, +} + +impl Default for MiscTitle { + fn default() -> Self { + Self { + component: Label::default() + .modifiers(TextModifiers::BOLD) + .text("Misc styles"), + } + } +} + +impl Component for MiscTitle { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct TransferTitle { + component: Label, +} + +impl Default for TransferTitle { + fn default() -> Self { + Self { + component: Label::default() + .modifiers(TextModifiers::BOLD) + .text("Transfer styles"), + } + } +} + +impl Component for TransferTitle { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct TransferTitle2 { + component: Label, +} + +impl Default for TransferTitle2 { + fn default() -> Self { + Self { + component: Label::default() + .modifiers(TextModifiers::BOLD) + .text("Transfer styles (2)"), + } + } +} + +impl Component for TransferTitle2 { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct AuthAddress { + component: InputColor, +} + +impl AuthAddress { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Ip Address", + IdTheme::AuthAddress, + value, + Msg::Theme(ThemeMsg::AuthAddressBlurDown), + Msg::Theme(ThemeMsg::AuthAddressBlurUp), + ), + } + } +} + +impl Component for AuthAddress { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct AuthBookmarks { + component: InputColor, +} + +impl AuthBookmarks { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Bookmarks", + IdTheme::AuthBookmarks, + value, + Msg::Theme(ThemeMsg::AuthBookmarksBlurDown), + Msg::Theme(ThemeMsg::AuthBookmarksBlurUp), + ), + } + } +} + +impl Component for AuthBookmarks { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct AuthPassword { + component: InputColor, +} + +impl AuthPassword { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Password", + IdTheme::AuthPassword, + value, + Msg::Theme(ThemeMsg::AuthPasswordBlurDown), + Msg::Theme(ThemeMsg::AuthPasswordBlurUp), + ), + } + } +} + +impl Component for AuthPassword { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct AuthPort { + component: InputColor, +} + +impl AuthPort { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Port", + IdTheme::AuthPort, + value, + Msg::Theme(ThemeMsg::AuthPortBlurDown), + Msg::Theme(ThemeMsg::AuthPortBlurUp), + ), + } + } +} + +impl Component for AuthPort { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct AuthProtocol { + component: InputColor, +} + +impl AuthProtocol { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Protocol", + IdTheme::AuthProtocol, + value, + Msg::Theme(ThemeMsg::AuthProtocolBlurDown), + Msg::Theme(ThemeMsg::AuthProtocolBlurUp), + ), + } + } +} + +impl Component for AuthProtocol { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct AuthRecentHosts { + component: InputColor, +} + +impl AuthRecentHosts { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Recent connections", + IdTheme::AuthRecentHosts, + value, + Msg::Theme(ThemeMsg::AuthRecentHostsBlurDown), + Msg::Theme(ThemeMsg::AuthRecentHostsBlurUp), + ), + } + } +} + +impl Component for AuthRecentHosts { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} +#[derive(MockComponent)] +pub struct AuthUsername { + component: InputColor, +} + +impl AuthUsername { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Username", + IdTheme::AuthUsername, + value, + Msg::Theme(ThemeMsg::AuthUsernameBlurDown), + Msg::Theme(ThemeMsg::AuthUsernameBlurUp), + ), + } + } +} + +impl Component for AuthUsername { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct ExplorerLocalBg { + component: InputColor, +} + +impl ExplorerLocalBg { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Local explorer background", + IdTheme::ExplorerLocalBg, + value, + Msg::Theme(ThemeMsg::ExplorerLocalBgBlurDown), + Msg::Theme(ThemeMsg::ExplorerLocalBgBlurUp), + ), + } + } +} + +impl Component for ExplorerLocalBg { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct ExplorerLocalFg { + component: InputColor, +} + +impl ExplorerLocalFg { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Local explorer foreground", + IdTheme::ExplorerLocalFg, + value, + Msg::Theme(ThemeMsg::ExplorerLocalFgBlurDown), + Msg::Theme(ThemeMsg::ExplorerLocalFgBlurUp), + ), + } + } +} + +impl Component for ExplorerLocalFg { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct ExplorerLocalHg { + component: InputColor, +} + +impl ExplorerLocalHg { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Local explorer highlighted", + IdTheme::ExplorerLocalHg, + value, + Msg::Theme(ThemeMsg::ExplorerLocalHgBlurDown), + Msg::Theme(ThemeMsg::ExplorerLocalHgBlurUp), + ), + } + } +} + +impl Component for ExplorerLocalHg { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct ExplorerRemoteBg { + component: InputColor, +} + +impl ExplorerRemoteBg { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Remote explorer background", + IdTheme::ExplorerRemoteBg, + value, + Msg::Theme(ThemeMsg::ExplorerRemoteBgBlurDown), + Msg::Theme(ThemeMsg::ExplorerRemoteBgBlurUp), + ), + } + } +} + +impl Component for ExplorerRemoteBg { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct ExplorerRemoteFg { + component: InputColor, +} + +impl ExplorerRemoteFg { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Remote explorer foreground", + IdTheme::ExplorerRemoteFg, + value, + Msg::Theme(ThemeMsg::ExplorerRemoteFgBlurDown), + Msg::Theme(ThemeMsg::ExplorerRemoteFgBlurUp), + ), + } + } +} + +impl Component for ExplorerRemoteFg { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct ExplorerRemoteHg { + component: InputColor, +} + +impl ExplorerRemoteHg { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Remote explorer highlighted", + IdTheme::ExplorerRemoteHg, + value, + Msg::Theme(ThemeMsg::ExplorerRemoteHgBlurDown), + Msg::Theme(ThemeMsg::ExplorerRemoteHgBlurUp), + ), + } + } +} + +impl Component for ExplorerRemoteHg { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct LogBg { + component: InputColor, +} + +impl LogBg { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Log window background", + IdTheme::LogBg, + value, + Msg::Theme(ThemeMsg::LogBgBlurDown), + Msg::Theme(ThemeMsg::LogBgBlurUp), + ), + } + } +} + +impl Component for LogBg { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct LogWindow { + component: InputColor, +} + +impl LogWindow { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Log window", + IdTheme::LogWindow, + value, + Msg::Theme(ThemeMsg::LogWindowBlurDown), + Msg::Theme(ThemeMsg::LogWindowBlurUp), + ), + } + } +} + +impl Component for LogWindow { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct MiscError { + component: InputColor, +} + +impl MiscError { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Error", + IdTheme::MiscError, + value, + Msg::Theme(ThemeMsg::MiscErrorBlurDown), + Msg::Theme(ThemeMsg::MiscErrorBlurUp), + ), + } + } +} + +impl Component for MiscError { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct MiscInfo { + component: InputColor, +} + +impl MiscInfo { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Info", + IdTheme::MiscInfo, + value, + Msg::Theme(ThemeMsg::MiscInfoBlurDown), + Msg::Theme(ThemeMsg::MiscInfoBlurUp), + ), + } + } +} + +impl Component for MiscInfo { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct MiscInput { + component: InputColor, +} + +impl MiscInput { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Input", + IdTheme::MiscInput, + value, + Msg::Theme(ThemeMsg::MiscInputBlurDown), + Msg::Theme(ThemeMsg::MiscInputBlurUp), + ), + } + } +} + +impl Component for MiscInput { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct MiscKeys { + component: InputColor, +} + +impl MiscKeys { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Key strokes", + IdTheme::MiscKeys, + value, + Msg::Theme(ThemeMsg::MiscKeysBlurDown), + Msg::Theme(ThemeMsg::MiscKeysBlurUp), + ), + } + } +} + +impl Component for MiscKeys { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct MiscQuit { + component: InputColor, +} + +impl MiscQuit { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Quit dialogs", + IdTheme::MiscQuit, + value, + Msg::Theme(ThemeMsg::MiscQuitBlurDown), + Msg::Theme(ThemeMsg::MiscQuitBlurUp), + ), + } + } +} + +impl Component for MiscQuit { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct MiscSave { + component: InputColor, +} + +impl MiscSave { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Save confirmations", + IdTheme::MiscSave, + value, + Msg::Theme(ThemeMsg::MiscSaveBlurDown), + Msg::Theme(ThemeMsg::MiscSaveBlurUp), + ), + } + } +} + +impl Component for MiscSave { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct MiscWarn { + component: InputColor, +} + +impl MiscWarn { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Warnings", + IdTheme::MiscWarn, + value, + Msg::Theme(ThemeMsg::MiscWarnBlurDown), + Msg::Theme(ThemeMsg::MiscWarnBlurUp), + ), + } + } +} + +impl Component for MiscWarn { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct ProgBarFull { + component: InputColor, +} + +impl ProgBarFull { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "'Full transfer' Progress bar", + IdTheme::ProgBarFull, + value, + Msg::Theme(ThemeMsg::ProgBarFullBlurDown), + Msg::Theme(ThemeMsg::ProgBarFullBlurUp), + ), + } + } +} + +impl Component for ProgBarFull { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct ProgBarPartial { + component: InputColor, +} + +impl ProgBarPartial { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "'Partial transfer' Progress bar", + IdTheme::ProgBarPartial, + value, + Msg::Theme(ThemeMsg::ProgBarPartialBlurDown), + Msg::Theme(ThemeMsg::ProgBarPartialBlurUp), + ), + } + } +} + +impl Component for ProgBarPartial { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct StatusHidden { + component: InputColor, +} + +impl StatusHidden { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Hidden files", + IdTheme::StatusHidden, + value, + Msg::Theme(ThemeMsg::StatusHiddenBlurDown), + Msg::Theme(ThemeMsg::StatusHiddenBlurUp), + ), + } + } +} + +impl Component for StatusHidden { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct StatusSorting { + component: InputColor, +} + +impl StatusSorting { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "File sorting", + IdTheme::StatusSorting, + value, + Msg::Theme(ThemeMsg::StatusSortingBlurDown), + Msg::Theme(ThemeMsg::StatusSortingBlurUp), + ), + } + } +} + +impl Component for StatusSorting { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +#[derive(MockComponent)] +pub struct StatusSync { + component: InputColor, +} + +impl StatusSync { + pub fn new(value: Color) -> Self { + Self { + component: InputColor::new( + "Synchronized browsing", + IdTheme::StatusSync, + value, + Msg::Theme(ThemeMsg::StatusSyncBlurDown), + Msg::Theme(ThemeMsg::StatusSyncBlurUp), + ), + } + } +} + +impl Component for StatusSync { + fn on(&mut self, ev: Event) -> Option { + self.component.on(ev) + } +} + +// -- input color + +#[derive(MockComponent)] +struct InputColor { + component: Input, + id: IdTheme, + on_key_down: Msg, + on_key_up: Msg, +} + +impl InputColor { + pub fn new(name: &str, id: IdTheme, color: Color, on_key_down: Msg, on_key_up: Msg) -> Self { + let value = crate::utils::fmt::fmt_color(&color); + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Color) + .placeholder("#aa33ee", Style::default().fg(Color::Rgb(128, 128, 128))) + .title(name, Alignment::Left) + .value(value), + id, + on_key_down, + on_key_up, + } + } + + fn update_color(&mut self, result: CmdResult) -> Option { + if let CmdResult::Changed(State::One(StateValue::String(color))) = result { + let color = tuirealm::utils::parser::parse_color(&color).unwrap(); + self.attr(Attribute::Foreground, AttrValue::Color(color)); + self.attr( + Attribute::Borders, + AttrValue::Borders( + Borders::default() + .modifiers(BorderType::Rounded) + .color(color), + ), + ); + Some(Msg::Theme(ThemeMsg::ColorChanged(self.id.clone(), color))) + } else { + self.attr(Attribute::Foreground, AttrValue::Color(Color::Red)); + self.attr( + Attribute::Borders, + AttrValue::Borders( + Borders::default() + .modifiers(BorderType::Rounded) + .color(Color::Red), + ), + ); + Some(Msg::None) + } + } +} + +impl Component for InputColor { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + let result = self.perform(Cmd::Cancel); + self.update_color(result) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + let result = self.perform(Cmd::Delete); + self.update_color(result) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => { + let result = self.perform(Cmd::Type(ch)); + self.update_color(result) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(self.on_key_down.clone()), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(self.on_key_up.clone()), + _ => None, + } + } +} diff --git a/src/ui/activities/setup/config.rs b/src/ui/activities/setup/config.rs index 8e811c50..55756799 100644 --- a/src/ui/activities/setup/config.rs +++ b/src/ui/activities/setup/config.rs @@ -29,7 +29,6 @@ // Locals use super::SetupActivity; // Ext -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::env; impl SetupActivity { @@ -94,11 +93,15 @@ impl SetupActivity { // Set editor if config client exists env::set_var("EDITOR", ctx.config().get_text_editor()); // Prepare terminal - if let Err(err) = disable_raw_mode() { + if let Err(err) = ctx.terminal().disable_raw_mode() { error!("Failed to disable raw mode: {}", err); } // Leave alternate mode - ctx.leave_alternate_screen(); + if let Err(err) = ctx.terminal().leave_alternate_screen() { + error!("Could not leave alternate screen: {}", err); + } + // Lock ports + assert!(self.app.lock_ports().is_ok()); // Get result let result: Result<(), String> = match ctx.config().iter_ssh_keys().nth(idx) { Some(key) => { @@ -120,13 +123,19 @@ impl SetupActivity { }; // Restore terminal // Clear screen - ctx.clear_screen(); + if let Err(err) = ctx.terminal().clear_screen() { + error!("Could not clear screen screen: {}", err); + } // Enter alternate mode - ctx.enter_alternate_screen(); + if let Err(err) = ctx.terminal().enter_alternate_screen() { + error!("Could not enter alternate screen: {}", err); + } // Re-enable raw mode - if let Err(err) = enable_raw_mode() { + if let Err(err) = ctx.terminal().enable_raw_mode() { error!("Failed to enter raw mode: {}", err); } + // Unlock ports + assert!(self.app.unlock_ports().is_ok()); // Return result result } diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index fc0730d5..ba75b96a 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -28,6 +28,7 @@ */ // Submodules mod actions; +mod components; mod config; mod update; mod view; @@ -38,71 +39,209 @@ use crate::config::themes::Theme; use crate::system::config_client::ConfigClient; use crate::system::theme_provider::ThemeProvider; // Ext -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; -use tuirealm::{Update, View}; +use std::time::Duration; +use tuirealm::listener::EventListenerCfg; +use tuirealm::props::Color; +use tuirealm::{application::PollStrategy, Application, NoUserEvent, Update}; // -- components -// -- common -const COMPONENT_TEXT_HELP: &str = "TEXT_HELP"; -const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER"; -const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR"; -const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT"; -const COMPONENT_RADIO_SAVE: &str = "RADIO_SAVE"; -const COMPONENT_RADIO_TAB: &str = "RADIO_TAB"; -// -- config -const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR"; -const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL"; -const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES"; -const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES"; -const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLACE"; -const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS"; -const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT"; -const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT"; -const COMPONENT_RADIO_NOTIFICATIONS_ENABLED: &str = "RADIO_NOTIFICATIONS_ENABLED"; -const COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD: &str = "INPUT_NOTIFICATIONS_THRESHOLD"; -// -- ssh keys -const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS"; -const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST"; -const COMPONENT_INPUT_SSH_USERNAME: &str = "INPUT_SSH_USERNAME"; -const COMPONENT_RADIO_DEL_SSH_KEY: &str = "RADIO_DEL_SSH_KEY"; -// -- theme -const COMPONENT_COLOR_AUTH_TITLE: &str = "COMPONENT_COLOR_AUTH_TITLE"; -const COMPONENT_COLOR_MISC_TITLE: &str = "COMPONENT_COLOR_MISC_TITLE"; -const COMPONENT_COLOR_TRANSFER_TITLE: &str = "COMPONENT_COLOR_TRANSFER_TITLE"; -const COMPONENT_COLOR_TRANSFER_TITLE_2: &str = "COMPONENT_COLOR_TRANSFER_TITLE_2"; -const COMPONENT_COLOR_AUTH_ADDR: &str = "COMPONENT_COLOR_AUTH_ADDR"; -const COMPONENT_COLOR_AUTH_BOOKMARKS: &str = "COMPONENT_COLOR_AUTH_BOOKMARKS"; -const COMPONENT_COLOR_AUTH_PASSWORD: &str = "COMPONENT_COLOR_AUTH_PASSWORD"; -const COMPONENT_COLOR_AUTH_PORT: &str = "COMPONENT_COLOR_AUTH_PORT"; -const COMPONENT_COLOR_AUTH_PROTOCOL: &str = "COMPONENT_COLOR_AUTH_PROTOCOL"; -const COMPONENT_COLOR_AUTH_RECENTS: &str = "COMPONENT_COLOR_AUTH_RECENTS"; -const COMPONENT_COLOR_AUTH_USERNAME: &str = "COMPONENT_COLOR_AUTH_USERNAME"; -const COMPONENT_COLOR_MISC_ERROR: &str = "COMPONENT_COLOR_MISC_ERROR"; -const COMPONENT_COLOR_MISC_INFO: &str = "COMPONENT_COLOR_MISC_INFO"; -const COMPONENT_COLOR_MISC_INPUT: &str = "COMPONENT_COLOR_MISC_INPUT"; -const COMPONENT_COLOR_MISC_KEYS: &str = "COMPONENT_COLOR_MISC_KEYS"; -const COMPONENT_COLOR_MISC_QUIT: &str = "COMPONENT_COLOR_MISC_QUIT"; -const COMPONENT_COLOR_MISC_SAVE: &str = "COMPONENT_COLOR_MISC_SAVE"; -const COMPONENT_COLOR_MISC_WARN: &str = "COMPONENT_COLOR_MISC_WARN"; -const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG: &str = - "COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG"; -const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG: &str = - "COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG"; -const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG: &str = - "COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG"; -const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG: &str = - "COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG"; -const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG: &str = - "COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG"; -const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG: &str = - "COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG"; -const COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL"; -const COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL"; -const COMPONENT_COLOR_TRANSFER_LOG_BG: &str = "COMPONENT_COLOR_TRANSFER_LOG_BG"; -const COMPONENT_COLOR_TRANSFER_LOG_WIN: &str = "COMPONENT_COLOR_TRANSFER_LOG_WIN"; -const COMPONENT_COLOR_TRANSFER_STATUS_SORTING: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SORTING"; -const COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN: &str = "COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN"; -const COMPONENT_COLOR_TRANSFER_STATUS_SYNC: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SYNC"; +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +enum Id { + Common(IdCommon), + Config(IdConfig), + Ssh(IdSsh), + Theme(IdTheme), +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +enum IdCommon { + ErrorPopup, + Footer, + GlobalListener, + Header, + Keybindings, + QuitPopup, + SavePopup, +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +enum IdConfig { + CheckUpdates, + DefaultProtocol, + GroupDirs, + HiddenFiles, + LocalFileFmt, + NotificationsEnabled, + NotificationsThreshold, + PromptOnFileReplace, + RemoteFileFmt, + TextEditor, +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +enum IdSsh { + DelSshKeyPopup, + SshHost, + SshKeys, + SshUsername, +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum IdTheme { + AuthAddress, + AuthBookmarks, + AuthPassword, + AuthPort, + AuthProtocol, + AuthRecentHosts, + AuthTitle, + AuthUsername, + ExplorerLocalBg, + ExplorerLocalFg, + ExplorerLocalHg, + ExplorerRemoteBg, + ExplorerRemoteFg, + ExplorerRemoteHg, + LogBg, + LogWindow, + MiscError, + MiscInfo, + MiscInput, + MiscKeys, + MiscQuit, + MiscSave, + MiscTitle, + MiscWarn, + ProgBarFull, + ProgBarPartial, + StatusHidden, + StatusSorting, + StatusSync, + TransferTitle, + TransferTitle2, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Msg { + Common(CommonMsg), + Config(ConfigMsg), + Ssh(SshMsg), + Theme(ThemeMsg), + None, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum CommonMsg { + ChangeLayout, + CloseErrorPopup, + CloseKeybindingsPopup, + CloseQuitPopup, + CloseSavePopup, + Quit, + RevertChanges, + SaveAndQuit, + SaveConfig, + ShowKeybindings, + ShowQuitPopup, + ShowSavePopup, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ConfigMsg { + CheckUpdatesBlurDown, + CheckUpdatesBlurUp, + ConfigChanged, + DefaultProtocolBlurDown, + DefaultProtocolBlurUp, + GroupDirsBlurDown, + GroupDirsBlurUp, + HiddenFilesBlurDown, + HiddenFilesBlurUp, + LocalFileFmtBlurDown, + LocalFileFmtBlurUp, + NotificationsEnabledBlurDown, + NotificationsEnabledBlurUp, + NotificationsThresholdBlurDown, + NotificationsThresholdBlurUp, + PromptOnFileReplaceBlurDown, + PromptOnFileReplaceBlurUp, + RemoteFileFmtBlurDown, + RemoteFileFmtBlurUp, + TextEditorBlurDown, + TextEditorBlurUp, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum SshMsg { + CloseDelSshKeyPopup, + CloseNewSshKeyPopup, + DeleteSshKey, + EditSshKey(usize), + SaveSshKey, + ShowDelSshKeyPopup, + ShowNewSshKeyPopup, + SshHostBlur, + SshUsernameBlur, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ThemeMsg { + AuthAddressBlurDown, + AuthAddressBlurUp, + AuthBookmarksBlurDown, + AuthBookmarksBlurUp, + AuthPasswordBlurDown, + AuthPasswordBlurUp, + AuthPortBlurDown, + AuthPortBlurUp, + AuthProtocolBlurDown, + AuthProtocolBlurUp, + AuthRecentHostsBlurDown, + AuthRecentHostsBlurUp, + AuthUsernameBlurDown, + AuthUsernameBlurUp, + ColorChanged(IdTheme, Color), + ExplorerLocalBgBlurDown, + ExplorerLocalBgBlurUp, + ExplorerLocalFgBlurDown, + ExplorerLocalFgBlurUp, + ExplorerLocalHgBlurDown, + ExplorerLocalHgBlurUp, + ExplorerRemoteBgBlurDown, + ExplorerRemoteBgBlurUp, + ExplorerRemoteFgBlurDown, + ExplorerRemoteFgBlurUp, + ExplorerRemoteHgBlurDown, + ExplorerRemoteHgBlurUp, + LogBgBlurDown, + LogBgBlurUp, + LogWindowBlurDown, + LogWindowBlurUp, + MiscErrorBlurDown, + MiscErrorBlurUp, + MiscInfoBlurDown, + MiscInfoBlurUp, + MiscInputBlurDown, + MiscInputBlurUp, + MiscKeysBlurDown, + MiscKeysBlurUp, + MiscQuitBlurDown, + MiscQuitBlurUp, + MiscSaveBlurDown, + MiscSaveBlurUp, + MiscWarnBlurDown, + MiscWarnBlurUp, + ProgBarFullBlurDown, + ProgBarFullBlurUp, + ProgBarPartialBlurDown, + ProgBarPartialBlurUp, + StatusHiddenBlurDown, + StatusHiddenBlurUp, + StatusSortingBlurDown, + StatusSortingBlurUp, + StatusSyncBlurDown, + StatusSyncBlurUp, +} // -- store const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED"; @@ -110,8 +249,8 @@ const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED"; /// ### ViewLayout /// /// Current view layout -#[derive(std::cmp::PartialEq)] -enum ViewLayout { +#[derive(PartialEq)] +pub enum ViewLayout { SetupForm, SshKeys, Theme, @@ -121,26 +260,28 @@ enum ViewLayout { /// /// Setup activity states holder pub struct SetupActivity { + app: Application, exit_reason: Option, context: Option, // Context holder - view: View, // View layout: ViewLayout, // View layout redraw: bool, } -impl Default for SetupActivity { - fn default() -> Self { - SetupActivity { +impl SetupActivity { + pub fn new(ticks: Duration) -> Self { + Self { + app: Application::init( + EventListenerCfg::default() + .default_input_listener(ticks) + .poll_timeout(ticks), + ), exit_reason: None, context: None, - view: View::init(), layout: ViewLayout::SetupForm, redraw: true, // Draw at first `on_draw` } } -} -impl SetupActivity { /// ### context /// /// Returns a reference to context @@ -205,11 +346,13 @@ impl Activity for SetupActivity { // Set context self.context = Some(context); // Clear terminal - self.context.as_mut().unwrap().clear_screen(); + if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() { + error!("Failed to clear screen: {}", err); + } // Set config changed to false self.set_config_changed(false); // Put raw mode on enabled - if let Err(err) = enable_raw_mode() { + if let Err(err) = self.context_mut().terminal().enable_raw_mode() { error!("Failed to enter raw mode: {}", err); } // Init view @@ -229,20 +372,25 @@ impl Activity for SetupActivity { if self.context.is_none() { return; } - // Read one event - if let Ok(Some(event)) = self.context().input_hnd().read_event() { - // Set redraw to true - self.redraw = true; - // Handle event - let msg = self.view.on(event); - self.update(msg); + match self.app.tick(PollStrategy::UpTo(3)) { + Ok(messages) => { + if !messages.is_empty() { + self.redraw = true; + } + for msg in messages.into_iter() { + let mut msg = Some(msg); + while msg.is_some() { + msg = self.update(msg); + } + } + } + Err(err) => { + self.mount_error(format!("Application error: {}", err)); + } } - // Redraw if necessary + // View if self.redraw { - // View self.view(); - // Redraw back to false - self.redraw = false; } } @@ -262,17 +410,12 @@ impl Activity for SetupActivity { /// This function finally releases the context fn on_destroy(&mut self) -> Option { // Disable raw mode - if let Err(err) = disable_raw_mode() { + if let Err(err) = self.context_mut().terminal().disable_raw_mode() { error!("Failed to disable raw mode: {}", err); } - self.context.as_ref()?; - // Clear terminal and return - match self.context.take() { - Some(mut ctx) => { - ctx.clear_screen(); - Some(ctx) - } - None => None, + if let Err(err) = self.context_mut().terminal().clear_screen() { + error!("Failed to clear screen: {}", err); } + self.context.take() } } diff --git a/src/ui/activities/setup/update.rs b/src/ui/activities/setup/update.rs index cbeae392..5e91aff3 100644 --- a/src/ui/activities/setup/update.rs +++ b/src/ui/activities/setup/update.rs @@ -28,737 +28,446 @@ */ // locals use super::{ - SetupActivity, ViewLayout, COMPONENT_COLOR_AUTH_ADDR, COMPONENT_COLOR_AUTH_BOOKMARKS, - COMPONENT_COLOR_AUTH_PASSWORD, COMPONENT_COLOR_AUTH_PORT, COMPONENT_COLOR_AUTH_PROTOCOL, - COMPONENT_COLOR_AUTH_RECENTS, COMPONENT_COLOR_AUTH_USERNAME, COMPONENT_COLOR_MISC_ERROR, - COMPONENT_COLOR_MISC_INFO, COMPONENT_COLOR_MISC_INPUT, COMPONENT_COLOR_MISC_KEYS, - COMPONENT_COLOR_MISC_QUIT, COMPONENT_COLOR_MISC_SAVE, COMPONENT_COLOR_MISC_WARN, - COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, - COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, - COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, - COMPONENT_COLOR_TRANSFER_LOG_BG, COMPONENT_COLOR_TRANSFER_LOG_WIN, - COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, - COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, COMPONENT_COLOR_TRANSFER_STATUS_SORTING, - COMPONENT_COLOR_TRANSFER_STATUS_SYNC, COMPONENT_INPUT_LOCAL_FILE_FMT, - COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, COMPONENT_INPUT_REMOTE_FILE_FMT, - COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR, - COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY, - COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES, - COMPONENT_RADIO_NOTIFICATIONS_ENABLED, COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, - COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, - COMPONENT_TEXT_HELP, + CommonMsg, ConfigMsg, Id, IdConfig, IdSsh, IdTheme, Msg, SetupActivity, SshMsg, ThemeMsg, + ViewLayout, }; -use crate::ui::keymap::*; -use crate::utils::parser::parse_color; // ext -use tuirealm::{Msg, Payload, Update, Value}; +use tuirealm::Update; -impl Update for SetupActivity { +impl Update for SetupActivity { /// ### update /// /// Update auth activity model based on msg /// The function exits when returns None - fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> { - match self.layout { - ViewLayout::SetupForm => self.update_setup(msg), - ViewLayout::SshKeys => self.update_ssh_keys(msg), - ViewLayout::Theme => self.update_theme(msg), + fn update(&mut self, msg: Option) -> Option { + match msg.unwrap_or(Msg::None) { + Msg::Common(msg) => self.common_update(msg), + Msg::Config(msg) => self.config_update(msg), + Msg::Ssh(msg) => self.ssh_update(msg), + Msg::Theme(msg) => self.theme_update(msg), + Msg::None => None, } } } impl SetupActivity { - fn update_setup(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> { - let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg)); - // Match msg - match ref_msg { - None => None, - Some(msg) => match msg { - // Input field - (COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_DEFAULT_PROTOCOL); - None - } - (COMPONENT_RADIO_DEFAULT_PROTOCOL, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_HIDDEN_FILES); - None - } - (COMPONENT_RADIO_HIDDEN_FILES, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_UPDATES); - None - } - (COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE); - None - } - (COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_GROUP_DIRS); - None - } - (COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT); - None - } - (COMPONENT_INPUT_LOCAL_FILE_FMT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT); - None - } - (COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED); - None - } - (COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD); - None - } - (COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_TEXT_EDITOR); - None - } - // Input field - (COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED); - None - } - (COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT); - None - } - (COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT); - None - } - (COMPONENT_INPUT_LOCAL_FILE_FMT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_GROUP_DIRS); - None - } - (COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE); - None - } - (COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_UPDATES); - None - } - (COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_HIDDEN_FILES); - None - } - (COMPONENT_RADIO_HIDDEN_FILES, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_DEFAULT_PROTOCOL); - None - } - (COMPONENT_RADIO_DEFAULT_PROTOCOL, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_TEXT_EDITOR); - None - } - (COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD); - None - } - // Error or - (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount text error - self.umount_error(); - None - } - (COMPONENT_TEXT_ERROR, _) => None, - // Exit - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save changes - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - // Exit - self.exit_reason = Some(super::ExitReason::Quit); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { - // Quit - self.exit_reason = Some(super::ExitReason::Quit); - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => { - // Umount popup - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, _) => None, - // Close help - (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount help - self.umount_help(); - None - } - (COMPONENT_TEXT_HELP, _) => None, - // Save popup - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save config - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => { - // Umount radio save - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, _) => None, - // Detect config changed - (_, Msg::OnChange(_)) => { - // An input field has changed value; report config changed - self.set_config_changed(true); - None - } - // Show help - (_, key) if key == &MSG_KEY_CTRL_H => { - // Show help - self.mount_help(); - None - } - (_, key) if key == &MSG_KEY_TAB => { - // Change view - if let Err(err) = self.action_change_tab(ViewLayout::SshKeys) { - self.mount_error(err.as_str()); + fn common_update(&mut self, msg: CommonMsg) -> Option { + match msg { + CommonMsg::ChangeLayout => { + let new_layout = match self.layout { + ViewLayout::SetupForm => ViewLayout::SshKeys, + ViewLayout::SshKeys => ViewLayout::Theme, + ViewLayout::Theme => ViewLayout::SetupForm, + }; + if let Err(err) = self.action_change_tab(new_layout) { + self.mount_error(err.as_str()); + } + } + CommonMsg::CloseErrorPopup => { + self.umount_error(); + } + CommonMsg::CloseKeybindingsPopup => { + self.umount_help(); + } + CommonMsg::CloseQuitPopup => { + self.umount_quit(); + } + CommonMsg::CloseSavePopup => { + self.umount_save_popup(); + } + CommonMsg::Quit => { + self.exit_reason = Some(super::ExitReason::Quit); + } + CommonMsg::RevertChanges => match self.layout { + ViewLayout::Theme => { + if let Err(err) = self.action_reset_theme() { + self.mount_error(err); } - None } - // Revert changes - (_, key) if key == &MSG_KEY_CTRL_R => { - // Revert changes + ViewLayout::SshKeys | ViewLayout::SetupForm => { if let Err(err) = self.action_reset_config() { - self.mount_error(err.as_str()); + self.mount_error(err); } - None } - // Save - (_, key) if key == &MSG_KEY_CTRL_S => { - // Show save - self.mount_save_popup(); - None - } - // - (_, key) if key == &MSG_KEY_ESC => { - self.action_on_esc(); - None - } - (_, _) => None, // Nothing to do }, + CommonMsg::SaveAndQuit => { + // Save changes + if let Err(err) = self.action_save_all() { + self.mount_error(err.as_str()); + } + // Exit + self.exit_reason = Some(super::ExitReason::Quit); + } + CommonMsg::SaveConfig => { + if let Err(err) = self.action_save_all() { + self.mount_error(err.as_str()); + } + self.umount_save_popup(); + } + CommonMsg::ShowKeybindings => { + self.mount_help(); + } + CommonMsg::ShowQuitPopup => { + self.action_on_esc(); + } + CommonMsg::ShowSavePopup => { + self.mount_save_popup(); + } } + None } - fn update_ssh_keys(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> { - let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg)); - // Match msg - match ref_msg { - None => None, - Some(msg) => match msg { - // Error or - (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount text error - self.umount_error(); - None - } - (COMPONENT_TEXT_ERROR, _) => None, - // Exit - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save changes - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - // Exit - self.exit_reason = Some(super::ExitReason::Quit); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { - // Quit - self.exit_reason = Some(super::ExitReason::Quit); - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => { - // Umount popup - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, _) => None, - // Close help - (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount help - self.umount_help(); - None - } - (COMPONENT_TEXT_HELP, _) => None, - // Delete key - (COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Delete key - self.action_delete_ssh_key(); - // Reload ssh keys - self.reload_ssh_keys(); - // Delete popup - self.umount_del_ssh_key(); - None - } - (COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(_)) => { - // Umount - self.umount_del_ssh_key(); - None - } - (COMPONENT_RADIO_DEL_SSH_KEY, _) => None, - // Save popup - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save config - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => { - // Umount radio save - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, _) => None, - // Edit SSH Key - // Show help - (_, key) if key == &MSG_KEY_CTRL_H => { - // Show help - self.mount_help(); - None - } - // New key - (COMPONENT_INPUT_SSH_HOST, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_SSH_USERNAME); - None - } - (COMPONENT_INPUT_SSH_USERNAME, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_SSH_HOST); - None - } - // New key - (COMPONENT_INPUT_SSH_USERNAME, key) | (COMPONENT_INPUT_SSH_USERNAME, key) - if key == &MSG_KEY_UP || key == &MSG_KEY_TAB => - { - self.view.active(COMPONENT_INPUT_SSH_HOST); - None - } - (COMPONENT_INPUT_SSH_HOST, key) | (COMPONENT_INPUT_SSH_HOST, key) - if key == &MSG_KEY_UP || key == &MSG_KEY_TAB => - { - self.view.active(COMPONENT_INPUT_SSH_USERNAME); - None - } - // New key - (COMPONENT_INPUT_SSH_HOST, Msg::OnSubmit(_)) - | (COMPONENT_INPUT_SSH_USERNAME, Msg::OnSubmit(_)) => { - // Save ssh key - self.action_new_ssh_key(); - self.umount_new_ssh_key(); - self.reload_ssh_keys(); - None - } - // New key - (COMPONENT_INPUT_SSH_HOST, key) | (COMPONENT_INPUT_SSH_USERNAME, key) - if key == &MSG_KEY_ESC => - { - // Umount new ssh key - self.umount_new_ssh_key(); - None - } - // New key - (COMPONENT_LIST_SSH_KEYS, key) if key == &MSG_KEY_CTRL_N => { - // Show new key popup - self.mount_new_ssh_key(); - None - } - // Edit key - (COMPONENT_LIST_SSH_KEYS, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { - // Edit ssh key - if let Err(err) = self.edit_ssh_key(*idx) { - self.mount_error(err.as_str()); - } - None - } - // Show delete - (COMPONENT_LIST_SSH_KEYS, key) | (COMPONENT_LIST_SSH_KEYS, key) - if key == &MSG_KEY_CTRL_E || key == &MSG_KEY_DEL => - { - // Show delete key - self.mount_del_ssh_key(); - None - } - (_, key) if key == &MSG_KEY_TAB => { - // Change view - if let Err(err) = self.action_change_tab(ViewLayout::Theme) { - self.mount_error(err.as_str()); - } - None - } - // Revert changes - (_, key) if key == &MSG_KEY_CTRL_R => { - // Revert changes - if let Err(err) = self.action_reset_config() { - self.mount_error(err.as_str()); - } - None - } - // Save - (_, key) if key == &MSG_KEY_CTRL_S => { - // Show save - self.mount_save_popup(); - None - } - // - (_, key) if key == &MSG_KEY_ESC => { - self.action_on_esc(); - None - } - (_, _) => None, // Nothing to do - }, + fn config_update(&mut self, msg: ConfigMsg) -> Option { + match msg { + ConfigMsg::CheckUpdatesBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::PromptOnFileReplace)) + .is_ok()); + } + ConfigMsg::CheckUpdatesBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::HiddenFiles)).is_ok()); + } + ConfigMsg::DefaultProtocolBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::HiddenFiles)).is_ok()); + } + ConfigMsg::DefaultProtocolBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); + } + ConfigMsg::GroupDirsBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::LocalFileFmt)).is_ok()); + } + ConfigMsg::GroupDirsBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::PromptOnFileReplace)) + .is_ok()); + } + ConfigMsg::HiddenFilesBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::CheckUpdates)).is_ok()); + } + ConfigMsg::HiddenFilesBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::DefaultProtocol)) + .is_ok()); + } + ConfigMsg::LocalFileFmtBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::RemoteFileFmt)) + .is_ok()); + } + ConfigMsg::LocalFileFmtBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::GroupDirs)).is_ok()); + } + ConfigMsg::NotificationsEnabledBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::NotificationsThreshold)) + .is_ok()); + } + ConfigMsg::NotificationsEnabledBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::RemoteFileFmt)) + .is_ok()); + } + ConfigMsg::NotificationsThresholdBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); + } + ConfigMsg::NotificationsThresholdBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::NotificationsEnabled)) + .is_ok()); + } + ConfigMsg::PromptOnFileReplaceBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::GroupDirs)).is_ok()); + } + ConfigMsg::PromptOnFileReplaceBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::CheckUpdates)).is_ok()); + } + ConfigMsg::RemoteFileFmtBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::NotificationsEnabled)) + .is_ok()); + } + ConfigMsg::RemoteFileFmtBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::LocalFileFmt)).is_ok()); + } + ConfigMsg::TextEditorBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::DefaultProtocol)) + .is_ok()); + } + ConfigMsg::TextEditorBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::NotificationsThreshold)) + .is_ok()); + } + ConfigMsg::ConfigChanged => { + self.set_config_changed(true); + } } + None } - fn update_theme(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> { - let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg)); - // Match msg - match ref_msg { - None => None, - Some(msg) => match msg { - // Input fields - (COMPONENT_COLOR_AUTH_PROTOCOL, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_ADDR); - None - } - (COMPONENT_COLOR_AUTH_ADDR, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_PORT); - None - } - (COMPONENT_COLOR_AUTH_PORT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_USERNAME); - None - } - (COMPONENT_COLOR_AUTH_USERNAME, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_PASSWORD); - None - } - (COMPONENT_COLOR_AUTH_PASSWORD, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS); - None - } - (COMPONENT_COLOR_AUTH_BOOKMARKS, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_RECENTS); - None - } - (COMPONENT_COLOR_AUTH_RECENTS, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_ERROR); - None - } - (COMPONENT_COLOR_MISC_ERROR, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_INFO); - None - } - (COMPONENT_COLOR_MISC_INFO, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_INPUT); - None - } - (COMPONENT_COLOR_MISC_INPUT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_KEYS); - None - } - (COMPONENT_COLOR_MISC_KEYS, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_QUIT); - None - } - (COMPONENT_COLOR_MISC_QUIT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_SAVE); - None - } - (COMPONENT_COLOR_MISC_SAVE, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_WARN); - None - } - (COMPONENT_COLOR_MISC_WARN, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, key) if key == &MSG_KEY_DOWN => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, key) if key == &MSG_KEY_DOWN => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, key) if key == &MSG_KEY_DOWN => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL); - None - } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL); - None - } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG); - None - } - (COMPONENT_COLOR_TRANSFER_LOG_BG, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN); - None - } - (COMPONENT_COLOR_TRANSFER_LOG_WIN, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_SORTING, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_SYNC, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL); - None - } - (COMPONENT_COLOR_AUTH_PROTOCOL, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC); - None - } - (COMPONENT_COLOR_AUTH_ADDR, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL); - None - } - (COMPONENT_COLOR_AUTH_PORT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_ADDR); - None - } - (COMPONENT_COLOR_AUTH_USERNAME, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_PORT); - None - } - (COMPONENT_COLOR_AUTH_PASSWORD, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_USERNAME); - None - } - (COMPONENT_COLOR_AUTH_BOOKMARKS, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_PASSWORD); - None - } - (COMPONENT_COLOR_AUTH_RECENTS, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS); - None - } - (COMPONENT_COLOR_MISC_ERROR, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_RECENTS); - None - } - (COMPONENT_COLOR_MISC_INFO, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_ERROR); - None - } - (COMPONENT_COLOR_MISC_INPUT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_INFO); - None - } - (COMPONENT_COLOR_MISC_KEYS, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_INPUT); - None - } - (COMPONENT_COLOR_MISC_QUIT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_KEYS); - None - } - (COMPONENT_COLOR_MISC_SAVE, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_QUIT); - None - } - (COMPONENT_COLOR_MISC_WARN, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_SAVE); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_WARN); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, key) if key == &MSG_KEY_UP => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, key) if key == &MSG_KEY_UP => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG); - None - } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, key) if key == &MSG_KEY_UP => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG); - None - } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL); - None - } - (COMPONENT_COLOR_TRANSFER_LOG_BG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL); - None - } - (COMPONENT_COLOR_TRANSFER_LOG_WIN, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_SORTING, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_SYNC, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN); - None - } - // On color change - (component, Msg::OnChange(Payload::One(Value::Str(color)))) => { - if let Some(color) = parse_color(color) { - self.action_save_color(component, color); - // Set unsaved changes to true - self.set_config_changed(true); - } - None - } - // Error or - (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount text error - self.umount_error(); - None - } - (COMPONENT_TEXT_ERROR, _) => None, - // Exit - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save changes - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - // Exit - self.exit_reason = Some(super::ExitReason::Quit); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { - // Quit - self.exit_reason = Some(super::ExitReason::Quit); - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => { - // Umount popup - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, _) => None, - // Close help - (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount help - self.umount_help(); - None - } - (COMPONENT_TEXT_HELP, _) => None, - // Save popup - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save config - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => { - // Umount radio save - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, _) => None, - // Edit SSH Key - // Show help - (_, key) if key == &MSG_KEY_CTRL_H => { - // Show help - self.mount_help(); - None - } - (_, key) if key == &MSG_KEY_TAB => { - // Change view - if let Err(err) = self.action_change_tab(ViewLayout::SetupForm) { - self.mount_error(err.as_str()); - } - None - } - // Revert changes - (_, key) if key == &MSG_KEY_CTRL_R => { - // Revert changes - if let Err(err) = self.action_reset_theme() { - self.mount_error(err.as_str()); - } - None - } - // Save - (_, key) if key == &MSG_KEY_CTRL_S => { - // Show save - self.mount_save_popup(); - None - } - // - (_, key) if key == &MSG_KEY_ESC => { - self.action_on_esc(); - None - } - (_, _) => None, // Nothing to do - }, + fn ssh_update(&mut self, msg: SshMsg) -> Option { + match msg { + SshMsg::CloseDelSshKeyPopup => { + self.umount_del_ssh_key(); + } + SshMsg::CloseNewSshKeyPopup => { + self.umount_new_ssh_key(); + } + SshMsg::DeleteSshKey => { + self.action_delete_ssh_key(); + self.umount_del_ssh_key(); + self.reload_ssh_keys(); + } + SshMsg::EditSshKey(i) => { + if let Err(err) = self.edit_ssh_key(i) { + self.mount_error(err.as_str()); + } + } + SshMsg::SaveSshKey => { + self.action_new_ssh_key(); + self.umount_new_ssh_key(); + self.reload_ssh_keys(); + } + SshMsg::ShowDelSshKeyPopup => { + self.mount_del_ssh_key(); + } + SshMsg::ShowNewSshKeyPopup => { + self.mount_new_ssh_key(); + } + SshMsg::SshHostBlur => { + assert!(self.app.active(&Id::Ssh(IdSsh::SshUsername)).is_ok()); + } + SshMsg::SshUsernameBlur => { + assert!(self.app.active(&Id::Ssh(IdSsh::SshHost)).is_ok()); + } + } + None + } + + fn theme_update(&mut self, msg: ThemeMsg) -> Option { + match msg { + ThemeMsg::AuthAddressBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthPort)).is_ok()); + } + ThemeMsg::AuthAddressBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthProtocol)).is_ok()); + } + ThemeMsg::AuthBookmarksBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::AuthRecentHosts)) + .is_ok()); + } + ThemeMsg::AuthBookmarksBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthPassword)).is_ok()); + } + ThemeMsg::AuthPasswordBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthBookmarks)).is_ok()); + } + ThemeMsg::AuthPasswordBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthUsername)).is_ok()); + } + ThemeMsg::AuthPortBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthUsername)).is_ok()); + } + ThemeMsg::AuthPortBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthAddress)).is_ok()); + } + ThemeMsg::AuthProtocolBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthAddress)).is_ok()); + } + ThemeMsg::AuthProtocolBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusSync)).is_ok()); + } + ThemeMsg::AuthRecentHostsBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscError)).is_ok()); + } + ThemeMsg::AuthRecentHostsBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthBookmarks)).is_ok()); + } + ThemeMsg::AuthUsernameBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthPassword)).is_ok()); + } + ThemeMsg::AuthUsernameBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthPort)).is_ok()); + } + ThemeMsg::MiscErrorBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscInfo)).is_ok()); + } + ThemeMsg::MiscErrorBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::AuthRecentHosts)) + .is_ok()); + } + ThemeMsg::MiscInfoBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscInput)).is_ok()); + } + ThemeMsg::MiscInfoBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscError)).is_ok()); + } + ThemeMsg::MiscInputBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscKeys)).is_ok()); + } + ThemeMsg::MiscInputBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscInfo)).is_ok()); + } + ThemeMsg::MiscKeysBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscQuit)).is_ok()); + } + ThemeMsg::MiscKeysBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscInput)).is_ok()); + } + ThemeMsg::MiscQuitBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscSave)).is_ok()); + } + ThemeMsg::MiscQuitBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscKeys)).is_ok()); + } + ThemeMsg::MiscSaveBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscWarn)).is_ok()); + } + ThemeMsg::MiscSaveBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscQuit)).is_ok()); + } + ThemeMsg::MiscWarnBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalBg)) + .is_ok()); + } + ThemeMsg::MiscWarnBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscSave)).is_ok()); + } + ThemeMsg::ExplorerLocalBgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalFg)) + .is_ok()); + } + ThemeMsg::ExplorerLocalBgBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscWarn)).is_ok()); + } + ThemeMsg::ExplorerLocalFgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalHg)) + .is_ok()); + } + ThemeMsg::ExplorerLocalFgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalBg)) + .is_ok()); + } + ThemeMsg::ExplorerLocalHgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteBg)) + .is_ok()); + } + ThemeMsg::ExplorerLocalHgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalFg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteBgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteFg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteBgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalHg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteFgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteHg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteFgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteBg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteHgBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::ProgBarFull)).is_ok()); + } + ThemeMsg::ExplorerRemoteHgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteFg)) + .is_ok()); + } + ThemeMsg::ProgBarFullBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::ProgBarPartial)).is_ok()); + } + ThemeMsg::ProgBarFullBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteHg)) + .is_ok()); + } + ThemeMsg::ProgBarPartialBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::LogBg)).is_ok()); + } + ThemeMsg::ProgBarPartialBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::ProgBarFull)).is_ok()); + } + ThemeMsg::LogBgBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::LogWindow)).is_ok()); + } + ThemeMsg::LogBgBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::ProgBarPartial)).is_ok()); + } + ThemeMsg::LogWindowBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusSorting)).is_ok()); + } + ThemeMsg::LogWindowBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::LogBg)).is_ok()); + } + ThemeMsg::StatusSortingBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusHidden)).is_ok()); + } + ThemeMsg::StatusSortingBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::LogWindow)).is_ok()); + } + ThemeMsg::StatusHiddenBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusSync)).is_ok()); + } + ThemeMsg::StatusHiddenBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusSorting)).is_ok()); + } + ThemeMsg::StatusSyncBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthProtocol)).is_ok()); + } + ThemeMsg::StatusSyncBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusHidden)).is_ok()); + } + ThemeMsg::ColorChanged(id, color) => { + self.action_save_color(id, color); + // Set unsaved changes to true + self.set_config_changed(true); + } } + None } } diff --git a/src/ui/activities/setup/view/mod.rs b/src/ui/activities/setup/view/mod.rs index f3452be4..4da7eaff 100644 --- a/src/ui/activities/setup/view/mod.rs +++ b/src/ui/activities/setup/view/mod.rs @@ -31,18 +31,15 @@ pub mod ssh_keys; pub mod theme; use super::*; +use crate::utils::ui::draw_area_in; pub use setup::*; pub use ssh_keys::*; pub use theme::*; -// Ext -use tui_realm_stdlib::{ - Input, InputPropsBuilder, List, ListPropsBuilder, Paragraph, ParagraphPropsBuilder, Radio, - RadioPropsBuilder, Span, SpanPropsBuilder, -}; -use tuirealm::props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan}; -use tuirealm::tui::{ - style::Color, - widgets::{BorderType, Borders}, + +use tuirealm::tui::widgets::Clear; +use tuirealm::{ + event::{Key, KeyEvent, KeyModifiers}, + Frame, Sub, SubClause, SubEventClause, }; impl SetupActivity { @@ -61,6 +58,7 @@ impl SetupActivity { /// /// View gui pub(super) fn view(&mut self) { + self.redraw = false; match self.layout { ViewLayout::SetupForm => self.view_setup(), ViewLayout::SshKeys => self.view_ssh_keys(), @@ -73,238 +71,229 @@ impl SetupActivity { /// ### mount_error /// /// Mount error box - pub(super) fn mount_error(&mut self, text: &str) { - self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, Color::Red); + pub(super) fn mount_error>(&mut self, text: S) { + assert!(self + .app + .remount( + Id::Common(IdCommon::ErrorPopup), + Box::new(components::ErrorPopup::new(text)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::Common(IdCommon::ErrorPopup)).is_ok()); } /// ### umount_error /// /// Umount error message pub(super) fn umount_error(&mut self) { - self.view.umount(super::COMPONENT_TEXT_ERROR); + let _ = self.app.umount(&Id::Common(IdCommon::ErrorPopup)); } /// ### mount_quit /// /// Mount quit popup pub(super) fn mount_quit(&mut self) { - self.mount_radio_dialog( - super::COMPONENT_RADIO_QUIT, - "There are unsaved changes! Save changes before leaving?", - &["Save", "Don't save", "Cancel"], - 0, - Color::LightRed, - ); + assert!(self + .app + .remount( + Id::Common(IdCommon::QuitPopup), + Box::new(components::QuitPopup::default()), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::Common(IdCommon::QuitPopup)).is_ok()); } /// ### umount_quit /// /// Umount quit pub(super) fn umount_quit(&mut self) { - self.view.umount(super::COMPONENT_RADIO_QUIT); + let _ = self.app.umount(&Id::Common(IdCommon::QuitPopup)); } /// ### mount_save_popup /// /// Mount save popup pub(super) fn mount_save_popup(&mut self) { - self.mount_radio_dialog( - super::COMPONENT_RADIO_SAVE, - "Save changes?", - &["Yes", "No"], - 0, - Color::LightYellow, - ); + assert!(self + .app + .remount( + Id::Common(IdCommon::SavePopup), + Box::new(components::SavePopup::default()), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::Common(IdCommon::SavePopup)).is_ok()); } /// ### umount_quit /// /// Umount quit pub(super) fn umount_save_popup(&mut self) { - self.view.umount(super::COMPONENT_RADIO_SAVE); - } - - pub(self) fn mount_header_tab(&mut self, idx: usize) { - self.view.mount( - super::COMPONENT_RADIO_TAB, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(Color::LightYellow) - .with_inverted_color(Color::Black) - .with_borders(Borders::BOTTOM, BorderType::Thick, Color::White) - .with_options(&[ - String::from("User Interface"), - String::from("SSH Keys"), - String::from("Theme"), - ]) - .with_value(idx) - .rewind(true) - .build(), - )), - ); - } - - pub(self) fn mount_footer(&mut self) { - self.view.mount( - super::COMPONENT_TEXT_FOOTER, - Box::new(Span::new( - SpanPropsBuilder::default() - .with_spans(vec![ - TextSpan::new("Press ").bold(), - TextSpan::new("").bold().fg(Color::Cyan), - TextSpan::new(" to show keybindings").bold(), - ]) - .build(), - )), - ); + let _ = self.app.umount(&Id::Common(IdCommon::SavePopup)); } /// ### mount_help /// /// Mount help pub(super) fn mount_help(&mut self) { - self.view.mount( - super::COMPONENT_TEXT_HELP, - Box::new(List::new( - ListPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, Color::White) - .with_highlighted_str(Some("?")) - .with_max_scroll_step(8) - .bold() - .with_title("Help", Alignment::Center) - .scrollable(true) - .with_rows( - TableBuilder::default() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Exit setup")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Change setup page")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Change cursor")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Change input field")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Select / Dismiss popup")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Delete SSH key")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" New SSH key")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Revert changes")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Save configuration")) - .build(), - ) - .build(), - )), - ); - // Active help - self.view.active(super::COMPONENT_TEXT_HELP); + assert!(self + .app + .remount( + Id::Common(IdCommon::Keybindings), + Box::new(components::Keybindings::default()), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::Common(IdCommon::Keybindings)).is_ok()); } /// ### umount_help /// /// Umount help pub(super) fn umount_help(&mut self) { - self.view.umount(super::COMPONENT_TEXT_HELP); + let _ = self.app.umount(&Id::Common(IdCommon::Keybindings)); } - // -- mount helpers - - fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) { - // Mount - self.view.mount( - id, - Box::new(Paragraph::new( - ParagraphPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Thick, color) - .with_foreground(color) - .bold() - .with_text_alignment(Alignment::Center) - .with_texts(vec![TextSpan::from(text)]) - .build(), - )), - ); - // Give focus to error - self.view.active(id); + pub(super) fn view_popups(&mut self, f: &mut Frame) { + if self.app.mounted(&Id::Common(IdCommon::ErrorPopup)) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::Common(IdCommon::ErrorPopup), f, popup); + } else if self.app.mounted(&Id::Common(IdCommon::QuitPopup)) { + // make popup + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::Common(IdCommon::QuitPopup), f, popup); + } else if self.app.mounted(&Id::Common(IdCommon::Keybindings)) { + // make popup + let popup = draw_area_in(f.size(), 50, 70); + f.render_widget(Clear, popup); + self.app.view(&Id::Common(IdCommon::Keybindings), f, popup); + } else if self.app.mounted(&Id::Common(IdCommon::SavePopup)) { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::Common(IdCommon::SavePopup), f, popup); + } } - fn mount_radio_dialog( - &mut self, - id: &str, - text: &str, - opts: &[&str], - default: usize, - color: Color, - ) { - self.view.mount( - id, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(color) - .with_inverted_color(Color::Black) - .with_borders(Borders::ALL, BorderType::Rounded, color) - .with_title(text, Alignment::Center) - .with_options(opts) - .with_value(default) - .rewind(true) - .build(), - )), - ); - // Active - self.view.active(id); + /// ### new_app + /// + /// Clean app up and remount common components and global listener + fn new_app(&mut self, layout: ViewLayout) { + self.app.umount_all(); + self.mount_global_listener(); + self.mount_commons(layout); } - fn mount_radio(&mut self, id: &str, text: &str, opts: &[&str], default: usize, color: Color) { - self.view.mount( - id, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(color) - .with_inverted_color(Color::Black) - .with_borders(Borders::ALL, BorderType::Rounded, color) - .with_title(text, Alignment::Left) - .with_options(opts) - .with_value(default) - .rewind(true) - .build(), - )), - ); + /// ### mount_commons + /// + /// Mount common components + fn mount_commons(&mut self, layout: ViewLayout) { + // Radio tab + assert!(self + .app + .remount( + Id::Common(IdCommon::Header), + Box::new(components::Header::new(layout)), + vec![], + ) + .is_ok()); + // Footer + assert!(self + .app + .remount( + Id::Common(IdCommon::Footer), + Box::new(components::Footer::default()), + vec![], + ) + .is_ok()); } - fn mount_input(&mut self, id: &str, label: &str, fg: Color, typ: InputType) { - self.mount_input_ex(id, label, fg, typ, None, None); + /// ### mount_global_listener + /// + /// Mount global listener + fn mount_global_listener(&mut self) { + assert!(self + .app + .mount( + Id::Common(IdCommon::GlobalListener), + Box::new(components::GlobalListener::default()), + vec![ + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Esc, + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Tab, + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('h'), + modifiers: KeyModifiers::CONTROL, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('r'), + modifiers: KeyModifiers::CONTROL, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('s'), + modifiers: KeyModifiers::CONTROL, + }), + Self::no_popup_mounted_clause(), + ), + ] + ) + .is_ok()); } - fn mount_input_ex( - &mut self, - id: &str, - label: &str, - fg: Color, - typ: InputType, - len: Option, - value: Option, - ) { - let mut props = InputPropsBuilder::default(); - props - .with_foreground(fg) - .with_borders(Borders::ALL, BorderType::Rounded, fg) - .with_label(label, Alignment::Left) - .with_input(typ); - if let Some(len) = len { - props.with_input_len(len); - } - if let Some(value) = value { - props.with_value(value); - } - self.view.mount(id, Box::new(Input::new(props.build()))); + /// ### no_popup_mounted_clause + /// + /// Returns a sub clause which requires that no popup is mounted in order to be satisfied + fn no_popup_mounted_clause() -> SubClause { + SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common( + IdCommon::ErrorPopup, + ))))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common( + IdCommon::Keybindings, + ))))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common( + IdCommon::QuitPopup, + ))))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common( + IdCommon::SavePopup, + ))))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh( + IdSsh::DelSshKeyPopup, + ))))), + Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh( + IdSsh::SshHost, + ))))), + )), + )), + )), + )), + ) } } diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 618aab88..b80ea25e 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -27,23 +27,15 @@ * SOFTWARE. */ // Locals -use super::{Context, InputType, SetupActivity}; +use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout}; use crate::filetransfer::FileTransferProtocol; use crate::fs::explorer::GroupDirs; -use crate::ui::components::bytes::{Bytes, BytesPropsBuilder}; -use crate::utils::ui::draw_area_in; +use crate::utils::fmt::fmt_bytes; + // Ext use std::path::PathBuf; -use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder}; -use tuirealm::tui::{ - layout::{Constraint, Direction, Layout}, - style::Color, - widgets::{BorderType, Borders, Clear}, -}; -use tuirealm::{ - props::{Alignment, PropsBuilder}, - Payload, Value, View, -}; +use tuirealm::tui::layout::{Constraint, Direction, Layout}; +use tuirealm::{State, StateValue}; impl SetupActivity { // -- view @@ -52,92 +44,17 @@ impl SetupActivity { /// /// Initialize setup view pub(super) fn init_setup(&mut self) { - // Init view - self.view = View::init(); - // Common stuff - // Radio tab - self.mount_header_tab(0); - // Footer - self.mount_footer(); - // Input fields - self.mount_input( - super::COMPONENT_INPUT_TEXT_EDITOR, - "Text editor", - Color::LightGreen, - InputType::Text, - ); - self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus - self.mount_radio( - super::COMPONENT_RADIO_DEFAULT_PROTOCOL, - "Default protocol", - &["SFTP", "SCP", "FTP", "FTPS", "AWS S3"], - 0, - Color::LightCyan, - ); - self.mount_radio( - super::COMPONENT_RADIO_HIDDEN_FILES, - "Show hidden files (by default)?", - &["Yes", "No"], - 0, - Color::LightRed, - ); - self.mount_radio( - super::COMPONENT_RADIO_UPDATES, - "Check for updates?", - &["Yes", "No"], - 0, - Color::LightYellow, - ); - self.mount_radio( - super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, - "Prompt when replacing existing files?", - &["Yes", "No"], - 0, - Color::LightCyan, - ); - self.mount_radio( - super::COMPONENT_RADIO_GROUP_DIRS, - "Group directories", - &["Display first", "Display last", "No"], - 0, - Color::LightMagenta, - ); - self.mount_input( - super::COMPONENT_INPUT_LOCAL_FILE_FMT, - "File formatter syntax (local)", - Color::LightGreen, - InputType::Text, - ); - self.mount_input( - super::COMPONENT_INPUT_REMOTE_FILE_FMT, - "File formatter syntax (remote)", - Color::LightCyan, - InputType::Text, - ); - self.mount_radio( - super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, - "Enable notifications?", - &["Yes", "No"], - 0, - Color::LightRed, - ); - self.view.mount( - super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, - Box::new(Bytes::new( - BytesPropsBuilder::default() - .with_foreground(Color::LightYellow) - .with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow) - .with_label("Notifications: minimum transfer size", Alignment::Left) - .build(), - )), - ); + // Init view (and mount commons) + self.new_app(ViewLayout::SetupForm); // Load values self.load_input_values(); + // Active text editor + assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); } pub(super) fn view_setup(&mut self) { let mut ctx: Context = self.context.take().unwrap(); - let _ = ctx.terminal().draw(|f| { + let _ = ctx.terminal().raw_mut().draw(|f| { // Prepare main chunks let chunks = Layout::default() .direction(Direction::Vertical) @@ -152,8 +69,8 @@ impl SetupActivity { ) .split(f.size()); // Render common widget - self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]); - self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]); + self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]); + self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]); // Make chunks (two columns) let ui_cfg_chunks = Layout::default() .direction(Direction::Horizontal) @@ -174,27 +91,27 @@ impl SetupActivity { .as_ref(), ) .split(ui_cfg_chunks[0]); - self.view - .render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks_col1[0]); - self.view.render( - super::COMPONENT_RADIO_DEFAULT_PROTOCOL, + self.app + .view(&Id::Config(IdConfig::TextEditor), f, ui_cfg_chunks_col1[0]); + self.app.view( + &Id::Config(IdConfig::DefaultProtocol), f, ui_cfg_chunks_col1[1], ); - self.view.render( - super::COMPONENT_RADIO_HIDDEN_FILES, + self.app + .view(&Id::Config(IdConfig::HiddenFiles), f, ui_cfg_chunks_col1[2]); + self.app.view( + &Id::Config(IdConfig::CheckUpdates), f, - ui_cfg_chunks_col1[2], + ui_cfg_chunks_col1[3], ); - self.view - .render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks_col1[3]); - self.view.render( - super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, + self.app.view( + &Id::Config(IdConfig::PromptOnFileReplace), f, ui_cfg_chunks_col1[4], ); - self.view - .render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks_col1[5]); + self.app + .view(&Id::Config(IdConfig::GroupDirs), f, ui_cfg_chunks_col1[5]); // Column 2 let ui_cfg_chunks_col2 = Layout::default() .direction(Direction::Vertical) @@ -209,59 +126,28 @@ impl SetupActivity { .as_ref(), ) .split(ui_cfg_chunks[1]); - self.view.render( - super::COMPONENT_INPUT_LOCAL_FILE_FMT, + self.app.view( + &Id::Config(IdConfig::LocalFileFmt), f, ui_cfg_chunks_col2[0], ); - self.view.render( - super::COMPONENT_INPUT_REMOTE_FILE_FMT, + self.app.view( + &Id::Config(IdConfig::RemoteFileFmt), f, ui_cfg_chunks_col2[1], ); - self.view.render( - super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, + self.app.view( + &Id::Config(IdConfig::NotificationsEnabled), f, ui_cfg_chunks_col2[2], ); - self.view.render( - super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, + self.app.view( + &Id::Config(IdConfig::NotificationsThreshold), f, ui_cfg_chunks_col2[3], ); // Popups - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { - if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_RADIO_QUIT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 50, 70); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_TEXT_HELP, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_RADIO_SAVE, f, popup); - } - } + self.view_popups(f); }); // Put context back to context self.context = Some(ctx); @@ -272,125 +158,127 @@ impl SetupActivity { /// Load values from configuration into input fields pub(crate) fn load_input_values(&mut self) { // Text editor - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) { - let text_editor: String = - String::from(self.config().get_text_editor().as_path().to_string_lossy()); - let props = InputPropsBuilder::from(props) - .with_value(text_editor) - .build(); - let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props); - } + let text_editor: String = + String::from(self.config().get_text_editor().as_path().to_string_lossy()); + assert!(self + .app + .remount( + Id::Config(IdConfig::TextEditor), + Box::new(components::TextEditor::new(text_editor.as_str())), + vec![] + ) + .is_ok()); // Protocol - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) { - let protocol: usize = match self.config().get_default_protocol() { - FileTransferProtocol::Sftp => 0, - FileTransferProtocol::Scp => 1, - FileTransferProtocol::Ftp(false) => 2, - FileTransferProtocol::Ftp(true) => 3, - FileTransferProtocol::AwsS3 => 4, - }; - let props = RadioPropsBuilder::from(props).with_value(protocol).build(); - let _ = self - .view - .update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::DefaultProtocol), + Box::new(components::DefaultProtocol::new( + self.config().get_default_protocol() + )), + vec![] + ) + .is_ok()); // Hidden files - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) { - let hidden: usize = match self.config().get_show_hidden_files() { - true => 0, - false => 1, - }; - let props = RadioPropsBuilder::from(props).with_value(hidden).build(); - let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::HiddenFiles), + Box::new(components::HiddenFiles::new( + self.config().get_show_hidden_files() + )), + vec![] + ) + .is_ok()); // Updates - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) { - let updates: usize = match self.config().get_check_for_updates() { - true => 0, - false => 1, - }; - let props = RadioPropsBuilder::from(props).with_value(updates).build(); - let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::CheckUpdates), + Box::new(components::CheckUpdates::new( + self.config().get_check_for_updates() + )), + vec![] + ) + .is_ok()); // File replace - if let Some(props) = self - .view - .get_props(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE) - { - let updates: usize = match self.config().get_prompt_on_file_replace() { - true => 0, - false => 1, - }; - let props = RadioPropsBuilder::from(props).with_value(updates).build(); - let _ = self - .view - .update(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::PromptOnFileReplace), + Box::new(components::PromptOnFileReplace::new( + self.config().get_prompt_on_file_replace() + )), + vec![] + ) + .is_ok()); // Group dirs - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) { - let dirs: usize = match self.config().get_group_dirs() { - Some(GroupDirs::First) => 0, - Some(GroupDirs::Last) => 1, - None => 2, - }; - let props = RadioPropsBuilder::from(props).with_value(dirs).build(); - let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::GroupDirs), + Box::new(components::GroupDirs::new(self.config().get_group_dirs())), + vec![] + ) + .is_ok()); // Local File Fmt - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) { - let file_fmt: String = self.config().get_local_file_fmt().unwrap_or_default(); - let props = InputPropsBuilder::from(props).with_value(file_fmt).build(); - let _ = self - .view - .update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::LocalFileFmt), + Box::new(components::LocalFileFmt::new( + &self.config().get_local_file_fmt().unwrap_or_default() + )), + vec![] + ) + .is_ok()); // Remote File Fmt - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) { - let file_fmt: String = self.config().get_remote_file_fmt().unwrap_or_default(); - let props = InputPropsBuilder::from(props).with_value(file_fmt).build(); - let _ = self - .view - .update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::RemoteFileFmt), + Box::new(components::RemoteFileFmt::new( + &self.config().get_remote_file_fmt().unwrap_or_default() + )), + vec![] + ) + .is_ok()); // Notifications enabled - if let Some(props) = self - .view - .get_props(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED) - { - let enabled: usize = match self.config().get_notifications() { - true => 0, - false => 1, - }; - let props = RadioPropsBuilder::from(props).with_value(enabled).build(); - let _ = self - .view - .update(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::NotificationsEnabled), + Box::new(components::NotificationsEnabled::new( + self.config().get_notifications() + )), + vec![] + ) + .is_ok()); // Notifications threshold - if let Some(props) = self - .view - .get_props(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD) - { - let value: u64 = self.config().get_notification_threshold(); - let props = BytesPropsBuilder::from(props).with_value(value).build(); - let _ = self - .view - .update(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, props); - } + assert!(self + .app + .remount( + Id::Config(IdConfig::NotificationsThreshold), + Box::new(components::NotificationsThreshold::new(&fmt_bytes( + self.config().get_notification_threshold() + ))), + vec![] + ) + .is_ok()); } /// ### collect_input_values /// /// Collect values from input and put them into the configuration pub(crate) fn collect_input_values(&mut self) { - if let Some(Payload::One(Value::Str(editor))) = - self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR) + if let Ok(State::One(StateValue::String(editor))) = + self.app.state(&Id::Config(IdConfig::TextEditor)) { self.config_mut() .set_text_editor(PathBuf::from(editor.as_str())); } - if let Some(Payload::One(Value::Usize(protocol))) = - self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) + if let Ok(State::One(StateValue::Usize(protocol))) = + self.app.state(&Id::Config(IdConfig::DefaultProtocol)) { let protocol: FileTransferProtocol = match protocol { 1 => FileTransferProtocol::Scp, @@ -401,37 +289,36 @@ impl SetupActivity { }; self.config_mut().set_default_protocol(protocol); } - if let Some(Payload::One(Value::Usize(opt))) = - self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES) + if let Ok(State::One(StateValue::Usize(opt))) = + self.app.state(&Id::Config(IdConfig::HiddenFiles)) { let show: bool = matches!(opt, 0); self.config_mut().set_show_hidden_files(show); } - if let Some(Payload::One(Value::Usize(opt))) = - self.view.get_state(super::COMPONENT_RADIO_UPDATES) + if let Ok(State::One(StateValue::Usize(opt))) = + self.app.state(&Id::Config(IdConfig::CheckUpdates)) { let check: bool = matches!(opt, 0); self.config_mut().set_check_for_updates(check); } - if let Some(Payload::One(Value::Usize(opt))) = self - .view - .get_state(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE) + if let Ok(State::One(StateValue::Usize(opt))) = + self.app.state(&Id::Config(IdConfig::PromptOnFileReplace)) { let check: bool = matches!(opt, 0); self.config_mut().set_prompt_on_file_replace(check); } - if let Some(Payload::One(Value::Str(fmt))) = - self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT) + if let Ok(State::One(StateValue::String(fmt))) = + self.app.state(&Id::Config(IdConfig::LocalFileFmt)) { self.config_mut().set_local_file_fmt(fmt); } - if let Some(Payload::One(Value::Str(fmt))) = - self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT) + if let Ok(State::One(StateValue::String(fmt))) = + self.app.state(&Id::Config(IdConfig::RemoteFileFmt)) { self.config_mut().set_remote_file_fmt(fmt); } - if let Some(Payload::One(Value::Usize(opt))) = - self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS) + if let Ok(State::One(StateValue::Usize(opt))) = + self.app.state(&Id::Config(IdConfig::GroupDirs)) { let dirs: Option = match opt { 0 => Some(GroupDirs::First), @@ -440,15 +327,14 @@ impl SetupActivity { }; self.config_mut().set_group_dirs(dirs); } - if let Some(Payload::One(Value::Usize(opt))) = self - .view - .get_state(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED) + if let Ok(State::One(StateValue::Usize(opt))) = + self.app.state(&Id::Config(IdConfig::NotificationsEnabled)) { self.config_mut().set_notifications(opt == 0); } - if let Some(Payload::One(Value::U64(bytes))) = self - .view - .get_state(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD) + if let Ok(State::One(StateValue::U64(bytes))) = self + .app + .state(&Id::Config(IdConfig::NotificationsThreshold)) { self.config_mut().set_notification_threshold(bytes); } diff --git a/src/ui/activities/setup/view/ssh_keys.rs b/src/ui/activities/setup/view/ssh_keys.rs index b7ddca94..2a501937 100644 --- a/src/ui/activities/setup/view/ssh_keys.rs +++ b/src/ui/activities/setup/view/ssh_keys.rs @@ -27,20 +27,12 @@ * SOFTWARE. */ // Locals -use super::{Context, SetupActivity}; -use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder}; +use super::{components, Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout}; use crate::utils::ui::draw_area_in; + // Ext -use tui_realm_stdlib::{Input, InputPropsBuilder}; -use tuirealm::tui::{ - layout::{Constraint, Direction, Layout}, - style::Color, - widgets::{BorderType, Borders, Clear}, -}; -use tuirealm::{ - props::{Alignment, PropsBuilder}, - View, -}; +use tuirealm::tui::layout::{Constraint, Direction, Layout}; +use tuirealm::tui::widgets::Clear; impl SetupActivity { // -- view @@ -49,34 +41,17 @@ impl SetupActivity { /// /// Initialize ssh keys view pub(super) fn init_ssh_keys(&mut self) { - // Init view - self.view = View::init(); - // Common stuff - // Radio tab - // Radio tab - self.mount_header_tab(1); - // Footer - self.mount_footer(); - self.view.mount( - super::COMPONENT_LIST_SSH_KEYS, - Box::new(BookmarkList::new( - BookmarkListPropsBuilder::default() - .with_title("SSH keys", Alignment::Left) - .with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen) - .with_background(Color::LightGreen) - .with_foreground(Color::Black) - .build(), - )), - ); - // Give focus - self.view.active(super::COMPONENT_LIST_SSH_KEYS); + // Init view (and mount commons) + self.new_app(ViewLayout::SshKeys); // Load keys self.reload_ssh_keys(); + // Give focus + assert!(self.app.active(&Id::Ssh(IdSsh::SshKeys)).is_ok()); } pub(crate) fn view_ssh_keys(&mut self) { let mut ctx: Context = self.context.take().unwrap(); - let _ = ctx.terminal().draw(|f| { + let _ = ctx.terminal().raw_mut().draw(|f| { // Prepare main chunks let chunks = Layout::default() .direction(Direction::Vertical) @@ -91,72 +66,31 @@ impl SetupActivity { ) .split(f.size()); // Render common widget - self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]); - self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]); - self.view - .render(super::COMPONENT_LIST_SSH_KEYS, f, chunks[1]); + self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]); + self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]); + self.app.view(&Id::Ssh(IdSsh::SshKeys), f, chunks[1]); // Popups - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { - if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_RADIO_QUIT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 50, 70); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_TEXT_HELP, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_RADIO_SAVE, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - self.view - .render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 50, 20); - f.render_widget(Clear, popup); - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(3), // Host - Constraint::Length(3), // Username - ] - .as_ref(), - ) - .split(popup); - self.view - .render(super::COMPONENT_INPUT_SSH_HOST, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_INPUT_SSH_USERNAME, f, popup_chunks[1]); - } + self.view_popups(f); + if self.app.mounted(&Id::Ssh(IdSsh::DelSshKeyPopup)) { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::Ssh(IdSsh::DelSshKeyPopup), f, popup); + } else if self.app.mounted(&Id::Ssh(IdSsh::SshHost)) { + let popup = draw_area_in(f.size(), 50, 20); + f.render_widget(Clear, popup); + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), // Host + Constraint::Length(3), // Username + ] + .as_ref(), + ) + .split(popup); + self.app.view(&Id::Ssh(IdSsh::SshHost), f, popup_chunks[0]); + self.app + .view(&Id::Ssh(IdSsh::SshUsername), f, popup_chunks[1]); } }); // Put context back to context @@ -169,82 +103,74 @@ impl SetupActivity { /// /// Mount delete ssh key component pub(crate) fn mount_del_ssh_key(&mut self) { - self.mount_radio_dialog( - super::COMPONENT_RADIO_DEL_SSH_KEY, - "Delete key?", - &["Yes", "No"], - 1, - Color::LightRed, - ); + assert!(self + .app + .remount( + Id::Ssh(IdSsh::DelSshKeyPopup), + Box::new(components::DelSshKeyPopup::default()), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::Ssh(IdSsh::DelSshKeyPopup)).is_ok()); } /// ### umount_del_ssh_key /// /// Umount delete ssh key pub(crate) fn umount_del_ssh_key(&mut self) { - self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY); + let _ = self.app.umount(&Id::Ssh(IdSsh::DelSshKeyPopup)); } /// ### mount_new_ssh_key /// /// Mount new ssh key prompt pub(crate) fn mount_new_ssh_key(&mut self) { - self.view.mount( - super::COMPONENT_INPUT_SSH_HOST, - Box::new(Input::new( - InputPropsBuilder::default() - .with_label("Hostname or address", Alignment::Center) - .with_borders( - Borders::TOP | Borders::RIGHT | Borders::LEFT, - BorderType::Plain, - Color::Reset, - ) - .build(), - )), - ); - self.view.mount( - super::COMPONENT_INPUT_SSH_USERNAME, - Box::new(Input::new( - InputPropsBuilder::default() - .with_label("Username", Alignment::Center) - .with_borders( - Borders::BOTTOM | Borders::RIGHT | Borders::LEFT, - BorderType::Plain, - Color::Reset, - ) - .build(), - )), - ); - self.view.active(super::COMPONENT_INPUT_SSH_HOST); + assert!(self + .app + .remount( + Id::Ssh(IdSsh::SshHost), + Box::new(components::SshHost::default()), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Ssh(IdSsh::SshUsername), + Box::new(components::SshUsername::default()), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::Ssh(IdSsh::SshHost)).is_ok()); } /// ### umount_new_ssh_key /// /// Umount new ssh key prompt pub(crate) fn umount_new_ssh_key(&mut self) { - self.view.umount(super::COMPONENT_INPUT_SSH_HOST); - self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME); + let _ = self.app.umount(&Id::Ssh(IdSsh::SshUsername)); + let _ = self.app.umount(&Id::Ssh(IdSsh::SshHost)); } /// ### reload_ssh_keys /// /// Reload ssh keys pub(crate) fn reload_ssh_keys(&mut self) { - // get props - if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) { - // Create texts - let keys: Vec = self - .config() - .iter_ssh_keys() - .map(|x| { - let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap(); - format!("{} at {}", addr, username) - }) - .collect(); - let props = BookmarkListPropsBuilder::from(props) - .with_bookmarks(keys) - .build(); - self.view.update(super::COMPONENT_LIST_SSH_KEYS, props); - } + let keys: Vec = self + .config() + .iter_ssh_keys() + .map(|x| { + let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap(); + format!("{} at {}", addr, username) + }) + .collect(); + assert!(self + .app + .remount( + Id::Ssh(IdSsh::SshKeys), + Box::new(components::SshKeys::new(&keys)), + vec![] + ) + .is_ok()); } } diff --git a/src/ui/activities/setup/view/theme.rs b/src/ui/activities/setup/view/theme.rs index 2c510344..4fd0361e 100644 --- a/src/ui/activities/setup/view/theme.rs +++ b/src/ui/activities/setup/view/theme.rs @@ -27,22 +27,10 @@ * SOFTWARE. */ // Locals -use super::{Context, SetupActivity}; -use crate::config::themes::Theme; -use crate::ui::components::color_picker::{ColorPicker, ColorPickerPropsBuilder}; -use crate::utils::parser::parse_color; -use crate::utils::ui::draw_area_in; +use super::{components, Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout}; + // Ext -use tui_realm_stdlib::{Label, LabelPropsBuilder}; -use tuirealm::tui::{ - layout::{Constraint, Direction, Layout}, - style::Color, - widgets::{BorderType, Borders, Clear}, -}; -use tuirealm::{ - props::{Alignment, PropsBuilder}, - Payload, Value, View, -}; +use tuirealm::tui::layout::{Constraint, Direction, Layout}; impl SetupActivity { // -- view @@ -51,96 +39,19 @@ impl SetupActivity { /// /// Initialize thene view pub(super) fn init_theme(&mut self) { - // Init view - self.view = View::init(); - // Common stuff - // Radio tab - self.mount_header_tab(2); - // Footer - self.mount_footer(); - // auth colors - self.mount_title(super::COMPONENT_COLOR_AUTH_TITLE, "Authentication styles"); - self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PROTOCOL, "Protocol"); - self.mount_color_picker(super::COMPONENT_COLOR_AUTH_ADDR, "Ip address"); - self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PORT, "Port"); - self.mount_color_picker(super::COMPONENT_COLOR_AUTH_USERNAME, "Username"); - self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PASSWORD, "Password"); - self.mount_color_picker(super::COMPONENT_COLOR_AUTH_BOOKMARKS, "Bookmarks"); - self.mount_color_picker(super::COMPONENT_COLOR_AUTH_RECENTS, "Recent connections"); - // Misc - self.mount_title(super::COMPONENT_COLOR_MISC_TITLE, "Misc styles"); - self.mount_color_picker(super::COMPONENT_COLOR_MISC_ERROR, "Error"); - self.mount_color_picker(super::COMPONENT_COLOR_MISC_INFO, "Info dialogs"); - self.mount_color_picker(super::COMPONENT_COLOR_MISC_INPUT, "Input fields"); - self.mount_color_picker(super::COMPONENT_COLOR_MISC_KEYS, "Key strokes"); - self.mount_color_picker(super::COMPONENT_COLOR_MISC_QUIT, "Quit dialogs"); - self.mount_color_picker(super::COMPONENT_COLOR_MISC_SAVE, "Save confirmations"); - self.mount_color_picker(super::COMPONENT_COLOR_MISC_WARN, "Warnings"); - // Transfer (1) - self.mount_title(super::COMPONENT_COLOR_TRANSFER_TITLE, "Transfer styles"); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, - "Local explorer background", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, - "Local explorer foreground", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, - "Local explorer highlighted", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, - "Remote explorer background", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, - "Remote explorer foreground", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, - "Remote explorer highlighted", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, - "'Full transfer' Progress bar", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, - "'Partial transfer' Progress bar", - ); - // Transfer (2) - self.mount_title( - super::COMPONENT_COLOR_TRANSFER_TITLE_2, - "Transfer styles (2)", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_LOG_BG, - "Log window background", - ); - self.mount_color_picker(super::COMPONENT_COLOR_TRANSFER_LOG_WIN, "Log window"); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING, - "File sorting", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, - "Hidden files", - ); - self.mount_color_picker( - super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC, - "Synchronized browsing", - ); + // Init view (and mount commons) + self.new_app(ViewLayout::Theme); + // Mount titles + self.load_titles(); // Load styles self.load_styles(); // Active first field - self.view.active(super::COMPONENT_COLOR_AUTH_PROTOCOL); + assert!(self.app.active(&Id::Theme(IdTheme::AuthProtocol)).is_ok()); } pub(super) fn view_theme(&mut self) { let mut ctx: Context = self.context.take().unwrap(); - let _ = ctx.terminal().draw(|f| { + let _ = ctx.terminal().raw_mut().draw(|f| { // Prepare main chunks let chunks = Layout::default() .direction(Direction::Vertical) @@ -155,8 +66,8 @@ impl SetupActivity { ) .split(f.size()); // Render common widget - self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]); - self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]); + self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]); + self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]); // Make chunks let colors_layout = Layout::default() .direction(Direction::Horizontal) @@ -186,34 +97,22 @@ impl SetupActivity { .as_ref(), ) .split(colors_layout[0]); - self.view - .render(super::COMPONENT_COLOR_AUTH_TITLE, f, auth_colors_layout[0]); - self.view.render( - super::COMPONENT_COLOR_AUTH_PROTOCOL, - f, - auth_colors_layout[1], - ); - self.view - .render(super::COMPONENT_COLOR_AUTH_ADDR, f, auth_colors_layout[2]); - self.view - .render(super::COMPONENT_COLOR_AUTH_PORT, f, auth_colors_layout[3]); - self.view.render( - super::COMPONENT_COLOR_AUTH_USERNAME, - f, - auth_colors_layout[4], - ); - self.view.render( - super::COMPONENT_COLOR_AUTH_PASSWORD, - f, - auth_colors_layout[5], - ); - self.view.render( - super::COMPONENT_COLOR_AUTH_BOOKMARKS, - f, - auth_colors_layout[6], - ); - self.view.render( - super::COMPONENT_COLOR_AUTH_RECENTS, + self.app + .view(&Id::Theme(IdTheme::AuthTitle), f, auth_colors_layout[0]); + self.app + .view(&Id::Theme(IdTheme::AuthProtocol), f, auth_colors_layout[1]); + self.app + .view(&Id::Theme(IdTheme::AuthAddress), f, auth_colors_layout[2]); + self.app + .view(&Id::Theme(IdTheme::AuthPort), f, auth_colors_layout[3]); + self.app + .view(&Id::Theme(IdTheme::AuthUsername), f, auth_colors_layout[4]); + self.app + .view(&Id::Theme(IdTheme::AuthPassword), f, auth_colors_layout[5]); + self.app + .view(&Id::Theme(IdTheme::AuthBookmarks), f, auth_colors_layout[6]); + self.app.view( + &Id::Theme(IdTheme::AuthRecentHosts), f, auth_colors_layout[7], ); @@ -233,22 +132,22 @@ impl SetupActivity { .as_ref(), ) .split(colors_layout[1]); - self.view - .render(super::COMPONENT_COLOR_MISC_TITLE, f, misc_colors_layout[0]); - self.view - .render(super::COMPONENT_COLOR_MISC_ERROR, f, misc_colors_layout[1]); - self.view - .render(super::COMPONENT_COLOR_MISC_INFO, f, misc_colors_layout[2]); - self.view - .render(super::COMPONENT_COLOR_MISC_INPUT, f, misc_colors_layout[3]); - self.view - .render(super::COMPONENT_COLOR_MISC_KEYS, f, misc_colors_layout[4]); - self.view - .render(super::COMPONENT_COLOR_MISC_QUIT, f, misc_colors_layout[5]); - self.view - .render(super::COMPONENT_COLOR_MISC_SAVE, f, misc_colors_layout[6]); - self.view - .render(super::COMPONENT_COLOR_MISC_WARN, f, misc_colors_layout[7]); + self.app + .view(&Id::Theme(IdTheme::MiscTitle), f, misc_colors_layout[0]); + self.app + .view(&Id::Theme(IdTheme::MiscError), f, misc_colors_layout[1]); + self.app + .view(&Id::Theme(IdTheme::MiscInfo), f, misc_colors_layout[2]); + self.app + .view(&Id::Theme(IdTheme::MiscInput), f, misc_colors_layout[3]); + self.app + .view(&Id::Theme(IdTheme::MiscKeys), f, misc_colors_layout[4]); + self.app + .view(&Id::Theme(IdTheme::MiscQuit), f, misc_colors_layout[5]); + self.app + .view(&Id::Theme(IdTheme::MiscSave), f, misc_colors_layout[6]); + self.app + .view(&Id::Theme(IdTheme::MiscWarn), f, misc_colors_layout[7]); let transfer_colors_layout_col1 = Layout::default() .direction(Direction::Vertical) @@ -266,38 +165,38 @@ impl SetupActivity { .as_ref(), ) .split(colors_layout[2]); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_TITLE, + self.app.view( + &Id::Theme(IdTheme::TransferTitle), f, transfer_colors_layout_col1[0], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, + self.app.view( + &Id::Theme(IdTheme::ExplorerLocalBg), f, transfer_colors_layout_col1[1], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, + self.app.view( + &Id::Theme(IdTheme::ExplorerLocalFg), f, transfer_colors_layout_col1[2], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, + self.app.view( + &Id::Theme(IdTheme::ExplorerLocalHg), f, transfer_colors_layout_col1[3], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, + self.app.view( + &Id::Theme(IdTheme::ExplorerRemoteBg), f, transfer_colors_layout_col1[4], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, + self.app.view( + &Id::Theme(IdTheme::ExplorerRemoteFg), f, transfer_colors_layout_col1[5], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, + self.app.view( + &Id::Theme(IdTheme::ExplorerRemoteHg), f, transfer_colors_layout_col1[6], ); @@ -317,332 +216,328 @@ impl SetupActivity { .as_ref(), ) .split(colors_layout[3]); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_TITLE_2, + self.app.view( + &Id::Theme(IdTheme::TransferTitle2), f, transfer_colors_layout_col2[0], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, + self.app.view( + &Id::Theme(IdTheme::ProgBarFull), f, transfer_colors_layout_col2[1], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, + self.app.view( + &Id::Theme(IdTheme::ProgBarPartial), f, transfer_colors_layout_col2[2], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_LOG_BG, + self.app.view( + &Id::Theme(IdTheme::LogBg), f, transfer_colors_layout_col2[3], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_LOG_WIN, + self.app.view( + &Id::Theme(IdTheme::LogWindow), f, transfer_colors_layout_col2[4], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING, + self.app.view( + &Id::Theme(IdTheme::StatusSorting), f, transfer_colors_layout_col2[5], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, + self.app.view( + &Id::Theme(IdTheme::StatusHidden), f, transfer_colors_layout_col2[6], ); - self.view.render( - super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC, + self.app.view( + &Id::Theme(IdTheme::StatusSync), f, transfer_colors_layout_col2[7], ); // Popups - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { - if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_RADIO_QUIT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 50, 70); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_TEXT_HELP, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - self.view.render(super::COMPONENT_RADIO_SAVE, f, popup); - } - } + self.view_popups(f); }); // Put context back to context self.context = Some(ctx); } + fn load_titles(&mut self) { + assert!(self + .app + .remount( + Id::Theme(IdTheme::AuthTitle), + Box::new(components::AuthTitle::default()), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::MiscTitle), + Box::new(components::MiscTitle::default()), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::TransferTitle), + Box::new(components::TransferTitle::default()), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::TransferTitle2), + Box::new(components::TransferTitle2::default()), + vec![] + ) + .is_ok()); + } + /// ### load_styles /// /// Load values from theme into input fields pub(crate) fn load_styles(&mut self) { let theme: Theme = self.theme().clone(); - self.update_color(super::COMPONENT_COLOR_AUTH_ADDR, theme.auth_address); - self.update_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS, theme.auth_bookmarks); - self.update_color(super::COMPONENT_COLOR_AUTH_PASSWORD, theme.auth_password); - self.update_color(super::COMPONENT_COLOR_AUTH_PORT, theme.auth_port); - self.update_color(super::COMPONENT_COLOR_AUTH_PROTOCOL, theme.auth_protocol); - self.update_color(super::COMPONENT_COLOR_AUTH_RECENTS, theme.auth_recents); - self.update_color(super::COMPONENT_COLOR_AUTH_USERNAME, theme.auth_username); - self.update_color(super::COMPONENT_COLOR_MISC_ERROR, theme.misc_error_dialog); - self.update_color(super::COMPONENT_COLOR_MISC_INFO, theme.misc_info_dialog); - self.update_color(super::COMPONENT_COLOR_MISC_INPUT, theme.misc_input_dialog); - self.update_color(super::COMPONENT_COLOR_MISC_KEYS, theme.misc_keys); - self.update_color(super::COMPONENT_COLOR_MISC_QUIT, theme.misc_quit_dialog); - self.update_color(super::COMPONENT_COLOR_MISC_SAVE, theme.misc_save_dialog); - self.update_color(super::COMPONENT_COLOR_MISC_WARN, theme.misc_warn_dialog); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, - theme.transfer_local_explorer_background, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, - theme.transfer_local_explorer_foreground, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, - theme.transfer_local_explorer_highlighted, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, - theme.transfer_remote_explorer_background, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, - theme.transfer_remote_explorer_foreground, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, - theme.transfer_remote_explorer_highlighted, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, - theme.transfer_progress_bar_full, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, - theme.transfer_progress_bar_partial, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_LOG_BG, - theme.transfer_log_background, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_LOG_WIN, - theme.transfer_log_window, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING, - theme.transfer_status_sorting, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, - theme.transfer_status_hidden, - ); - self.update_color( - super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC, - theme.transfer_status_sync_browsing, - ); - } - - /// ### collect_styles - /// - /// Collect values from input and put them into the theme. - /// If a component has an invalid color, returns Err(component_id) - pub(crate) fn collect_styles(&mut self) -> Result<(), &'static str> { - // auth - let auth_address: Color = self - .get_color(super::COMPONENT_COLOR_AUTH_ADDR) - .map_err(|_| super::COMPONENT_COLOR_AUTH_ADDR)?; - let auth_bookmarks: Color = self - .get_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS) - .map_err(|_| super::COMPONENT_COLOR_AUTH_BOOKMARKS)?; - let auth_password: Color = self - .get_color(super::COMPONENT_COLOR_AUTH_PASSWORD) - .map_err(|_| super::COMPONENT_COLOR_AUTH_PASSWORD)?; - let auth_port: Color = self - .get_color(super::COMPONENT_COLOR_AUTH_PORT) - .map_err(|_| super::COMPONENT_COLOR_AUTH_PORT)?; - let auth_protocol: Color = self - .get_color(super::COMPONENT_COLOR_AUTH_PROTOCOL) - .map_err(|_| super::COMPONENT_COLOR_AUTH_PROTOCOL)?; - let auth_recents: Color = self - .get_color(super::COMPONENT_COLOR_AUTH_RECENTS) - .map_err(|_| super::COMPONENT_COLOR_AUTH_RECENTS)?; - let auth_username: Color = self - .get_color(super::COMPONENT_COLOR_AUTH_USERNAME) - .map_err(|_| super::COMPONENT_COLOR_AUTH_USERNAME)?; - // misc - let misc_error_dialog: Color = self - .get_color(super::COMPONENT_COLOR_MISC_ERROR) - .map_err(|_| super::COMPONENT_COLOR_MISC_ERROR)?; - let misc_info_dialog: Color = self - .get_color(super::COMPONENT_COLOR_MISC_INFO) - .map_err(|_| super::COMPONENT_COLOR_MISC_INFO)?; - let misc_input_dialog: Color = self - .get_color(super::COMPONENT_COLOR_MISC_INPUT) - .map_err(|_| super::COMPONENT_COLOR_MISC_INPUT)?; - let misc_keys: Color = self - .get_color(super::COMPONENT_COLOR_MISC_KEYS) - .map_err(|_| super::COMPONENT_COLOR_MISC_KEYS)?; - let misc_quit_dialog: Color = self - .get_color(super::COMPONENT_COLOR_MISC_QUIT) - .map_err(|_| super::COMPONENT_COLOR_MISC_QUIT)?; - let misc_save_dialog: Color = self - .get_color(super::COMPONENT_COLOR_MISC_SAVE) - .map_err(|_| super::COMPONENT_COLOR_MISC_SAVE)?; - let misc_warn_dialog: Color = self - .get_color(super::COMPONENT_COLOR_MISC_WARN) - .map_err(|_| super::COMPONENT_COLOR_MISC_WARN)?; - // transfer - let transfer_local_explorer_background: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)?; - let transfer_local_explorer_foreground: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)?; - let transfer_local_explorer_highlighted: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)?; - let transfer_remote_explorer_background: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)?; - let transfer_remote_explorer_foreground: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)?; - let transfer_remote_explorer_highlighted: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)?; - let transfer_log_background: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_LOG_BG) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_BG)?; - let transfer_log_window: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_LOG_WIN) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_WIN)?; - let transfer_progress_bar_full: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)?; - let transfer_progress_bar_partial: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)?; - let transfer_status_hidden: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)?; - let transfer_status_sorting: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)?; - let transfer_status_sync_browsing: Color = self - .get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC) - .map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)?; - // Update theme - let mut theme: &mut Theme = self.theme_mut(); - theme.auth_address = auth_address; - theme.auth_bookmarks = auth_bookmarks; - theme.auth_password = auth_password; - theme.auth_port = auth_port; - theme.auth_protocol = auth_protocol; - theme.auth_recents = auth_recents; - theme.auth_username = auth_username; - theme.misc_error_dialog = misc_error_dialog; - theme.misc_info_dialog = misc_info_dialog; - theme.misc_input_dialog = misc_input_dialog; - theme.misc_keys = misc_keys; - theme.misc_quit_dialog = misc_quit_dialog; - theme.misc_save_dialog = misc_save_dialog; - theme.misc_warn_dialog = misc_warn_dialog; - theme.transfer_local_explorer_background = transfer_local_explorer_background; - theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground; - theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted; - theme.transfer_remote_explorer_background = transfer_remote_explorer_background; - theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground; - theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted; - theme.transfer_log_background = transfer_log_background; - theme.transfer_log_window = transfer_log_window; - theme.transfer_progress_bar_full = transfer_progress_bar_full; - theme.transfer_progress_bar_partial = transfer_progress_bar_partial; - theme.transfer_status_hidden = transfer_status_hidden; - theme.transfer_status_sorting = transfer_status_sorting; - theme.transfer_status_sync_browsing = transfer_status_sync_browsing; - Ok(()) - } - - /// ### update_color - /// - /// Update color for provided component - fn update_color(&mut self, component: &str, color: Color) { - if let Some(props) = self.view.get_props(component) { - self.view.update( - component, - ColorPickerPropsBuilder::from(props) - .with_color(&color) - .build(), - ); - } - } - - /// ### get_color - /// - /// Get color from component - fn get_color(&self, component: &str) -> Result { - match self.view.get_state(component) { - Some(Payload::One(Value::Str(color))) => match parse_color(color.as_str()) { - Some(c) => Ok(c), - None => Err(()), - }, - _ => Err(()), - } - } - - /// ### mount_color_picker - /// - /// Mount color picker with provided data - fn mount_color_picker(&mut self, id: &str, label: &str) { - self.view.mount( - id, - Box::new(ColorPicker::new( - ColorPickerPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, Color::Reset) - .with_label(label.to_string(), Alignment::Left) - .build(), - )), - ); - } - - /// ### mount_title - /// - /// Mount title - fn mount_title(&mut self, id: &str, text: &str) { - self.view.mount( - id, - Box::new(Label::new( - LabelPropsBuilder::default() - .bold() - .with_text(text.to_string()) - .build(), - )), - ); + assert!(self + .app + .remount( + Id::Theme(IdTheme::AuthAddress), + Box::new(components::AuthAddress::new(theme.auth_address)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::AuthBookmarks), + Box::new(components::AuthBookmarks::new(theme.auth_bookmarks)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::AuthPassword), + Box::new(components::AuthPassword::new(theme.auth_password)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::AuthPort), + Box::new(components::AuthPort::new(theme.auth_port)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::AuthProtocol), + Box::new(components::AuthProtocol::new(theme.auth_protocol)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::AuthRecentHosts), + Box::new(components::AuthRecentHosts::new(theme.auth_recents)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::AuthUsername), + Box::new(components::AuthUsername::new(theme.auth_username)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::MiscError), + Box::new(components::MiscError::new(theme.misc_error_dialog)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::MiscInfo), + Box::new(components::MiscInfo::new(theme.misc_info_dialog)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::MiscInput), + Box::new(components::MiscInput::new(theme.misc_input_dialog)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::MiscKeys), + Box::new(components::MiscKeys::new(theme.misc_keys)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::MiscQuit), + Box::new(components::MiscQuit::new(theme.misc_quit_dialog)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::MiscSave), + Box::new(components::MiscSave::new(theme.misc_save_dialog)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::MiscWarn), + Box::new(components::MiscWarn::new(theme.misc_warn_dialog)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::ExplorerLocalBg), + Box::new(components::ExplorerLocalBg::new( + theme.transfer_local_explorer_background + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::ExplorerLocalFg), + Box::new(components::ExplorerLocalFg::new( + theme.transfer_local_explorer_foreground + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::ExplorerLocalHg), + Box::new(components::ExplorerLocalHg::new( + theme.transfer_local_explorer_highlighted + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::ExplorerRemoteBg), + Box::new(components::ExplorerRemoteBg::new( + theme.transfer_remote_explorer_background + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::ExplorerRemoteFg), + Box::new(components::ExplorerRemoteFg::new( + theme.transfer_remote_explorer_foreground + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::ExplorerRemoteHg), + Box::new(components::ExplorerRemoteHg::new( + theme.transfer_remote_explorer_highlighted + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::ProgBarFull), + Box::new(components::ProgBarFull::new( + theme.transfer_progress_bar_full + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::ProgBarPartial), + Box::new(components::ProgBarPartial::new( + theme.transfer_progress_bar_partial + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::LogBg), + Box::new(components::LogBg::new(theme.transfer_log_background)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::LogWindow), + Box::new(components::LogWindow::new(theme.transfer_log_window)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::StatusSorting), + Box::new(components::StatusSorting::new( + theme.transfer_status_sorting + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::StatusHidden), + Box::new(components::StatusHidden::new(theme.transfer_status_hidden)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::Theme(IdTheme::StatusSync), + Box::new(components::StatusSync::new( + theme.transfer_status_sync_browsing + )), + vec![] + ) + .is_ok()); } } diff --git a/src/ui/components/bookmark_list.rs b/src/ui/components/bookmark_list.rs deleted file mode 100644 index 01d81c0d..00000000 --- a/src/ui/components/bookmark_list.rs +++ /dev/null @@ -1,456 +0,0 @@ -//! ## Bookmark list -//! -//! `BookmarkList` component renders a bookmark list tab - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// ext -use tui_realm_stdlib::utils::get_block; -use tuirealm::event::{Event, KeyCode}; -use tuirealm::props::{Alignment, BlockTitle, BordersProps, Props, PropsBuilder}; -use tuirealm::tui::{ - layout::{Corner, Rect}, - style::{Color, Style}, - text::Span, - widgets::{BorderType, Borders, List, ListItem, ListState}, -}; -use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value}; - -// -- props -const PROP_BOOKMARKS: &str = "bookmarks"; - -pub struct BookmarkListPropsBuilder { - props: Option, -} - -impl Default for BookmarkListPropsBuilder { - fn default() -> Self { - BookmarkListPropsBuilder { - props: Some(Props::default()), - } - } -} - -impl PropsBuilder for BookmarkListPropsBuilder { - fn build(&mut self) -> Props { - self.props.take().unwrap() - } - - fn hidden(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = false; - } - self - } - - fn visible(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = true; - } - self - } -} - -impl From for BookmarkListPropsBuilder { - fn from(props: Props) -> Self { - BookmarkListPropsBuilder { props: Some(props) } - } -} - -impl BookmarkListPropsBuilder { - /// ### with_foreground - /// - /// Set foreground color for area - pub fn with_foreground(&mut self, color: Color) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.foreground = color; - } - self - } - - /// ### with_background - /// - /// Set background color for area - pub fn with_background(&mut self, color: Color) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.background = color; - } - self - } - - /// ### with_borders - /// - /// Set component borders style - pub fn with_borders( - &mut self, - borders: Borders, - variant: BorderType, - color: Color, - ) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.borders = BordersProps { - borders, - variant, - color, - } - } - self - } - - pub fn with_title>(&mut self, text: S, alignment: Alignment) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.title = Some(BlockTitle::new(text, alignment)); - } - self - } - - pub fn with_bookmarks(&mut self, bookmarks: Vec) -> &mut Self { - if let Some(props) = self.props.as_mut() { - let bookmarks: Vec = bookmarks.into_iter().map(PropValue::Str).collect(); - props - .own - .insert(PROP_BOOKMARKS, PropPayload::Vec(bookmarks)); - } - self - } -} - -// -- states - -/// ## OwnStates -/// -/// OwnStates contains states for this component -#[derive(Clone)] -struct OwnStates { - list_index: usize, // Index of selected element in list - list_len: usize, // Length of file list - focus: bool, // Has focus? -} - -impl Default for OwnStates { - fn default() -> Self { - OwnStates { - list_index: 0, - list_len: 0, - focus: false, - } - } -} - -impl OwnStates { - /// ### set_list_len - /// - /// Set list length - pub fn set_list_len(&mut self, len: usize) { - self.list_len = len; - } - - /// ### get_list_index - /// - /// Return current value for list index - pub fn get_list_index(&self) -> usize { - self.list_index - } - - /// ### incr_list_index - /// - /// Incremenet list index - pub fn incr_list_index(&mut self) { - // Check if index is at last element - if self.list_index + 1 < self.list_len { - self.list_index += 1; - } - } - - /// ### decr_list_index - /// - /// Decrement list index - pub fn decr_list_index(&mut self) { - // Check if index is bigger than 0 - if self.list_index > 0 { - self.list_index -= 1; - } - } - - /// ### reset_list_index - /// - /// Reset list index to 0 - pub fn reset_list_index(&mut self) { - self.list_index = 0; - } -} - -// -- Component - -/// ## BookmarkList -/// -/// Bookmark list component -pub struct BookmarkList { - props: Props, - states: OwnStates, -} - -impl BookmarkList { - /// ### new - /// - /// Instantiates a new FileList starting from Props - /// The method also initializes the component states. - pub fn new(props: Props) -> Self { - // Initialize states - let mut states: OwnStates = OwnStates::default(); - // Set list length - states.set_list_len(Self::bookmarks_len(&props)); - BookmarkList { props, states } - } - - fn bookmarks_len(props: &Props) -> usize { - match props.own.get(PROP_BOOKMARKS) { - None => 0, - Some(bookmarks) => bookmarks.unwrap_vec().len(), - } - } -} - -impl Component for BookmarkList { - #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Frame, area: Rect) { - if self.props.visible { - // Make list - let list_item: Vec = match self.props.own.get(PROP_BOOKMARKS) { - Some(PropPayload::Vec(lines)) => lines - .iter() - .map(|x| x.unwrap_str()) - .map(|x| ListItem::new(Span::from(x.to_string()))) - .collect(), - _ => vec![], - }; - let (fg, bg): (Color, Color) = match self.states.focus { - true => (self.props.foreground, self.props.background), - false => (Color::Reset, Color::Reset), - }; - // Render - let mut state: ListState = ListState::default(); - state.select(Some(self.states.list_index)); - render.render_stateful_widget( - List::new(list_item) - .block(get_block( - &self.props.borders, - self.props.title.as_ref(), - self.states.focus, - )) - .start_corner(Corner::TopLeft) - .highlight_style( - Style::default() - .bg(bg) - .fg(fg) - .add_modifier(self.props.modifiers), - ), - area, - &mut state, - ); - } - } - - fn update(&mut self, props: Props) -> Msg { - self.props = props; - // re-Set list length - self.states.set_list_len(Self::bookmarks_len(&self.props)); - // Reset list index - self.states.reset_list_index(); - Msg::None - } - - fn get_props(&self) -> Props { - self.props.clone() - } - - fn on(&mut self, ev: Event) -> Msg { - // Match event - if let Event::Key(key) = ev { - match key.code { - KeyCode::Down => { - // Update states - self.states.incr_list_index(); - Msg::None - } - KeyCode::Up => { - // Update states - self.states.decr_list_index(); - Msg::None - } - KeyCode::PageDown => { - // Update states - for _ in 0..8 { - self.states.incr_list_index(); - } - Msg::None - } - KeyCode::PageUp => { - // Update states - for _ in 0..8 { - self.states.decr_list_index(); - } - Msg::None - } - KeyCode::Enter => { - // Report event - Msg::OnSubmit(self.get_state()) - } - _ => { - // Return key event to activity - Msg::OnKey(key) - } - } - } else { - // Unhandled event - Msg::None - } - } - - fn get_state(&self) -> Payload { - Payload::One(Value::Usize(self.states.get_list_index())) - } - - fn blur(&mut self) { - self.states.focus = false; - } - - fn active(&mut self) { - self.states.focus = true; - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - use pretty_assertions::assert_eq; - use tuirealm::event::KeyEvent; - - #[test] - fn test_ui_components_bookmarks_list() { - // Make component - let mut component: BookmarkList = BookmarkList::new( - BookmarkListPropsBuilder::default() - .hidden() - .visible() - .with_foreground(Color::Red) - .with_background(Color::Blue) - .with_borders(Borders::ALL, BorderType::Double, Color::Red) - .with_title("filelist", Alignment::Left) - .with_bookmarks(vec![String::from("file1"), String::from("file2")]) - .build(), - ); - assert_eq!(component.props.foreground, Color::Red); - assert_eq!(component.props.background, Color::Blue); - assert_eq!(component.props.visible, true); - assert_eq!(component.props.title.as_ref().unwrap().text(), "filelist"); - assert_eq!( - component - .props - .own - .get(PROP_BOOKMARKS) - .unwrap() - .unwrap_vec() - .len(), - 2 - ); - // Verify states - assert_eq!(component.states.list_index, 0); - assert_eq!(component.states.list_len, 2); - assert_eq!(component.states.focus, false); - // Focus - component.active(); - assert_eq!(component.states.focus, true); - component.blur(); - assert_eq!(component.states.focus, false); - // Update - let props = BookmarkListPropsBuilder::from(component.get_props()) - .with_foreground(Color::Yellow) - .hidden() - .build(); - assert_eq!(component.update(props), Msg::None); - assert_eq!(component.props.foreground, Color::Yellow); - assert_eq!(component.props.visible, false); - // Increment list index - component.states.list_index += 1; - assert_eq!(component.states.list_index, 1); - // Update - component.update( - BookmarkListPropsBuilder::from(component.get_props()) - .with_bookmarks(vec![ - String::from("file1"), - String::from("file2"), - String::from("file3"), - ]) - .build(), - ); - // Verify states - assert_eq!(component.states.list_index, 0); - assert_eq!(component.states.list_len, 3); - // get value - assert_eq!(component.get_state(), Payload::One(Value::Usize(0))); - // Render - assert_eq!(component.states.list_index, 0); - // Handle inputs - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Down))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 1); - // Index should be decremented - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Up))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 0); - // Index should be 2 - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 2); - // Index should be 0 - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 0); - // Enter - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Enter))), - Msg::OnSubmit(Payload::One(Value::Usize(0))) - ); - // On key - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))), - Msg::OnKey(KeyEvent::from(KeyCode::Backspace)) - ); - } -} diff --git a/src/ui/components/bytes.rs b/src/ui/components/bytes.rs deleted file mode 100644 index 92d2a387..00000000 --- a/src/ui/components/bytes.rs +++ /dev/null @@ -1,310 +0,0 @@ -//! ## Bytes -//! -//! `Bytes` component extends an `Input` component in order to provide an input type for byte size. - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// locals -use crate::utils::fmt::fmt_bytes; -use crate::utils::parser::parse_bytesize; -// ext -use tui_realm_stdlib::{Input, InputPropsBuilder}; -use tuirealm::event::Event; -use tuirealm::props::{Alignment, Props, PropsBuilder}; -use tuirealm::tui::{ - layout::Rect, - style::Color, - widgets::{BorderType, Borders}, -}; -use tuirealm::{Component, Frame, Msg, Payload, Value}; - -// -- props - -/// ## BytesPropsBuilder -/// -/// A wrapper around an `InputPropsBuilder` -pub struct BytesPropsBuilder { - puppet: InputPropsBuilder, -} - -impl Default for BytesPropsBuilder { - fn default() -> Self { - Self { - puppet: InputPropsBuilder::default(), - } - } -} - -impl PropsBuilder for BytesPropsBuilder { - fn build(&mut self) -> Props { - self.puppet.build() - } - - fn hidden(&mut self) -> &mut Self { - self.puppet.hidden(); - self - } - - fn visible(&mut self) -> &mut Self { - self.puppet.visible(); - self - } -} - -impl From for BytesPropsBuilder { - fn from(props: Props) -> Self { - BytesPropsBuilder { - puppet: InputPropsBuilder::from(props), - } - } -} - -impl BytesPropsBuilder { - /// ### with_borders - /// - /// Set component borders style - pub fn with_borders( - &mut self, - borders: Borders, - variant: BorderType, - color: Color, - ) -> &mut Self { - self.puppet.with_borders(borders, variant, color); - self - } - - /// ### with_label - /// - /// Set input label - pub fn with_label>(&mut self, label: S, alignment: Alignment) -> &mut Self { - self.puppet.with_label(label, alignment); - self - } - - /// ### with_color - /// - /// Set initial value for component - pub fn with_foreground(&mut self, color: Color) -> &mut Self { - self.puppet.with_foreground(color); - self - } - - /// ### with_color - /// - /// Set initial value for component - pub fn with_value(&mut self, val: u64) -> &mut Self { - self.puppet.with_value(fmt_bytes(val)); - self - } -} - -// -- component - -/// ## Bytes -/// -/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker -pub struct Bytes { - input: Input, - native_color: Color, -} - -impl Bytes { - /// ### new - /// - /// Instantiate a new `Bytes` - pub fn new(props: Props) -> Self { - // Instantiate a new color picker using input - Self { - native_color: props.foreground, - input: Input::new(props), - } - } - - /// ### update_colors - /// - /// Update colors to match selected color, with provided one - fn update_colors(&mut self, color: Color) { - let mut props = self.get_props(); - props.foreground = color; - props.borders.color = color; - let _ = self.input.update(props); - } -} - -impl Component for Bytes { - /// ### render - /// - /// Based on the current properties and states, renders a widget using the provided render engine in the provided Area - /// If focused, cursor is also set (if supported by widget) - #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Frame, area: Rect) { - self.input.render(render, area); - } - - /// ### update - /// - /// Update component properties - /// Properties should first be retrieved through `get_props` which creates a builder from - /// existing properties and then edited before calling update. - /// Returns a Msg to the view - fn update(&mut self, props: Props) -> Msg { - let msg: Msg = self.input.update(props); - match msg { - Msg::OnChange(Payload::One(Value::Str(input))) => { - match parse_bytesize(input.as_str()) { - Some(bytes) => { - // return OK - self.update_colors(self.native_color); - Msg::OnChange(Payload::One(Value::U64(bytes.as_u64()))) - } - None => { - // Invalid color - self.update_colors(Color::Red); - Msg::None - } - } - } - msg => msg, - } - } - - /// ### get_props - /// - /// Returns a props builder starting from component properties. - /// This returns a prop builder in order to make easier to create - /// new properties for the element. - fn get_props(&self) -> Props { - self.input.get_props() - } - - /// ### on - /// - /// Handle input event and update internal states. - /// Returns a Msg to the view - fn on(&mut self, ev: Event) -> Msg { - // Capture message from input - match self.input.on(ev) { - Msg::OnChange(Payload::One(Value::Str(input))) => { - // Capture color and validate - match parse_bytesize(input.as_str()) { - Some(bytes) => { - // Update color and return OK - self.update_colors(self.native_color); - Msg::OnChange(Payload::One(Value::U64(bytes.as_u64()))) - } - None => { - // Invalid color - self.update_colors(Color::Red); - Msg::None - } - } - } - Msg::OnSubmit(_) => Msg::None, - msg => msg, - } - } - - /// ### get_state - /// - /// Get current state from component - /// For this component returns Unsigned if the input type is a number, otherwise a text - /// The value is always the current input. - fn get_state(&self) -> Payload { - match self.input.get_state() { - Payload::One(Value::Str(bytes)) => match parse_bytesize(bytes.as_str()) { - None => Payload::None, - Some(bytes) => Payload::One(Value::U64(bytes.as_u64())), - }, - _ => Payload::None, - } - } - - // -- events - - /// ### blur - /// - /// Blur component; basically remove focus - fn blur(&mut self) { - self.input.blur(); - } - - /// ### active - /// - /// Active component; basically give focus - fn active(&mut self) { - self.input.active(); - } -} - -#[cfg(test)] -mod test { - use super::*; - - use crossterm::event::{KeyCode, KeyEvent}; - use pretty_assertions::assert_eq; - - #[test] - fn bytes_input() { - let mut component: Bytes = Bytes::new( - BytesPropsBuilder::default() - .visible() - .with_value(1024) - .with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0)) - .with_label("omar", Alignment::Left) - .with_foreground(Color::Red) - .build(), - ); - // Focus - component.blur(); - component.active(); - // Get value - assert_eq!(component.get_state(), Payload::One(Value::U64(1024))); - // Set an invalid color - let props = InputPropsBuilder::from(component.get_props()) - .with_value(String::from("#pippo1")) - .hidden() - .build(); - assert_eq!(component.update(props), Msg::None); - assert_eq!(component.get_state(), Payload::None); - // Reset color - let props = BytesPropsBuilder::from(component.get_props()) - .with_value(111) - .hidden() - .build(); - assert_eq!( - component.update(props), - Msg::OnChange(Payload::One(Value::U64(111))) - ); - // Backspace (invalid) - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))), - Msg::None - ); - // Press '1' - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Char('B')))), - Msg::OnChange(Payload::One(Value::U64(111))) - ); - } -} diff --git a/src/ui/components/color_picker.rs b/src/ui/components/color_picker.rs deleted file mode 100644 index 36ae99de..00000000 --- a/src/ui/components/color_picker.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! ## ColorPicker -//! -//! `ColorPicker` component extends an `Input` component in order to provide some extra features -//! for the color picker. - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// locals -use crate::utils::fmt::fmt_color; -use crate::utils::parser::parse_color; -// ext -use tui_realm_stdlib::{Input, InputPropsBuilder}; -use tuirealm::event::Event; -use tuirealm::props::{Alignment, Props, PropsBuilder}; -use tuirealm::tui::{ - layout::Rect, - style::Color, - widgets::{BorderType, Borders}, -}; -use tuirealm::{Component, Frame, Msg, Payload, Value}; - -// -- props - -/// ## ColorPickerPropsBuilder -/// -/// A wrapper around an `InputPropsBuilder` -pub struct ColorPickerPropsBuilder { - puppet: InputPropsBuilder, -} - -impl Default for ColorPickerPropsBuilder { - fn default() -> Self { - Self { - puppet: InputPropsBuilder::default(), - } - } -} - -impl PropsBuilder for ColorPickerPropsBuilder { - fn build(&mut self) -> Props { - self.puppet.build() - } - - fn hidden(&mut self) -> &mut Self { - self.puppet.hidden(); - self - } - - fn visible(&mut self) -> &mut Self { - self.puppet.visible(); - self - } -} - -impl From for ColorPickerPropsBuilder { - fn from(props: Props) -> Self { - ColorPickerPropsBuilder { - puppet: InputPropsBuilder::from(props), - } - } -} - -impl ColorPickerPropsBuilder { - /// ### with_borders - /// - /// Set component borders style - pub fn with_borders( - &mut self, - borders: Borders, - variant: BorderType, - color: Color, - ) -> &mut Self { - self.puppet.with_borders(borders, variant, color); - self - } - - /// ### with_label - /// - /// Set input label - pub fn with_label>(&mut self, label: S, alignment: Alignment) -> &mut Self { - self.puppet.with_label(label, alignment); - self - } - - /// ### with_color - /// - /// Set initial value for component - pub fn with_color(&mut self, color: &Color) -> &mut Self { - self.puppet.with_value(fmt_color(color)); - self - } -} - -// -- component - -/// ## ColorPicker -/// -/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker -pub struct ColorPicker { - input: Input, -} - -impl ColorPicker { - /// ### new - /// - /// Instantiate a new `ColorPicker` - pub fn new(props: Props) -> Self { - // Instantiate a new color picker using input - Self { - input: Input::new(props), - } - } - - /// ### update_colors - /// - /// Update colors to match selected color, with provided one - fn update_colors(&mut self, color: Color) { - let mut props = self.get_props(); - props.foreground = color; - props.borders.color = color; - let _ = self.input.update(props); - } -} - -impl Component for ColorPicker { - /// ### render - /// - /// Based on the current properties and states, renders a widget using the provided render engine in the provided Area - /// If focused, cursor is also set (if supported by widget) - #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Frame, area: Rect) { - self.input.render(render, area); - } - - /// ### update - /// - /// Update component properties - /// Properties should first be retrieved through `get_props` which creates a builder from - /// existing properties and then edited before calling update. - /// Returns a Msg to the view - fn update(&mut self, props: Props) -> Msg { - let msg: Msg = self.input.update(props); - match msg { - Msg::OnChange(Payload::One(Value::Str(input))) => match parse_color(input.as_str()) { - Some(color) => { - // Update color and return OK - self.update_colors(color); - Msg::OnChange(Payload::One(Value::Str(input))) - } - None => { - // Invalid color - self.update_colors(Color::Red); - Msg::None - } - }, - msg => msg, - } - } - - /// ### get_props - /// - /// Returns a props builder starting from component properties. - /// This returns a prop builder in order to make easier to create - /// new properties for the element. - fn get_props(&self) -> Props { - self.input.get_props() - } - - /// ### on - /// - /// Handle input event and update internal states. - /// Returns a Msg to the view - fn on(&mut self, ev: Event) -> Msg { - // Capture message from input - match self.input.on(ev) { - Msg::OnChange(Payload::One(Value::Str(input))) => { - // Capture color and validate - match parse_color(input.as_str()) { - Some(color) => { - // Update color and return OK - self.update_colors(color); - Msg::OnChange(Payload::One(Value::Str(input))) - } - None => { - // Invalid color - self.update_colors(Color::Red); - Msg::None - } - } - } - Msg::OnSubmit(_) => Msg::None, - msg => msg, - } - } - - /// ### get_state - /// - /// Get current state from component - /// For this component returns Unsigned if the input type is a number, otherwise a text - /// The value is always the current input. - fn get_state(&self) -> Payload { - match self.input.get_state() { - Payload::One(Value::Str(color)) => match parse_color(color.as_str()) { - None => Payload::None, - Some(_) => Payload::One(Value::Str(color)), - }, - _ => Payload::None, - } - } - - // -- events - - /// ### blur - /// - /// Blur component; basically remove focus - fn blur(&mut self) { - self.input.blur(); - } - - /// ### active - /// - /// Active component; basically give focus - fn active(&mut self) { - self.input.active(); - } -} - -#[cfg(test)] -mod test { - use super::*; - - use crossterm::event::{KeyCode, KeyEvent}; - use pretty_assertions::assert_eq; - - #[test] - fn test_ui_components_color_picker() { - let mut component: ColorPicker = ColorPicker::new( - ColorPickerPropsBuilder::default() - .visible() - .with_color(&Color::Rgb(204, 170, 0)) - .with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0)) - .with_label("omar", Alignment::Left) - .build(), - ); - // Focus - component.blur(); - component.active(); - // Get value - assert_eq!( - component.get_state(), - Payload::One(Value::Str(String::from("#ccaa00"))) - ); - // Set an invalid color - let props = InputPropsBuilder::from(component.get_props()) - .with_value(String::from("#pippo1")) - .hidden() - .build(); - assert_eq!(component.update(props), Msg::None); - assert_eq!(component.get_state(), Payload::None); - // Reset color - let props = ColorPickerPropsBuilder::from(component.get_props()) - .with_color(&Color::Rgb(204, 170, 0)) - .hidden() - .build(); - assert_eq!( - component.update(props), - Msg::OnChange(Payload::One(Value::Str("#ccaa00".to_string()))) - ); - // Backspace (invalid) - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))), - Msg::None - ); - // Press '1' - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Char('1')))), - Msg::OnChange(Payload::One(Value::Str(String::from("#ccaa01")))) - ); - } -} diff --git a/src/ui/components/file_list.rs b/src/ui/components/file_list.rs deleted file mode 100644 index 59829737..00000000 --- a/src/ui/components/file_list.rs +++ /dev/null @@ -1,765 +0,0 @@ -//! ## FileList -//! -//! `FileList` component renders a file list tab - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// ext -use tui_realm_stdlib::utils::get_block; -use tuirealm::event::{Event, KeyCode, KeyModifiers}; -use tuirealm::props::{ - Alignment, BlockTitle, BordersProps, PropPayload, PropValue, Props, PropsBuilder, -}; -use tuirealm::tui::{ - layout::{Corner, Rect}, - style::{Color, Style}, - text::Span, - widgets::{BorderType, Borders, List, ListItem, ListState}, -}; -use tuirealm::{Component, Frame, Msg, Payload, Value}; - -// -- props - -const PROP_FILES: &str = "files"; -const PALETTE_HIGHLIGHT_COLOR: &str = "props-highlight-color"; - -pub struct FileListPropsBuilder { - props: Option, -} - -impl Default for FileListPropsBuilder { - fn default() -> Self { - FileListPropsBuilder { - props: Some(Props::default()), - } - } -} - -impl PropsBuilder for FileListPropsBuilder { - fn build(&mut self) -> Props { - self.props.take().unwrap() - } - - fn hidden(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = false; - } - self - } - - fn visible(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = true; - } - self - } -} - -impl From for FileListPropsBuilder { - fn from(props: Props) -> Self { - FileListPropsBuilder { props: Some(props) } - } -} - -impl FileListPropsBuilder { - /// ### with_foreground - /// - /// Set foreground color for area - pub fn with_foreground(&mut self, color: Color) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.foreground = color; - } - self - } - - /// ### with_background - /// - /// Set background color for area - pub fn with_background(&mut self, color: Color) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.background = color; - } - self - } - - /// ### with_highlight_color - /// - /// Set highlighted color - pub fn with_highlight_color(&mut self, color: Color) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.palette.insert(PALETTE_HIGHLIGHT_COLOR, color); - } - self - } - - /// ### with_borders - /// - /// Set component borders style - pub fn with_borders( - &mut self, - borders: Borders, - variant: BorderType, - color: Color, - ) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.borders = BordersProps { - borders, - variant, - color, - } - } - self - } - - pub fn with_title>(&mut self, text: S, alignment: Alignment) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.title = Some(BlockTitle::new(text, alignment)); - } - self - } - - pub fn with_files(&mut self, files: Vec) -> &mut Self { - if let Some(props) = self.props.as_mut() { - let files: Vec = files.into_iter().map(PropValue::Str).collect(); - props.own.insert(PROP_FILES, PropPayload::Vec(files)); - } - self - } -} - -// -- states - -/// ## OwnStates -/// -/// OwnStates contains states for this component -#[derive(Clone)] -struct OwnStates { - list_index: usize, // Index of selected element in list - selected: Vec, // Selected files - focus: bool, // Has focus? -} - -impl Default for OwnStates { - fn default() -> Self { - OwnStates { - list_index: 0, - selected: Vec::new(), - focus: false, - } - } -} - -impl OwnStates { - /// ### init_list_states - /// - /// Initialize list states - pub fn init_list_states(&mut self, len: usize) { - self.selected = Vec::with_capacity(len); - self.fix_list_index(); - } - - /// ### list_index - /// - /// Return current value for list index - pub fn list_index(&self) -> usize { - self.list_index - } - - /// ### incr_list_index - /// - /// Incremenet list index. - /// If `can_rewind` is `true` the index rewinds when boundary is reached - pub fn incr_list_index(&mut self, can_rewind: bool) { - // Check if index is at last element - if self.list_index + 1 < self.list_len() { - self.list_index += 1; - } else if can_rewind { - self.list_index = 0; - } - } - - /// ### decr_list_index - /// - /// Decrement list index - /// If `can_rewind` is `true` the index rewinds when boundary is reached - pub fn decr_list_index(&mut self, can_rewind: bool) { - // Check if index is bigger than 0 - if self.list_index > 0 { - self.list_index -= 1; - } else if self.list_len() > 0 && can_rewind { - self.list_index = self.list_len() - 1; - } - } - - /// ### list_len - /// - /// Returns the length of the file list, which is actually the capacity of the selection vector - pub fn list_len(&self) -> usize { - self.selected.capacity() - } - - /// ### is_selected - /// - /// Returns whether the file with index `entry` is selected - pub fn is_selected(&self, entry: usize) -> bool { - self.selected.contains(&entry) - } - - /// ### is_selection_empty - /// - /// Returns whether the selection is currently empty - pub fn is_selection_empty(&self) -> bool { - self.selected.is_empty() - } - - /// ### get_selection - /// - /// Returns current file selection - pub fn get_selection(&self) -> Vec { - self.selected.clone() - } - - /// ### fix_list_index - /// - /// Keep index if possible, otherwise set to lenght - 1 - fn fix_list_index(&mut self) { - if self.list_index >= self.list_len() && self.list_len() > 0 { - self.list_index = self.list_len() - 1; - } else if self.list_len() == 0 { - self.list_index = 0; - } - } - - // -- select manipulation - - /// ### toggle_file - /// - /// Select or deselect file with provided entry index - pub fn toggle_file(&mut self, entry: usize) { - match self.is_selected(entry) { - true => self.deselect(entry), - false => self.select(entry), - } - } - - /// ### select_all - /// - /// Select all files - pub fn select_all(&mut self) { - for i in 0..self.list_len() { - self.select(i); - } - } - - /// ### select - /// - /// Select provided index if not selected yet - fn select(&mut self, entry: usize) { - if !self.is_selected(entry) { - self.selected.push(entry); - } - } - - /// ### deselect - /// - /// Remove element file with associated index - fn deselect(&mut self, entry: usize) { - if self.is_selected(entry) { - self.selected.retain(|&x| x != entry); - } - } -} - -// -- Component - -/// ## FileList -/// -/// File list component -pub struct FileList { - props: Props, - states: OwnStates, -} - -impl FileList { - /// ### new - /// - /// Instantiates a new FileList starting from Props - /// The method also initializes the component states. - pub fn new(props: Props) -> Self { - // Initialize states - let mut states: OwnStates = OwnStates::default(); - // Init list states - states.init_list_states(Self::files_len(&props)); - FileList { props, states } - } - - fn files_len(props: &Props) -> usize { - match props.own.get(PROP_FILES) { - None => 0, - Some(files) => files.unwrap_vec().len(), - } - } -} - -impl Component for FileList { - #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Frame, area: Rect) { - if self.props.visible { - // Make list - let list_item: Vec = match self.props.own.get(PROP_FILES) { - Some(PropPayload::Vec(lines)) => lines - .iter() - .enumerate() - .map(|(num, line)| { - let to_display: String = match self.states.is_selected(num) { - true => format!("*{}", line.unwrap_str()), - false => line.unwrap_str().to_string(), - }; - ListItem::new(Span::from(to_display)) - }) - .collect(), - _ => vec![], - }; - let highlighted_color: Color = match self.props.palette.get(PALETTE_HIGHLIGHT_COLOR) { - Some(c) => *c, - _ => Color::Reset, - }; - let (h_fg, h_bg): (Color, Color) = match self.states.focus { - true => (Color::Black, highlighted_color), - false => (highlighted_color, self.props.background), - }; - // Render - let mut state: ListState = ListState::default(); - state.select(Some(self.states.list_index)); - render.render_stateful_widget( - List::new(list_item) - .block(get_block( - &self.props.borders, - self.props.title.as_ref(), - self.states.focus, - )) - .start_corner(Corner::TopLeft) - .style( - Style::default() - .fg(self.props.foreground) - .bg(self.props.background), - ) - .highlight_style( - Style::default() - .bg(h_bg) - .fg(h_fg) - .add_modifier(self.props.modifiers), - ), - area, - &mut state, - ); - } - } - - fn update(&mut self, props: Props) -> Msg { - self.props = props; - // re-Set list states - self.states.init_list_states(Self::files_len(&self.props)); - Msg::None - } - - fn get_props(&self) -> Props { - self.props.clone() - } - - fn on(&mut self, ev: Event) -> Msg { - // Match event - if let Event::Key(key) = ev { - match key.code { - KeyCode::Down => { - // Update states - self.states.incr_list_index(true); - Msg::None - } - KeyCode::Up => { - // Update states - self.states.decr_list_index(true); - Msg::None - } - KeyCode::PageDown => { - // Update states - for _ in 0..8 { - self.states.incr_list_index(false); - } - Msg::None - } - KeyCode::PageUp => { - // Update states - for _ in 0..8 { - self.states.decr_list_index(false); - } - Msg::None - } - KeyCode::Char('a') => match key.modifiers.intersects(KeyModifiers::CONTROL) { - // CTRL+A - true => { - // Select all - self.states.select_all(); - Msg::None - } - false => Msg::OnKey(key), - }, - KeyCode::Char('m') => { - // Toggle current file in selection - self.states.toggle_file(self.states.list_index()); - Msg::None - } - KeyCode::Enter => Msg::OnSubmit(self.get_state()), - _ => { - // Return key event to activity - Msg::OnKey(key) - } - } - } else { - // Unhandled event - Msg::None - } - } - - /// ### get_state - /// - /// Get state returns for this component two different payloads based on the states: - /// - if the file selection is empty, returns the highlighted item as `One` of `Usize` - /// - if at least one item is selected, return the selected as a `Vec` of `Usize` - fn get_state(&self) -> Payload { - match self.states.is_selection_empty() { - true => Payload::One(Value::Usize(self.states.list_index())), - false => Payload::Vec( - self.states - .get_selection() - .into_iter() - .map(Value::Usize) - .collect(), - ), - } - } - - // -- events - - /// ### blur - /// - /// Blur component; basically remove focus - fn blur(&mut self) { - self.states.focus = false; - } - - /// ### active - /// - /// Active component; basically give focus - fn active(&mut self) { - self.states.focus = true; - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - use pretty_assertions::assert_eq; - use tuirealm::event::{KeyEvent, KeyModifiers}; - - #[test] - fn test_ui_components_file_list_states() { - let mut states: OwnStates = OwnStates::default(); - assert_eq!(states.list_len(), 0); - assert_eq!(states.selected.len(), 0); - assert_eq!(states.focus, false); - // Init states - states.init_list_states(4); - assert_eq!(states.list_len(), 4); - assert_eq!(states.selected.len(), 0); - assert!(states.is_selection_empty()); - // Select all files - states.select_all(); - assert_eq!(states.list_len(), 4); - assert_eq!(states.selected.len(), 4); - assert_eq!(states.is_selection_empty(), false); - assert_eq!(states.get_selection(), vec![0, 1, 2, 3]); - // Verify reset - states.init_list_states(5); - assert_eq!(states.list_len(), 5); - assert_eq!(states.selected.len(), 0); - // Toggle file - states.toggle_file(2); - assert_eq!(states.list_len(), 5); - assert_eq!(states.selected.len(), 1); - assert_eq!(states.selected[0], 2); - states.toggle_file(4); - assert_eq!(states.list_len(), 5); - assert_eq!(states.selected.len(), 2); - assert_eq!(states.selected[1], 4); - states.toggle_file(2); - assert_eq!(states.list_len(), 5); - assert_eq!(states.selected.len(), 1); - assert_eq!(states.selected[0], 4); - // Select twice (nothing should change) - states.select(4); - assert_eq!(states.list_len(), 5); - assert_eq!(states.selected.len(), 1); - assert_eq!(states.selected[0], 4); - // Deselect not-selectd item - states.deselect(2); - assert_eq!(states.list_len(), 5); - assert_eq!(states.selected.len(), 1); - assert_eq!(states.selected[0], 4); - // Index - states.init_list_states(2); - // Incr - states.incr_list_index(false); - assert_eq!(states.list_index(), 1); - states.incr_list_index(false); - assert_eq!(states.list_index(), 1); - states.incr_list_index(true); - assert_eq!(states.list_index(), 0); - // Decr - states.list_index = 1; - states.decr_list_index(false); - assert_eq!(states.list_index(), 0); - states.decr_list_index(false); - assert_eq!(states.list_index(), 0); - states.decr_list_index(true); - assert_eq!(states.list_index(), 1); - // Try fixing index - states.init_list_states(5); - states.list_index = 4; - states.init_list_states(3); - assert_eq!(states.list_index(), 2); - states.init_list_states(6); - assert_eq!(states.list_index(), 2); - // Focus - states.focus = true; - assert_eq!(states.focus, true); - } - - #[test] - fn test_ui_components_file_list() { - // Make component - let mut component: FileList = FileList::new( - FileListPropsBuilder::default() - .hidden() - .visible() - .with_foreground(Color::Red) - .with_background(Color::Blue) - .with_highlight_color(Color::LightRed) - .with_borders(Borders::ALL, BorderType::Double, Color::Red) - .with_title("files", Alignment::Left) - .with_files(vec![String::from("file1"), String::from("file2")]) - .build(), - ); - assert_eq!( - *component - .props - .palette - .get(PALETTE_HIGHLIGHT_COLOR) - .unwrap(), - Color::LightRed - ); - assert_eq!(component.props.foreground, Color::Red); - assert_eq!(component.props.background, Color::Blue); - assert_eq!(component.props.visible, true); - assert_eq!(component.props.title.as_ref().unwrap().text(), "files"); - assert_eq!( - component - .props - .own - .get(PROP_FILES) - .as_ref() - .unwrap() - .unwrap_vec() - .len(), - 2 - ); - // Verify states - assert_eq!(component.states.list_index, 0); - assert_eq!(component.states.selected.len(), 0); - assert_eq!(component.states.list_len(), 2); - assert_eq!(component.states.selected.capacity(), 2); - assert_eq!(component.states.focus, false); - // Focus - component.active(); - assert_eq!(component.states.focus, true); - component.blur(); - assert_eq!(component.states.focus, false); - // Update - let props = FileListPropsBuilder::from(component.get_props()) - .with_foreground(Color::Yellow) - .hidden() - .build(); - assert_eq!(component.update(props), Msg::None); - assert_eq!(component.props.visible, false); - assert_eq!(component.props.foreground, Color::Yellow); - // Increment list index - component.states.list_index += 1; - assert_eq!(component.states.list_index, 1); - // Update - component.update( - FileListPropsBuilder::from(component.get_props()) - .with_files(vec![ - String::from("file1"), - String::from("file2"), - String::from("file3"), - ]) - .build(), - ); - // Verify states - assert_eq!(component.states.list_index, 1); // Kept - assert_eq!(component.states.list_len(), 3); - // get value - assert_eq!(component.get_state(), Payload::One(Value::Usize(1))); - // Render - assert_eq!(component.states.list_index, 1); - // Handle inputs - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Down))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 2); - // Index should be decremented - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Up))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 1); - // Index should be 2 - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 2); - // Index should be 0 - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 0); - // Enter - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Enter))), - Msg::OnSubmit(Payload::One(Value::Usize(0))) - ); - // On key - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))), - Msg::OnKey(KeyEvent::from(KeyCode::Backspace)) - ); - // Verify 'A' still works - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Char('a')))), - Msg::OnKey(KeyEvent::from(KeyCode::Char('a'))) - ); - // Ctrl + a - assert_eq!( - component.on(Event::Key(KeyEvent::new( - KeyCode::Char('a'), - KeyModifiers::CONTROL - ))), - Msg::None - ); - assert_eq!(component.states.selected.len(), component.states.list_len()); - } - - #[test] - fn test_ui_components_file_list_selection() { - // Make component - let mut component: FileList = FileList::new( - FileListPropsBuilder::default() - .with_files(vec![ - String::from("file1"), - String::from("file2"), - String::from("file3"), - ]) - .build(), - ); - // Get state - assert_eq!(component.get_state(), Payload::One(Value::Usize(0))); - // Select one - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))), - Msg::None - ); - // Now should be a vec - assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(0)])); - // De-select - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))), - Msg::None - ); - assert_eq!(component.get_state(), Payload::One(Value::Usize(0))); - // Go down - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Down))), - Msg::None - ); - // Select - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))), - Msg::None - ); - assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(1)])); - // Go down and select - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Down))), - Msg::None - ); - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))), - Msg::None - ); - assert_eq!( - component.get_state(), - Payload::Vec(vec![Value::Usize(1), Value::Usize(2)]) - ); - // Select all - assert_eq!( - component.on(Event::Key(KeyEvent { - code: KeyCode::Char('a'), - modifiers: KeyModifiers::CONTROL, - })), - Msg::None - ); - // All selected - assert_eq!( - component.get_state(), - Payload::Vec(vec![Value::Usize(1), Value::Usize(2), Value::Usize(0)]) - ); - // Update files - component.update( - FileListPropsBuilder::from(component.get_props()) - .with_files(vec![String::from("file1"), String::from("file2")]) - .build(), - ); - // Selection should now be empty - assert_eq!(component.get_state(), Payload::One(Value::Usize(1))); - } -} diff --git a/src/ui/components/logbox.rs b/src/ui/components/logbox.rs deleted file mode 100644 index 41ac7d11..00000000 --- a/src/ui/components/logbox.rs +++ /dev/null @@ -1,433 +0,0 @@ -//! ## LogBox -//! -//! `LogBox` component renders a log box view - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// ext -use tui_realm_stdlib::utils::{get_block, wrap_spans}; -use tuirealm::event::{Event, KeyCode}; -use tuirealm::props::{ - Alignment, BlockTitle, BordersProps, Props, PropsBuilder, Table as TextTable, -}; -use tuirealm::tui::{ - layout::{Corner, Rect}, - style::{Color, Style}, - widgets::{BorderType, Borders, List, ListItem, ListState}, -}; -use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value}; - -// -- props - -const PROP_TABLE: &str = "table"; - -pub struct LogboxPropsBuilder { - props: Option, -} - -impl Default for LogboxPropsBuilder { - fn default() -> Self { - LogboxPropsBuilder { - props: Some(Props::default()), - } - } -} - -impl PropsBuilder for LogboxPropsBuilder { - fn build(&mut self) -> Props { - self.props.take().unwrap() - } - - fn hidden(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = false; - } - self - } - - fn visible(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = true; - } - self - } -} - -impl From for LogboxPropsBuilder { - fn from(props: Props) -> Self { - LogboxPropsBuilder { props: Some(props) } - } -} - -impl LogboxPropsBuilder { - /// ### with_borders - /// - /// Set component borders style - pub fn with_borders( - &mut self, - borders: Borders, - variant: BorderType, - color: Color, - ) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.borders = BordersProps { - borders, - variant, - color, - } - } - self - } - - /// ### with_background - /// - /// Set background color for area - pub fn with_background(&mut self, color: Color) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.background = color; - } - self - } - - pub fn with_title>(&mut self, text: S, alignment: Alignment) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.title = Some(BlockTitle::new(text, alignment)); - } - self - } - - pub fn with_log(&mut self, table: TextTable) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props - .own - .insert(PROP_TABLE, PropPayload::One(PropValue::Table(table))); - } - self - } -} - -// -- states - -/// ## OwnStates -/// -/// OwnStates contains states for this component -#[derive(Clone)] -struct OwnStates { - list_index: usize, // Index of selected element in list - list_len: usize, // Length of file list - focus: bool, // Has focus? -} - -impl Default for OwnStates { - fn default() -> Self { - OwnStates { - list_index: 0, - list_len: 0, - focus: false, - } - } -} - -impl OwnStates { - /// ### set_list_len - /// - /// Set list length - pub fn set_list_len(&mut self, len: usize) { - self.list_len = len; - } - - /// ### get_list_index - /// - /// Return current value for list index - pub fn get_list_index(&self) -> usize { - self.list_index - } - - /// ### incr_list_index - /// - /// Incremenet list index - pub fn incr_list_index(&mut self) { - // Check if index is at last element - if self.list_index + 1 < self.list_len { - self.list_index += 1; - } - } - - /// ### decr_list_index - /// - /// Decrement list index - pub fn decr_list_index(&mut self) { - // Check if index is bigger than 0 - if self.list_index > 0 { - self.list_index -= 1; - } - } - - /// ### reset_list_index - /// - /// Reset list index to last element - pub fn reset_list_index(&mut self) { - self.list_index = 0; // Last element is always 0 - } -} - -// -- Component - -/// ## LogBox -/// -/// LogBox list component -pub struct LogBox { - props: Props, - states: OwnStates, -} - -impl LogBox { - /// ### new - /// - /// Instantiates a new FileList starting from Props - /// The method also initializes the component states. - pub fn new(props: Props) -> Self { - // Initialize states - let mut states: OwnStates = OwnStates::default(); - // Set list length - states.set_list_len(Self::table_len(&props)); - // Reset list index - states.reset_list_index(); - LogBox { props, states } - } - - fn table_len(props: &Props) -> usize { - match props.own.get(PROP_TABLE) { - Some(PropPayload::One(PropValue::Table(table))) => table.len(), - _ => 0, - } - } -} - -impl Component for LogBox { - #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Frame, area: Rect) { - if self.props.visible { - let width: usize = area.width as usize - 4; - // Make list - let list_items: Vec = match self.props.own.get(PROP_TABLE) { - Some(PropPayload::One(PropValue::Table(table))) => table - .iter() - .map(|row| ListItem::new(wrap_spans(row, width, &self.props))) - .collect(), // Make List item from TextSpan - _ => Vec::new(), - }; - let w = List::new(list_items) - .block(get_block( - &self.props.borders, - self.props.title.as_ref(), - self.states.focus, - )) - .start_corner(Corner::BottomLeft) - .highlight_symbol(">> ") - .style(Style::default().bg(self.props.background)) - .highlight_style(Style::default().add_modifier(self.props.modifiers)); - let mut state: ListState = ListState::default(); - state.select(Some(self.states.list_index)); - render.render_stateful_widget(w, area, &mut state); - } - } - - fn update(&mut self, props: Props) -> Msg { - self.props = props; - // re-Set list length - self.states.set_list_len(Self::table_len(&self.props)); - // Reset list index - self.states.reset_list_index(); - Msg::None - } - - fn get_props(&self) -> Props { - self.props.clone() - } - - fn on(&mut self, ev: Event) -> Msg { - // Match event - if let Event::Key(key) = ev { - match key.code { - KeyCode::Up => { - // Update states - self.states.incr_list_index(); - Msg::None - } - KeyCode::Down => { - // Update states - self.states.decr_list_index(); - Msg::None - } - KeyCode::PageUp => { - // Update states - for _ in 0..8 { - self.states.incr_list_index(); - } - Msg::None - } - KeyCode::PageDown => { - // Update states - for _ in 0..8 { - self.states.decr_list_index(); - } - Msg::None - } - _ => { - // Return key event to activity - Msg::OnKey(key) - } - } - } else { - // Unhandled event - Msg::None - } - } - - fn get_state(&self) -> Payload { - Payload::One(Value::Usize(self.states.get_list_index())) - } - - fn blur(&mut self) { - self.states.focus = false; - } - - fn active(&mut self) { - self.states.focus = true; - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - use pretty_assertions::assert_eq; - use tuirealm::event::{KeyCode, KeyEvent}; - use tuirealm::props::{TableBuilder, TextSpan}; - use tuirealm::tui::style::Color; - - #[test] - fn test_ui_components_logbox() { - let mut component: LogBox = LogBox::new( - LogboxPropsBuilder::default() - .hidden() - .visible() - .with_borders(Borders::ALL, BorderType::Double, Color::Red) - .with_background(Color::Blue) - .with_title("Log", Alignment::Left) - .with_log( - TableBuilder::default() - .add_col(TextSpan::from("12:29")) - .add_col(TextSpan::from("system crashed")) - .add_row() - .add_col(TextSpan::from("12:38")) - .add_col(TextSpan::from("system alive")) - .build(), - ) - .build(), - ); - assert_eq!(component.props.visible, true); - assert_eq!(component.props.background, Color::Blue); - assert_eq!(component.props.title.as_ref().unwrap().text(), "Log"); - // Verify states - assert_eq!(component.states.list_index, 0); - assert_eq!(component.states.list_len, 2); - assert_eq!(component.states.focus, false); - // Focus - component.active(); - assert_eq!(component.states.focus, true); - component.blur(); - assert_eq!(component.states.focus, false); - // Update - let props = LogboxPropsBuilder::from(component.get_props()) - .hidden() - .build(); - assert_eq!(component.update(props), Msg::None); - assert_eq!(component.props.visible, false); - // Increment list index - component.states.list_index += 1; - assert_eq!(component.states.list_index, 1); - // Update - component.update( - LogboxPropsBuilder::from(component.get_props()) - .with_log( - TableBuilder::default() - .add_col(TextSpan::from("12:29")) - .add_col(TextSpan::from("system crashed")) - .add_row() - .add_col(TextSpan::from("12:38")) - .add_col(TextSpan::from("system alive")) - .add_row() - .add_col(TextSpan::from("12:41")) - .add_col(TextSpan::from("system is going down for REBOOT")) - .build(), - ) - .build(), - ); - // Verify states - assert_eq!(component.states.list_index, 0); // Last item - assert_eq!(component.states.list_len, 3); - // get value - assert_eq!(component.get_state(), Payload::One(Value::Usize(0))); - // RenderData - assert_eq!(component.states.list_index, 0); - // Set cursor to 0 - component.states.list_index = 0; - // Handle inputs - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Up))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 1); - // Index should be decremented - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Down))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 0); - // Index should be 2 - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 2); - // Index should be 0 - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))), - Msg::None - ); - // Index should be incremented - assert_eq!(component.states.list_index, 0); - // On key - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))), - Msg::OnKey(KeyEvent::from(KeyCode::Backspace)) - ); - } -} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs deleted file mode 100644 index 342483df..00000000 --- a/src/ui/components/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! ## Components -//! -//! `Components` is the module which contains the definitions for all the GUI components for termscp - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// exports -pub mod bookmark_list; -pub mod bytes; -pub mod color_picker; -pub mod file_list; -pub mod logbox; diff --git a/src/ui/context.rs b/src/ui/context.rs index 37ce5457..776cb38f 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -26,34 +26,21 @@ * SOFTWARE. */ // Locals -use super::input::InputHandler; use super::store::Store; use crate::filetransfer::FileTransferParams; use crate::system::config_client::ConfigClient; use crate::system::theme_provider::ThemeProvider; -// Includes -#[cfg(target_family = "unix")] -use crossterm::{ - event::DisableMouseCapture, - execute, - terminal::{EnterAlternateScreen, LeaveAlternateScreen}, -}; -use std::io::{stdout, Stdout}; -use tuirealm::tui::backend::CrosstermBackend; -use tuirealm::tui::Terminal; - -type TuiTerminal = Terminal>; +use tuirealm::terminal::TerminalBridge; /// ## Context /// -/// Context holds data structures used by the ui +/// Context holds data structures shared by the activities pub struct Context { ft_params: Option, config_client: ConfigClient, pub(crate) store: Store, - input_hnd: InputHandler, - pub(crate) terminal: TuiTerminal, + pub(crate) terminal: TerminalBridge, theme_provider: ThemeProvider, error: Option, } @@ -71,8 +58,7 @@ impl Context { ft_params: None, config_client, store: Store::init(), - input_hnd: InputHandler::new(), - terminal: Terminal::new(CrosstermBackend::new(Self::stdout())).unwrap(), + terminal: TerminalBridge::new().expect("Could not initialize terminal"), theme_provider, error, } @@ -92,10 +78,6 @@ impl Context { &mut self.config_client } - pub(crate) fn input_hnd(&self) -> &InputHandler { - &self.input_hnd - } - pub(crate) fn store(&self) -> &Store { &self.store } @@ -112,7 +94,7 @@ impl Context { &mut self.theme_provider } - pub fn terminal(&mut self) -> &mut TuiTerminal { + pub fn terminal(&mut self) -> &mut TerminalBridge { &mut self.terminal } @@ -137,75 +119,13 @@ impl Context { pub fn error(&mut self) -> Option { self.error.take() } - - /// ### enter_alternate_screen - /// - /// Enter alternate screen (gui window) - #[cfg(target_family = "unix")] - pub fn enter_alternate_screen(&mut self) { - match execute!( - self.terminal.backend_mut(), - EnterAlternateScreen, - DisableMouseCapture - ) { - Err(err) => error!("Failed to enter alternate screen: {}", err), - Ok(_) => info!("Entered alternate screen"), - } - } - - /// ### enter_alternate_screen - /// - /// Enter alternate screen (gui window) - #[cfg(target_family = "windows")] - pub fn enter_alternate_screen(&self) {} - - /// ### leave_alternate_screen - /// - /// Go back to normal screen (gui window) - #[cfg(target_family = "unix")] - pub fn leave_alternate_screen(&mut self) { - match execute!( - self.terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ) { - Err(err) => error!("Failed to leave alternate screen: {}", err), - Ok(_) => info!("Left alternate screen"), - } - } - - /// ### leave_alternate_screen - /// - /// Go back to normal screen (gui window) - #[cfg(target_family = "windows")] - pub fn leave_alternate_screen(&self) {} - - /// ### clear_screen - /// - /// Clear terminal screen - pub fn clear_screen(&mut self) { - match self.terminal.clear() { - Err(err) => error!("Failed to clear screen: {}", err), - Ok(_) => info!("Cleared screen"), - } - } - - #[cfg(target_family = "unix")] - fn stdout() -> Stdout { - let mut stdout = stdout(); - assert!(execute!(stdout, EnterAlternateScreen).is_ok()); - stdout - } - - #[cfg(target_family = "windows")] - fn stdout() -> Stdout { - stdout() - } } impl Drop for Context { fn drop(&mut self) { // Re-enable terminal stuff - self.leave_alternate_screen(); + let _ = self.terminal.disable_raw_mode(); + let _ = self.terminal.leave_alternate_screen(); + let _ = self.terminal.clear_screen(); } } diff --git a/src/ui/input.rs b/src/ui/input.rs deleted file mode 100644 index 083d4887..00000000 --- a/src/ui/input.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! ## Input -//! -//! `input` is the module which provides all the functionalities related to input events in the user interface - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -use crossterm::event::{poll, read, Event}; -use std::time::Duration; - -/// ## InputHandler -/// -/// InputHandler is the struct which runs a thread which waits for -/// input events from the user and reports them through a receiver -pub(crate) struct InputHandler; - -impl InputHandler { - /// ### InputHandler - /// - /// - pub(crate) fn new() -> InputHandler { - InputHandler {} - } - - /// ### fetch_events - /// - /// Check if new events have been received from handler - #[allow(dead_code)] - pub(crate) fn fetch_events(&self) -> Result, ()> { - let mut inbox: Vec = Vec::new(); - loop { - match self.read_event() { - Ok(ev_opt) => match ev_opt { - Some(ev) => inbox.push(ev), - None => break, - }, - Err(_) => return Err(()), - } - } - Ok(inbox) - } - - /// ### read_event - /// - /// Read event from input listener - pub(crate) fn read_event(&self) -> Result, ()> { - if let Ok(available) = poll(Duration::from_millis(10)) { - match available { - true => { - // Read event - if let Ok(ev) = read() { - Ok(Some(ev)) - } else { - Err(()) - } - } - false => Ok(None), - } - } else { - Err(()) - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_ui_input_new() { - let _: InputHandler = InputHandler::new(); - } - - /* ERRORS ON GITHUB ACTIONS - #[test] - fn test_ui_input_fetch() { - let input_hnd: InputHandler = InputHandler::new(); - // Try recv - assert_eq!(input_hnd.fetch_messages().ok().unwrap().len(), 0); - }*/ -} diff --git a/src/ui/keymap.rs b/src/ui/keymap.rs deleted file mode 100644 index 99bc124f..00000000 --- a/src/ui/keymap.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! ## Keymap -//! -//! Keymap contains pub constants which can be used in the `update` function to match messages - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -use tuirealm::event::{KeyCode, KeyEvent, KeyModifiers}; -use tuirealm::Msg; - -// -- Special keys - -pub const MSG_KEY_ENTER: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_ESC: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Esc, - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_TAB: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Tab, - modifiers: KeyModifiers::NONE, -}); -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, -}); -pub const MSG_KEY_LEFT: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_RIGHT: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Right, - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_UP: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Up, - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_SPACE: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char(' '), - modifiers: KeyModifiers::NONE, -}); - -// -- char keys - -pub const MSG_KEY_CHAR_A: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('a'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_B: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('b'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_C: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_D: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('d'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_E: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('e'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_F: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('f'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_G: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('g'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_H: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('h'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_I: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('i'), - modifiers: KeyModifiers::NONE, -}); -/* -pub const MSG_KEY_CHAR_J: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('j'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_K: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('k'), - modifiers: KeyModifiers::NONE, -}); -*/ -pub const MSG_KEY_CHAR_L: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('l'), - modifiers: KeyModifiers::NONE, -}); -/* -pub const MSG_KEY_CHAR_M: Msg = Msg::OnKey(KeyEvent { NOTE: used for mark - code: KeyCode::Char('m'), - modifiers: KeyModifiers::NONE, -}); -*/ -pub const MSG_KEY_CHAR_N: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('n'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_O: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('o'), - modifiers: KeyModifiers::NONE, -}); -/* -pub const MSG_KEY_CHAR_P: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('p'), - modifiers: KeyModifiers::NONE, -}); -*/ -pub const MSG_KEY_CHAR_Q: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_R: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('r'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_S: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('s'), - modifiers: KeyModifiers::NONE, -}); -/* -pub const MSG_KEY_CHAR_T: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('t'), - modifiers: KeyModifiers::NONE, -}); -*/ -pub const MSG_KEY_CHAR_U: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('u'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_V: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('v'), - modifiers: KeyModifiers::NONE, -}); -pub const MSG_KEY_CHAR_W: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('w'), - modifiers: KeyModifiers::NONE, -}); -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, -}); -*/ - -// -- control -pub const MSG_KEY_CTRL_C: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, -}); -pub const MSG_KEY_CTRL_E: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('e'), - modifiers: KeyModifiers::CONTROL, -}); -pub const MSG_KEY_CTRL_H: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('h'), - modifiers: KeyModifiers::CONTROL, -}); -pub const MSG_KEY_CTRL_N: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('n'), - modifiers: KeyModifiers::CONTROL, -}); -pub const MSG_KEY_CTRL_R: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('r'), - modifiers: KeyModifiers::CONTROL, -}); -pub const MSG_KEY_CTRL_S: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('s'), - modifiers: KeyModifiers::CONTROL, -}); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 268a5ab3..4b7cc906 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -27,8 +27,5 @@ */ // Modules pub mod activities; -pub(crate) mod components; pub mod context; -pub(crate) mod input; -pub(crate) mod keymap; pub(crate) mod store; diff --git a/src/utils/parser.rs b/src/utils/parser.rs index b6d8841b..d640a8e0 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -101,7 +101,7 @@ lazy_static! { * - group 1: amount (number) * - group 4: unit (K, M, G, T, P) */ - static ref BYTESIZE_REGEX: Regex = Regex::new(r"(:?([0-9])+)( )*(:?[KMGTP])?B").unwrap(); + static ref BYTESIZE_REGEX: Regex = Regex::new(r"(:?([0-9])+)( )*(:?[KMGTP])?B$").unwrap(); } // -- remote opts @@ -1133,5 +1133,8 @@ mod tests { assert_eq!(parse_bytesize("2 GB").unwrap().as_u64(), 2147483648); assert_eq!(parse_bytesize("1 TB").unwrap().as_u64(), 1099511627776); assert!(parse_bytesize("1 XB").is_none()); + assert!(parse_bytesize("1 GB aaaaa").is_none()); + assert!(parse_bytesize("1 GBaaaaa").is_none()); + assert!(parse_bytesize("1MBaaaaa").is_none()); } } From b36d3d571209e6129963c16eecb98b55b1d91b93 Mon Sep 17 00:00:00 2001 From: veeso Date: Wed, 1 Dec 2021 20:29:18 +0100 Subject: [PATCH 13/45] Removed key modifiers none on popups, since on windows they won't work --- .../filetransfer/components/popups.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index a124013a..a0318172 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -109,7 +109,7 @@ impl Component for CopyPopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -339,7 +339,7 @@ impl Component for ExecPopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -561,7 +561,7 @@ impl Component for FindPopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -647,7 +647,7 @@ impl Component for GoToPopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -895,7 +895,7 @@ impl Component for MkdirPopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -979,7 +979,7 @@ impl Component for NewfilePopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -1063,7 +1063,7 @@ impl Component for OpenWithPopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -1281,7 +1281,7 @@ impl Component for RenamePopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -1504,7 +1504,7 @@ impl Component for SaveAsPopup { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) From 68cc9ecc049b0427be167b5d4763d33f4995ef72 Mon Sep 17 00:00:00 2001 From: veeso Date: Fri, 3 Dec 2021 16:44:02 +0100 Subject: [PATCH 14/45] Removed unused focus, fixed partial prog bar alignment --- src/ui/activities/filetransfer/components/log.rs | 2 -- src/ui/activities/filetransfer/misc.rs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ui/activities/filetransfer/components/log.rs b/src/ui/activities/filetransfer/components/log.rs index 2ddb935c..9d4fd0ba 100644 --- a/src/ui/activities/filetransfer/components/log.rs +++ b/src/ui/activities/filetransfer/components/log.rs @@ -229,7 +229,6 @@ impl Component for Log { struct OwnStates { list_index: usize, // Index of selected element in list list_len: usize, // Length of file list - focus: bool, // Has focus? } impl Default for OwnStates { @@ -237,7 +236,6 @@ impl Default for OwnStates { OwnStates { list_index: 0, list_len: 0, - focus: false, } } } diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 808e0694..60e98c27 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -421,7 +421,7 @@ impl FileTransferActivity { .attr( &Id::ProgressBarPartial, Attribute::Title, - AttrValue::Title((filename, Alignment::Left)) + AttrValue::Title((filename, Alignment::Center)) ) .is_ok()); } From bb588c96d2300bcb69e47dd0fcd8b0cc8c7eb7f4 Mon Sep 17 00:00:00 2001 From: veeso Date: Thu, 9 Dec 2021 10:13:28 +0100 Subject: [PATCH 15/45] updated donation link in help --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 8d770cd0..6250361c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,7 +74,7 @@ Address syntax can be: - `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol Please, report issues to -Please, consider supporting the author ")] +Please, consider supporting the author ")] struct Args { #[argh(switch, short = 'c', description = "open termscp configuration")] config: bool, From 008c9af22a0b41b1f3dbf55961461a40675f1876 Mon Sep 17 00:00:00 2001 From: veeso Date: Thu, 9 Dec 2021 18:07:36 +0100 Subject: [PATCH 16/45] Removed filetransfer module; migrated to remotefs crate --- .github/actions-rs/grcov.yml | 1 - .github/workflows/coverage.yml | 4 +- .github/workflows/linux.yml | 4 +- CHANGELOG.md | 1 + Cargo.lock | 139 +- Cargo.toml | 10 +- README.md | 4 +- docs/developer.md | 10 - src/activity_manager.rs | 5 +- src/config/bookmarks.rs | 11 +- src/config/params.rs | 23 +- src/{fs => }/explorer/builder.rs | 0 src/{fs => }/explorer/formatter.rs | 404 ++--- src/{fs => }/explorer/mod.rs | 198 ++- src/filetransfer/builder.rs | 213 +++ src/filetransfer/mod.rs | 426 +----- src/filetransfer/params.rs | 6 +- src/filetransfer/transfer/ftp.rs | 960 ------------ src/filetransfer/transfer/mod.rs | 20 - src/filetransfer/transfer/s3/mod.rs | 699 --------- src/filetransfer/transfer/s3/object.rs | 247 --- src/filetransfer/transfer/scp.rs | 1347 ----------------- src/filetransfer/transfer/sftp.rs | 1116 -------------- src/fs/mod.rs | 578 ------- src/host/mod.rs | 372 ++--- src/main.rs | 2 +- src/system/auto_update.rs | 12 +- src/system/config_client.rs | 2 +- src/system/sshkey_storage.rs | 18 +- src/ui/activities/auth/components/mod.rs | 10 +- .../filetransfer/actions/change_dir.rs | 70 +- .../activities/filetransfer/actions/copy.rs | 42 +- .../activities/filetransfer/actions/delete.rs | 18 +- .../activities/filetransfer/actions/edit.rs | 29 +- .../activities/filetransfer/actions/exec.rs | 7 +- .../activities/filetransfer/actions/find.rs | 29 +- .../activities/filetransfer/actions/mkdir.rs | 10 +- src/ui/activities/filetransfer/actions/mod.rs | 28 +- .../filetransfer/actions/newfile.rs | 12 +- .../activities/filetransfer/actions/open.rs | 35 +- .../activities/filetransfer/actions/rename.rs | 35 +- .../activities/filetransfer/actions/save.rs | 28 +- .../activities/filetransfer/actions/submit.rs | 100 +- .../activities/filetransfer/components/log.rs | 11 +- .../filetransfer/components/popups.rs | 41 +- .../components/transfer/file_list.rs | 11 +- src/ui/activities/filetransfer/lib/browser.rs | 6 +- src/ui/activities/filetransfer/misc.rs | 10 +- src/ui/activities/filetransfer/mod.rs | 24 +- src/ui/activities/filetransfer/session.rs | 246 +-- src/ui/activities/filetransfer/update.rs | 4 +- src/ui/activities/filetransfer/view.rs | 6 +- src/ui/activities/setup/components/config.rs | 2 +- src/ui/activities/setup/components/mod.rs | 10 +- src/ui/activities/setup/view/setup.rs | 2 +- src/utils/fmt.rs | 16 +- src/utils/parser.rs | 114 -- src/utils/test_helpers.rs | 147 +- tests/docker-compose.yml | 35 - tests/test.sh | 29 - 60 files changed, 1185 insertions(+), 6814 deletions(-) rename src/{fs => }/explorer/builder.rs (100%) rename src/{fs => }/explorer/formatter.rs (73%) rename src/{fs => }/explorer/mod.rs (78%) create mode 100644 src/filetransfer/builder.rs delete mode 100644 src/filetransfer/transfer/ftp.rs delete mode 100644 src/filetransfer/transfer/mod.rs delete mode 100644 src/filetransfer/transfer/s3/mod.rs delete mode 100644 src/filetransfer/transfer/s3/object.rs delete mode 100644 src/filetransfer/transfer/scp.rs delete mode 100644 src/filetransfer/transfer/sftp.rs delete mode 100644 src/fs/mod.rs delete mode 100644 tests/docker-compose.yml delete mode 100755 tests/test.sh diff --git a/.github/actions-rs/grcov.yml b/.github/actions-rs/grcov.yml index 4fea5aab..82e7ae28 100644 --- a/.github/actions-rs/grcov.yml +++ b/.github/actions-rs/grcov.yml @@ -8,7 +8,6 @@ ignore: - "../*" - src/main.rs - src/activity_manager.rs - - src/filetransfer/transfer/s3/mod.rs - src/support.rs - src/system/notifications.rs - "src/ui/activities/*" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8c8753ed..efe9fc0d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -13,8 +13,6 @@ jobs: - uses: actions/checkout@v2 - name: Install dependencies run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev - - name: Setup containers - run: docker-compose -f "tests/docker-compose.yml" up -d --build - name: Setup nightly toolchain uses: actions-rs/toolchain@v1 with: @@ -24,7 +22,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --no-default-features --features github-actions --features with-containers --no-fail-fast + args: --no-default-features --features github-actions --no-fail-fast env: CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index de3d1c47..071c0f81 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -13,8 +13,6 @@ jobs: - uses: actions/checkout@v2 - name: Install dependencies run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev - - name: Setup containers - run: docker-compose -f "tests/docker-compose.yml" up -d --build - uses: actions-rs/toolchain@v1 with: toolchain: stable @@ -24,7 +22,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --no-default-features --features github-actions --features with-containers --no-fail-fast + args: --no-default-features --features github-actions --no-fail-fast - name: Format run: cargo fmt --all -- --check - name: Clippy diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c5db355..1bca34aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Released on FIXME: - Dependencies: - Updated `tui-realm` to `1.3.0` - Updated `tui-realm-stdlib` to `1.1.4` + - Removed `rust-s3`, `ssh2`, `suppaftp`; replaced by `remotefs 0.1.1` - Removed `crossterm` (since bridged by tui-realm) ## 0.7.0 diff --git a/Cargo.lock b/Cargo.lock index 43a3e13d..6d1e714d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "attohttpc" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" +checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072" dependencies = [ "http", "log", @@ -134,21 +134,7 @@ dependencies = [ "serde", "serde_json", "url", - "wildmatch 1.1.0", -] - -[[package]] -name = "attohttpc" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8bda305457262b339322106c776e3fd21df860018e566eb6a5b1aa4b6ae02d" -dependencies = [ - "http", - "log", - "native-tls", - "openssl", - "url", - "wildmatch 1.1.0", + "wildmatch", ] [[package]] @@ -159,13 +145,13 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "aws-creds" -version = "0.26.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1331d069460a674d42bd27c12b47ce578f789954c7bd7f239fd030771eca6616" +checksum = "460a75eac8f3cb7683e0a9a588a83c3ff039331ea7bfbfbfcecf1dacab276e11" dependencies = [ "anyhow", - "attohttpc 0.16.3", - "dirs 3.0.2", + "attohttpc", + "dirs 4.0.0", "rust-ini", "serde", "serde-xml-rs", @@ -456,6 +442,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "ctor" version = "0.1.21" @@ -529,15 +525,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dirs" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs" version = "4.0.0" @@ -799,7 +786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" dependencies = [ "digest", - "hmac", + "hmac 0.10.1", ] [[package]] @@ -808,7 +795,17 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ - "crypto-mac", + "crypto-mac 0.10.1", + "digest", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", "digest", ] @@ -1133,15 +1130,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "minidom" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332592c2149fc7dd40a64fc9ef6f0d65607284b474cef9817d1fc8c7e7b3608e" -dependencies = [ - "quick-xml", -] - [[package]] name = "miniz_oxide" version = "0.4.4" @@ -1686,6 +1674,26 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "remotefs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72915b01014a11d7e21b3a28141ff32b881bd8103c0f65269cc7932a03ae61c" +dependencies = [ + "chrono", + "lazy_static", + "log", + "path-slash", + "regex", + "rust-s3", + "ssh2", + "ssh2-config", + "suppaftp", + "thiserror", + "users", + "wildmatch", +] + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1754,35 +1762,34 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b134767a87e0b086f73a4ce569ac9ce7d202f39c8eab6caa266e2617e73ac6" +checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "ordered-multimap", ] [[package]] name = "rust-s3" -version = "0.27.0-rc4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c93272c1d654d492f8ab30b94cd43d98f2700b1db55b2576aff7712ce40e3ef" +checksum = "18c58d4682844a5d6301efbf915dd7a9d3d638006f9bb821527a0bbbf2a4cfc2" dependencies = [ "anyhow", "async-trait", - "attohttpc 0.17.0", + "attohttpc", "aws-creds", "aws-region", "base64", "cfg-if 1.0.0", "chrono", "hex", - "hmac", + "hmac 0.11.0", "http", "log", "maybe-async", "md5", - "minidom", "percent-encoding", "serde", "serde-xml-rs", @@ -1926,9 +1933,9 @@ dependencies = [ [[package]] name = "serde-xml-rs" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" +checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa" dependencies = [ "log", "serde", @@ -1977,7 +1984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", - "parking_lot 0.10.2", + "parking_lot 0.11.2", "serial_test_derive", ] @@ -2086,6 +2093,16 @@ dependencies = [ "parking_lot 0.10.2", ] +[[package]] +name = "ssh2-config" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e64d0ea4897c9415c34011a4cdf21a0e0168c200595f1f543be1ca807942d8" +dependencies = [ + "thiserror", + "wildmatch", +] + [[package]] name = "strum" version = "0.8.0" @@ -2110,12 +2127,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "suppaftp" -version = "4.1.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a4d861acfdc117c6d373c3b743c534dbbbb2d782e7646b27439a7c5282ad6a" +checksum = "3c42610de5ed0db28274b24c010aa530ebc391e5a4e9bd44a08e9daa8a245cc5" dependencies = [ "chrono", "lazy_static", + "log", "native-tls", "regex", "thiserror", @@ -2214,18 +2232,15 @@ dependencies = [ "magic-crypt", "notify-rust", "open", - "path-slash", "pretty_assertions", "rand 0.8.4", "regex", + "remotefs", "rpassword", - "rust-s3", "self_update", "serde", "serial_test", "simplelog", - "ssh2", - "suppaftp", "tempfile", "textwrap", "thiserror", @@ -2234,7 +2249,7 @@ dependencies = [ "tuirealm", "users", "whoami", - "wildmatch 2.1.0", + "wildmatch", ] [[package]] @@ -2662,12 +2677,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wildmatch" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" - [[package]] name = "wildmatch" version = "2.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2c556090..3a2cf96e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,13 +48,11 @@ notify-rust = { version = "4.5.3", default-features = false, features = [ "d" ] open = "2.0.1" rand = "0.8.4" regex = "1.5.4" +remotefs = { version = "0.1.1", features = [ "aws-s3", "ftp", "ssh" ] } rpassword = "5.0.1" -rust-s3 = { version = "0.27.0-rc4", default-features = false, features = [ "sync-native-tls", "sync" ] } self_update = { version = "0.27.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] } serde = { version = "^1.0.0", features = [ "derive" ] } simplelog = "0.10.0" -ssh2 = "0.9.0" -suppaftp = { version = "4.1.2", features = [ "secure" ] } tempfile = "3.1.0" textwrap = "0.14.2" thiserror = "^1.0.0" @@ -71,14 +69,8 @@ serial_test = "^0.5.1" [features] default = [ "with-keyring" ] github-actions = [ ] -with-s3-ci = [] -with-containers = [] with-keyring = [ "keyring" ] [target."cfg(target_family = \"unix\")"] [target."cfg(target_family = \"unix\")".dependencies] users = "0.11.0" - -[target."cfg(target_os = \"windows\")"] -[target."cfg(target_os = \"windows\")".dependencies] -path-slash = "0.1.4" diff --git a/README.md b/README.md index 03145842..ef5fa789 100644 --- a/README.md +++ b/README.md @@ -287,11 +287,9 @@ termscp is powered by these awesome projects: - [edit](https://github.com/milkey-mouse/edit) - [keyring-rs](https://github.com/hwchen/keyring-rs) - [open-rs](https://github.com/Byron/open-rs) +- [remotefs](https://github.com/veeso/remotefs-rs) - [rpassword](https://github.com/conradkleinespel/rpassword) -- [rust-s3](https://github.com/durch/rust-s3) - [self_update](https://github.com/jaemk/self_update) -- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) -- [suppaftp](https://github.com/veeso/suppaftp) - [textwrap](https://github.com/mgeisler/textwrap) - [tui-rs](https://github.com/fdehau/tui-rs) - [tui-realm](https://github.com/veeso/tui-realm) diff --git a/docs/developer.md b/docs/developer.md index 81e4bb03..a9d606f4 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -3,7 +3,6 @@ Document audience: developers - [Developer Manual](#developer-manual) - - [How to test](#how-to-test) - [How termscp works](#how-termscp-works) - [Activities](#activities) - [The Context](#the-context) @@ -11,15 +10,6 @@ Document audience: developers Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at This chapter describes how termscp works and the guide lines to implement stuff such as file transfers and add features to the user interface. -## How to test - -First an introduction to tests. - -Usually it's enough to run `cargo test`, but please note that whenever you're working on file transfer you'll need one more step. -In order to run tests with file transfers, you need to start the file transfer server containers, which can be started via `docker`. - -To run all tests with file transfers just run: `./tests/test.sh` - --- ## How termscp works diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 1171b39d..fa68e898 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -26,7 +26,7 @@ * SOFTWARE. */ // Deps -use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; +use crate::filetransfer::FileTransferParams; use crate::host::{HostError, Localhost}; use crate::system::config_client::ConfigClient; use crate::system::environment; @@ -192,7 +192,6 @@ impl ActivityManager { } }; // Prepare activity - let protocol: FileTransferProtocol = ft_params.protocol; let host: Localhost = match Localhost::new(self.local_dir.clone()) { Ok(host) => host, Err(err) => { @@ -203,7 +202,7 @@ impl ActivityManager { } }; let mut activity: FileTransferActivity = - FileTransferActivity::new(host, protocol, self.ticks); + FileTransferActivity::new(host, ft_params, self.ticks); // Prepare result let result: Option; // Create activity diff --git a/src/config/bookmarks.rs b/src/config/bookmarks.rs index dd7fcdf4..05b35b7b 100644 --- a/src/config/bookmarks.rs +++ b/src/config/bookmarks.rs @@ -36,7 +36,7 @@ use std::str::FromStr; /// /// UserHosts contains all the hosts saved by the user in the data storage /// It contains both `Bookmark` -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Default)] pub struct UserHosts { pub bookmarks: HashMap, pub recents: HashMap, @@ -76,15 +76,6 @@ pub struct S3Params { // -- impls -impl Default for UserHosts { - fn default() -> Self { - Self { - bookmarks: HashMap::new(), - recents: HashMap::new(), - } - } -} - impl From for Bookmark { fn from(params: FileTransferParams) -> Self { let protocol: FileTransferProtocol = params.protocol; diff --git a/src/config/params.rs b/src/config/params.rs index f20f4e31..30a483c4 100644 --- a/src/config/params.rs +++ b/src/config/params.rs @@ -35,7 +35,7 @@ use std::path::PathBuf; pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB -#[derive(Deserialize, Serialize, std::fmt::Debug)] +#[derive(Deserialize, Serialize, Debug, Default)] /// ## UserConfig /// /// UserConfig contains all the configurations for the user, @@ -45,7 +45,7 @@ pub struct UserConfig { pub remote: RemoteConfig, } -#[derive(Deserialize, Serialize, std::fmt::Debug)] +#[derive(Deserialize, Serialize, Debug)] /// ## UserInterfaceConfig /// /// UserInterfaceConfig provides all the keys to configure the user interface @@ -62,7 +62,7 @@ pub struct UserInterfaceConfig { pub notification_threshold: Option, // @! Since 0.7.0; Default 512MB } -#[derive(Deserialize, Serialize, std::fmt::Debug)] +#[derive(Deserialize, Serialize, Debug, Default)] /// ## RemoteConfig /// /// Contains configuratio related to remote hosts @@ -70,15 +70,6 @@ pub struct RemoteConfig { pub ssh_keys: HashMap, // Association between host name and path to private key } -impl Default for UserConfig { - fn default() -> Self { - UserConfig { - user_interface: UserInterfaceConfig::default(), - remote: RemoteConfig::default(), - } - } -} - impl Default for UserInterfaceConfig { fn default() -> Self { UserInterfaceConfig { @@ -99,14 +90,6 @@ impl Default for UserInterfaceConfig { } } -impl Default for RemoteConfig { - fn default() -> Self { - RemoteConfig { - ssh_keys: HashMap::new(), - } - } -} - // Tests #[cfg(test)] diff --git a/src/fs/explorer/builder.rs b/src/explorer/builder.rs similarity index 100% rename from src/fs/explorer/builder.rs rename to src/explorer/builder.rs diff --git a/src/fs/explorer/formatter.rs b/src/explorer/formatter.rs similarity index 73% rename from src/fs/explorer/formatter.rs rename to src/explorer/formatter.rs index 440e89a8..e63b3337 100644 --- a/src/fs/explorer/formatter.rs +++ b/src/explorer/formatter.rs @@ -26,18 +26,18 @@ * SOFTWARE. */ // Locals -use super::FsEntry; use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time}; use crate::utils::path::diff_paths; // Ext use bytesize::ByteSize; use regex::Regex; +use remotefs::Entry; use std::path::PathBuf; #[cfg(target_family = "unix")] use users::{get_group_by_gid, get_user_by_uid}; // Types -// FmtCallback: Formatter, fsentry: &FsEntry, cur_str, prefix, length, extra -type FmtCallback = fn(&Formatter, &FsEntry, &str, &str, Option<&usize>, Option<&String>) -> String; +// FmtCallback: Formatter, fsentry: &Entry, cur_str, prefix, length, extra +type FmtCallback = fn(&Formatter, &Entry, &str, &str, Option<&usize>, Option<&String>) -> String; // Keys const FMT_KEY_ATIME: &str = "ATIME"; @@ -66,7 +66,7 @@ lazy_static! { /// ## CallChainBlock /// -/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry. +/// Call Chain block is a block in a chain of functions which are called in order to format the Entry. /// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found /// a chain of function is made using the Formatters method. /// This method provides an extremely fast way to format fs entries @@ -105,7 +105,7 @@ impl CallChainBlock { /// ### next /// /// Call next callback in the CallChain - pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String { + pub fn next(&self, fmt: &Formatter, fsentry: &Entry, cur_str: &str) -> String { // Call func let new_str: String = (self.func)( fmt, @@ -177,7 +177,7 @@ impl Formatter { /// ### fmt /// /// Format fsentry - pub fn fmt(&self, fsentry: &FsEntry) -> String { + pub fn fmt(&self, fsentry: &Entry) -> String { // Execute callchain blocks self.call_chain.next(self, fsentry, "") } @@ -189,7 +189,7 @@ impl Formatter { /// Format last access time fn fmt_atime( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -197,7 +197,7 @@ impl Formatter { ) -> String { // Get date (use extra args as format or default "%b %d %Y %H:%M") let datetime: String = fmt_time( - fsentry.get_last_access_time(), + fsentry.metadata().atime, match fmt_extra { Some(fmt) => fmt.as_ref(), None => "%b %d %Y %H:%M", @@ -218,7 +218,7 @@ impl Formatter { /// Format creation time fn fmt_ctime( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -226,7 +226,7 @@ impl Formatter { ) -> String { // Get date let datetime: String = fmt_time( - fsentry.get_creation_time(), + fsentry.metadata().ctime, match fmt_extra { Some(fmt) => fmt.as_ref(), None => "%b %d %Y %H:%M", @@ -247,7 +247,7 @@ impl Formatter { /// Format owner group fn fmt_group( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -255,7 +255,7 @@ impl Formatter { ) -> String { // Get username #[cfg(target_family = "unix")] - let group: String = match fsentry.get_group() { + let group: String = match fsentry.metadata().gid { Some(gid) => match get_group_by_gid(gid) { Some(user) => user.name().to_string_lossy().to_string(), None => gid.to_string(), @@ -263,7 +263,7 @@ impl Formatter { None => 0.to_string(), }; #[cfg(target_os = "windows")] - let group: String = match fsentry.get_group() { + let group: String = match fsentry.metadata().gid { Some(gid) => gid.to_string(), None => 0.to_string(), }; @@ -282,7 +282,7 @@ impl Formatter { /// Format last change time fn fmt_mtime( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -290,7 +290,7 @@ impl Formatter { ) -> String { // Get date let datetime: String = fmt_time( - fsentry.get_last_change_time(), + fsentry.metadata().mtime, match fmt_extra { Some(fmt) => fmt.as_ref(), None => "%b %d %Y %H:%M", @@ -311,7 +311,7 @@ impl Formatter { /// Format file name fn fmt_name( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -322,7 +322,7 @@ impl Formatter { Some(l) => *l, None => 24, }; - let name: &str = fsentry.get_name(); + let name: &str = fsentry.name(); let last_idx: usize = match fsentry.is_dir() { // NOTE: For directories is l - 2, since we push '/' to name true => file_len - 2, @@ -344,19 +344,16 @@ impl Formatter { /// Format path fn fmt_path( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, fmt_extra: Option<&String>, ) -> String { let p = match fmt_extra { - None => fsentry.get_abs_path(), - Some(rel) => diff_paths( - fsentry.get_abs_path().as_path(), - PathBuf::from(rel.as_str()).as_path(), - ) - .unwrap_or_else(|| fsentry.get_abs_path()), + None => fsentry.path().to_path_buf(), + Some(rel) => diff_paths(fsentry.path(), PathBuf::from(rel.as_str()).as_path()) + .unwrap_or_else(|| fsentry.path().to_path_buf()), }; format!( "{}{}{}", @@ -374,7 +371,7 @@ impl Formatter { /// Format file permissions fn fmt_pex( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, @@ -382,7 +379,7 @@ impl Formatter { ) -> String { // Create mode string let mut pex: String = String::with_capacity(10); - let file_type: char = match fsentry.is_symlink() { + let file_type: char = match fsentry.metadata().symlink.is_some() { true => 'l', false => match fsentry.is_dir() { true => 'd', @@ -390,10 +387,16 @@ impl Formatter { }, }; pex.push(file_type); - match fsentry.get_unix_pex() { + match fsentry.metadata().mode { None => pex.push_str("?????????"), - Some((owner, group, others)) => pex.push_str( - format!("{}{}{}", fmt_pex(owner), fmt_pex(group), fmt_pex(others)).as_str(), + Some(mode) => pex.push_str( + format!( + "{}{}{}", + fmt_pex(mode.user()), + fmt_pex(mode.group()), + fmt_pex(mode.others()) + ) + .as_str(), ), } // Add to cur str, prefix and the key value @@ -405,7 +408,7 @@ impl Formatter { /// Format file size fn fmt_size( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, @@ -413,7 +416,7 @@ impl Formatter { ) -> String { if fsentry.is_file() { // Get byte size - let size: ByteSize = ByteSize(fsentry.get_size() as u64); + let size: ByteSize = ByteSize(fsentry.metadata().size); // Add to cur str, prefix and the key value format!("{}{}{:10}", cur_str, prefix, size.to_string()) } else { @@ -427,7 +430,7 @@ impl Formatter { /// Format file symlink (if any) fn fmt_symlink( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -439,16 +442,13 @@ impl Formatter { None => 21, }; // Replace `FMT_KEY_NAME` with name - match fsentry.is_symlink() { - false => format!("{}{} ", cur_str, prefix), - true => format!( + match fsentry.metadata().symlink.as_deref() { + None => format!("{}{} ", cur_str, prefix), + Some(p) => format!( "{}{}-> {:0width$}", cur_str, prefix, - fmt_path_elide( - fsentry.get_realfile().get_abs_path().as_path(), - file_len - 1 - ), + fmt_path_elide(p, file_len - 1), width = file_len ), } @@ -459,7 +459,7 @@ impl Formatter { /// Format owner user fn fmt_user( &self, - fsentry: &FsEntry, + fsentry: &Entry, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, @@ -467,7 +467,7 @@ impl Formatter { ) -> String { // Get username #[cfg(target_family = "unix")] - let username: String = match fsentry.get_user() { + let username: String = match fsentry.metadata().uid { Some(uid) => match get_user_by_uid(uid) { Some(user) => user.name().to_string_lossy().to_string(), None => uid.to_string(), @@ -475,7 +475,7 @@ impl Formatter { None => 0.to_string(), }; #[cfg(target_os = "windows")] - let username: String = match fsentry.get_user() { + let username: String = match fsentry.metadata().uid { Some(uid) => uid.to_string(), None => 0.to_string(), }; @@ -489,7 +489,7 @@ impl Formatter { /// It does nothing, just returns cur_str fn fmt_fallback( &self, - _fsentry: &FsEntry, + _fsentry: &Entry, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, @@ -574,9 +574,9 @@ impl Formatter { mod tests { use super::*; - use crate::fs::{FsDirectory, FsFile, UnixPex}; use pretty_assertions::assert_eq; + use remotefs::fs::{Directory, File, Metadata, UnixPex}; use std::path::PathBuf; use std::time::SystemTime; @@ -585,19 +585,21 @@ mod tests { // Make a dummy formatter let dummy_formatter: Formatter = Formatter::new(""); // Make a dummy entry - let t_now: SystemTime = SystemTime::now(); - let dummy_entry: FsEntry = FsEntry::File(FsFile { + let t: SystemTime = SystemTime::now(); + let dummy_entry: Entry = Entry::File(File { name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from("/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 8192, + symlink: None, + uid: Some(0), + gid: Some(0), + mode: Some(UnixPex::from(0o644)), + }, }); let prefix: String = String::from("h"); let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None); @@ -626,18 +628,20 @@ mod tests { let formatter: Formatter = Formatter::default(); // Experiments :D let t: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::File(FsFile { + let entry: Entry = Entry::File(File { name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from("/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 8192, + symlink: None, + uid: Some(0), + gid: Some(0), + mode: Some(UnixPex::from(0o644)), + }, }); #[cfg(target_family = "unix")] assert_eq!( @@ -656,18 +660,20 @@ mod tests { ) ); // Elide name - let entry: FsEntry = FsEntry::File(FsFile { + let entry: Entry = Entry::File(File { name: String::from("piroparoporoperoperupupu.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from("/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 8192, + symlink: None, + uid: Some(0), + gid: Some(0), + mode: Some(UnixPex::from(0o644)), + }, }); #[cfg(target_family = "unix")] assert_eq!( @@ -686,18 +692,20 @@ mod tests { ) ); // No pex - let entry: FsEntry = FsEntry::File(FsFile { + let entry: Entry = Entry::File(File { name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: None, // UNIX only + path: PathBuf::from("/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 8192, + symlink: None, + uid: Some(0), + gid: Some(0), + mode: None, + }, }); #[cfg(target_family = "unix")] assert_eq!( @@ -716,18 +724,20 @@ mod tests { ) ); // No user - let entry: FsEntry = FsEntry::File(FsFile { + let entry: Entry = Entry::File(File { name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: None, // UNIX only - group: Some(0), // UNIX only - unix_pex: None, // UNIX only + path: PathBuf::from("/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 8192, + symlink: None, + uid: None, + gid: Some(0), + mode: None, + }, }); #[cfg(target_family = "unix")] assert_eq!( @@ -752,24 +762,27 @@ mod tests { // Make default let formatter: Formatter = Formatter::default(); // Experiments :D - let t_now: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::Directory(FsDirectory { + let t: SystemTime = SystemTime::now(); + let entry: Entry = Entry::Directory(Directory { name: String::from("projects"), - abs_path: PathBuf::from("/home/cvisintin/projects"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only + path: PathBuf::from("/home/cvisintin/projects"), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 4096, + symlink: None, + uid: Some(0), + gid: Some(0), + mode: Some(UnixPex::from(0o755)), + }, }); #[cfg(target_family = "unix")] assert_eq!( formatter.fmt(&entry), format!( "projects/ drwxr-xr-x root {}", - fmt_time(t_now, "%b %d %Y %H:%M") + fmt_time(t, "%b %d %Y %H:%M") ) ); #[cfg(target_os = "windows")] @@ -777,27 +790,30 @@ mod tests { formatter.fmt(&entry), format!( "projects/ drwxr-xr-x 0 {}", - fmt_time(t_now, "%b %d %Y %H:%M") + fmt_time(t, "%b %d %Y %H:%M") ) ); // No pex, no user - let entry: FsEntry = FsEntry::Directory(FsDirectory { + let entry: Entry = Entry::Directory(Directory { name: String::from("projects"), - abs_path: PathBuf::from("/home/cvisintin/projects"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: None, // UNIX only - group: Some(0), // UNIX only - unix_pex: None, // UNIX only + path: PathBuf::from("/home/cvisintin/projects"), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 4096, + symlink: None, + uid: None, + gid: Some(0), + mode: None, + }, }); #[cfg(target_family = "unix")] assert_eq!( formatter.fmt(&entry), format!( "projects/ d????????? 0 {}", - fmt_time(t_now, "%b %d %Y %H:%M") + fmt_time(t, "%b %d %Y %H:%M") ) ); #[cfg(target_os = "windows")] @@ -805,7 +821,7 @@ mod tests { formatter.fmt(&entry), format!( "projects/ d????????? 0 {}", - fmt_time(t_now, "%b %d %Y %H:%M") + fmt_time(t, "%b %d %Y %H:%M") ) ); } @@ -816,29 +832,19 @@ mod tests { Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}"); // Directory (with symlink) let t: SystemTime = SystemTime::now(); - let pointer: FsEntry = FsEntry::File(FsFile { - name: String::from("project.info"), - abs_path: PathBuf::from("/project.info"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: None, // UNIX only - }); - let entry: FsEntry = FsEntry::Directory(FsDirectory { + let entry: Entry = Entry::Directory(Directory { name: String::from("projects"), - abs_path: PathBuf::from("/home/cvisintin/project"), - last_change_time: t, - last_access_time: t, - creation_time: t, - symlink: Some(Box::new(pointer)), // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only + path: PathBuf::from("/home/cvisintin/project"), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 4096, + symlink: Some(PathBuf::from("project.info")), + uid: None, + gid: None, + mode: Some(UnixPex::from(0o755)), + }, }); assert_eq!(formatter.fmt(&entry), format!( "projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}", @@ -847,16 +853,19 @@ mod tests { fmt_time(t, "%a %b %d %Y %H:%M"), )); // Directory without symlink - let entry: FsEntry = FsEntry::Directory(FsDirectory { + let entry: Entry = Entry::Directory(Directory { name: String::from("projects"), - abs_path: PathBuf::from("/home/cvisintin/project"), - last_change_time: t, - last_access_time: t, - creation_time: t, - symlink: None, // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only + path: PathBuf::from("/home/cvisintin/project"), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 4096, + symlink: None, + uid: None, + gid: None, + mode: Some(UnixPex::from(0o755)), + }, }); assert_eq!(formatter.fmt(&entry), format!( "projects/ 0 0 drwxr-xr-x {} {} {}", @@ -865,31 +874,20 @@ mod tests { fmt_time(t, "%a %b %d %Y %H:%M"), )); // File with symlink - let pointer: FsEntry = FsEntry::File(FsFile { - name: String::from("project.info"), - abs_path: PathBuf::from("/project.info"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: None, // UNIX only - }); - let entry: FsEntry = FsEntry::File(FsFile { + let entry: Entry = Entry::File(File { name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: Some(Box::new(pointer)), // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from("/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 8192, + symlink: Some(PathBuf::from("project.info")), + uid: None, + gid: None, + mode: Some(UnixPex::from(0o644)), + }, }); assert_eq!(formatter.fmt(&entry), format!( "bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}", @@ -898,18 +896,20 @@ mod tests { fmt_time(t, "%a %b %d %Y %H:%M"), )); // File without symlink - let entry: FsEntry = FsEntry::File(FsFile { + let entry: Entry = Entry::File(File { name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from("/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 8192, + symlink: None, + uid: None, + gid: None, + mode: Some(UnixPex::from(0o644)), + }, }); assert_eq!(formatter.fmt(&entry), format!( "bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}", @@ -923,18 +923,20 @@ mod tests { #[cfg(target_family = "unix")] fn should_fmt_path() { let t: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::File(FsFile { + let entry: Entry = Entry::File(File { name: String::from("bar.txt"), - abs_path: PathBuf::from("/tmp/a/b/c/bar.txt"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from("/tmp/a/b/c/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + mtime: t, + size: 8192, + symlink: Some(PathBuf::from("project.info")), + uid: None, + gid: None, + mode: Some(UnixPex::from(0o644)), + }, }); let formatter: Formatter = Formatter::new("File path: {PATH}"); assert_eq!( @@ -955,7 +957,7 @@ mod tests { /// Dummy formatter, just yelds an 'A' at the end of the current string fn dummy_fmt( _fmt: &Formatter, - _entry: &FsEntry, + _entry: &Entry, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, diff --git a/src/fs/explorer/mod.rs b/src/explorer/mod.rs similarity index 78% rename from src/fs/explorer/mod.rs rename to src/explorer/mod.rs index cb5436f1..9dbc4252 100644 --- a/src/fs/explorer/mod.rs +++ b/src/explorer/mod.rs @@ -29,9 +29,9 @@ pub(crate) mod builder; mod formatter; // Locals -use super::FsEntry; use formatter::Formatter; // Ext +use remotefs::fs::Entry; use std::cmp::Reverse; use std::collections::VecDeque; use std::path::{Path, PathBuf}; @@ -77,8 +77,8 @@ pub struct FileExplorer { pub(crate) file_sorting: FileSorting, // File sorting criteria pub(crate) group_dirs: Option, // If Some, defines how to group directories pub(crate) opts: ExplorerOpts, // Explorer options - pub(crate) fmt: Formatter, // FsEntry formatter - files: Vec, // Files in directory + pub(crate) fmt: Formatter, // Entry formatter + files: Vec, // Files in directory } impl Default for FileExplorer { @@ -121,7 +121,7 @@ impl FileExplorer { /// Set Explorer files /// This method will also sort entries based on current options /// Once all sorting have been performed, index is moved to first valid entry. - pub fn set_files(&mut self, files: Vec) { + pub fn set_files(&mut self, files: Vec) { self.files = files; // Sort self.sort(); @@ -149,7 +149,7 @@ impl FileExplorer { /// /// Iterate over files /// Filters are applied based on current options (e.g. hidden files not returned) - pub fn iter_files(&self) -> impl Iterator + '_ { + pub fn iter_files(&self) -> impl Iterator + '_ { // Filter let opts: ExplorerOpts = self.opts; Box::new(self.files.iter().filter(move |x| { @@ -166,14 +166,14 @@ impl FileExplorer { /// ### iter_files_all /// /// Iterate all files; doesn't care about options - pub fn iter_files_all(&self) -> impl Iterator + '_ { + pub fn iter_files_all(&self) -> impl Iterator + '_ { Box::new(self.files.iter()) } /// ### get /// /// Get file at relative index - pub fn get(&self, idx: usize) -> Option<&FsEntry> { + pub fn get(&self, idx: usize) -> Option<&Entry> { let opts: ExplorerOpts = self.opts; let filtered = self .files @@ -196,7 +196,7 @@ impl FileExplorer { /// ### fmt_file /// /// Format a file entry - pub fn fmt_file(&self, entry: &FsEntry) -> String { + pub fn fmt_file(&self, entry: &Entry) -> String { self.fmt.fmt(entry) } @@ -256,17 +256,15 @@ impl FileExplorer { /// /// Sort explorer files by their name. All names are converted to lowercase fn sort_files_by_name(&mut self) { - self.files - .sort_by_key(|x: &FsEntry| x.get_name().to_lowercase()); + self.files.sort_by_key(|x: &Entry| x.name().to_lowercase()); } /// ### sort_files_by_mtime /// /// Sort files by mtime; the newest comes first fn sort_files_by_mtime(&mut self) { - self.files.sort_by(|a: &FsEntry, b: &FsEntry| { - b.get_last_change_time().cmp(&a.get_last_change_time()) - }); + self.files + .sort_by(|a: &Entry, b: &Entry| b.metadata().mtime.cmp(&a.metadata().mtime)); } /// ### sort_files_by_creation_time @@ -274,28 +272,29 @@ impl FileExplorer { /// Sort files by creation time; the newest comes first fn sort_files_by_creation_time(&mut self) { self.files - .sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time())); + .sort_by_key(|b: &Entry| Reverse(b.metadata().ctime)); } /// ### sort_files_by_size /// /// Sort files by size fn sort_files_by_size(&mut self) { - self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_size())); + self.files + .sort_by_key(|b: &Entry| Reverse(b.metadata().size)); } /// ### sort_files_directories_first /// /// Sort files; directories come first fn sort_files_directories_first(&mut self) { - self.files.sort_by_key(|x: &FsEntry| x.is_file()); + self.files.sort_by_key(|x: &Entry| x.is_file()); } /// ### sort_files_directories_last /// /// Sort files; directories come last fn sort_files_directories_last(&mut self) { - self.files.sort_by_key(|x: &FsEntry| x.is_dir()); + self.files.sort_by_key(|x: &Entry| x.is_dir()); } /// ### toggle_hidden_files @@ -363,10 +362,10 @@ impl FromStr for GroupDirs { mod tests { use super::*; - use crate::fs::{FsDirectory, FsFile, UnixPex}; use crate::utils::fmt::fmt_time; use pretty_assertions::assert_eq; + use remotefs::fs::{Directory, File, Metadata, UnixPex}; use std::thread::sleep; use std::time::{Duration, SystemTime}; @@ -430,10 +429,7 @@ mod tests { assert!(explorer.get(100).is_none()); //assert_eq!(explorer.count(), 6); // Verify (files are sorted by name) - assert_eq!( - explorer.files.get(0).unwrap().get_name(), - String::from(".git/") - ); + assert_eq!(explorer.files.get(0).unwrap().name(), ".git/"); // Iter files (all) assert_eq!(explorer.iter_files_all().count(), 6); // Iter files (hidden excluded) (.git, .gitignore are hidden) @@ -461,47 +457,41 @@ mod tests { ]); explorer.sort_by(FileSorting::Name); // First entry should be "Cargo.lock" - assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock"); + assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock"); // Last should be "src/" - assert_eq!(explorer.files.get(8).unwrap().get_name(), "src/"); + assert_eq!(explorer.files.get(8).unwrap().name(), "src/"); } #[test] fn test_fs_explorer_sort_by_mtime() { let mut explorer: FileExplorer = FileExplorer::default(); - let entry1: FsEntry = make_fs_entry("README.md", false); + let entry1: Entry = make_fs_entry("README.md", false); // Wait 1 sec sleep(Duration::from_secs(1)); - let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false); + let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false); // Create files (files are then sorted by name) explorer.set_files(vec![entry1, entry2]); explorer.sort_by(FileSorting::ModifyTime); // First entry should be "CODE_OF_CONDUCT.md" - assert_eq!( - explorer.files.get(0).unwrap().get_name(), - "CODE_OF_CONDUCT.md" - ); + assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md"); // Last should be "src/" - assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); + assert_eq!(explorer.files.get(1).unwrap().name(), "README.md"); } #[test] fn test_fs_explorer_sort_by_creation_time() { let mut explorer: FileExplorer = FileExplorer::default(); - let entry1: FsEntry = make_fs_entry("README.md", false); + let entry1: Entry = make_fs_entry("README.md", false); // Wait 1 sec sleep(Duration::from_secs(1)); - let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false); + let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false); // Create files (files are then sorted by name) explorer.set_files(vec![entry1, entry2]); explorer.sort_by(FileSorting::CreationTime); // First entry should be "CODE_OF_CONDUCT.md" - assert_eq!( - explorer.files.get(0).unwrap().get_name(), - "CODE_OF_CONDUCT.md" - ); + assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md"); // Last should be "src/" - assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); + assert_eq!(explorer.files.get(1).unwrap().name(), "README.md"); } #[test] @@ -510,14 +500,14 @@ mod tests { // Create files (files are then sorted by name) explorer.set_files(vec![ make_fs_entry_with_size("README.md", false, 1024), - make_fs_entry("src/", true), + make_fs_entry_with_size("src/", true, 4096), make_fs_entry_with_size("CONTRIBUTING.md", false, 256), ]); explorer.sort_by(FileSorting::Size); // Directory has size 4096 - assert_eq!(explorer.files.get(0).unwrap().get_name(), "src/"); - assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); - assert_eq!(explorer.files.get(2).unwrap().get_name(), "CONTRIBUTING.md"); + assert_eq!(explorer.files.get(0).unwrap().name(), "src/"); + assert_eq!(explorer.files.get(1).unwrap().name(), "README.md"); + assert_eq!(explorer.files.get(2).unwrap().name(), "CONTRIBUTING.md"); } #[test] @@ -539,12 +529,12 @@ mod tests { explorer.sort_by(FileSorting::Name); explorer.group_dirs_by(Some(GroupDirs::First)); // First entry should be "docs" - assert_eq!(explorer.files.get(0).unwrap().get_name(), "docs/"); - assert_eq!(explorer.files.get(1).unwrap().get_name(), "src/"); + assert_eq!(explorer.files.get(0).unwrap().name(), "docs/"); + assert_eq!(explorer.files.get(1).unwrap().name(), "src/"); // 3rd is file first for alphabetical order - assert_eq!(explorer.files.get(2).unwrap().get_name(), "Cargo.lock"); + assert_eq!(explorer.files.get(2).unwrap().name(), "Cargo.lock"); // Last should be "README.md" (last file for alphabetical ordening) - assert_eq!(explorer.files.get(9).unwrap().get_name(), "README.md"); + assert_eq!(explorer.files.get(9).unwrap().name(), "README.md"); } #[test] @@ -566,12 +556,12 @@ mod tests { explorer.sort_by(FileSorting::Name); explorer.group_dirs_by(Some(GroupDirs::Last)); // Last entry should be "src" - assert_eq!(explorer.files.get(8).unwrap().get_name(), "docs/"); - assert_eq!(explorer.files.get(9).unwrap().get_name(), "src/"); + assert_eq!(explorer.files.get(8).unwrap().name(), "docs/"); + assert_eq!(explorer.files.get(9).unwrap().name(), "src/"); // first is file for alphabetical order - assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock"); + assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock"); // Last in files should be "README.md" (last file for alphabetical ordening) - assert_eq!(explorer.files.get(7).unwrap().get_name(), "README.md"); + assert_eq!(explorer.files.get(7).unwrap().name(), "README.md"); } #[test] @@ -579,18 +569,20 @@ mod tests { let explorer: FileExplorer = FileExplorer::default(); // Create fs entry let t: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::File(FsFile { + let entry: Entry = Entry::File(File { name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t, - last_access_time: t, - creation_time: t, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from("/bar.txt"), + extension: Some(String::from("txt")), + metadata: Metadata { + atime: t, + ctime: t, + size: 8192, + mtime: t, + symlink: None, + uid: Some(0), + gid: Some(0), + mode: Some(UnixPex::from(0o644)), + }, }); #[cfg(target_family = "unix")] assert_eq!( @@ -654,67 +646,61 @@ mod tests { ]); explorer.del_entry(0); assert_eq!(explorer.files.len(), 3); - assert_eq!(explorer.files[0].get_name(), "docs/"); + assert_eq!(explorer.files[0].name(), "docs/"); explorer.del_entry(5); assert_eq!(explorer.files.len(), 3); } - fn make_fs_entry(name: &str, is_dir: bool) -> FsEntry { - let t_now: SystemTime = SystemTime::now(); + fn make_fs_entry(name: &str, is_dir: bool) -> Entry { + let t: SystemTime = SystemTime::now(); + let metadata = Metadata { + atime: t, + ctime: t, + mtime: t, + symlink: None, + gid: Some(0), + uid: Some(0), + mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })), + size: 64, + }; match is_dir { - false => FsEntry::File(FsFile { + false => Entry::File(File { name: name.to_string(), - abs_path: PathBuf::from(name), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: 64, - ftype: None, // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from(name), + extension: None, + metadata, }), - true => FsEntry::Directory(FsDirectory { + true => Entry::Directory(Directory { name: name.to_string(), - abs_path: PathBuf::from(name), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only + path: PathBuf::from(name), + metadata, }), } } - fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> FsEntry { - let t_now: SystemTime = SystemTime::now(); + fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> Entry { + let t: SystemTime = SystemTime::now(); + let metadata = Metadata { + atime: t, + ctime: t, + mtime: t, + symlink: None, + gid: Some(0), + uid: Some(0), + mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })), + size: size as u64, + }; match is_dir { - false => FsEntry::File(FsFile { + false => Entry::File(File { name: name.to_string(), - abs_path: PathBuf::from(name), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: size, - ftype: None, // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: PathBuf::from(name), + extension: None, + metadata, }), - true => FsEntry::Directory(FsDirectory { + true => Entry::Directory(Directory { name: name.to_string(), - abs_path: PathBuf::from(name), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only + path: PathBuf::from(name), + metadata, }), } } diff --git a/src/filetransfer/builder.rs b/src/filetransfer/builder.rs new file mode 100644 index 00000000..4942c24c --- /dev/null +++ b/src/filetransfer/builder.rs @@ -0,0 +1,213 @@ +//! ## builder +//! +//! Remotefs client builder + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::params::{AwsS3Params, GenericProtocolParams}; +use super::{FileTransferProtocol, ProtocolParams}; +use crate::system::config_client::ConfigClient; +use crate::system::sshkey_storage::SshKeyStorage; + +use remotefs::client::{ + aws_s3::AwsS3Fs, + ftp::FtpFs, + ssh::{ScpFs, SftpFs, SshOpts}, +}; +use remotefs::RemoteFs; + +/// Remotefs builder +pub struct Builder; + +impl Builder { + /// Build RemoteFs client from protocol and params. + /// + /// if protocol and parameters are inconsistent, the function will panic. + pub fn build( + protocol: FileTransferProtocol, + params: ProtocolParams, + config_client: &ConfigClient, + ) -> Box { + match (protocol, params) { + (FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => { + Box::new(Self::aws_s3_client(params)) + } + (FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => { + Box::new(Self::ftp_client(params, secure)) + } + (FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => { + Box::new(Self::scp_client(params, config_client)) + } + (FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => { + Box::new(Self::sftp_client(params, config_client)) + } + (protocol, params) => { + error!("Invalid params for protocol '{:?}'", protocol); + panic!( + "Invalid protocol '{:?}' with parameters of type {:?}", + protocol, params + ) + } + } + } + + /// Build aws s3 client from parameters + fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs { + let mut client = AwsS3Fs::new(params.bucket_name, params.region); + if let Some(profile) = params.profile { + client = client.profile(profile); + } + client + } + + /// Build ftp client from parameters + fn ftp_client(params: GenericProtocolParams, secure: bool) -> FtpFs { + let mut client = FtpFs::new(params.address, params.port).passive_mode(); + if let Some(username) = params.username { + client = client.username(username); + } + if let Some(password) = params.password { + client = client.password(password); + } + if secure { + client = client.secure(true, true); + } + client + } + + /// Build scp client + fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs { + Self::build_ssh_opts(params, config_client).into() + } + + /// Build sftp client + fn sftp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> SftpFs { + Self::build_ssh_opts(params, config_client).into() + } + + /// Build ssh options from generic protocol params and client configuration + fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts { + let mut opts = SshOpts::new(params.address) + .key_storage(Box::new(Self::make_ssh_storage(config_client))) + .port(params.port); + if let Some(username) = params.username { + opts = opts.username(username); + } + if let Some(password) = params.password { + opts = opts.password(password); + } + opts + } + + /// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded) + fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage { + SshKeyStorage::storage_from_config(config_client) + } +} + +#[cfg(test)] +mod test { + + use super::*; + + use std::path::{Path, PathBuf}; + use tempfile::TempDir; + + #[test] + fn should_build_aws_s3_fs() { + let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test"))); + let config_client = get_config_client(); + let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client); + } + + #[test] + fn should_build_ftp_fs() { + let params = ProtocolParams::Generic( + GenericProtocolParams::default() + .address("127.0.0.1") + .port(21) + .username(Some("omar")) + .password(Some("qwerty123")), + ); + let config_client = get_config_client(); + let _ = Builder::build(FileTransferProtocol::Ftp(true), params, &config_client); + } + + #[test] + fn should_build_scp_fs() { + let params = ProtocolParams::Generic( + GenericProtocolParams::default() + .address("127.0.0.1") + .port(22) + .username(Some("omar")) + .password(Some("qwerty123")), + ); + let config_client = get_config_client(); + let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client); + } + + #[test] + fn should_build_sftp_fs() { + let params = ProtocolParams::Generic( + GenericProtocolParams::default() + .address("127.0.0.1") + .port(22) + .username(Some("omar")) + .password(Some("qwerty123")), + ); + let config_client = get_config_client(); + let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client); + } + + #[test] + #[should_panic] + fn should_not_build_fs() { + let params = ProtocolParams::Generic( + GenericProtocolParams::default() + .address("127.0.0.1") + .port(22) + .username(Some("omar")) + .password(Some("qwerty123")), + ); + let config_client = get_config_client(); + let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client); + } + + fn get_config_client() -> ConfigClient { + let tmp_dir: TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path()) + .ok() + .unwrap() + } + + /// Get paths for configuration and keys directory + fn get_paths(dir: &Path) -> (PathBuf, PathBuf) { + let mut k: PathBuf = PathBuf::from(dir); + let mut c: PathBuf = k.clone(); + k.push("ssh-keys/"); + c.push("config.toml"); + (c, k) + } +} diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index cfc11954..6e201cbd 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -1,6 +1,6 @@ //! ## FileTransfer //! -//! `filetransfer` is the module which provides the trait file transfers must implement and the different file transfers +//! `filetransfer` is the module which provides the file transfer protocols and remotefs builders /** * MIT License @@ -25,21 +25,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -// locals -use crate::fs::{FsEntry, FsFile}; -// ext -use std::fs::File; -use std::io::{self, Read, Write}; -use std::path::{Path, PathBuf}; -use thiserror::Error; -use wildmatch::WildMatch; -// exports +mod builder; pub mod params; -mod transfer; // -- export types +pub use builder::Builder; pub use params::{FileTransferParams, ProtocolParams}; -pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer}; /// ## FileTransferProtocol /// @@ -53,325 +44,6 @@ pub enum FileTransferProtocol { AwsS3, } -/// ## FileTransferError -/// -/// FileTransferError defines the possible errors available for a file transfer -#[derive(Debug)] -pub struct FileTransferError { - code: FileTransferErrorType, - msg: Option, -} - -/// ## FileTransferErrorType -/// -/// FileTransferErrorType defines the possible errors available for a file transfer -#[derive(Error, Debug, Clone, Copy, PartialEq)] -pub enum FileTransferErrorType { - #[error("Authentication failed")] - AuthenticationFailed, - #[error("Bad address syntax")] - BadAddress, - #[error("Connection error")] - ConnectionError, - #[error("SSL error")] - SslError, - #[error("Could not stat directory")] - DirStatFailed, - #[error("Directory already exists")] - DirectoryAlreadyExists, - #[error("Failed to create file")] - FileCreateDenied, - #[error("No such file or directory")] - NoSuchFileOrDirectory, - #[error("Not enough permissions")] - PexError, - #[error("Protocol error")] - ProtocolError, - #[error("Uninitialized session")] - UninitializedSession, - #[error("Unsupported feature")] - UnsupportedFeature, -} - -impl FileTransferError { - /// ### new - /// - /// Instantiates a new FileTransferError - pub fn new(code: FileTransferErrorType) -> FileTransferError { - FileTransferError { code, msg: None } - } - - /// ### new_ex - /// - /// Instantiates a new FileTransferError with message - pub fn new_ex(code: FileTransferErrorType, msg: String) -> FileTransferError { - let mut err: FileTransferError = FileTransferError::new(code); - err.msg = Some(msg); - err - } - - /// ### kind - /// - /// Returns the error kind - pub fn kind(&self) -> FileTransferErrorType { - self.code - } -} - -impl std::fmt::Display for FileTransferError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match &self.msg { - Some(msg) => write!(f, "{} ({})", self.code, msg), - None => write!(f, "{}", self.code), - } - } -} - -/// ## FileTransferResult -/// -/// Result type returned by a `FileTransfer` implementation -pub type FileTransferResult = Result; - -/// ## FileTransfer -/// -/// File transfer trait must be implemented by all the file transfers and defines the method used by a generic file transfer -pub trait FileTransfer { - /// ### connect - /// - /// Connect to the remote server - /// Can return banner / welcome message on success - fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult>; - - /// ### disconnect - /// - /// Disconnect from the remote server - fn disconnect(&mut self) -> FileTransferResult<()>; - - /// ### is_connected - /// - /// Indicates whether the client is connected to remote - fn is_connected(&self) -> bool; - - /// ### pwd - /// - /// Print working directory - - fn pwd(&mut self) -> FileTransferResult; - - /// ### change_dir - /// - /// Change working directory - - fn change_dir(&mut self, dir: &Path) -> FileTransferResult; - - /// ### copy - /// - /// Copy file to destination - fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()>; - - /// ### list_dir - /// - /// List directory entries - - fn list_dir(&mut self, path: &Path) -> FileTransferResult>; - - /// ### mkdir - /// - /// Make directory - /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` - fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()>; - - /// ### remove - /// - /// Remove a file or a directory - fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()>; - - /// ### rename - /// - /// Rename file or a directory - fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()>; - - /// ### stat - /// - /// Stat file and return FsEntry - fn stat(&mut self, path: &Path) -> FileTransferResult; - - /// ### exec - /// - /// Execute a command on remote host - fn exec(&mut self, cmd: &str) -> FileTransferResult; - - /// ### send_file - /// - /// Send file to remote - /// File name is referred to the name of the file as it will be saved - /// Data contains the file data - /// Returns file and its size. - /// By default returns unsupported feature - fn send_file( - &mut self, - _local: &FsFile, - _file_name: &Path, - ) -> FileTransferResult> { - Err(FileTransferError::new( - FileTransferErrorType::UnsupportedFeature, - )) - } - - /// ### recv_file - /// - /// Receive file from remote with provided name - /// Returns file and its size - /// By default returns unsupported feature - fn recv_file(&mut self, _file: &FsFile) -> FileTransferResult> { - Err(FileTransferError::new( - FileTransferErrorType::UnsupportedFeature, - )) - } - - /// ### on_sent - /// - /// Finalize send method. - /// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())` - /// The purpose of this method is to finalize the connection with the peer when writing data. - /// This is necessary for some protocols such as FTP. - /// You must call this method each time you want to finalize the write of the remote file. - /// By default this function returns already `Ok(())` - fn on_sent(&mut self, _writable: Box) -> FileTransferResult<()> { - Ok(()) - } - - /// ### on_recv - /// - /// Finalize recv method. - /// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())` - /// The purpose of this method is to finalize the connection with the peer when reading data. - /// This mighe be necessary for some protocols. - /// You must call this method each time you want to finalize the read of the remote file. - /// By default this function returns already `Ok(())` - fn on_recv(&mut self, _readable: Box) -> FileTransferResult<()> { - Ok(()) - } - - /// ### send_file_wno_stream - /// - /// Send a file to remote WITHOUT using streams. - /// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer. - /// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent` - /// If the function returns error kind() `UnsupportedFeature`, then he should call this function. - /// By default this function uses the streams function to copy content from reader to writer - fn send_file_wno_stream( - &mut self, - src: &FsFile, - dest: &Path, - mut reader: Box, - ) -> FileTransferResult<()> { - match self.is_connected() { - true => { - let mut stream = self.send_file(src, dest)?; - io::copy(&mut reader, &mut stream).map_err(|e| { - FileTransferError::new_ex(FileTransferErrorType::ProtocolError, e.to_string()) - })?; - self.on_sent(stream) - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### recv_file_wno_stream - /// - /// Receive a file from remote WITHOUT using streams. - /// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer. - /// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent` - /// If the function returns error kind() `UnsupportedFeature`, then he should call this function. - /// For safety reasons this function doesn't accept the `Write` trait, but the destination path. - /// By default this function uses the streams function to copy content from reader to writer - fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> { - match self.is_connected() { - true => { - let mut writer = File::create(dest).map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - format!("Could not open local file: {}", e), - ) - })?; - let mut stream = self.recv_file(src)?; - io::copy(&mut stream, &mut writer) - .map(|_| ()) - .map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - e.to_string(), - ) - })?; - self.on_recv(stream) - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### find - /// - /// Find files from current directory (in all subdirectories) whose name matches the provided search - /// Search supports wildcards ('?', '*') - fn find(&mut self, search: &str) -> FileTransferResult> { - match self.is_connected() { - true => { - // Starting from current directory, iter dir - match self.pwd() { - Ok(p) => self.iter_search(p.as_path(), &WildMatch::new(search)), - Err(err) => Err(err), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### iter_search - /// - /// Search recursively in `dir` for file matching the wildcard. - /// NOTE: DON'T RE-IMPLEMENT THIS FUNCTION, unless the file transfer provides a faster way to do so - /// NOTE: don't call this method from outside; consider it as private - fn iter_search(&mut self, dir: &Path, filter: &WildMatch) -> FileTransferResult> { - let mut drained: Vec = Vec::new(); - // Scan directory - match self.list_dir(dir) { - Ok(entries) => { - /* For each entry: - - if is dir: call iter_search with `dir` - - push `iter_search` result to `drained` - - if is file: check if it matches `filter` - - if it matches `filter`: push to to filter - */ - for entry in entries.iter() { - match entry { - FsEntry::Directory(dir) => { - // If directory name, matches wildcard, push it to drained - if filter.matches(dir.name.as_str()) { - drained.push(FsEntry::Directory(dir.clone())); - } - drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?); - } - FsEntry::File(file) => { - if filter.matches(file.name.as_str()) { - drained.push(FsEntry::File(file.clone())); - } - } - } - } - Ok(drained) - } - Err(err) => Err(err), - } - } -} - // Traits impl std::string::ToString for FileTransferProtocol { @@ -479,96 +151,4 @@ mod tests { assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP")); assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3")); } - - #[test] - fn test_filetransfer_mod_error() { - let err: FileTransferError = FileTransferError::new_ex( - FileTransferErrorType::NoSuchFileOrDirectory, - String::from("non va una mazza"), - ); - assert_eq!(*err.msg.as_ref().unwrap(), String::from("non va una mazza")); - assert_eq!( - format!("{}", err), - String::from("No such file or directory (non va una mazza)") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::AuthenticationFailed) - ), - String::from("Authentication failed") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::BadAddress) - ), - String::from("Bad address syntax") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::ConnectionError) - ), - String::from("Connection error") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::DirStatFailed) - ), - String::from("Could not stat directory") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::FileCreateDenied) - ), - String::from("Failed to create file") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::NoSuchFileOrDirectory) - ), - String::from("No such file or directory") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::PexError) - ), - String::from("Not enough permissions") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::ProtocolError) - ), - String::from("Protocol error") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::SslError) - ), - String::from("SSL error") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::UninitializedSession) - ), - String::from("Uninitialized session") - ); - assert_eq!( - format!( - "{}", - FileTransferError::new(FileTransferErrorType::UnsupportedFeature) - ), - String::from("Unsupported feature") - ); - let err = FileTransferError::new(FileTransferErrorType::UnsupportedFeature); - assert_eq!(err.kind(), FileTransferErrorType::UnsupportedFeature); - } } diff --git a/src/filetransfer/params.rs b/src/filetransfer/params.rs index c274ceb5..74066e8b 100644 --- a/src/filetransfer/params.rs +++ b/src/filetransfer/params.rs @@ -103,8 +103,7 @@ impl Default for ProtocolParams { } impl ProtocolParams { - /// ### generic_params - /// + #[cfg(test)] /// Retrieve generic parameters from protocol params if any pub fn generic_params(&self) -> Option<&GenericProtocolParams> { match self { @@ -120,8 +119,7 @@ impl ProtocolParams { } } - /// ### s3_params - /// + #[cfg(test)] /// Retrieve AWS S3 parameters if any pub fn s3_params(&self) -> Option<&AwsS3Params> { match self { diff --git a/src/filetransfer/transfer/ftp.rs b/src/filetransfer/transfer/ftp.rs deleted file mode 100644 index 112c455b..00000000 --- a/src/filetransfer/transfer/ftp.rs +++ /dev/null @@ -1,960 +0,0 @@ -//! ## FTP transfer -//! -//! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS file transfer - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -use super::{ - FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams, -}; -use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; -use crate::utils::fmt::shadow_password; -use crate::utils::path; - -// Includes -use std::convert::TryFrom; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -use std::time::UNIX_EPOCH; -use suppaftp::native_tls::TlsConnector; -use suppaftp::{ - list::{File, PosixPexQuery}, - status::FILE_UNAVAILABLE, - types::{FileType, Response}, - FtpError, FtpStream, -}; - -/// ## FtpFileTransfer -/// -/// Ftp file transfer struct -pub struct FtpFileTransfer { - stream: Option, - ftps: bool, -} - -impl FtpFileTransfer { - /// ### new - /// - /// Instantiates a new `FtpFileTransfer` - pub fn new(ftps: bool) -> FtpFileTransfer { - FtpFileTransfer { stream: None, ftps } - } - - /// ### resolve - /// - /// Fix provided path; on Windows fixes the backslashes, converting them to slashes - /// While on POSIX does nothing - #[cfg(target_os = "windows")] - fn resolve(p: &Path) -> PathBuf { - PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str()) - } - - #[cfg(target_family = "unix")] - fn resolve(p: &Path) -> PathBuf { - p.to_path_buf() - } - - /// ### parse_list_lines - /// - /// Parse all lines of LIST command output and instantiates a vector of FsEntry from it. - /// This function also converts from `suppaftp::list::File` to `FsEntry` - fn parse_list_lines(&mut self, path: &Path, lines: Vec) -> Vec { - // Iter and collect - lines - .into_iter() - .map(File::try_from) // Try to convert to file - .flatten() // Remove errors - .map(|x| { - let mut abs_path: PathBuf = path.to_path_buf(); - abs_path.push(x.name()); - match x.is_directory() { - true => FsEntry::Directory(FsDirectory { - name: x.name().to_string(), - abs_path, - last_access_time: x.modified(), - last_change_time: x.modified(), - creation_time: x.modified(), - symlink: None, - user: x.uid(), - group: x.gid(), - unix_pex: Some(Self::query_unix_pex(&x)), - }), - false => FsEntry::File(FsFile { - name: x.name().to_string(), - size: x.size(), - ftype: abs_path - .extension() - .map(|ext| String::from(ext.to_str().unwrap_or(""))), - last_access_time: x.modified(), - last_change_time: x.modified(), - creation_time: x.modified(), - user: x.uid(), - group: x.gid(), - symlink: Self::get_symlink_entry(path, x.symlink()), - abs_path, - unix_pex: Some(Self::query_unix_pex(&x)), - }), - } - }) - .collect() - } - - /// ### get_symlink_entry - /// - /// Get FsEntry from symlink - fn get_symlink_entry(wrkdir: &Path, link: Option<&Path>) -> Option> { - match link { - None => None, - Some(p) => { - // Make abs path - let abs_path: PathBuf = path::absolutize(wrkdir, p); - Some(Box::new(FsEntry::File(FsFile { - name: p - .file_name() - .map(|x| x.to_str().unwrap_or("").to_string()) - .unwrap_or_default(), - ftype: abs_path - .extension() - .map(|ext| String::from(ext.to_str().unwrap_or(""))), - size: 0, - last_access_time: UNIX_EPOCH, - last_change_time: UNIX_EPOCH, - creation_time: UNIX_EPOCH, - user: None, - group: None, - symlink: None, - unix_pex: None, - abs_path, - }))) - } - } - } - - /// ### query_unix_pex - /// - /// Returns unix pex in tuple of values - fn query_unix_pex(f: &File) -> (UnixPex, UnixPex, UnixPex) { - ( - UnixPex::new( - f.can_read(PosixPexQuery::Owner), - f.can_write(PosixPexQuery::Owner), - f.can_execute(PosixPexQuery::Owner), - ), - UnixPex::new( - f.can_read(PosixPexQuery::Group), - f.can_write(PosixPexQuery::Group), - f.can_execute(PosixPexQuery::Group), - ), - UnixPex::new( - f.can_read(PosixPexQuery::Others), - f.can_write(PosixPexQuery::Others), - f.can_execute(PosixPexQuery::Others), - ), - ) - } -} - -impl FileTransfer for FtpFileTransfer { - /// ### connect - /// - /// Connect to the remote server - - fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult> { - let params = match params.generic_params() { - Some(params) => params, - None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)), - }; - // Get stream - info!("Connecting to {}:{}", params.address, params.port); - let mut stream: FtpStream = - match FtpStream::connect(format!("{}:{}", params.address, params.port)) { - Ok(stream) => stream, - Err(err) => { - error!("Failed to connect: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )); - } - }; - // If SSL, open secure session - if self.ftps { - info!("Setting up TLS stream..."); - let ctx = match TlsConnector::builder() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - .build() - { - Ok(tls) => tls, - Err(err) => { - error!("Failed to setup TLS stream: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::SslError, - err.to_string(), - )); - } - }; - stream = match stream.into_secure(ctx, params.address.as_str()) { - Ok(s) => s, - Err(err) => { - error!("Failed to setup TLS stream: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::SslError, - err.to_string(), - )); - } - }; - } - // Login (use anonymous if credentials are unspecified) - let username: String = match ¶ms.username { - Some(u) => u.to_string(), - None => String::from("anonymous"), - }; - let password: String = match ¶ms.password { - Some(pwd) => pwd.to_string(), - None => String::new(), - }; - info!( - "Signin in with username: {}, password: {}", - username, - shadow_password(password.as_str()) - ); - if let Err(err) = stream.login(username.as_str(), password.as_str()) { - error!("Login failed: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::AuthenticationFailed, - err.to_string(), - )); - } - debug!("Setting transfer type to Binary"); - // Initialize file type - if let Err(err) = stream.transfer_type(FileType::Binary) { - error!("Failed to set transfer type to binary: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )); - } - // Set stream - self.stream = Some(stream); - info!("Connection successfully established"); - // Return OK - Ok(self - .stream - .as_ref() - .unwrap() - .get_welcome_msg() - .map(|x| x.to_string())) - } - - /// ### disconnect - /// - /// Disconnect from the remote server - - fn disconnect(&mut self) -> FileTransferResult<()> { - info!("Disconnecting from FTP server..."); - match &mut self.stream { - Some(stream) => match stream.quit() { - Ok(_) => { - self.stream = None; - Ok(()) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### is_connected - /// - /// Indicates whether the client is connected to remote - fn is_connected(&self) -> bool { - self.stream.is_some() - } - - /// ### pwd - /// - /// Print working directory - - fn pwd(&mut self) -> FileTransferResult { - info!("PWD"); - match &mut self.stream { - Some(stream) => match stream.pwd() { - Ok(path) => Ok(PathBuf::from(path.as_str())), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### change_dir - /// - /// Change working directory - - fn change_dir(&mut self, dir: &Path) -> FileTransferResult { - let dir: PathBuf = Self::resolve(dir); - info!("Changing directory to {}", dir.display()); - match &mut self.stream { - Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) { - Ok(_) => Ok(dir), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### copy - /// - /// Copy file to destination - fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> FileTransferResult<()> { - // FTP doesn't support file copy - debug!("COPY issues (will fail, since unsupported)"); - Err(FileTransferError::new( - FileTransferErrorType::UnsupportedFeature, - )) - } - - /// ### list_dir - /// - /// List directory entries - - fn list_dir(&mut self, path: &Path) -> FileTransferResult> { - let dir: PathBuf = Self::resolve(path); - info!("LIST dir {}", dir.display()); - match &mut self.stream { - Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) { - Ok(lines) => { - debug!("Got {} lines in LIST result", lines.len()); - // Iterate over entries - Ok(self.parse_list_lines(path, lines)) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::DirStatFailed, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### mkdir - /// - /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` - fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> { - let dir: PathBuf = Self::resolve(dir); - info!("MKDIR {}", dir.display()); - match &mut self.stream { - Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) { - Ok(_) => Ok(()), - Err(FtpError::UnexpectedResponse(Response { - // Directory already exists - code: FILE_UNAVAILABLE, - body: _, - })) => { - error!("Directory {} already exists", dir.display()); - Err(FileTransferError::new( - FileTransferErrorType::DirectoryAlreadyExists, - )) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### remove - /// - /// Remove a file or a directory - fn remove(&mut self, fsentry: &FsEntry) -> FileTransferResult<()> { - if self.stream.is_none() { - return Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )); - } - info!("Removing entry {}", fsentry.get_abs_path().display()); - let wrkdir: PathBuf = self.pwd()?; - match fsentry { - // Match fs entry... - FsEntry::File(file) => { - // Go to parent directory - if let Some(parent_dir) = file.abs_path.parent() { - debug!("Changing wrkdir to {}", parent_dir.display()); - self.change_dir(parent_dir)?; - } - debug!("entry is a file; removing file {}", file.abs_path.display()); - // Remove file directly - let result = self - .stream - .as_mut() - .unwrap() - .rm(file.name.as_ref()) - .map(|_| ()) - .map_err(|e| { - FileTransferError::new_ex(FileTransferErrorType::PexError, e.to_string()) - }); - // Go to source directory - match self.change_dir(wrkdir.as_path()) { - Err(err) => Err(err), - Ok(_) => result, - } - } - FsEntry::Directory(dir) => { - // Get directory files - debug!("Entry is a directory; iterating directory entries"); - let result = match self.list_dir(dir.abs_path.as_path()) { - Ok(files) => { - // Remove recursively files - debug!("Removing {} entries from directory...", files.len()); - for file in files.iter() { - if let Err(err) = self.remove(file) { - return Err(FileTransferError::new_ex( - FileTransferErrorType::PexError, - err.to_string(), - )); - } - } - // Once all files in directory have been deleted, remove directory - debug!("Finally removing directory {}...", dir.name); - // Enter parent directory - if let Some(parent_dir) = dir.abs_path.parent() { - debug!( - "Changing wrkdir to {} to delete directory {}", - parent_dir.display(), - dir.name - ); - self.change_dir(parent_dir)?; - } - match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) { - Ok(_) => { - debug!("Removed {}", dir.abs_path.display()); - Ok(()) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::PexError, - err.to_string(), - )), - } - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::DirStatFailed, - err.to_string(), - )), - }; - // Restore directory - match self.change_dir(wrkdir.as_path()) { - Err(err) => Err(err), - Ok(_) => result, - } - } - } - } - - /// ### rename - /// - /// Rename file or a directory - fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()> { - let dst: PathBuf = Self::resolve(dst); - info!( - "Renaming {} to {}", - file.get_abs_path().display(), - dst.display() - ); - match &mut self.stream { - Some(stream) => { - // Get name - let src_name: String = match file { - FsEntry::Directory(dir) => dir.name.clone(), - FsEntry::File(file) => file.name.clone(), - }; - // Only names are supported - match stream.rename(src_name.as_str(), &dst.as_path().to_string_lossy()) { - Ok(_) => Ok(()), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - err.to_string(), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### stat - /// - /// Stat file and return FsEntry - fn stat(&mut self, _path: &Path) -> FileTransferResult { - match &mut self.stream { - Some(_) => Err(FileTransferError::new( - FileTransferErrorType::UnsupportedFeature, - )), - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### exec - /// - /// Execute a command on remote host - fn exec(&mut self, _cmd: &str) -> FileTransferResult { - Err(FileTransferError::new( - FileTransferErrorType::UnsupportedFeature, - )) - } - - /// ### send_file - /// - /// Send file to remote - /// File name is referred to the name of the file as it will be saved - /// Data contains the file data - /// Returns file and its size - fn send_file( - &mut self, - _local: &FsFile, - file_name: &Path, - ) -> FileTransferResult> { - let file_name: PathBuf = Self::resolve(file_name); - info!("Sending file {}", file_name.display()); - match &mut self.stream { - Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) { - Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### recv_file - /// - /// Receive file from remote with provided name - /// Returns file and its size - fn recv_file(&mut self, file: &FsFile) -> FileTransferResult> { - info!("Receiving file {}", file.abs_path.display()); - match &mut self.stream { - Some(stream) => match stream.retr_as_stream(&file.abs_path.as_path().to_string_lossy()) - { - Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::NoSuchFileOrDirectory, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### on_sent - /// - /// Finalize send method. - /// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())` - /// The purpose of this method is to finalize the connection with the peer when writing data. - /// This is necessary for some protocols such as FTP. - /// You must call this method each time you want to finalize the write of the remote file. - fn on_sent(&mut self, writable: Box) -> FileTransferResult<()> { - info!("Finalizing put stream"); - match &mut self.stream { - Some(stream) => match stream.finalize_put_stream(writable) { - Ok(_) => Ok(()), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### on_recv - /// - /// Finalize recv method. - /// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())` - /// The purpose of this method is to finalize the connection with the peer when reading data. - /// This mighe be necessary for some protocols. - /// You must call this method each time you want to finalize the read of the remote file. - fn on_recv(&mut self, readable: Box) -> FileTransferResult<()> { - info!("Finalizing get"); - match &mut self.stream { - Some(stream) => match stream.finalize_retr_stream(readable) { - Ok(_) => Ok(()), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - }, - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use crate::filetransfer::params::GenericProtocolParams; - use crate::utils::file::open_file; - #[cfg(feature = "with-containers")] - use crate::utils::test_helpers::write_file; - use crate::utils::test_helpers::{create_sample_file_entry, make_fsentry}; - - use pretty_assertions::assert_eq; - use std::io::{Read, Write}; - use std::time::Duration; - - #[test] - fn test_filetransfer_ftp_new() { - let ftp: FtpFileTransfer = FtpFileTransfer::new(false); - assert_eq!(ftp.ftps, false); - assert!(ftp.stream.is_none()); - // FTPS - let ftp: FtpFileTransfer = FtpFileTransfer::new(true); - assert_eq!(ftp.ftps, true); - assert!(ftp.stream.is_none()); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_ftp_server() { - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - // Sample file - let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); - // Connect - let hostname: String = String::from("127.0.0.1"); - assert!(ftp - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address(hostname) - .port(10021) - .username(Some("test")) - .password(Some("test")) - )) - .is_ok()); - assert_eq!(ftp.is_connected(), true); - // Get pwd - assert_eq!(ftp.pwd().unwrap(), PathBuf::from("/")); - // List dir (dir is empty) - assert_eq!(ftp.list_dir(&Path::new("/")).unwrap().len(), 0); - // Make directory - assert!(ftp.mkdir(PathBuf::from("/home").as_path()).is_ok()); - // Remake directory (should report already exists) - assert_eq!( - ftp.mkdir(PathBuf::from("/home").as_path()) - .err() - .unwrap() - .kind(), - FileTransferErrorType::DirectoryAlreadyExists - ); - // Make directory (err) - assert!(ftp.mkdir(PathBuf::from("/root/pommlar").as_path()).is_err()); - // Change directory - assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok()); - // Change directory (err) - assert!(ftp - .change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path()) - .is_err()); - // Copy (not supported) - assert!(ftp - .copy(&FsEntry::File(entry.clone()), PathBuf::from("/").as_path()) - .is_err()); - // Exec (not supported) - assert!(ftp.exec("echo 1;").is_err()); - // Upload 2 files - let mut writable = ftp - .send_file(&entry, PathBuf::from("omar.txt").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(ftp.on_sent(writable).is_ok()); - let mut writable = ftp - .send_file(&entry, PathBuf::from("README.md").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(ftp.on_sent(writable).is_ok()); - // Upload file (err) - assert!(ftp - .send_file(&entry, PathBuf::from("/ommlar/omarone").as_path()) - .is_err()); - // List dir - let list: Vec = ftp.list_dir(PathBuf::from("/home").as_path()).ok().unwrap(); - assert_eq!(list.len(), 2); - // Find - assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok()); - assert_eq!(ftp.find("*.txt").ok().unwrap().len(), 1); - assert_eq!(ftp.find("*.md").ok().unwrap().len(), 1); - assert_eq!(ftp.find("*.jpeg").ok().unwrap().len(), 0); - assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok()); - // Rename - assert!(ftp.mkdir(PathBuf::from("/uploads").as_path()).is_ok()); - assert!(ftp - .rename( - list.get(0).unwrap(), - PathBuf::from("/uploads/README.txt").as_path() - ) - .is_ok()); - // Rename (err) - assert!(ftp - .rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path()) - .is_err()); - let dummy: FsEntry = FsEntry::File(FsFile { - name: String::from("cucumber.txt"), - abs_path: PathBuf::from("/cucumber.txt"), - last_change_time: UNIX_EPOCH, - last_access_time: UNIX_EPOCH, - creation_time: UNIX_EPOCH, - size: 0, - ftype: Some(String::from("txt")), // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }); - assert!(ftp - .rename(&dummy, PathBuf::from("/a/b/c").as_path()) - .is_err()); - // Remove - assert!(ftp.remove(list.get(1).unwrap()).is_ok()); - assert!(ftp.remove(list.get(1).unwrap()).is_err()); - // Receive file - let mut writable = ftp - .send_file(&entry, PathBuf::from("/uploads/README.txt").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(ftp.on_sent(writable).is_ok()); - let file: FsFile = ftp - .list_dir(PathBuf::from("/uploads").as_path()) - .ok() - .unwrap() - .get(0) - .unwrap() - .clone() - .unwrap_file(); - let mut readable = ftp.recv_file(&file).ok().unwrap(); - let mut data: Vec = vec![0; 1024]; - assert!(readable.read(&mut data).is_ok()); - assert!(ftp.on_recv(readable).is_ok()); - // Receive file (err) - assert!(ftp.recv_file(&entry).is_err()); - // Cleanup - assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok()); - assert!(ftp - .remove(&make_fsentry(PathBuf::from("/home"), true)) - .is_ok()); - assert!(ftp - .remove(&make_fsentry(PathBuf::from("/uploads"), true)) - .is_ok()); - // Disconnect - assert!(ftp.disconnect().is_ok()); - assert_eq!(ftp.is_connected(), false); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_ftp_server_bad_auth() { - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - // Connect - assert!(ftp - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10021) - .username(Some("omar")) - .password(Some("ommlar")) - )) - .is_err()); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_ftp_no_credentials() { - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - assert!(ftp - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10021) - .username::<&str>(None) - .password::<&str>(None) - )) - .is_err()); - } - - #[test] - fn test_filetransfer_ftp_server_bad_server() { - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - // Connect - assert!(ftp - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("mybad.veribad.server") - .port(21) - .username::<&str>(None) - .password::<&str>(None) - )) - .is_err()); - } - - #[test] - fn test_filetransfer_ftp_parse_list_line_unix() { - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - // Simple file - let file: FsFile = ftp - .parse_list_lines( - PathBuf::from("/tmp").as_path(), - vec!["-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt".to_string()], - ) - .get(0) - .unwrap() - .clone() - .unwrap_file(); - assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt")); - assert_eq!(file.name, String::from("omar.txt")); - assert_eq!(file.size, 8192); - assert!(file.symlink.is_none()); - assert_eq!(file.user, None); - assert_eq!(file.group, None); - assert_eq!( - file.unix_pex.unwrap(), - (UnixPex::from(6), UnixPex::from(6), UnixPex::from(4)) - ); - assert_eq!( - file.last_access_time - .duration_since(UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!( - file.last_change_time - .duration_since(UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!( - file.creation_time.duration_since(UNIX_EPOCH).ok().unwrap(), - Duration::from_secs(1541376000) - ); - } - - #[test] - fn test_filetransfer_ftp_list_dir_dos_syntax() { - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - // Connect - assert!(ftp - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("test.rebex.net") - .port(21) - .username(Some("demo")) - .password(Some("password")) - )) - .is_ok()); - // Pwd - assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/")); - // List dir - let files: Vec = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap(); - // There should be at least 1 file - assert!(files.len() > 0); - // Disconnect - assert!(ftp.disconnect().is_ok()); - } - - #[test] - fn test_filetransfer_ftp_uninitialized() { - let file: FsFile = FsFile { - name: String::from("omar.txt"), - abs_path: PathBuf::from("/omar.txt"), - last_change_time: UNIX_EPOCH, - last_access_time: UNIX_EPOCH, - creation_time: UNIX_EPOCH, - size: 0, - ftype: Some(String::from("txt")), // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }; - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - assert!(ftp.change_dir(Path::new("/tmp")).is_err()); - assert!(ftp.disconnect().is_err()); - assert!(ftp.list_dir(Path::new("/tmp")).is_err()); - assert!(ftp.mkdir(Path::new("/tmp")).is_err()); - assert!(ftp - .remove(&make_fsentry(PathBuf::from("/nowhere"), false)) - .is_err()); - assert!(ftp - .rename( - &make_fsentry(PathBuf::from("/nowhere"), false), - PathBuf::from("/culonia").as_path() - ) - .is_err()); - assert!(ftp.pwd().is_err()); - assert!(ftp.stat(Path::new("/tmp")).is_err()); - assert!(ftp.recv_file(&file).is_err()); - assert!(ftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err()); - let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); - let readable: Box = Box::new(std::fs::File::open(temp.path()).unwrap()); - assert!(ftp.on_recv(readable).is_err()); - let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); - let writable: Box = - Box::new(open_file(temp.path(), true, true, true).ok().unwrap()); - assert!(ftp.on_sent(writable).is_err()); - } -} diff --git a/src/filetransfer/transfer/mod.rs b/src/filetransfer/transfer/mod.rs deleted file mode 100644 index 955e5445..00000000 --- a/src/filetransfer/transfer/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! # transfer -//! -//! This module exposes all the file transfers supported by termscp - -// -- import -use super::{ - FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams, -}; - -// -- modules -mod ftp; -mod s3; -mod scp; -mod sftp; - -// -- export -pub use self::s3::S3FileTransfer; -pub use ftp::FtpFileTransfer; -pub use scp::ScpFileTransfer; -pub use sftp::SftpFileTransfer; diff --git a/src/filetransfer/transfer/s3/mod.rs b/src/filetransfer/transfer/s3/mod.rs deleted file mode 100644 index 142913f2..00000000 --- a/src/filetransfer/transfer/s3/mod.rs +++ /dev/null @@ -1,699 +0,0 @@ -//! ## S3 transfer -//! -//! S3 file transfer module - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// -- mod -mod object; - -// Locals -use super::{ - FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams, -}; -use crate::fs::{FsDirectory, FsEntry, FsFile}; -use crate::utils::path; -use object::S3Object; - -// ext -use s3::creds::Credentials; -use s3::serde_types::Object; -use s3::{Bucket, Region}; -use std::fs::File; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -/// ## S3FileTransfer -/// -/// Aws s3 file transfer -pub struct S3FileTransfer { - bucket: Option, - wrkdir: PathBuf, -} - -impl Default for S3FileTransfer { - fn default() -> Self { - Self { - bucket: None, - wrkdir: PathBuf::from("/"), - } - } -} - -impl S3FileTransfer { - /// ### list_objects - /// - /// List objects contained in `p` path - fn list_objects(&self, p: &Path, list_dir: bool) -> FileTransferResult> { - // Make path relative - let key: String = Self::fmt_path(p, list_dir); - debug!("Query list directory {}; key: {}", p.display(), key); - self.query_objects(key, true) - } - - /// ### stat_object - /// - /// Stat an s3 object - fn stat_object(&self, p: &Path) -> FileTransferResult { - let key: String = Self::fmt_path(p, false); - debug!("Query stat object {}; key: {}", p.display(), key); - let objects = self.query_objects(key, false)?; - // Absolutize path - let absol: PathBuf = path::absolutize(Path::new("/"), p); - // Find associated object - match objects - .into_iter() - .find(|x| x.path.as_path() == absol.as_path()) - { - Some(obj) => Ok(obj), - None => Err(FileTransferError::new_ex( - FileTransferErrorType::NoSuchFileOrDirectory, - format!("{}: No such file or directory", p.display()), - )), - } - } - - /// ### query_objects - /// - /// Query objects at key - fn query_objects( - &self, - key: String, - only_direct_children: bool, - ) -> FileTransferResult> { - let results = self.bucket.as_ref().unwrap().list(key.clone(), None); - match results { - Ok(entries) => { - let mut objects: Vec = Vec::new(); - entries.iter().for_each(|x| { - x.contents - .iter() - .filter(|x| { - if only_direct_children { - Self::list_object_should_be_kept(x, key.as_str()) - } else { - true - } - }) - .for_each(|x| objects.push(S3Object::from(x))) - }); - debug!("Found objects: {:?}", objects); - Ok(objects) - } - Err(e) => Err(FileTransferError::new_ex( - FileTransferErrorType::DirStatFailed, - e.to_string(), - )), - } - } - - /// ### list_object_should_be_kept - /// - /// Returns whether object should be kept after list command. - /// The object won't be kept if: - /// - /// 1. is not a direct child of provided dir - fn list_object_should_be_kept(obj: &Object, dir: &str) -> bool { - Self::is_direct_child(obj.key.as_str(), dir) - } - - /// ### is_direct_child - /// - /// Checks whether Object's key is direct child of `parent` path. - fn is_direct_child(key: &str, parent: &str) -> bool { - key == format!("{}{}", parent, S3Object::object_name(key)) - || key == format!("{}{}/", parent, S3Object::object_name(key)) - } - - /// ### resolve - /// - /// Make s3 absolute path from a given path - fn resolve(&self, p: &Path) -> PathBuf { - path::diff_paths(path::absolutize(self.wrkdir.as_path(), p), &Path::new("/")) - .unwrap_or_default() - } - - /// ### fmt_fs_entry_path - /// - /// fmt path for fsentry according to format expected by s3 - fn fmt_fs_file_path(f: &FsFile) -> String { - Self::fmt_path(f.abs_path.as_path(), false) - } - - /// ### fmt_path - /// - /// fmt path for fsentry according to format expected by s3 - fn fmt_path(p: &Path, is_dir: bool) -> String { - // prevent root as slash - if p == Path::new("/") { - return "".to_string(); - } - // Remove root only if absolute - #[cfg(target_family = "unix")] - let is_absolute: bool = p.is_absolute(); - // NOTE: don't use is_absolute: on windows won't work - #[cfg(target_family = "windows")] - let is_absolute: bool = p.display().to_string().starts_with('/'); - let p: PathBuf = match is_absolute { - true => path::diff_paths(p, &Path::new("/")).unwrap_or_default(), - false => p.to_path_buf(), - }; - // NOTE: windows only: resolve paths - #[cfg(target_family = "windows")] - let p: PathBuf = PathBuf::from(path_slash::PathExt::to_slash_lossy(p.as_path()).as_str()); - // Fmt - match is_dir { - true => { - let mut p: String = p.display().to_string(); - if !p.ends_with('/') { - p.push('/'); - } - p - } - false => p.to_string_lossy().to_string(), - } - } -} - -impl FileTransfer for S3FileTransfer { - /// ### connect - /// - /// Connect to the remote server - /// Can return banner / welcome message on success - fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult> { - // Verify parameters are S3 - let params = match params.s3_params() { - Some(params) => params, - None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)), - }; - // Load credentials - debug!("Loading credentials... (profile {:?})", params.profile); - let credentials: Credentials = - Credentials::new(None, None, None, None, params.profile.as_deref()).map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::AuthenticationFailed, - format!("Could not load s3 credentials: {}", e), - ) - })?; - // Parse region - debug!("Parsing region {}", params.region); - let region: Region = Region::from_str(params.region.as_str()).map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::AuthenticationFailed, - format!("Could not parse s3 region: {}", e), - ) - })?; - debug!( - "Credentials loaded! Connecting to bucket {}...", - params.bucket_name - ); - self.bucket = Some( - Bucket::new(params.bucket_name.as_str(), region, credentials).map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::AuthenticationFailed, - format!("Could not connect to bucket {}: {}", params.bucket_name, e), - ) - })?, - ); - info!("Connection successfully established"); - Ok(None) - } - - /// ### disconnect - /// - /// Disconnect from the remote server - fn disconnect(&mut self) -> FileTransferResult<()> { - info!("Disconnecting from S3 bucket..."); - match self.bucket.take() { - Some(bucket) => { - drop(bucket); - Ok(()) - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### is_connected - /// - /// Indicates whether the client is connected to remote - fn is_connected(&self) -> bool { - self.bucket.is_some() - } - - /// ### pwd - /// - /// Print working directory - fn pwd(&mut self) -> FileTransferResult { - info!("PWD"); - match self.is_connected() { - true => Ok(self.wrkdir.clone()), - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### change_dir - /// - /// Change working directory - fn change_dir(&mut self, dir: &Path) -> FileTransferResult { - match &self.bucket.is_some() { - true => { - // Always allow entering root - if dir == Path::new("/") { - self.wrkdir = dir.to_path_buf(); - info!("New working directory: {}", self.wrkdir.display()); - return Ok(self.wrkdir.clone()); - } - // Check if directory exists - debug!("Entering directory {}...", dir.display()); - let dir_p: PathBuf = self.resolve(dir); - let dir_s: String = Self::fmt_path(dir_p.as_path(), true); - debug!("Searching for key {} (path: {})...", dir_s, dir_p.display()); - // Check if directory already exists - if self - .stat_object(PathBuf::from(dir_s.as_str()).as_path()) - .is_ok() - { - self.wrkdir = path::absolutize(Path::new("/"), dir_p.as_path()); - info!("New working directory: {}", self.wrkdir.display()); - Ok(self.wrkdir.clone()) - } else { - Err(FileTransferError::new( - FileTransferErrorType::NoSuchFileOrDirectory, - )) - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### copy - /// - /// Copy file to destination - fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> FileTransferResult<()> { - Err(FileTransferError::new( - FileTransferErrorType::UnsupportedFeature, - )) - } - - /// ### list_dir - /// - /// List directory entries - fn list_dir(&mut self, path: &Path) -> FileTransferResult> { - match self.is_connected() { - true => self - .list_objects(path, true) - .map(|x| x.into_iter().map(|x| x.into()).collect()), - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### mkdir - /// - /// Make directory - /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` - fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> { - match &self.bucket { - Some(bucket) => { - let dir: String = Self::fmt_path(self.resolve(dir).as_path(), true); - debug!("Making directory {}...", dir); - // Check if directory already exists - if self - .stat_object(PathBuf::from(dir.as_str()).as_path()) - .is_ok() - { - error!("Directory {} already exists", dir); - return Err(FileTransferError::new( - FileTransferErrorType::DirectoryAlreadyExists, - )); - } - bucket - .put_object(dir.as_str(), &[]) - .map(|_| ()) - .map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - format!("Could not make directory: {}", e), - ) - }) - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### remove - /// - /// Remove a file or a directory - fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()> { - let path = Self::fmt_path( - path::diff_paths(file.get_abs_path(), &Path::new("/")) - .unwrap_or_default() - .as_path(), - file.is_dir(), - ); - info!("Removing object {}...", path); - match &self.bucket { - Some(bucket) => bucket.delete_object(path).map(|_| ()).map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not remove file: {}", e), - ) - }), - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### rename - /// - /// Rename file or a directory - fn rename(&mut self, _file: &FsEntry, _dst: &Path) -> FileTransferResult<()> { - Err(FileTransferError::new( - FileTransferErrorType::UnsupportedFeature, - )) - } - - /// ### stat - /// - /// Stat file and return FsEntry - fn stat(&mut self, p: &Path) -> FileTransferResult { - match self.is_connected() { - true => { - // First try as a "file" - let path: PathBuf = self.resolve(p); - if let Ok(obj) = self.stat_object(path.as_path()) { - return Ok(obj.into()); - } - // Try as a "directory" - debug!("Failed to stat object as file; trying as a directory..."); - let path: PathBuf = PathBuf::from(Self::fmt_path(path.as_path(), true)); - self.stat_object(path.as_path()).map(|x| x.into()) - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### exec - /// - /// Execute a command on remote host - fn exec(&mut self, _cmd: &str) -> FileTransferResult { - Err(FileTransferError::new( - FileTransferErrorType::UnsupportedFeature, - )) - } - - /// ### send_file_wno_stream - /// - /// Send a file to remote WITHOUT using streams. - /// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer. - /// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent` - /// If the function returns error kind() `UnsupportedFeature`, then he should call this function. - /// By default this function uses the streams function to copy content from reader to writer - fn send_file_wno_stream( - &mut self, - _src: &FsFile, - dest: &Path, - mut reader: Box, - ) -> FileTransferResult<()> { - match &mut self.bucket { - Some(bucket) => { - let key = Self::fmt_path(dest, false); - info!("Query PUT for key '{}'", key); - bucket - .put_object_stream(&mut reader, key.as_str()) - .map(|_| ()) - .map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not put file: {}", e), - ) - }) - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### recv_file_wno_stream - /// - /// Receive a file from remote WITHOUT using streams. - /// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer. - /// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent` - /// If the function returns error kind() `UnsupportedFeature`, then he should call this function. - /// By default this function uses the streams function to copy content from reader to writer - fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> { - match &mut self.bucket { - Some(bucket) => { - let mut writer = File::create(dest).map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - format!("Could not open local file: {}", e), - ) - })?; - let key = Self::fmt_fs_file_path(src); - info!("Query GET for key '{}'", key); - bucket - .get_object_stream(key.as_str(), &mut writer) - .map(|_| ()) - .map_err(|e| { - FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not get file: {}", e), - ) - }) - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } -} - -#[cfg(test)] -mod test { - - use super::*; - #[cfg(feature = "with-s3-ci")] - use crate::filetransfer::params::AwsS3Params; - #[cfg(feature = "with-s3-ci")] - use crate::utils::random; - use crate::utils::test_helpers; - - use pretty_assertions::assert_eq; - #[cfg(feature = "with-s3-ci")] - use std::env; - #[cfg(feature = "with-s3-ci")] - use tempfile::NamedTempFile; - - #[test] - fn s3_new() { - let s3: S3FileTransfer = S3FileTransfer::default(); - assert_eq!(s3.wrkdir.as_path(), Path::new("/")); - assert!(s3.bucket.is_none()); - } - - #[test] - fn s3_is_direct_child() { - assert_eq!(S3FileTransfer::is_direct_child("pippo/", ""), true); - assert_eq!( - S3FileTransfer::is_direct_child("pippo/sottocartella/", ""), - false - ); - assert_eq!( - S3FileTransfer::is_direct_child("pippo/sottocartella/", "pippo/"), - true - ); - assert_eq!( - S3FileTransfer::is_direct_child("pippo/sottocartella/", "pippo"), // This case must be handled indeed - false - ); - assert_eq!( - S3FileTransfer::is_direct_child( - "pippo/sottocartella/readme.md", - "pippo/sottocartella/" - ), - true - ); - assert_eq!( - S3FileTransfer::is_direct_child( - "pippo/sottocartella/readme.md", - "pippo/sottocartella/" - ), - true - ); - } - - #[test] - fn s3_resolve() { - let mut s3: S3FileTransfer = S3FileTransfer::default(); - s3.wrkdir = PathBuf::from("/tmp"); - // Absolute - assert_eq!( - s3.resolve(&Path::new("/tmp/sottocartella/")).as_path(), - Path::new("tmp/sottocartella") - ); - // Relative - assert_eq!( - s3.resolve(&Path::new("subfolder/")).as_path(), - Path::new("tmp/subfolder") - ); - } - - #[test] - fn s3_fmt_fs_file_path() { - let f: FsFile = - test_helpers::make_fsentry(&Path::new("/tmp/omar.txt"), false).unwrap_file(); - assert_eq!( - S3FileTransfer::fmt_fs_file_path(&f).as_str(), - "tmp/omar.txt" - ); - } - - #[test] - fn s3_fmt_path() { - assert_eq!( - S3FileTransfer::fmt_path(&Path::new("/tmp/omar.txt"), false).as_str(), - "tmp/omar.txt" - ); - assert_eq!( - S3FileTransfer::fmt_path(&Path::new("omar.txt"), false).as_str(), - "omar.txt" - ); - assert_eq!( - S3FileTransfer::fmt_path(&Path::new("/tmp/subfolder"), true).as_str(), - "tmp/subfolder/" - ); - assert_eq!( - S3FileTransfer::fmt_path(&Path::new("tmp/subfolder"), true).as_str(), - "tmp/subfolder/" - ); - assert_eq!( - S3FileTransfer::fmt_path(&Path::new("tmp"), true).as_str(), - "tmp/" - ); - assert_eq!( - S3FileTransfer::fmt_path(&Path::new("tmp/"), true).as_str(), - "tmp/" - ); - assert_eq!(S3FileTransfer::fmt_path(&Path::new("/"), true).as_str(), ""); - } - - // -- test transfer - #[cfg(feature = "with-s3-ci")] - #[test] - fn s3_filetransfer() { - // Gather s3 environment args - let bucket: String = env::var("AWS_S3_BUCKET").ok().unwrap(); - let region: String = env::var("AWS_S3_REGION").ok().unwrap(); - let params = get_ftparams(bucket, region); - // Get transfer - let mut s3 = S3FileTransfer::default(); - // Connect - assert!(s3.connect(¶ms).is_ok()); - // Check is connected - assert_eq!(s3.is_connected(), true); - // Pwd - assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/")); - // Go to github-ci directory - assert!(s3.change_dir(&Path::new("/github-ci")).is_ok()); - assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/github-ci")); - // Find - assert_eq!(s3.find("*.jpg").ok().unwrap().len(), 1); - // List directory (3 entries) - assert_eq!(s3.list_dir(&Path::new("/github-ci")).ok().unwrap().len(), 3); - // Go to playground - assert!(s3.change_dir(&Path::new("/github-ci/playground")).is_ok()); - assert_eq!( - s3.pwd().ok().unwrap(), - PathBuf::from("/github-ci/playground") - ); - // Create directory - let dir_name: String = format!("{}/", random::random_alphanumeric_with_len(8)); - let mut dir_path: PathBuf = PathBuf::from("/github-ci/playground"); - dir_path.push(dir_name.as_str()); - let dir_entry = test_helpers::make_fsentry(dir_path.as_path(), true); - assert!(s3.mkdir(dir_path.as_path()).is_ok()); - assert!(s3.change_dir(dir_path.as_path()).is_ok()); - // Copy/rename file is unsupported - assert!(s3.copy(&dir_entry, &Path::new("/copia")).is_err()); - assert!(s3.rename(&dir_entry, &Path::new("/copia")).is_err()); - // Exec is unsupported - assert!(s3.exec("omar!").is_err()); - // Stat file - let entry = s3 - .stat(&Path::new("/github-ci/avril_lavigne.jpg")) - .ok() - .unwrap() - .unwrap_file(); - assert_eq!(entry.name.as_str(), "avril_lavigne.jpg"); - assert_eq!( - entry.abs_path.as_path(), - Path::new("/github-ci/avril_lavigne.jpg") - ); - assert_eq!(entry.ftype.as_deref().unwrap(), "jpg"); - assert_eq!(entry.size, 101738); - assert_eq!(entry.user, None); - assert_eq!(entry.group, None); - assert_eq!(entry.unix_pex, None); - // Download file - let (local_file_entry, local_file): (FsFile, NamedTempFile) = - test_helpers::create_sample_file_entry(); - let remote_entry = - test_helpers::make_fsentry(&Path::new("/github-ci/avril_lavigne.jpg"), false) - .unwrap_file(); - assert!(s3 - .recv_file_wno_stream(&remote_entry, local_file.path()) - .is_ok()); - // Upload file - let mut dest_path = dir_path.clone(); - dest_path.push("aurellia_lavagna.jpg"); - let reader = Box::new(File::open(local_file.path()).ok().unwrap()); - assert!(s3 - .send_file_wno_stream(&local_file_entry, dest_path.as_path(), reader) - .is_ok()); - // Remove temp dir - assert!(s3.remove(&dir_entry).is_ok()); - // Disconnect - assert!(s3.disconnect().is_ok()); - } - - #[cfg(feature = "with-s3-ci")] - fn get_ftparams(bucket: String, region: String) -> ProtocolParams { - ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, None)) - } -} diff --git a/src/filetransfer/transfer/s3/object.rs b/src/filetransfer/transfer/s3/object.rs deleted file mode 100644 index 245447a1..00000000 --- a/src/filetransfer/transfer/s3/object.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! ## S3 object -//! -//! This module exposes the S3Object structure, which is an intermediate structure to work with -//! S3 objects. Easy to be converted into a FsEntry. - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -use super::{FsDirectory, FsEntry, FsFile, Object}; -use crate::utils::parser::parse_datetime; -use crate::utils::path; - -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// ## S3Object -/// -/// An intermediate struct to work with s3 `Object`. -/// Really easy to be converted into a `FsEntry` -#[derive(Debug)] -pub struct S3Object { - pub name: String, - pub path: PathBuf, - pub size: usize, - pub last_modified: SystemTime, - /// Whether or not represents a directory. I already know directories don't exist in s3! - pub is_dir: bool, -} - -impl From<&Object> for S3Object { - fn from(obj: &Object) -> Self { - let is_dir: bool = obj.key.ends_with('/'); - let abs_path: PathBuf = path::absolutize( - PathBuf::from("/").as_path(), - PathBuf::from(obj.key.as_str()).as_path(), - ); - let last_modified: SystemTime = - match parse_datetime(obj.last_modified.as_str(), "%Y-%m-%dT%H:%M:%S%Z") { - Ok(dt) => dt, - Err(_) => UNIX_EPOCH, - }; - Self { - name: Self::object_name(obj.key.as_str()), - path: abs_path, - size: obj.size as usize, - last_modified, - is_dir, - } - } -} - -impl From for FsEntry { - fn from(obj: S3Object) -> Self { - let abs_path: PathBuf = path::absolutize(Path::new("/"), obj.path.as_path()); - match obj.is_dir { - true => FsEntry::Directory(FsDirectory { - name: obj.name, - abs_path, - last_change_time: obj.last_modified, - last_access_time: obj.last_modified, - creation_time: obj.last_modified, - symlink: None, - user: None, - group: None, - unix_pex: None, - }), - false => FsEntry::File(FsFile { - name: obj.name, - ftype: obj - .path - .extension() - .map(|x| x.to_string_lossy().to_string()), - abs_path, - size: obj.size, - last_change_time: obj.last_modified, - last_access_time: obj.last_modified, - creation_time: obj.last_modified, - symlink: None, - user: None, - group: None, - unix_pex: None, - }), - } - } -} - -impl S3Object { - /// ### object_name - /// - /// Get object name from key - pub fn object_name(key: &str) -> String { - let mut tokens = key.split('/'); - let count = tokens.clone().count(); - let demi_last: String = match count > 1 { - true => tokens.nth(count - 2).unwrap().to_string(), - false => String::new(), - }; - if let Some(last) = tokens.last() { - // If last is not empty, return last one - if !last.is_empty() { - return last.to_string(); - } - } - // Return demi last - demi_last - } -} - -#[cfg(test)] -mod test { - - use super::*; - - use pretty_assertions::assert_eq; - use std::time::Duration; - - #[test] - fn object_to_s3object_file() { - let obj: Object = Object { - key: String::from("pippo/sottocartella/chiedo.gif"), - e_tag: String::default(), - size: 1516966, - owner: None, - storage_class: String::default(), - last_modified: String::from("2021-08-28T10:20:37.000Z"), - }; - let s3_obj: S3Object = S3Object::from(&obj); - assert_eq!(s3_obj.name.as_str(), "chiedo.gif"); - assert_eq!( - s3_obj.path.as_path(), - Path::new("/pippo/sottocartella/chiedo.gif") - ); - assert_eq!(s3_obj.size, 1516966); - assert_eq!(s3_obj.is_dir, false); - assert_eq!( - s3_obj - .last_modified - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1630146037) - ); - } - - #[test] - fn object_to_s3object_dir() { - let obj: Object = Object { - key: String::from("temp/"), - e_tag: String::default(), - size: 0, - owner: None, - storage_class: String::default(), - last_modified: String::from("2021-08-28T10:20:37.000Z"), - }; - let s3_obj: S3Object = S3Object::from(&obj); - assert_eq!(s3_obj.name.as_str(), "temp"); - assert_eq!(s3_obj.path.as_path(), Path::new("/temp")); - assert_eq!(s3_obj.size, 0); - assert_eq!(s3_obj.is_dir, true); - assert_eq!( - s3_obj - .last_modified - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1630146037) - ); - } - - #[test] - fn fsentry_from_s3obj_file() { - let obj: S3Object = S3Object { - name: String::from("chiedo.gif"), - path: PathBuf::from("/pippo/sottocartella/chiedo.gif"), - size: 1516966, - is_dir: false, - last_modified: UNIX_EPOCH, - }; - let entry: FsFile = FsEntry::from(obj).unwrap_file(); - assert_eq!(entry.name.as_str(), "chiedo.gif"); - assert_eq!( - entry.abs_path.as_path(), - Path::new("/pippo/sottocartella/chiedo.gif") - ); - assert_eq!(entry.creation_time, UNIX_EPOCH); - assert_eq!(entry.last_change_time, UNIX_EPOCH); - assert_eq!(entry.last_access_time, UNIX_EPOCH); - assert_eq!(entry.size, 1516966); - assert_eq!(entry.ftype.unwrap().as_str(), "gif"); - assert_eq!(entry.user, None); - assert_eq!(entry.group, None); - assert_eq!(entry.unix_pex, None); - } - - #[test] - fn fsentry_from_s3obj_directory() { - let obj: S3Object = S3Object { - name: String::from("temp"), - path: PathBuf::from("/temp"), - size: 0, - is_dir: true, - last_modified: UNIX_EPOCH, - }; - let entry: FsDirectory = FsEntry::from(obj).unwrap_dir(); - assert_eq!(entry.name.as_str(), "temp"); - assert_eq!(entry.abs_path.as_path(), Path::new("/temp")); - assert_eq!(entry.creation_time, UNIX_EPOCH); - assert_eq!(entry.last_change_time, UNIX_EPOCH); - assert_eq!(entry.last_access_time, UNIX_EPOCH); - assert_eq!(entry.user, None); - assert_eq!(entry.group, None); - assert_eq!(entry.unix_pex, None); - } - - #[test] - fn object_name() { - assert_eq!( - S3Object::object_name("pippo/sottocartella/chiedo.gif").as_str(), - "chiedo.gif" - ); - assert_eq!( - S3Object::object_name("pippo/sottocartella/").as_str(), - "sottocartella" - ); - assert_eq!(S3Object::object_name("pippo/").as_str(), "pippo"); - } -} diff --git a/src/filetransfer/transfer/scp.rs b/src/filetransfer/transfer/scp.rs deleted file mode 100644 index 78eb1510..00000000 --- a/src/filetransfer/transfer/scp.rs +++ /dev/null @@ -1,1347 +0,0 @@ -//! ## SCP transfer -//! -//! `scps_transfer` is the module which provides the implementation for the SCP file transfer - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// Locals -use super::{ - FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams, -}; -use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; -use crate::system::sshkey_storage::SshKeyStorage; -use crate::utils::fmt::{fmt_time, shadow_password}; -use crate::utils::parser::parse_lstime; - -// Includes -use regex::Regex; -use ssh2::{Channel, Session}; -use std::io::{BufReader, BufWriter, Read, Write}; -use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; -use std::ops::Range; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; - -/// ## ScpFileTransfer -/// -/// SCP file transfer structure -pub struct ScpFileTransfer { - session: Option, - wrkdir: PathBuf, - key_storage: SshKeyStorage, -} - -impl ScpFileTransfer { - /// ### new - /// - /// Instantiates a new ScpFileTransfer - pub fn new(key_storage: SshKeyStorage) -> ScpFileTransfer { - ScpFileTransfer { - session: None, - wrkdir: PathBuf::from("~"), - key_storage, - } - } - - /// ### resolve - /// - /// Fix provided path; on Windows fixes the backslashes, converting them to slashes - /// While on POSIX does nothing - #[cfg(target_os = "windows")] - fn resolve(p: &Path) -> PathBuf { - PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str()) - } - - #[cfg(target_family = "unix")] - fn resolve(p: &Path) -> PathBuf { - p.to_path_buf() - } - - /// ### absolutize - /// - /// Absolutize target path if relative. - /// This also converts backslashes to slashes if relative - fn absolutize(wrkdir: &Path, target: &Path) -> PathBuf { - match target.is_absolute() { - true => target.to_path_buf(), - false => { - let mut p: PathBuf = wrkdir.to_path_buf(); - p.push(target); - Self::resolve(p.as_path()) - } - } - } - - /// ### parse_ls_output - /// - /// Parse a line of `ls -l` output and tokenize the output into a `FsEntry` - fn parse_ls_output(&mut self, path: &Path, line: &str) -> Result { - // Prepare list regex - // NOTE: about this damn regex - lazy_static! { - static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap(); - } - debug!("Parsing LS line: '{}'", line); - // Apply regex to result - match LS_RE.captures(line) { - // String matches regex - Some(metadata) => { - // NOTE: metadata fmt: (regex, file_type, permissions, link_count, uid, gid, filesize, mtime, filename) - // Expected 7 + 1 (8) values: + 1 cause regex is repeated at 0 - if metadata.len() < 8 { - return Err(()); - } - // Collect metadata - // Get if is directory and if is symlink - let (mut is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str() - { - "-" => (false, false), - "l" => (false, true), - "d" => (true, false), - _ => return Err(()), // Ignore special files - }; - // Check string length (unix pex) - if metadata.get(2).unwrap().as_str().len() < 9 { - return Err(()); - } - - let pex = |range: Range| { - let mut count: u8 = 0; - for (i, c) in metadata.get(2).unwrap().as_str()[range].chars().enumerate() { - match c { - '-' => {} - _ => { - count += match i { - 0 => 4, - 1 => 2, - 2 => 1, - _ => 0, - } - } - } - } - count - }; - - // Get unix pex - let unix_pex = ( - UnixPex::from(pex(0..3)), - UnixPex::from(pex(3..6)), - UnixPex::from(pex(6..9)), - ); - - // Parse mtime and convert to SystemTime - let mtime: SystemTime = match parse_lstime( - metadata.get(7).unwrap().as_str(), - "%b %d %Y", - "%b %d %H:%M", - ) { - Ok(t) => t, - Err(_) => SystemTime::UNIX_EPOCH, - }; - // Get uid - let uid: Option = match metadata.get(4).unwrap().as_str().parse::() { - Ok(uid) => Some(uid), - Err(_) => None, - }; - // Get gid - let gid: Option = match metadata.get(5).unwrap().as_str().parse::() { - Ok(gid) => Some(gid), - Err(_) => None, - }; - // Get filesize - let filesize: usize = metadata - .get(6) - .unwrap() - .as_str() - .parse::() - .unwrap_or(0); - // Get link and name - let (file_name, symlink_path): (String, Option) = match is_symlink { - true => self.get_name_and_link(metadata.get(8).unwrap().as_str()), - false => (String::from(metadata.get(8).unwrap().as_str()), None), - }; - // Check if file_name is '.' or '..' - if file_name.as_str() == "." || file_name.as_str() == ".." { - debug!("File name is {}; ignoring entry", file_name); - return Err(()); - } - // Get symlink; PATH mustn't be equal to filename - let symlink: Option> = match symlink_path { - None => None, - Some(p) => match p.file_name().unwrap_or_else(|| std::ffi::OsStr::new("")) - == file_name.as_str() - { - // If name is equal, don't stat path; otherwise it would get stuck - true => None, - false => match self.stat(p.as_path()) { - // If path match filename - Ok(e) => { - // If e is a directory, set is_dir to true - if e.is_dir() { - is_dir = true; - } - Some(Box::new(e)) - } - Err(_) => None, // Ignore errors - }, - }, - }; - // Re-check if is directory - let mut abs_path: PathBuf = PathBuf::from(path); - abs_path.push(file_name.as_str()); - let abs_path: PathBuf = Self::resolve(abs_path.as_path()); - // Get extension - let extension: Option = abs_path - .as_path() - .extension() - .map(|s| String::from(s.to_string_lossy())); - // Return - debug!("Follows LS line '{}' attributes", line); - debug!("Is directory? {}", is_dir); - debug!("Is symlink? {}", is_symlink); - debug!("name: {}", file_name); - debug!("abs_path: {}", abs_path.display()); - debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("symlink: {:?}", symlink); - debug!("user: {:?}", uid); - debug!("group: {:?}", gid); - debug!("unix_pex: {:?}", unix_pex); - debug!("---------------------------------------"); - // Push to entries - Ok(match is_dir { - true => FsEntry::Directory(FsDirectory { - name: file_name, - abs_path, - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - symlink, - user: uid, - group: gid, - unix_pex: Some(unix_pex), - }), - false => FsEntry::File(FsFile { - name: file_name, - abs_path, - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - size: filesize, - ftype: extension, - symlink, - user: uid, - group: gid, - unix_pex: Some(unix_pex), - }), - }) - } - None => Err(()), - } - } - - /// ### get_name_and_link - /// - /// Returns from a `ls -l` command output file name token, the name of the file and the symbolic link (if there is any) - fn get_name_and_link(&self, token: &str) -> (String, Option) { - let tokens: Vec<&str> = token.split(" -> ").collect(); - let filename: String = String::from(*tokens.get(0).unwrap()); - let symlink: Option = tokens.get(1).map(PathBuf::from); - (filename, symlink) - } - - /// ### perform_shell_cmd_with - /// - /// Perform a shell command, but change directory to specified path first - fn perform_shell_cmd_with_path( - &mut self, - path: &Path, - cmd: &str, - ) -> FileTransferResult { - self.perform_shell_cmd(format!("cd \"{}\"; {}", path.display(), cmd).as_str()) - } - - /// ### perform_shell_cmd - /// - /// Perform a shell command and read the output from shell - /// This operation is, obviously, blocking. - fn perform_shell_cmd(&mut self, cmd: &str) -> FileTransferResult { - match self.session.as_mut() { - Some(session) => { - debug!("Running command: {}", cmd); - // Create channel - let mut channel: Channel = match session.channel_session() { - Ok(ch) => ch, - Err(err) => { - return Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not open channel: {}", err), - )) - } - }; - // Execute command - if let Err(err) = channel.exec(cmd) { - return Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not execute command \"{}\": {}", cmd, err), - )); - } - // Read output - let mut output: String = String::new(); - match channel.read_to_string(&mut output) { - Ok(_) => { - // Wait close - let _ = channel.wait_close(); - debug!("Command output: {}", output); - Ok(output) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not read output: {}", err), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } -} - -impl FileTransfer for ScpFileTransfer { - /// ### connect - /// - /// Connect to the remote server - fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult> { - let params = match params.generic_params() { - Some(params) => params, - None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)), - }; - // Setup tcp stream - info!("Connecting to {}:{}", params.address, params.port); - let socket_addresses: Vec = - match format!("{}:{}", params.address, params.port).to_socket_addrs() { - Ok(s) => s.collect(), - Err(err) => { - return Err(FileTransferError::new_ex( - FileTransferErrorType::BadAddress, - err.to_string(), - )) - } - }; - let mut tcp: Option = None; - // Try addresses - for socket_addr in socket_addresses.iter() { - debug!("Trying socket address {}", socket_addr); - match TcpStream::connect_timeout(socket_addr, Duration::from_secs(30)) { - Ok(stream) => { - debug!("{} succeded", socket_addr); - tcp = Some(stream); - break; - } - Err(_) => continue, - } - } - // If stream is None, return connection timeout - let tcp: TcpStream = match tcp { - Some(t) => t, - None => { - error!("No suitable socket address found; connection timeout"); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - String::from("Connection timeout"), - )); - } - }; - // Create session - let mut session: Session = match Session::new() { - Ok(s) => s, - Err(err) => { - error!("Could not create session: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )); - } - }; - // Set TCP stream - session.set_tcp_stream(tcp); - // Open connection - debug!("Initializing handshake"); - if let Err(err) = session.handshake() { - error!("Handshake failed: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )); - } - let username: String = match ¶ms.username { - Some(u) => u.to_string(), - None => String::from(""), - }; - // Check if it is possible to authenticate using a RSA key - match self - .key_storage - .resolve(params.address.as_str(), username.as_str()) - { - Some(rsa_key) => { - debug!( - "Authenticating with user {} and RSA key {}", - username, - rsa_key.display() - ); - // Authenticate with RSA key - if let Err(err) = session.userauth_pubkey_file( - username.as_str(), - None, - rsa_key.as_path(), - params.password.as_deref(), - ) { - error!("Authentication failed: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::AuthenticationFailed, - err.to_string(), - )); - } - } - None => { - // Proceeed with username/password authentication - debug!( - "Authenticating with username {} and password {}", - username, - shadow_password(params.password.as_deref().unwrap_or("")) - ); - if let Err(err) = session.userauth_password( - username.as_str(), - params - .password - .as_ref() - .cloned() - .unwrap_or_else(|| String::from("")) - .as_str(), - ) { - error!("Authentication failed: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::AuthenticationFailed, - err.to_string(), - )); - } - } - } - // Get banner - let banner: Option = session.banner().map(String::from); - debug!( - "Connection established: {}", - banner.as_deref().unwrap_or("") - ); - // Set session - self.session = Some(session); - // Get working directory - debug!("Getting working directory..."); - self.wrkdir = self - .perform_shell_cmd("pwd") - .map(|x| PathBuf::from(x.as_str().trim()))?; - info!( - "Connection established; working directory: {}", - self.wrkdir.display() - ); - Ok(banner) - } - - /// ### disconnect - /// - /// Disconnect from the remote server - fn disconnect(&mut self) -> FileTransferResult<()> { - info!("Disconnecting from remote..."); - match self.session.as_ref() { - Some(session) => { - // Disconnect (greet server with 'Mandi' as they do in Friuli) - match session.disconnect(None, "Mandi!", None) { - Ok(()) => { - // Set session to none - self.session = None; - Ok(()) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### is_connected - /// - /// Indicates whether the client is connected to remote - fn is_connected(&self) -> bool { - self.session.is_some() - } - - /// ### pwd - /// - /// Print working directory - - fn pwd(&mut self) -> FileTransferResult { - info!("PWD: {}", self.wrkdir.display()); - match self.is_connected() { - true => Ok(self.wrkdir.clone()), - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### change_dir - /// - /// Change working directory - - fn change_dir(&mut self, dir: &Path) -> FileTransferResult { - match self.is_connected() { - true => { - let p: PathBuf = self.wrkdir.clone(); - let remote_path: PathBuf = Self::absolutize(Path::new("."), dir); - info!("Changing working directory to {}", remote_path.display()); - // Change directory - match self.perform_shell_cmd_with_path( - p.as_path(), - format!("cd \"{}\"; echo $?; pwd", remote_path.display()).as_str(), - ) { - Ok(output) => { - // Trim - let output: String = String::from(output.as_str().trim()); - // Check if output starts with 0; should be 0{PWD} - match output.as_str().starts_with('0') { - true => { - // Set working directory - self.wrkdir = PathBuf::from(&output.as_str()[1..].trim()); - info!("Changed working directory to {}", self.wrkdir.display()); - Ok(self.wrkdir.clone()) - } - false => Err(FileTransferError::new_ex( - // No such file or directory - FileTransferErrorType::NoSuchFileOrDirectory, - format!("\"{}\"", dir.display()), - )), - } - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### copy - /// - /// Copy file to destination - fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()> { - match self.is_connected() { - true => { - let dst: PathBuf = Self::resolve(dst); - info!( - "Copying {} to {}", - src.get_abs_path().display(), - dst.display() - ); - // Run `cp -rf` - let p: PathBuf = self.wrkdir.clone(); - match self.perform_shell_cmd_with_path( - p.as_path(), - format!( - "cp -rf \"{}\" \"{}\"; echo $?", - src.get_abs_path().display(), - dst.display() - ) - .as_str(), - ) { - Ok(output) => - // Check if output is 0 - { - match output.as_str().trim() == "0" { - true => Ok(()), // File copied - false => Err(FileTransferError::new_ex( - // Could not copy file - FileTransferErrorType::FileCreateDenied, - format!("\"{}\"", dst.display()), - )), - } - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### list_dir - /// - /// List directory entries - - fn list_dir(&mut self, path: &Path) -> FileTransferResult> { - match self.is_connected() { - true => { - // Send ls -l to path - info!("Getting file entries in {}", path.display()); - let path: PathBuf = Self::resolve(path); - let p: PathBuf = self.wrkdir.clone(); - match self.perform_shell_cmd_with_path( - p.as_path(), - format!("unset LANG; ls -la \"{}/\"", path.display()).as_str(), - ) { - Ok(output) => { - // Split output by (\r)\n - let lines: Vec<&str> = output.as_str().lines().collect(); - let mut entries: Vec = Vec::with_capacity(lines.len()); - for line in lines.iter() { - // First line must always be ignored - // Parse row, if ok push to entries - if let Ok(entry) = self.parse_ls_output(path.as_path(), line) { - entries.push(entry); - } - } - info!( - "Found {} out of {} valid file entries", - entries.len(), - lines.len() - ); - Ok(entries) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### mkdir - /// - /// Make directory - /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` - fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> { - match self.is_connected() { - true => { - let dir: PathBuf = Self::resolve(dir); - info!("Making directory {}", dir.display()); - let p: PathBuf = self.wrkdir.clone(); - // If directory already exists, return Err - let mut dir_stat_path: PathBuf = dir.clone(); - dir_stat_path.push("./"); - if self.stat(dir_stat_path.as_path()).is_ok() { - error!("Directory {} already exists", dir.display()); - return Err(FileTransferError::new( - FileTransferErrorType::DirectoryAlreadyExists, - )); - } - // Mkdir dir && echo 0 - match self.perform_shell_cmd_with_path( - p.as_path(), - format!("mkdir \"{}\"; echo $?", dir.display()).as_str(), - ) { - Ok(output) => { - // Check if output is 0 - match output.as_str().trim() == "0" { - true => Ok(()), // Directory created - false => Err(FileTransferError::new_ex( - // Could not create directory - FileTransferErrorType::FileCreateDenied, - format!("\"{}\"", dir.display()), - )), - } - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### remove - /// - /// Remove a file or a directory - fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()> { - // Yay, we have rm -rf here :D - match self.is_connected() { - true => { - // Get path - let path: PathBuf = file.get_abs_path(); - info!("Removing file {}", path.display()); - let p: PathBuf = self.wrkdir.clone(); - match self.perform_shell_cmd_with_path( - p.as_path(), - format!("rm -rf \"{}\"; echo $?", path.display()).as_str(), - ) { - Ok(output) => { - // Check if output is 0 - match output.as_str().trim() == "0" { - true => Ok(()), // Directory created - false => Err(FileTransferError::new_ex( - // Could not create directory - FileTransferErrorType::PexError, - format!("\"{}\"", path.display()), - )), - } - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### rename - /// - /// Rename file or a directory - fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()> { - match self.is_connected() { - true => { - // Get path - let dst: PathBuf = Self::resolve(dst); - let path: PathBuf = file.get_abs_path(); - info!("Renaming {} to {}", path.display(), dst.display()); - let p: PathBuf = self.wrkdir.clone(); - match self.perform_shell_cmd_with_path( - p.as_path(), - format!( - "mv -f \"{}\" \"{}\"; echo $?", - path.display(), - dst.display() - ) - .as_str(), - ) { - Ok(output) => { - // Check if output is 0 - match output.as_str().trim() == "0" { - true => Ok(()), // File renamed - false => Err(FileTransferError::new_ex( - // Could not move file - FileTransferErrorType::PexError, - format!("\"{}\"", path.display()), - )), - } - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### stat - /// - /// Stat file and return FsEntry - fn stat(&mut self, path: &Path) -> FileTransferResult { - let path: PathBuf = Self::absolutize(self.wrkdir.as_path(), path); - match self.is_connected() { - true => { - let p: PathBuf = self.wrkdir.clone(); - info!("Stat {}", path.display()); - // make command; Directories require `-d` option - let cmd: String = match path.to_string_lossy().ends_with('/') { - true => format!("ls -ld \"{}\"", path.display()), - false => format!("ls -l \"{}\"", path.display()), - }; - match self.perform_shell_cmd_with_path(p.as_path(), cmd.as_str()) { - Ok(line) => { - // Parse ls line - let parent: PathBuf = match path.as_path().parent() { - Some(p) => PathBuf::from(p), - None => { - return Err(FileTransferError::new_ex( - FileTransferErrorType::DirStatFailed, - String::from("Path has no parent"), - )) - } - }; - match self.parse_ls_output(parent.as_path(), line.as_str().trim()) { - Ok(entry) => Ok(entry), - Err(_) => Err(FileTransferError::new( - FileTransferErrorType::NoSuchFileOrDirectory, - )), - } - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### exec - /// - /// Execute a command on remote host - fn exec(&mut self, cmd: &str) -> FileTransferResult { - match self.is_connected() { - true => { - let p: PathBuf = self.wrkdir.clone(); - info!("Executing command {}", cmd); - match self.perform_shell_cmd_with_path(p.as_path(), cmd) { - Ok(output) => Ok(output), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### send_file - /// - /// Send file to remote - /// File name is referred to the name of the file as it will be saved - /// Data contains the file data - /// Returns file and its size - fn send_file( - &mut self, - local: &FsFile, - file_name: &Path, - ) -> FileTransferResult> { - match self.session.as_ref() { - Some(session) => { - let file_name: PathBuf = Self::absolutize(self.wrkdir.as_path(), file_name); - info!( - "Sending file {} to {}", - local.abs_path.display(), - file_name.display() - ); - // Set blocking to true - debug!("blocking channel..."); - session.set_blocking(true); - // Calculate file mode - let mode: i32 = match local.unix_pex { - None => 0o644, - Some((u, g, o)) => { - ((u.as_byte() as i32) << 6) - + ((g.as_byte() as i32) << 3) - + (o.as_byte() as i32) - } - }; - // Calculate mtime, atime - let times: (u64, u64) = { - let mtime: u64 = match local - .last_change_time - .duration_since(SystemTime::UNIX_EPOCH) - { - Ok(durr) => durr.as_secs() as u64, - Err(_) => 0, - }; - let atime: u64 = match local - .last_access_time - .duration_since(SystemTime::UNIX_EPOCH) - { - Ok(durr) => durr.as_secs() as u64, - Err(_) => 0, - }; - (mtime, atime) - }; - // We need to get the size of local; NOTE: don't use the `size` attribute, since might be out of sync - let file_size: u64 = match std::fs::metadata(local.abs_path.as_path()) { - Ok(metadata) => metadata.len(), - Err(_) => local.size as u64, // NOTE: fallback to fsentry size - }; - debug!( - "File mode {:?}; mtime: {}, atime: {}; file size: {}", - mode, times.0, times.1, file_size - ); - // Send file - match session.scp_send(file_name.as_path(), mode, file_size, Some(times)) { - Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### recv_file - /// - /// Receive file from remote with provided name - /// Returns file and its size - fn recv_file(&mut self, file: &FsFile) -> FileTransferResult> { - match self.session.as_ref() { - Some(session) => { - info!("Receiving file {}", file.abs_path.display()); - // Set blocking to true - debug!("Set blocking..."); - session.set_blocking(true); - match session.scp_recv(file.abs_path.as_path()) { - Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use crate::filetransfer::params::GenericProtocolParams; - use crate::utils::test_helpers::make_fsentry; - use pretty_assertions::assert_eq; - - #[cfg(feature = "with-containers")] - use crate::utils::test_helpers::{create_sample_file_entry, write_file, write_ssh_key}; - - #[test] - fn test_filetransfer_scp_new() { - let client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); - assert!(client.session.is_none()); - assert_eq!(client.is_connected(), false); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_scp_server() { - let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); - // Sample file - let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); - // Connect - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10222) - .username(Some("sftp")) - .password(Some("password")) - )) - .is_ok()); - // Check session and sftp - assert!(client.session.is_some()); - assert_eq!(client.wrkdir, PathBuf::from("/config")); - assert_eq!(client.is_connected(), true); - // Pwd - assert_eq!(client.wrkdir.clone(), client.pwd().ok().unwrap()); - // Stat - let stat: FsFile = client - .stat(PathBuf::from("sshd.pid").as_path()) - .ok() - .unwrap() - .unwrap_file(); - assert_eq!(stat.abs_path, PathBuf::from("/config/sshd.pid")); - let stat: FsDirectory = client - .stat(PathBuf::from("/config/").as_path()) - .ok() - .unwrap() - .unwrap_dir(); - assert_eq!(stat.abs_path, PathBuf::from("/config/")); - // Stat (err) - assert!(client - .stat(PathBuf::from("/config/5t0ca220.log").as_path()) - .is_err()); - // List dir (dir has 4 (one is hidden :D) entries) - assert!(client.list_dir(&Path::new("/config")).unwrap().len() >= 4); - // Make directory - assert!(client.mkdir(PathBuf::from("/tmp/omar").as_path()).is_ok()); - // Remake directory (should report already exists) - assert_eq!( - client - .mkdir(PathBuf::from("/tmp/omar").as_path()) - .err() - .unwrap() - .kind(), - FileTransferErrorType::DirectoryAlreadyExists - ); - // Make directory (err) - assert!(client - .mkdir(PathBuf::from("/root/aaaaa/pommlar").as_path()) - .is_err()); - // Change directory - assert!(client - .change_dir(PathBuf::from("/tmp/omar").as_path()) - .is_ok()); - // Change directory (err) - assert!(client - .change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path()) - .is_err()); - // Copy file - assert!(client - .copy( - &make_fsentry(PathBuf::from("/config/sshd.pid"), false), - PathBuf::from("/tmp/sshd.pid").as_path() - ) - .is_ok()); - // Copy dir - assert!(client - .copy( - &make_fsentry(PathBuf::from("/tmp/omar"), true), - PathBuf::from("/tmp/ommlar").as_path() - ) - .is_ok()); - // Copy (err) - assert!(client - .copy( - &make_fsentry(PathBuf::from("/tmp/zattera"), false), - PathBuf::from("/").as_path() - ) - .is_err()); - // Exec - assert_eq!(client.exec("echo 5").ok().unwrap().as_str(), "5\n"); - // Change dir to ommlar - assert!(client - .change_dir(PathBuf::from("/tmp/ommlar/").as_path()) - .is_ok()); - // Upload 2 files - let mut writable = client - .send_file(&entry, PathBuf::from("omar.txt").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(client.on_sent(writable).is_ok()); - let mut writable = client - .send_file(&entry, PathBuf::from("README.md").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(client.on_sent(writable).is_ok()); - // Upload file (err) - assert!(client - .send_file(&entry, PathBuf::from("/ommlar/omarone").as_path()) - .is_err()); - // List dir - let list: Vec = client - .list_dir(PathBuf::from("/tmp/ommlar").as_path()) - .ok() - .unwrap(); - assert_eq!(list.len(), 2); - // Find - assert_eq!(client.find("*.txt").ok().unwrap().len(), 1); - assert_eq!(client.find("*.md").ok().unwrap().len(), 1); - assert_eq!(client.find("*.jpeg").ok().unwrap().len(), 0); - // Rename - assert!(client - .mkdir(PathBuf::from("/tmp/uploads").as_path()) - .is_ok()); - assert!(client - .rename( - list.get(0).unwrap(), - PathBuf::from("/tmp/uploads/README.txt").as_path() - ) - .is_ok()); - // Rename (err) - assert!(client - .rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path()) - .is_err()); - let dummy: FsEntry = FsEntry::File(FsFile { - name: String::from("cucumber.txt"), - abs_path: PathBuf::from("/cucumber.txt"), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, - size: 0, - ftype: Some(String::from("txt")), // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }); - assert!(client - .rename(&dummy, PathBuf::from("/a/b/c").as_path()) - .is_err()); - // Remove - assert!(client.remove(list.get(1).unwrap()).is_ok()); - // Receive file - let mut writable = client - .send_file(&entry, PathBuf::from("/tmp/uploads/README.txt").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(client.on_sent(writable).is_ok()); - let file: FsFile = client - .list_dir(PathBuf::from("/tmp/uploads").as_path()) - .ok() - .unwrap() - .get(0) - .unwrap() - .clone() - .unwrap_file(); - let mut readable = client.recv_file(&file).ok().unwrap(); - let mut data: Vec = vec![0; 1024]; - assert!(readable.read(&mut data).is_ok()); - assert!(client.on_recv(readable).is_ok()); - // Receive file (err) - assert!(client.recv_file(&entry).is_err()); - // Cleanup - assert!(client.change_dir(PathBuf::from("/").as_path()).is_ok()); - assert!(client - .remove(&make_fsentry(PathBuf::from("/tmp/ommlar"), true)) - .is_ok()); - assert!(client - .remove(&make_fsentry(PathBuf::from("/tmp/omar"), true)) - .is_ok()); - assert!(client - .remove(&make_fsentry(PathBuf::from("/tmp/uploads"), true)) - .is_ok()); - // Disconnect - assert!(client.disconnect().is_ok()); - assert_eq!(client.is_connected(), false); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_scp_ssh_storage() { - let mut storage: SshKeyStorage = SshKeyStorage::empty(); - let key_file: tempfile::NamedTempFile = write_ssh_key(); - storage.add_key("127.0.0.1", "sftp", key_file.path().to_path_buf()); - let mut client: ScpFileTransfer = ScpFileTransfer::new(storage); - // Connect - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10222) - .username(Some("sftp")) - .password::<&str>(None) - )) - .is_ok()); - assert_eq!(client.is_connected(), true); - assert!(client.disconnect().is_ok()); - } - - #[test] - fn test_filetransfer_scp_bad_auth() { - let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10222) - .username(Some("sftp")) - .password(Some("badpassword")) - )) - .is_err()); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_scp_no_credentials() { - let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10222) - .username::<&str>(None) - .password::<&str>(None) - )) - .is_err()); - } - - #[test] - fn test_filetransfer_scp_bad_server() { - let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("mybad.verybad.server") - .port(10222) - .username(Some("sftp")) - .password(Some("password")) - )) - .is_err()); - } - - #[test] - fn test_filetransfer_scp_parse_ls() { - let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); - // File - let entry: FsFile = client - .parse_ls_output( - PathBuf::from("/tmp").as_path(), - "-rw-r--r-- 1 root root 2056 giu 13 21:11 Cargo.toml", - ) - .ok() - .unwrap() - .unwrap_file(); - assert_eq!(entry.name.as_str(), "Cargo.toml"); - assert_eq!(entry.abs_path, PathBuf::from("/tmp/Cargo.toml")); - assert_eq!( - entry.unix_pex.unwrap(), - (UnixPex::from(6), UnixPex::from(4), UnixPex::from(4)) - ); - assert_eq!(entry.size, 2056); - assert_eq!(entry.ftype.unwrap().as_str(), "toml"); - assert!(entry.symlink.is_none()); - // File (year) - let entry: FsFile = client - .parse_ls_output( - PathBuf::from("/tmp").as_path(), - "-rw-rw-rw- 1 root root 3368 nov 7 2020 CODE_OF_CONDUCT.md", - ) - .ok() - .unwrap() - .unwrap_file(); - assert_eq!(entry.name.as_str(), "CODE_OF_CONDUCT.md"); - assert_eq!(entry.abs_path, PathBuf::from("/tmp/CODE_OF_CONDUCT.md")); - assert_eq!( - entry.unix_pex.unwrap(), - (UnixPex::from(6), UnixPex::from(6), UnixPex::from(6)) - ); - assert_eq!(entry.size, 3368); - assert_eq!(entry.ftype.unwrap().as_str(), "md"); - assert!(entry.symlink.is_none()); - // Directory - let entry: FsDirectory = client - .parse_ls_output( - PathBuf::from("/tmp").as_path(), - "drwxr-xr-x 1 root root 512 giu 13 21:11 docs", - ) - .ok() - .unwrap() - .unwrap_dir(); - assert_eq!(entry.name.as_str(), "docs"); - assert_eq!(entry.abs_path, PathBuf::from("/tmp/docs")); - assert_eq!( - entry.unix_pex.unwrap(), - (UnixPex::from(7), UnixPex::from(5), UnixPex::from(5)) - ); - assert!(entry.symlink.is_none()); - // Short metadata - assert!(client - .parse_ls_output( - PathBuf::from("/tmp").as_path(), - "drwxr-xr-x 1 root root 512 giu 13 21:11", - ) - .is_err()); - // Special file - assert!(client - .parse_ls_output( - PathBuf::from("/tmp").as_path(), - "crwxr-xr-x 1 root root 512 giu 13 21:11 ttyS1", - ) - .is_err()); - // Bad pex - assert!(client - .parse_ls_output( - PathBuf::from("/tmp").as_path(), - "-rwxr-xr 1 root root 512 giu 13 21:11 ttyS1", - ) - .is_err()); - } - - #[test] - fn test_filetransfer_scp_get_name_and_link() { - let client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); - assert_eq!( - client.get_name_and_link("Cargo.toml"), - (String::from("Cargo.toml"), None) - ); - assert_eq!( - client.get_name_and_link("Cargo -> Cargo.toml"), - (String::from("Cargo"), Some(PathBuf::from("Cargo.toml"))) - ); - } - - #[test] - fn test_filetransfer_scp_uninitialized() { - let file: FsFile = FsFile { - name: String::from("omar.txt"), - abs_path: PathBuf::from("/omar.txt"), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, - size: 0, - ftype: Some(String::from("txt")), // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }; - let mut scp: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); - assert!(scp.change_dir(Path::new("/tmp")).is_err()); - assert!(scp.disconnect().is_err()); - assert!(scp.exec("echo 5").is_err()); - assert!(scp.list_dir(Path::new("/tmp")).is_err()); - assert!(scp.mkdir(Path::new("/tmp")).is_err()); - assert!(scp.pwd().is_err()); - assert!(scp - .remove(&make_fsentry(PathBuf::from("/nowhere"), false)) - .is_err()); - assert!(scp - .rename( - &make_fsentry(PathBuf::from("/nowhere"), false), - PathBuf::from("/culonia").as_path() - ) - .is_err()); - assert!(scp.stat(Path::new("/tmp")).is_err()); - assert!(scp.recv_file(&file).is_err()); - assert!(scp.send_file(&file, Path::new("/tmp/omar.txt")).is_err()); - } -} diff --git a/src/filetransfer/transfer/sftp.rs b/src/filetransfer/transfer/sftp.rs deleted file mode 100644 index 49ec4f9c..00000000 --- a/src/filetransfer/transfer/sftp.rs +++ /dev/null @@ -1,1116 +0,0 @@ -//! ## SFTP transfer -//! -//! `sftp_transfer` is the module which provides the implementation for the SFTP file transfer - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// Locals -use super::{ - FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams, -}; -use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; -use crate::system::sshkey_storage::SshKeyStorage; -use crate::utils::fmt::{fmt_time, shadow_password}; - -// Includes -use ssh2::{Channel, FileStat, OpenFlags, OpenType, Session, Sftp}; -use std::io::{BufReader, BufWriter, Read, Write}; -use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; - -/// ## SftpFileTransfer -/// -/// SFTP file transfer structure -pub struct SftpFileTransfer { - session: Option, - sftp: Option, - wrkdir: PathBuf, - key_storage: SshKeyStorage, -} - -impl SftpFileTransfer { - /// ### new - /// - /// Instantiates a new SftpFileTransfer - pub fn new(key_storage: SshKeyStorage) -> SftpFileTransfer { - SftpFileTransfer { - session: None, - sftp: None, - wrkdir: PathBuf::from("~"), - key_storage, - } - } - - /// ### get_abs_path - /// - /// Get absolute path from path argument and check if it exists - fn get_remote_path(&self, p: &Path) -> FileTransferResult { - match p.is_relative() { - true => { - let mut root: PathBuf = self.wrkdir.clone(); - root.push(p); - match self.sftp.as_ref().unwrap().realpath(root.as_path()) { - Ok(p) => match self.sftp.as_ref().unwrap().stat(p.as_path()) { - Ok(_) => Ok(p), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::NoSuchFileOrDirectory, - err.to_string(), - )), - }, - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::NoSuchFileOrDirectory, - err.to_string(), - )), - } - } - false => match self.sftp.as_ref().unwrap().realpath(p) { - Ok(p) => match self.sftp.as_ref().unwrap().stat(p.as_path()) { - Ok(_) => Ok(p), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::NoSuchFileOrDirectory, - err.to_string(), - )), - }, - Err(_) => Err(FileTransferError::new( - FileTransferErrorType::NoSuchFileOrDirectory, - )), - }, - } - } - - /// ### get_abs_path - /// - /// Returns absolute path on remote, but without errors - fn get_abs_path(&self, p: &Path) -> PathBuf { - match p.is_relative() { - true => { - let mut root: PathBuf = self.wrkdir.clone(); - root.push(p); - match self.sftp.as_ref().unwrap().realpath(root.as_path()) { - Ok(p) => p, - Err(_) => root, - } - } - false => PathBuf::from(p), - } - } - - /// ### make_fsentry - /// - /// Make fsentry from path and metadata - fn make_fsentry(&mut self, path: &Path, metadata: &FileStat) -> FsEntry { - // Get common parameters - let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); - let file_type: Option = path - .extension() - .map(|ext| String::from(ext.to_str().unwrap_or(""))); - let uid: Option = metadata.uid; - let gid: Option = metadata.gid; - let pex: Option<(UnixPex, UnixPex, UnixPex)> = metadata.perm.map(|x| { - ( - UnixPex::from(((x >> 6) & 0x7) as u8), - UnixPex::from(((x >> 3) & 0x7) as u8), - UnixPex::from((x & 0x7) as u8), - ) - }); - let size: u64 = metadata.size.unwrap_or(0); - let mut atime: SystemTime = SystemTime::UNIX_EPOCH; - atime = atime - .checked_add(Duration::from_secs(metadata.atime.unwrap_or(0))) - .unwrap_or(SystemTime::UNIX_EPOCH); - let mut mtime: SystemTime = SystemTime::UNIX_EPOCH; - mtime = mtime - .checked_add(Duration::from_secs(metadata.mtime.unwrap_or(0))) - .unwrap_or(SystemTime::UNIX_EPOCH); - // Check if symlink - let is_symlink: bool = metadata.file_type().is_symlink(); - let symlink: Option> = match is_symlink { - true => { - // Read symlink - match self.sftp.as_ref().unwrap().readlink(path) { - Ok(p) => match self.stat(p.as_path()) { - Ok(entry) => Some(Box::new(entry)), - Err(_) => None, // Ignore errors - }, - Err(_) => None, - } - } - false => None, - }; - debug!("Follows {} attributes", path.display()); - debug!("Is directory? {}", metadata.is_dir()); - debug!("Is symlink? {}", is_symlink); - debug!("name: {}", file_name); - debug!("abs_path: {}", path.display()); - debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("symlink: {:?}", symlink); - debug!("user: {:?}", uid); - debug!("group: {:?}", gid); - debug!("unix_pex: {:?}", pex); - debug!("---------------------------------------"); - // Is a directory? - match metadata.is_dir() { - true => FsEntry::Directory(FsDirectory { - name: file_name, - abs_path: PathBuf::from(path), - last_change_time: mtime, - last_access_time: atime, - creation_time: SystemTime::UNIX_EPOCH, - symlink, - user: uid, - group: gid, - unix_pex: pex, - }), - false => FsEntry::File(FsFile { - name: file_name, - abs_path: PathBuf::from(path), - size: size as usize, - ftype: file_type, - last_change_time: mtime, - last_access_time: atime, - creation_time: SystemTime::UNIX_EPOCH, - symlink, - user: uid, - group: gid, - unix_pex: pex, - }), - } - } - - /// ### perform_shell_cmd_with - /// - /// Perform a shell command, but change directory to specified path first - fn perform_shell_cmd_with_path(&mut self, cmd: &str) -> FileTransferResult { - self.perform_shell_cmd(format!("cd \"{}\"; {}", self.wrkdir.display(), cmd).as_str()) - } - - /// ### perform_shell_cmd - /// - /// Perform a shell command and read the output from shell - /// This operation is, obviously, blocking. - fn perform_shell_cmd(&mut self, cmd: &str) -> FileTransferResult { - match self.session.as_mut() { - Some(session) => { - // Create channel - debug!("Running command: {}", cmd); - let mut channel: Channel = match session.channel_session() { - Ok(ch) => ch, - Err(err) => { - return Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not open channel: {}", err), - )) - } - }; - // Execute command - if let Err(err) = channel.exec(cmd) { - return Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not execute command \"{}\": {}", cmd, err), - )); - } - // Read output - let mut output: String = String::new(); - match channel.read_to_string(&mut output) { - Ok(_) => { - // Wait close - let _ = channel.wait_close(); - debug!("Command output: {}", output); - Ok(output) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - format!("Could not read output: {}", err), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } -} - -impl FileTransfer for SftpFileTransfer { - /// ### connect - /// - /// Connect to the remote server - fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult> { - let params = match params.generic_params() { - Some(params) => params, - None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)), - }; - // Setup tcp stream - info!("Connecting to {}:{}", params.address, params.port); - let socket_addresses: Vec = - match format!("{}:{}", params.address, params.port).to_socket_addrs() { - Ok(s) => s.collect(), - Err(err) => { - return Err(FileTransferError::new_ex( - FileTransferErrorType::BadAddress, - err.to_string(), - )) - } - }; - let mut tcp: Option = None; - // Try addresses - for socket_addr in socket_addresses.iter() { - debug!("Trying socket address {}", socket_addr); - match TcpStream::connect_timeout(socket_addr, Duration::from_secs(30)) { - Ok(stream) => { - tcp = Some(stream); - break; - } - Err(_) => continue, - } - } - // If stream is None, return connection timeout - let tcp: TcpStream = match tcp { - Some(t) => t, - None => { - error!("No suitable socket address found; connection timeout"); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - String::from("Connection timeout"), - )); - } - }; - // Create session - let mut session: Session = match Session::new() { - Ok(s) => s, - Err(err) => { - error!("Could not create session: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )); - } - }; - // Set TCP stream - session.set_tcp_stream(tcp); - // Open connection - debug!("Initializing handshake"); - if let Err(err) = session.handshake() { - error!("Handshake failed: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )); - } - let username: String = match ¶ms.username { - Some(u) => u.to_string(), - None => String::from(""), - }; - // Check if it is possible to authenticate using a RSA key - match self - .key_storage - .resolve(params.address.as_str(), username.as_str()) - { - Some(rsa_key) => { - debug!( - "Authenticating with user {} and RSA key {}", - username, - rsa_key.display() - ); - // Authenticate with RSA key - if let Err(err) = session.userauth_pubkey_file( - username.as_str(), - None, - rsa_key.as_path(), - params.password.as_deref(), - ) { - error!("Authentication failed: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::AuthenticationFailed, - err.to_string(), - )); - } - } - None => { - // Proceeed with username/password authentication - debug!( - "Authenticating with username {} and password {}", - username, - shadow_password(params.password.as_deref().unwrap_or("")) - ); - if let Err(err) = session.userauth_password( - username.as_str(), - params - .password - .as_ref() - .cloned() - .unwrap_or_else(|| String::from("")) - .as_str(), - ) { - error!("Authentication failed: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::AuthenticationFailed, - err.to_string(), - )); - } - } - } - // Set blocking to true - session.set_blocking(true); - // Get Sftp client - debug!("Getting SFTP client..."); - let sftp: Sftp = match session.sftp() { - Ok(s) => s, - Err(err) => { - error!("Could not get sftp client: {}", err); - return Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )); - } - }; - // Get working directory - debug!("Getting working directory..."); - self.wrkdir = match sftp.realpath(PathBuf::from(".").as_path()) { - Ok(p) => p, - Err(err) => { - return Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )) - } - }; - // Set session - let banner: Option = session.banner().map(String::from); - self.session = Some(session); - // Set sftp - self.sftp = Some(sftp); - info!( - "Connection established: {}; working directory {}", - banner.as_deref().unwrap_or(""), - self.wrkdir.display() - ); - Ok(banner) - } - - /// ### disconnect - /// - /// Disconnect from the remote server - fn disconnect(&mut self) -> FileTransferResult<()> { - info!("Disconnecting from remote..."); - match self.session.as_ref() { - Some(session) => { - // Disconnect (greet server with 'Mandi' as they do in Friuli) - match session.disconnect(None, "Mandi!", None) { - Ok(()) => { - // Set session and sftp to none - self.session = None; - self.sftp = None; - Ok(()) - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ConnectionError, - err.to_string(), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### is_connected - /// - /// Indicates whether the client is connected to remote - fn is_connected(&self) -> bool { - self.session.is_some() - } - - /// ### pwd - /// - /// Print working directory - fn pwd(&mut self) -> FileTransferResult { - info!("PWD: {}", self.wrkdir.display()); - match self.sftp { - Some(_) => Ok(self.wrkdir.clone()), - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### change_dir - /// - /// Change working directory - fn change_dir(&mut self, dir: &Path) -> FileTransferResult { - match self.sftp.as_ref() { - Some(_) => { - // Change working directory - self.wrkdir = self.get_remote_path(dir)?; - info!("Changed working directory to {}", self.wrkdir.display()); - Ok(self.wrkdir.clone()) - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### copy - /// - /// Copy file to destination - fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()> { - // NOTE: use SCP command to perform copy (UNSAFE) - match self.is_connected() { - true => { - let dst: PathBuf = self.get_abs_path(dst); - info!( - "Copying {} to {}", - src.get_abs_path().display(), - dst.display() - ); - // Run `cp -rf` - match self.perform_shell_cmd_with_path( - format!( - "cp -rf \"{}\" \"{}\"; echo $?", - src.get_abs_path().display(), - dst.display() - ) - .as_str(), - ) { - Ok(output) => - // Check if output is 0 - { - match output.as_str().trim() == "0" { - true => Ok(()), // File copied - false => Err(FileTransferError::new_ex( - // Could not copy file - FileTransferErrorType::FileCreateDenied, - format!("\"{}\"", dst.display()), - )), - } - } - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - } - } - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### list_dir - /// - /// List directory entries - fn list_dir(&mut self, path: &Path) -> FileTransferResult> { - match self.sftp.as_ref() { - Some(sftp) => { - // Get path - let dir: PathBuf = self.get_remote_path(path)?; - info!("Getting file entries in {}", path.display()); - // Get files - match sftp.readdir(dir.as_path()) { - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::DirStatFailed, - err.to_string(), - )), - Ok(files) => { - // Allocate vector - let mut entries: Vec = Vec::with_capacity(files.len()); - // Iterate over files - for (path, metadata) in files { - entries.push(self.make_fsentry(path.as_path(), &metadata)); - } - Ok(entries) - } - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### mkdir - /// - /// Make directory - /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` - fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> { - match self.sftp.as_ref() { - Some(sftp) => { - // Make directory - let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path()); - // If directory already exists, return Err - if sftp.stat(path.as_path()).is_ok() { - error!("Directory {} already exists", path.display()); - return Err(FileTransferError::new( - FileTransferErrorType::DirectoryAlreadyExists, - )); - } - info!("Making directory {}", path.display()); - match sftp.mkdir(path.as_path(), 0o775) { - Ok(_) => Ok(()), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - err.to_string(), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### remove - /// - /// Remove a file or a directory - fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()> { - if self.sftp.is_none() { - return Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )); - } - // Match if file is a file or a directory - info!("Removing file {}", file.get_abs_path().display()); - match file { - FsEntry::File(f) => { - // Remove file - match self.sftp.as_ref().unwrap().unlink(f.abs_path.as_path()) { - Ok(_) => Ok(()), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::PexError, - err.to_string(), - )), - } - } - FsEntry::Directory(d) => { - // Remove recursively - debug!("{} is a directory; removing all directory entries", d.name); - // Get directory files - let directory_content: Vec = self.list_dir(d.abs_path.as_path())?; - for entry in directory_content.iter() { - if let Err(err) = self.remove(entry) { - return Err(err); - } - } - // Finally remove directory - match self.sftp.as_ref().unwrap().rmdir(d.abs_path.as_path()) { - Ok(_) => Ok(()), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::PexError, - err.to_string(), - )), - } - } - } - } - - /// ### rename - /// - /// Rename file or a directory - fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()> { - match self.sftp.as_ref() { - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - Some(sftp) => { - info!( - "Moving {} to {}", - file.get_abs_path().display(), - dst.display() - ); - // Resolve destination path - let abs_dst: PathBuf = self.get_abs_path(dst); - // Get abs path of entry - let abs_src: PathBuf = file.get_abs_path(); - match sftp.rename(abs_src.as_path(), abs_dst.as_path(), None) { - Ok(_) => Ok(()), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - err.to_string(), - )), - } - } - } - } - - /// ### stat - /// - /// Stat file and return FsEntry - fn stat(&mut self, path: &Path) -> FileTransferResult { - match self.sftp.as_ref() { - Some(sftp) => { - // Get path - let dir: PathBuf = self.get_remote_path(path)?; - info!("Stat file {}", dir.display()); - // Get file - match sftp.stat(dir.as_path()) { - Ok(metadata) => Ok(self.make_fsentry(dir.as_path(), &metadata)), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::NoSuchFileOrDirectory, - err.to_string(), - )), - } - } - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### exec - /// - /// Execute a command on remote host - fn exec(&mut self, cmd: &str) -> FileTransferResult { - info!("Executing command {}", cmd); - match self.is_connected() { - true => match self.perform_shell_cmd_with_path(cmd) { - Ok(output) => Ok(output), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::ProtocolError, - err.to_string(), - )), - }, - false => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - } - } - - /// ### send_file - /// - /// Send file to remote - /// File name is referred to the name of the file as it will be saved - /// Data contains the file data - fn send_file( - &mut self, - local: &FsFile, - file_name: &Path, - ) -> FileTransferResult> { - match self.sftp.as_ref() { - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - Some(sftp) => { - let remote_path: PathBuf = self.get_abs_path(file_name); - info!( - "Sending file {} to {}", - local.abs_path.display(), - remote_path.display() - ); - // Calculate file mode - let mode: i32 = match local.unix_pex { - None => 0o644, - Some((u, g, o)) => { - ((u.as_byte() as i32) << 6) - + ((g.as_byte() as i32) << 3) - + (o.as_byte() as i32) - } - }; - debug!("File mode {:?}", mode); - match sftp.open_mode( - remote_path.as_path(), - OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE, - mode, - OpenType::File, - ) { - Ok(file) => Ok(Box::new(BufWriter::with_capacity(65536, file))), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::FileCreateDenied, - err.to_string(), - )), - } - } - } - } - - /// ### recv_file - /// - /// Receive file from remote with provided name - fn recv_file(&mut self, file: &FsFile) -> FileTransferResult> { - match self.sftp.as_ref() { - None => Err(FileTransferError::new( - FileTransferErrorType::UninitializedSession, - )), - Some(sftp) => { - // Get remote file name - let remote_path: PathBuf = self.get_remote_path(file.abs_path.as_path())?; - info!("Receiving file {}", remote_path.display()); - // Open remote file - match sftp.open(remote_path.as_path()) { - Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))), - Err(err) => Err(FileTransferError::new_ex( - FileTransferErrorType::NoSuchFileOrDirectory, - err.to_string(), - )), - } - } - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use crate::filetransfer::params::GenericProtocolParams; - use crate::utils::test_helpers::make_fsentry; - #[cfg(feature = "with-containers")] - use crate::utils::test_helpers::{ - create_sample_file, create_sample_file_entry, write_file, write_ssh_key, - }; - use pretty_assertions::assert_eq; - #[cfg(feature = "with-containers")] - use std::fs::File; - - #[test] - fn test_filetransfer_sftp_new() { - let client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); - assert!(client.session.is_none()); - assert!(client.sftp.is_none()); - assert_eq!(client.wrkdir, PathBuf::from("~")); - assert_eq!(client.is_connected(), false); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_sftp_server() { - let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); - // Sample file - let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); - // Connect - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10022) - .username(Some("sftp")) - .password(Some("password")) - )) - .is_ok()); - // Check session and sftp - assert!(client.session.is_some()); - assert!(client.sftp.is_some()); - assert_eq!(client.wrkdir, PathBuf::from("/config")); - assert_eq!(client.is_connected(), true); - // Pwd - assert_eq!(client.wrkdir.clone(), client.pwd().ok().unwrap()); - // Stat - let stat: FsFile = client - .stat(PathBuf::from("/config/sshd.pid").as_path()) - .ok() - .unwrap() - .unwrap_file(); - assert_eq!(stat.name.as_str(), "sshd.pid"); - let stat: FsDirectory = client - .stat(PathBuf::from("/config").as_path()) - .ok() - .unwrap() - .unwrap_dir(); - assert_eq!(stat.name.as_str(), "config"); - // Stat (err) - assert!(client - .stat(PathBuf::from("/config/5t0ca220.log").as_path()) - .is_err()); - // List dir (dir has 4 (one is hidden :D) entries) - assert!(client.list_dir(&Path::new("/config")).unwrap().len() >= 4); - // Make directory - assert!(client.mkdir(PathBuf::from("/tmp/omar").as_path()).is_ok()); - // Remake directory (should report already exists) - assert_eq!( - client - .mkdir(PathBuf::from("/tmp/omar").as_path()) - .err() - .unwrap() - .kind(), - FileTransferErrorType::DirectoryAlreadyExists - ); - // Make directory (err) - assert!(client - .mkdir(PathBuf::from("/root/aaaaa/pommlar").as_path()) - .is_err()); - // Change directory - assert!(client - .change_dir(PathBuf::from("/tmp/omar").as_path()) - .is_ok()); - // Change directory (err) - assert!(client - .change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path()) - .is_err()); - // Copy (not supported) - assert!(client - .copy(&FsEntry::File(entry.clone()), PathBuf::from("/").as_path()) - .is_err()); - // Exec - assert_eq!(client.exec("echo 5").ok().unwrap().as_str(), "5\n"); - // Upload 2 files - let mut writable = client - .send_file(&entry, PathBuf::from("omar.txt").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(client.on_sent(writable).is_ok()); - let mut writable = client - .send_file(&entry, PathBuf::from("README.md").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(client.on_sent(writable).is_ok()); - // Upload file without stream - let reader = Box::new(File::open(entry.abs_path.as_path()).ok().unwrap()); - assert!(client - .send_file_wno_stream(&entry, PathBuf::from("README2.md").as_path(), reader) - .is_ok()); - // Upload file (err) - assert!(client - .send_file(&entry, PathBuf::from("/ommlar/omarone").as_path()) - .is_err()); - // List dir - let list: Vec = client - .list_dir(PathBuf::from("/tmp/omar").as_path()) - .ok() - .unwrap(); - assert_eq!(list.len(), 3); - // Find - assert_eq!(client.find("*.txt").ok().unwrap().len(), 1); - assert_eq!(client.find("*.md").ok().unwrap().len(), 2); - assert_eq!(client.find("*.jpeg").ok().unwrap().len(), 0); - // Rename - assert!(client - .mkdir(PathBuf::from("/tmp/uploads").as_path()) - .is_ok()); - assert!(client - .rename( - list.get(0).unwrap(), - PathBuf::from("/tmp/uploads/README.txt").as_path() - ) - .is_ok()); - // Rename (err) - assert!(client - .rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path()) - .is_err()); - let dummy: FsEntry = FsEntry::File(FsFile { - name: String::from("cucumber.txt"), - abs_path: PathBuf::from("/cucumber.txt"), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, - size: 0, - ftype: Some(String::from("txt")), // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }); - assert!(client - .rename(&dummy, PathBuf::from("/a/b/c").as_path()) - .is_err()); - // Remove - assert!(client.remove(list.get(1).unwrap()).is_ok()); - assert!(client.remove(list.get(1).unwrap()).is_err()); - // Receive file - let mut writable = client - .send_file(&entry, PathBuf::from("/tmp/uploads/README.txt").as_path()) - .ok() - .unwrap(); - write_file(&file, &mut writable); - assert!(client.on_sent(writable).is_ok()); - let file: FsFile = client - .list_dir(PathBuf::from("/tmp/uploads").as_path()) - .ok() - .unwrap() - .get(0) - .unwrap() - .clone() - .unwrap_file(); - let mut readable = client.recv_file(&file).ok().unwrap(); - let mut data: Vec = vec![0; 1024]; - assert!(readable.read(&mut data).is_ok()); - assert!(client.on_recv(readable).is_ok()); - let dest_file = create_sample_file(); - // Receive file wno stream - assert!(client.recv_file_wno_stream(&file, dest_file.path()).is_ok()); - // Receive file (err) - assert!(client.recv_file(&entry).is_err()); - // Cleanup - assert!(client.change_dir(PathBuf::from("/").as_path()).is_ok()); - assert!(client - .remove(&make_fsentry(PathBuf::from("/tmp/omar"), true)) - .is_ok()); - assert!(client - .remove(&make_fsentry(PathBuf::from("/tmp/uploads"), true)) - .is_ok()); - // Disconnect - assert!(client.disconnect().is_ok()); - assert_eq!(client.is_connected(), false); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_sftp_ssh_storage() { - let mut storage: SshKeyStorage = SshKeyStorage::empty(); - let key_file: tempfile::NamedTempFile = write_ssh_key(); - storage.add_key("127.0.0.1", "sftp", key_file.path().to_path_buf()); - let mut client: SftpFileTransfer = SftpFileTransfer::new(storage); - // Connect - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10022) - .username(Some("sftp")) - .password::<&str>(None) - )) - .is_ok()); - assert_eq!(client.is_connected(), true); - assert!(client.disconnect().is_ok()); - } - - #[test] - fn test_filetransfer_sftp_bad_auth() { - let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10022) - .username(Some("sftp")) - .password(Some("badpassword")) - )) - .is_err()); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_sftp_no_credentials() { - let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10022) - .username::<&str>(None) - .password::<&str>(None) - )) - .is_err()); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_filetransfer_sftp_get_remote_path() { - let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); - // Connect - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("127.0.0.1") - .port(10022) - .username(Some("sftp")) - .password(Some("password")) - )) - .is_ok()); - // get realpath - assert!(client - .change_dir(PathBuf::from("/config").as_path()) - .is_ok()); - assert_eq!( - client - .get_remote_path(PathBuf::from("sshd.pid").as_path()) - .ok() - .unwrap(), - PathBuf::from("/config/sshd.pid") - ); - // No such file - assert!(client - .get_remote_path(PathBuf::from("omarone.txt").as_path()) - .is_err()); - // Ok abs path - assert_eq!( - client - .get_remote_path(PathBuf::from("/config/sshd.pid").as_path()) - .ok() - .unwrap(), - PathBuf::from("/config/sshd.pid") - ); - } - - #[test] - fn test_filetransfer_sftp_bad_server() { - let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); - assert!(client - .connect(&ProtocolParams::Generic( - GenericProtocolParams::default() - .address("myverybad.verybad.server") - .port(10022) - .username(Some("sftp")) - .password(Some("password")) - )) - .is_err()); - } - - #[test] - fn test_filetransfer_sftp_uninitialized() { - let file: FsFile = FsFile { - name: String::from("omar.txt"), - abs_path: PathBuf::from("/omar.txt"), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, - size: 0, - ftype: Some(String::from("txt")), // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }; - let mut sftp: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); - assert!(sftp.change_dir(Path::new("/tmp")).is_err()); - assert!(sftp - .copy( - &make_fsentry(PathBuf::from("/nowhere"), false), - PathBuf::from("/culonia").as_path() - ) - .is_err()); - assert!(sftp.exec("echo 5").is_err()); - assert!(sftp.disconnect().is_err()); - assert!(sftp.list_dir(Path::new("/tmp")).is_err()); - assert!(sftp.mkdir(Path::new("/tmp")).is_err()); - assert!(sftp.pwd().is_err()); - assert!(sftp - .remove(&make_fsentry(PathBuf::from("/nowhere"), false)) - .is_err()); - assert!(sftp - .rename( - &make_fsentry(PathBuf::from("/nowhere"), false), - PathBuf::from("/culonia").as_path() - ) - .is_err()); - assert!(sftp.stat(Path::new("/tmp")).is_err()); - assert!(sftp.recv_file(&file).is_err()); - assert!(sftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err()); - } -} diff --git a/src/fs/mod.rs b/src/fs/mod.rs deleted file mode 100644 index d04d3f54..00000000 --- a/src/fs/mod.rs +++ /dev/null @@ -1,578 +0,0 @@ -//! ## Fs -//! -//! `fs` is the module which provides file system entities - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// Mod -pub mod explorer; -// Ext -use std::path::PathBuf; -use std::time::SystemTime; - -/// ## FsEntry -/// -/// FsEntry represents a generic entry in a directory - -#[derive(Clone, std::fmt::Debug)] -pub enum FsEntry { - Directory(FsDirectory), - File(FsFile), -} - -/// ## FsDirectory -/// -/// Directory provides an interface to file system directories - -#[derive(Clone, std::fmt::Debug)] -pub struct FsDirectory { - pub name: String, - pub abs_path: PathBuf, - pub last_change_time: SystemTime, - pub last_access_time: SystemTime, - pub creation_time: SystemTime, - pub symlink: Option>, // UNIX only - pub user: Option, // UNIX only - pub group: Option, // UNIX only - pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only -} - -/// ### FsFile -/// -/// FsFile provides an interface to file system files - -#[derive(Clone, std::fmt::Debug)] -pub struct FsFile { - pub name: String, - pub abs_path: PathBuf, - pub last_change_time: SystemTime, - pub last_access_time: SystemTime, - pub creation_time: SystemTime, - pub size: usize, - pub ftype: Option, // File type - pub symlink: Option>, // UNIX only - pub user: Option, // UNIX only - pub group: Option, // UNIX only - pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only -} - -/// ## UnixPex -/// -/// Describes the permissions on POSIX system. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub struct UnixPex { - read: bool, - write: bool, - execute: bool, -} - -impl UnixPex { - /// ### new - /// - /// Instantiates a new `UnixPex` - pub fn new(read: bool, write: bool, execute: bool) -> Self { - Self { - read, - write, - execute, - } - } - - /// ### can_read - /// - /// Returns whether user can read - pub fn can_read(&self) -> bool { - self.read - } - - /// ### can_write - /// - /// Returns whether user can write - pub fn can_write(&self) -> bool { - self.write - } - - /// ### can_execute - /// - /// Returns whether user can execute - pub fn can_execute(&self) -> bool { - self.execute - } - - /// ### as_byte - /// - /// Convert permission to byte as on POSIX systems - pub fn as_byte(&self) -> u8 { - ((self.read as u8) << 2) + ((self.write as u8) << 1) + (self.execute as u8) - } -} - -impl From for UnixPex { - fn from(bits: u8) -> Self { - Self { - read: ((bits >> 2) & 0x01) != 0, - write: ((bits >> 1) & 0x01) != 0, - execute: (bits & 0x01) != 0, - } - } -} - -impl FsEntry { - /// ### get_abs_path - /// - /// Get absolute path from `FsEntry` - pub fn get_abs_path(&self) -> PathBuf { - match self { - FsEntry::Directory(dir) => dir.abs_path.clone(), - FsEntry::File(file) => file.abs_path.clone(), - } - } - - /// ### get_name - /// - /// Get file name from `FsEntry` - pub fn get_name(&self) -> &'_ str { - match self { - FsEntry::Directory(dir) => dir.name.as_ref(), - FsEntry::File(file) => file.name.as_ref(), - } - } - - /// ### get_last_change_time - /// - /// Get last change time from `FsEntry` - pub fn get_last_change_time(&self) -> SystemTime { - match self { - FsEntry::Directory(dir) => dir.last_change_time, - FsEntry::File(file) => file.last_change_time, - } - } - - /// ### get_last_access_time - /// - /// Get access time from `FsEntry` - pub fn get_last_access_time(&self) -> SystemTime { - match self { - FsEntry::Directory(dir) => dir.last_access_time, - FsEntry::File(file) => file.last_access_time, - } - } - - /// ### get_creation_time - /// - /// Get creation time from `FsEntry` - pub fn get_creation_time(&self) -> SystemTime { - match self { - FsEntry::Directory(dir) => dir.creation_time, - FsEntry::File(file) => file.creation_time, - } - } - - /// ### get_size - /// - /// Get size from `FsEntry`. For directories is always 4096 - pub fn get_size(&self) -> usize { - match self { - FsEntry::Directory(_) => 4096, - FsEntry::File(file) => file.size, - } - } - - /// ### get_ftype - /// - /// Get file type from `FsEntry`. For directories is always None - pub fn get_ftype(&self) -> Option { - match self { - FsEntry::Directory(_) => None, - FsEntry::File(file) => file.ftype.clone(), - } - } - - /// ### get_user - /// - /// Get uid from `FsEntry` - pub fn get_user(&self) -> Option { - match self { - FsEntry::Directory(dir) => dir.user, - FsEntry::File(file) => file.user, - } - } - - /// ### get_group - /// - /// Get gid from `FsEntry` - pub fn get_group(&self) -> Option { - match self { - FsEntry::Directory(dir) => dir.group, - FsEntry::File(file) => file.group, - } - } - - /// ### get_unix_pex - /// - /// Get unix pex from `FsEntry` - pub fn get_unix_pex(&self) -> Option<(UnixPex, UnixPex, UnixPex)> { - match self { - FsEntry::Directory(dir) => dir.unix_pex, - FsEntry::File(file) => file.unix_pex, - } - } - - /// ### is_symlink - /// - /// Returns whether the `FsEntry` is a symlink - pub fn is_symlink(&self) -> bool { - match self { - FsEntry::Directory(dir) => dir.symlink.is_some(), - FsEntry::File(file) => file.symlink.is_some(), - } - } - - /// ### is_dir - /// - /// Returns whether a FsEntry is a directory - pub fn is_dir(&self) -> bool { - matches!(self, FsEntry::Directory(_)) - } - - /// ### is_file - /// - /// Returns whether a FsEntry is a File - pub fn is_file(&self) -> bool { - matches!(self, FsEntry::File(_)) - } - - /// ### is_hidden - /// - /// Returns whether FsEntry is hidden - pub fn is_hidden(&self) -> bool { - self.get_name().starts_with('.') - } - - /// ### get_realfile - /// - /// Return the real file pointed by a `FsEntry` - pub fn get_realfile(&self) -> FsEntry { - match self { - FsEntry::Directory(dir) => match &dir.symlink { - Some(symlink) => symlink.get_realfile(), - None => self.clone(), - }, - FsEntry::File(file) => match &file.symlink { - Some(symlink) => symlink.get_realfile(), - None => self.clone(), - }, - } - } - - /// ### unwrap_file - /// - /// Unwrap FsEntry as FsFile - pub fn unwrap_file(self) -> FsFile { - match self { - FsEntry::File(file) => file, - _ => panic!("unwrap_file: not a file"), - } - } - - #[cfg(test)] - /// ### unwrap_dir - /// - /// Unwrap FsEntry as FsDirectory - pub fn unwrap_dir(self) -> FsDirectory { - match self { - FsEntry::Directory(dir) => dir, - _ => panic!("unwrap_dir: not a directory"), - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_fs_fsentry_dir() { - let t_now: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::Directory(FsDirectory { - name: String::from("foo"), - abs_path: PathBuf::from("/foo"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only - }); - assert_eq!(entry.get_abs_path(), PathBuf::from("/foo")); - assert_eq!(entry.get_name(), String::from("foo")); - assert_eq!(entry.get_last_access_time(), t_now); - assert_eq!(entry.get_last_change_time(), t_now); - assert_eq!(entry.get_creation_time(), t_now); - assert_eq!(entry.get_size(), 4096); - assert_eq!(entry.get_ftype(), None); - assert_eq!(entry.get_user(), Some(0)); - assert_eq!(entry.get_group(), Some(0)); - assert_eq!(entry.is_symlink(), false); - assert_eq!(entry.is_dir(), true); - assert_eq!(entry.is_file(), false); - assert_eq!( - entry.get_unix_pex(), - Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))) - ); - assert_eq!(entry.unwrap_dir().abs_path, PathBuf::from("/foo")); - } - - #[test] - fn test_fs_fsentry_file() { - let t_now: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::File(FsFile { - name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }); - assert_eq!(entry.get_abs_path(), PathBuf::from("/bar.txt")); - assert_eq!(entry.get_name(), String::from("bar.txt")); - assert_eq!(entry.get_last_access_time(), t_now); - assert_eq!(entry.get_last_change_time(), t_now); - assert_eq!(entry.get_creation_time(), t_now); - assert_eq!(entry.get_size(), 8192); - assert_eq!(entry.get_ftype(), Some(String::from("txt"))); - assert_eq!(entry.get_user(), Some(0)); - assert_eq!(entry.get_group(), Some(0)); - assert_eq!( - entry.get_unix_pex(), - Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))) - ); - assert_eq!(entry.is_symlink(), false); - assert_eq!(entry.is_dir(), false); - assert_eq!(entry.is_file(), true); - assert_eq!(entry.unwrap_file().abs_path, PathBuf::from("/bar.txt")); - } - - #[test] - #[should_panic] - fn test_fs_fsentry_file_unwrap_bad() { - let t_now: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::File(FsFile { - name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }); - entry.unwrap_dir(); - } - - #[test] - #[should_panic] - fn test_fs_fsentry_dir_unwrap_bad() { - let t_now: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::Directory(FsDirectory { - name: String::from("foo"), - abs_path: PathBuf::from("/foo"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only - }); - entry.unwrap_file(); - } - - #[test] - fn test_fs_fsentry_hidden_files() { - let t_now: SystemTime = SystemTime::now(); - let entry: FsEntry = FsEntry::File(FsFile { - name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }); - assert_eq!(entry.is_hidden(), false); - let entry: FsEntry = FsEntry::File(FsFile { - name: String::from(".gitignore"), - abs_path: PathBuf::from("/.gitignore"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }); - assert_eq!(entry.is_hidden(), true); - let entry: FsEntry = FsEntry::Directory(FsDirectory { - name: String::from(".git"), - abs_path: PathBuf::from("/.git"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only - }); - assert_eq!(entry.is_hidden(), true); - } - - #[test] - fn test_fs_fsentry_realfile_none() { - let t_now: SystemTime = SystemTime::now(); - // With file... - let entry: FsEntry = FsEntry::File(FsFile { - name: String::from("bar.txt"), - abs_path: PathBuf::from("/bar.txt"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: 8192, - ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only - }); - // Symlink is None... - assert_eq!( - entry.get_realfile().get_abs_path(), - PathBuf::from("/bar.txt") - ); - // With directory... - let entry: FsEntry = FsEntry::Directory(FsDirectory { - name: String::from("foo"), - abs_path: PathBuf::from("/foo"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only - }); - assert_eq!(entry.get_realfile().get_abs_path(), PathBuf::from("/foo")); - } - - #[test] - fn test_fs_fsentry_realfile_some() { - let t_now: SystemTime = SystemTime::now(); - // Prepare entries - // root -> child -> target - let entry_target: FsEntry = FsEntry::Directory(FsDirectory { - name: String::from("projects"), - abs_path: PathBuf::from("/home/cvisintin/projects"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), // UNIX only - }); - let entry_child: FsEntry = FsEntry::Directory(FsDirectory { - name: String::from("projects"), - abs_path: PathBuf::from("/develop/projects"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - symlink: Some(Box::new(entry_target)), - user: Some(0), - group: Some(0), - unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), - }); - let entry_root: FsEntry = FsEntry::File(FsFile { - name: String::from("projects"), - abs_path: PathBuf::from("/projects"), - last_change_time: t_now, - last_access_time: t_now, - creation_time: t_now, - size: 8, - ftype: None, - symlink: Some(Box::new(entry_child)), - user: Some(0), - group: Some(0), - unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), - }); - assert_eq!(entry_root.is_symlink(), true); - // get real file - let real_file: FsEntry = entry_root.get_realfile(); - // real file must be projects in /home/cvisintin - assert_eq!( - real_file.get_abs_path(), - PathBuf::from("/home/cvisintin/projects") - ); - } - - #[test] - fn unix_pex() { - let pex: UnixPex = UnixPex::from(4); - assert_eq!(pex.can_read(), true); - assert_eq!(pex.can_write(), false); - assert_eq!(pex.can_execute(), false); - let pex: UnixPex = UnixPex::from(0); - assert_eq!(pex.can_read(), false); - assert_eq!(pex.can_write(), false); - assert_eq!(pex.can_execute(), false); - let pex: UnixPex = UnixPex::from(3); - assert_eq!(pex.can_read(), false); - assert_eq!(pex.can_write(), true); - assert_eq!(pex.can_execute(), true); - let pex: UnixPex = UnixPex::from(7); - assert_eq!(pex.can_read(), true); - assert_eq!(pex.can_write(), true); - assert_eq!(pex.can_execute(), true); - let pex: UnixPex = UnixPex::from(3); - assert_eq!(pex.as_byte(), 3); - let pex: UnixPex = UnixPex::from(7); - assert_eq!(pex.as_byte(), 7); - } -} diff --git a/src/host/mod.rs b/src/host/mod.rs index 481bcba0..c3147150 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -26,7 +26,10 @@ * SOFTWARE. */ // ext -use std::fs::{self, File, Metadata, OpenOptions}; +#[cfg(target_family = "unix")] +use remotefs::fs::UnixPex; +use remotefs::fs::{Directory, Entry, File, Metadata}; +use std::fs::{self, File as StdFile, OpenOptions}; use std::path::{Path, PathBuf}; use std::time::SystemTime; use thiserror::Error; @@ -38,9 +41,6 @@ use std::fs::set_permissions; use std::os::unix::fs::{MetadataExt, PermissionsExt}; // Locals -#[cfg(target_family = "unix")] -use crate::fs::UnixPex; -use crate::fs::{FsDirectory, FsEntry, FsFile}; use crate::utils::path; /// ## HostErrorType @@ -118,7 +118,7 @@ impl std::fmt::Display for HostError { /// It provides functions to navigate across the local host file system pub struct Localhost { wrkdir: PathBuf, - files: Vec, + files: Vec, } impl Localhost { @@ -169,7 +169,7 @@ impl Localhost { /// /// List files in current directory #[allow(dead_code)] - pub fn list_dir(&self) -> Vec { + pub fn list_dir(&self) -> Vec { self.files.clone() } @@ -177,7 +177,7 @@ impl Localhost { /// /// Change working directory with the new provided directory pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result { - let new_dir: PathBuf = self.to_abs_path(new_dir); + let new_dir: PathBuf = self.to_path(new_dir); info!("Changing localhost directory to {}...", new_dir.display()); // Check whether directory exists if !self.file_exists(new_dir.as_path()) { @@ -227,7 +227,7 @@ impl Localhost { /// Extended option version of makedir. /// ignex: don't report error if directory already exists pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> { - let dir_path: PathBuf = self.to_abs_path(dir_name); + let dir_path: PathBuf = self.to_path(dir_name); info!("Making directory {}", dir_path.display()); // If dir already exists, return Error if dir_path.exists() { @@ -265,25 +265,25 @@ impl Localhost { /// ### remove /// /// Remove file entry - pub fn remove(&mut self, entry: &FsEntry) -> Result<(), HostError> { + pub fn remove(&mut self, entry: &Entry) -> Result<(), HostError> { match entry { - FsEntry::Directory(dir) => { + Entry::Directory(dir) => { // If file doesn't exist; return error - debug!("Removing directory {}", dir.abs_path.display()); - if !dir.abs_path.as_path().exists() { + debug!("Removing directory {}", dir.path.display()); + if !dir.path.as_path().exists() { error!("Directory doesn't exist"); return Err(HostError::new( HostErrorType::NoSuchFileOrDirectory, None, - dir.abs_path.as_path(), + dir.path.as_path(), )); } // Remove - match std::fs::remove_dir_all(dir.abs_path.as_path()) { + match std::fs::remove_dir_all(dir.path.as_path()) { Ok(_) => { // Update dir self.files = self.scan_dir(self.wrkdir.as_path())?; - info!("Removed directory {}", dir.abs_path.display()); + info!("Removed directory {}", dir.path.display()); Ok(()) } Err(err) => { @@ -291,28 +291,28 @@ impl Localhost { Err(HostError::new( HostErrorType::DeleteFailed, Some(err), - dir.abs_path.as_path(), + dir.path.as_path(), )) } } } - FsEntry::File(file) => { + Entry::File(file) => { // If file doesn't exist; return error - debug!("Removing file {}", file.abs_path.display()); - if !file.abs_path.as_path().exists() { + debug!("Removing file {}", file.path.display()); + if !file.path.as_path().exists() { error!("File doesn't exist"); return Err(HostError::new( HostErrorType::NoSuchFileOrDirectory, None, - file.abs_path.as_path(), + file.path.as_path(), )); } // Remove - match std::fs::remove_file(file.abs_path.as_path()) { + match std::fs::remove_file(file.path.as_path()) { Ok(_) => { // Update dir self.files = self.scan_dir(self.wrkdir.as_path())?; - info!("Removed file {}", file.abs_path.display()); + info!("Removed file {}", file.path.display()); Ok(()) } Err(err) => { @@ -320,7 +320,7 @@ impl Localhost { Err(HostError::new( HostErrorType::DeleteFailed, Some(err), - file.abs_path.as_path(), + file.path.as_path(), )) } } @@ -331,15 +331,14 @@ impl Localhost { /// ### rename /// /// Rename file or directory to new name - pub fn rename(&mut self, entry: &FsEntry, dst_path: &Path) -> Result<(), HostError> { - let abs_path: PathBuf = entry.get_abs_path(); - match std::fs::rename(abs_path.as_path(), dst_path) { + pub fn rename(&mut self, entry: &Entry, dst_path: &Path) -> Result<(), HostError> { + match std::fs::rename(entry.path(), dst_path) { Ok(_) => { // Scan dir self.files = self.scan_dir(self.wrkdir.as_path())?; debug!( "Moved file {} to {}", - entry.get_abs_path().display(), + entry.path().display(), dst_path.display() ); Ok(()) @@ -347,14 +346,14 @@ impl Localhost { Err(err) => { error!( "Failed to move {} to {}: {}", - entry.get_abs_path().display(), + entry.path().display(), dst_path.display(), err ); Err(HostError::new( HostErrorType::CouldNotCreateFile, Some(err), - abs_path.as_path(), + entry.path(), )) } } @@ -363,17 +362,17 @@ impl Localhost { /// ### copy /// /// Copy file to destination path - pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> { + pub fn copy(&mut self, entry: &Entry, dst: &Path) -> Result<(), HostError> { // Get absolute path of dest - let dst: PathBuf = self.to_abs_path(dst); + let dst: PathBuf = self.to_path(dst); info!( "Copying file {} to {}", - entry.get_abs_path().display(), + entry.path().display(), dst.display() ); // Match entry match entry { - FsEntry::File(file) => { + Entry::File(file) => { // Copy file // If destination path is a directory, push file name let dst: PathBuf = match dst.as_path().is_dir() { @@ -385,29 +384,29 @@ impl Localhost { false => dst.clone(), }; // Copy entry path to dst path - if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) { + if let Err(err) = std::fs::copy(file.path.as_path(), dst.as_path()) { error!("Failed to copy file: {}", err); return Err(HostError::new( HostErrorType::CouldNotCreateFile, Some(err), - file.abs_path.as_path(), + file.path.as_path(), )); } info!("File copied"); } - FsEntry::Directory(dir) => { + Entry::Directory(dir) => { // If destination path doesn't exist, create destination if !dst.exists() { debug!("Directory {} doesn't exist; creating it", dst.display()); self.mkdir(dst.as_path())?; } // Scan dir - let dir_files: Vec = self.scan_dir(dir.abs_path.as_path())?; + let dir_files: Vec = self.scan_dir(dir.path.as_path())?; // Iterate files for dir_entry in dir_files.iter() { // Calculate dst let mut sub_dst: PathBuf = dst.clone(); - sub_dst.push(dir_entry.get_name()); + sub_dst.push(dir_entry.name()); // Call function recursively self.copy(dir_entry, sub_dst.as_path())?; } @@ -439,12 +438,12 @@ impl Localhost { /// ### stat /// - /// Stat file and create a FsEntry + /// Stat file and create a Entry #[cfg(target_family = "unix")] - pub fn stat(&self, path: &Path) -> Result { + pub fn stat(&self, path: &Path) -> Result { info!("Stating file {}", path.display()); - let path: PathBuf = self.to_abs_path(path); - let attr: Metadata = match fs::metadata(path.as_path()) { + let path: PathBuf = self.to_path(path); + let attr = match fs::metadata(path.as_path()) { Ok(metadata) => metadata, Err(err) => { error!("Could not read file metadata: {}", err); @@ -455,49 +454,38 @@ impl Localhost { )); } }; - let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); + let name = String::from(path.file_name().unwrap().to_str().unwrap_or("")); // Match dir / file + let metadata = Metadata { + atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), + ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), + gid: Some(attr.gid()), + mode: Some(UnixPex::from(attr.mode())), + mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), + size: if path.is_dir() { + attr.blksize() + } else { + attr.len() + }, + symlink: fs::read_link(path.as_path()).ok(), + uid: Some(attr.uid()), + }; Ok(match path.is_dir() { - true => FsEntry::Directory(FsDirectory { - name: file_name, - abs_path: path.clone(), - last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), - last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), - creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - symlink: match fs::read_link(path.as_path()) { - Ok(p) => match self.stat(p.as_path()) { - Ok(entry) => Some(Box::new(entry)), - Err(_) => None, - }, - Err(_) => None, - }, - user: Some(attr.uid()), - group: Some(attr.gid()), - unix_pex: Some(self.u32_to_mode(attr.mode())), + true => Entry::Directory(Directory { + name, + path, + metadata, }), false => { // Is File - let extension: Option = path + let extension = path .extension() .map(|s| String::from(s.to_str().unwrap_or(""))); - FsEntry::File(FsFile { - name: file_name, - abs_path: path.clone(), - last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), - last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), - creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - size: attr.len() as usize, - ftype: extension, - symlink: match fs::read_link(path.as_path()) { - Ok(p) => match self.stat(p.as_path()) { - Ok(entry) => Some(Box::new(entry)), - Err(_) => None, - }, - Err(_) => None, // Ignore errors - }, - user: Some(attr.uid()), - group: Some(attr.gid()), - unix_pex: Some(self.u32_to_mode(attr.mode())), + Entry::File(File { + name, + path, + extension, + metadata, }) } }) @@ -505,12 +493,12 @@ impl Localhost { /// ### stat /// - /// Stat file and create a FsEntry + /// Stat file and create a Entry #[cfg(target_os = "windows")] - pub fn stat(&self, path: &Path) -> Result { - let path: PathBuf = self.to_abs_path(path); + pub fn stat(&self, path: &Path) -> Result { + let path: PathBuf = self.to_path(path); info!("Stating file {}", path.display()); - let attr: Metadata = match fs::metadata(path.as_path()) { + let attr = match fs::metadata(path.as_path()) { Ok(metadata) => metadata, Err(err) => { error!("Could not read file metadata: {}", err); @@ -521,49 +509,38 @@ impl Localhost { )); } }; - let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); + let name = String::from(path.file_name().unwrap().to_str().unwrap_or("")); + let metadata = Metadata { + atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), + ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), + mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), + size: if path.is_dir() { + attr.blksize() + } else { + attr.len() + }, + symlink: fs::read_link(path.as_path()).ok(), + uid: None, + gid: None, + mode: None, + }; // Match dir / file Ok(match path.is_dir() { - true => FsEntry::Directory(FsDirectory { - name: file_name, - abs_path: path.clone(), - last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), - last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), - creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - symlink: match fs::read_link(path.as_path()) { - Ok(p) => match self.stat(p.as_path()) { - Ok(entry) => Some(Box::new(entry)), - Err(_) => None, // Ignore errors - }, - Err(_) => None, - }, - user: None, - group: None, - unix_pex: None, + true => Entry::Directory(Directory { + name, + path, + metadata, }), false => { // Is File - let extension: Option = path + let extension = path .extension() .map(|s| String::from(s.to_str().unwrap_or(""))); - FsEntry::File(FsFile { - name: file_name, - abs_path: path.clone(), - last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), - last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), - creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - size: attr.len() as usize, - ftype: extension, - symlink: match fs::read_link(path.as_path()) { - Ok(p) => match self.stat(p.as_path()) { - Ok(entry) => Some(Box::new(entry)), - Err(_) => None, - }, - Err(_) => None, - }, - user: None, - group: None, - unix_pex: None, + Entry::File(File { + name, + path, + extension, + metadata, }) } }) @@ -601,13 +578,13 @@ impl Localhost { /// /// Change file mode to file, according to UNIX permissions #[cfg(target_family = "unix")] - pub fn chmod(&self, path: &Path, pex: (u8, u8, u8)) -> Result<(), HostError> { - let path: PathBuf = self.to_abs_path(path); + pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> { + let path: PathBuf = self.to_path(path); // Get metadta match fs::metadata(path.as_path()) { Ok(metadata) => { let mut mpex = metadata.permissions(); - mpex.set_mode(self.mode_to_u32(pex)); + mpex.set_mode(pex.into()); match set_permissions(path.as_path(), mpex) { Ok(_) => { info!("Changed mode for {} to {:?}", path.display(), pex); @@ -641,8 +618,8 @@ impl Localhost { /// ### open_file_read /// /// Open file for read - pub fn open_file_read(&self, file: &Path) -> Result { - let file: PathBuf = self.to_abs_path(file); + pub fn open_file_read(&self, file: &Path) -> Result { + let file: PathBuf = self.to_path(file); info!("Opening file {} for read", file.display()); if !self.file_exists(file.as_path()) { error!("File doesn't exist!"); @@ -673,8 +650,8 @@ impl Localhost { /// ### open_file_write /// /// Open file for write - pub fn open_file_write(&self, file: &Path) -> Result { - let file: PathBuf = self.to_abs_path(file); + pub fn open_file_write(&self, file: &Path) -> Result { + let file: PathBuf = self.to_path(file); info!("Opening file {} for write", file.display()); match OpenOptions::new() .create(true) @@ -711,11 +688,11 @@ impl Localhost { /// ### scan_dir /// /// Get content of the current directory as a list of fs entry - pub fn scan_dir(&self, dir: &Path) -> Result, HostError> { + pub fn scan_dir(&self, dir: &Path) -> Result, HostError> { info!("Reading directory {}", dir.display()); match std::fs::read_dir(dir) { Ok(e) => { - let mut fs_entries: Vec = Vec::new(); + let mut fs_entries: Vec = Vec::new(); for entry in e.flatten() { // NOTE: 0.4.1, don't fail if stat for one file fails match self.stat(entry.path().as_path()) { @@ -737,7 +714,7 @@ impl Localhost { /// /// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course. /// The `search` argument supports wilcards ('*', '?') - pub fn find(&self, search: &str) -> Result, HostError> { + pub fn find(&self, search: &str) -> Result, HostError> { self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search)) } @@ -748,9 +725,9 @@ impl Localhost { /// Recursive call for `find` method. /// Search in current directory for files which match `filter`. /// If a directory is found in current directory, `iter_search` will be called using that dir as argument. - fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result, HostError> { + fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result, HostError> { // Scan directory - let mut drained: Vec = Vec::new(); + let mut drained: Vec = Vec::new(); match self.scan_dir(dir) { Err(err) => Err(err), Ok(entries) => { @@ -763,16 +740,16 @@ impl Localhost { */ for entry in entries.iter() { match entry { - FsEntry::Directory(dir) => { + Entry::Directory(dir) => { // If directory matches; push directory to drained if filter.matches(dir.name.as_str()) { - drained.push(FsEntry::Directory(dir.clone())); + drained.push(Entry::Directory(dir.clone())); } - drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?); + drained.append(&mut self.iter_search(dir.path.as_path(), filter)?); } - FsEntry::File(file) => { + Entry::File(file) => { if filter.matches(file.name.as_str()) { - drained.push(FsEntry::File(file.clone())); + drained.push(Entry::File(file.clone())); } } } @@ -782,29 +759,10 @@ impl Localhost { } } - /// ### u32_to_mode - /// - /// Return string with format xxxxxx to tuple of permissions (user, group, others) - #[cfg(target_family = "unix")] - fn u32_to_mode(&self, mode: u32) -> (UnixPex, UnixPex, UnixPex) { - let user: UnixPex = UnixPex::from(((mode >> 6) & 0x7) as u8); - let group: UnixPex = UnixPex::from(((mode >> 3) & 0x7) as u8); - let others: UnixPex = UnixPex::from((mode & 0x7) as u8); - (user, group, others) - } - - /// mode_to_u32 - /// - /// Convert owner,group,others to u32 - #[cfg(target_family = "unix")] - fn mode_to_u32(&self, mode: (u8, u8, u8)) -> u32 { - ((mode.0 as u32) << 6) + ((mode.1 as u32) << 3) + mode.2 as u32 - } - - /// ### to_abs_path + /// ### to_path /// /// Convert path to absolute path - fn to_abs_path(&self, p: &Path) -> PathBuf { + fn to_path(&self, p: &Path) -> PathBuf { path::absolutize(self.wrkdir.as_path(), p) } } @@ -819,7 +777,7 @@ mod tests { use pretty_assertions::assert_eq; #[cfg(target_family = "unix")] - use std::fs::File; + use std::fs::File as StdFile; #[cfg(target_family = "unix")] use std::io::Write; @@ -970,7 +928,7 @@ mod tests { fn test_host_localhost_symlinks() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create sample file - assert!(File::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); + assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); // Create symlink assert!(symlink( format!("{}/foo.txt", tmpdir.path().display()), @@ -979,33 +937,33 @@ mod tests { .is_ok()); // Get dir let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); // Verify files - let file_0: &FsEntry = files.get(0).unwrap(); + let file_0: &Entry = files.get(0).unwrap(); match file_0 { - FsEntry::File(file_0) => { + Entry::File(file_0) => { if file_0.name == String::from("foo.txt") { - assert!(file_0.symlink.is_none()); + assert!(file_0.metadata.symlink.is_none()); } else { assert_eq!( - *file_0.symlink.as_ref().unwrap().get_abs_path(), - PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) + file_0.metadata.symlink.as_ref().unwrap(), + &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) ); } } _ => panic!("expected entry 0 to be file: {:?}", file_0), }; // Verify simlink - let file_1: &FsEntry = files.get(1).unwrap(); + let file_1: &Entry = files.get(1).unwrap(); match file_1 { - FsEntry::File(file_1) => { + Entry::File(file_1) => { if file_1.name == String::from("bar.txt") { assert_eq!( - *file_1.symlink.as_ref().unwrap().get_abs_path(), - PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) + file_1.metadata.symlink.as_ref().unwrap(), + &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) ); } else { - assert!(file_1.symlink.is_none()); + assert!(file_1.metadata.symlink.is_none()); } } _ => panic!("expected entry 0 to be file: {:?}", file_1), @@ -1017,10 +975,10 @@ mod tests { fn test_host_localhost_mkdir() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 0); // There should be 0 files now assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now // Try to re-create directory assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err()); @@ -1042,19 +1000,19 @@ mod tests { fn test_host_localhost_remove() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create sample file - assert!(File::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); + assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now // Remove file assert!(host.remove(files.get(0).unwrap()).is_ok()); // There should be 0 files now - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 0); // There should be 0 files now // Create directory assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); // Delete directory - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now assert!(host.remove(files.get(0).unwrap()).is_ok()); // Remove unexisting directory @@ -1073,11 +1031,11 @@ mod tests { // Create sample file let src_path: PathBuf = PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str()); - assert!(File::create(src_path.as_path()).is_ok()); + assert!(StdFile::create(src_path.as_path()).is_ok()); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now - assert_eq!(files.get(0).unwrap().get_name(), "foo.txt"); + assert_eq!(files.get(0).unwrap().name(), "foo.txt"); // Rename file let dst_path: PathBuf = PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str()); @@ -1085,9 +1043,9 @@ mod tests { .rename(files.get(0).unwrap(), dst_path.as_path()) .is_ok()); // There should be still 1 file now, but named bar.txt - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 0 files now - assert_eq!(files.get(0).unwrap().get_name(), "bar.txt"); + assert_eq!(files.get(0).unwrap().name(), "bar.txt"); // Fail let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu"); assert!(host @@ -1101,16 +1059,16 @@ mod tests { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let file: tempfile::NamedTempFile = create_sample_file(); let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - // mode_to_u32 - assert_eq!(host.mode_to_u32((6, 4, 4)), 0o644); - assert_eq!(host.mode_to_u32((7, 7, 5)), 0o775); // Chmod to file - assert!(host.chmod(file.path(), (7, 7, 5)).is_ok()); + assert!(host.chmod(file.path(), UnixPex::from(0o755)).is_ok()); // Chmod to dir - assert!(host.chmod(tmpdir.path(), (7, 5, 0)).is_ok()); + assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok()); // Error assert!(host - .chmod(Path::new("/tmp/krgiogoiegj/kwrgnoerig"), (7, 7, 7)) + .chmod( + Path::new("/tmp/krgiogoiegj/kwrgnoerig"), + UnixPex::from(0o777) + ) .is_err()); } @@ -1122,15 +1080,15 @@ mod tests { let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); file1_path.push("foo.txt"); // Write file 1 - let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); + let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); assert!(file1.write_all(b"Hello world!\n").is_ok()); // Get file 2 path let mut file2_path: PathBuf = PathBuf::from(tmpdir.path()); file2_path.push("bar.txt"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let file1_entry: FsEntry = host.files.get(0).unwrap().clone(); - assert_eq!(file1_entry.get_name(), String::from("foo.txt")); + let file1_entry: Entry = host.files.get(0).unwrap().clone(); + assert_eq!(file1_entry.name(), String::from("foo.txt")); // Copy assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); // Verify host has two files @@ -1152,14 +1110,14 @@ mod tests { let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); file1_path.push("foo.txt"); // Write file 1 - let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); + let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); assert!(file1.write_all(b"Hello world!\n").is_ok()); // Get file 2 path let file2_path: PathBuf = PathBuf::from("bar.txt"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let file1_entry: FsEntry = host.files.get(0).unwrap().clone(); - assert_eq!(file1_entry.get_name(), String::from("foo.txt")); + let file1_entry: Entry = host.files.get(0).unwrap().clone(); + assert_eq!(file1_entry.name(), String::from("foo.txt")); // Copy assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); // Verify host has two files @@ -1178,15 +1136,15 @@ mod tests { let mut file1_path: PathBuf = dir_src.clone(); file1_path.push("foo.txt"); // Write file 1 - let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); + let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); assert!(file1.write_all(b"Hello world!\n").is_ok()); // Copy dir src to dir ddest let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path()); dir_dest.push("test_dest_dir/"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone(); - assert_eq!(dir_src_entry.get_name(), String::from("test_dir")); + let dir_src_entry: Entry = host.files.get(0).unwrap().clone(); + assert_eq!(dir_src_entry.name(), String::from("test_dir")); // Copy assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); // Verify host has two files @@ -1209,14 +1167,14 @@ mod tests { let mut file1_path: PathBuf = dir_src.clone(); file1_path.push("foo.txt"); // Write file 1 - let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); + let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); assert!(file1.write_all(b"Hello world!\n").is_ok()); // Copy dir src to dir ddest let dir_dest: PathBuf = PathBuf::from("test_dest_dir/"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone(); - assert_eq!(dir_src_entry.get_name(), String::from("test_dir")); + let dir_src_entry: Entry = host.files.get(0).unwrap().clone(); + assert_eq!(dir_src_entry.name(), String::from("test_dir")); // Copy assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); // Verify host has two files @@ -1255,20 +1213,20 @@ mod tests { assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok()); let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap(); // Find txt files - let mut result: Vec = host.find("*.txt").ok().unwrap(); - result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase()); + let mut result: Vec = host.find("*.txt").ok().unwrap(); + result.sort_by_key(|x: &Entry| x.name().to_lowercase()); // There should be 3 entries assert_eq!(result.len(), 3); // Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt) - assert_eq!(result[0].get_name(), "errors.txt"); - assert_eq!(result[1].get_name(), "omar.txt"); - assert_eq!(result[2].get_name(), "pippo.txt"); + assert_eq!(result[0].name(), "errors.txt"); + assert_eq!(result[1].name(), "omar.txt"); + assert_eq!(result[2].name(), "pippo.txt"); // Search for directory - let mut result: Vec = host.find("examples*").ok().unwrap(); - result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase()); + let mut result: Vec = host.find("examples*").ok().unwrap(); + result.sort_by_key(|x: &Entry| x.name().to_lowercase()); assert_eq!(result.len(), 2); - assert_eq!(result[0].get_name(), "examples"); - assert_eq!(result[1].get_name(), "examples.csv"); + assert_eq!(result[0].name(), "examples"); + assert_eq!(result[1].name(), "examples.csv"); } #[test] diff --git a/src/main.rs b/src/main.rs index 6250361c..64828f4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,8 +45,8 @@ use std::time::Duration; // Include mod activity_manager; mod config; +mod explorer; mod filetransfer; -mod fs; mod host; mod support; mod system; diff --git a/src/system/auto_update.rs b/src/system/auto_update.rs index 6f551713..57dec380 100644 --- a/src/system/auto_update.rs +++ b/src/system/auto_update.rs @@ -57,7 +57,7 @@ pub struct Release { /// /// The update structure defines the options used to install the update. /// Once you're fine with the options, just call the `upgrade()` method to upgrade termscp. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Update { ask_confirm: bool, progress: bool, @@ -141,16 +141,6 @@ impl Update { } } } - -impl Default for Update { - fn default() -> Self { - Self { - progress: false, - ask_confirm: false, - } - } -} - impl From for UpdateStatus { fn from(s: Status) -> Self { match s { diff --git a/src/system/config_client.rs b/src/system/config_client.rs index 80840ceb..365db280 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -30,8 +30,8 @@ use crate::config::{ params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD}, serialization::{deserialize, serialize, SerializerError, SerializerErrorKind}, }; +use crate::explorer::GroupDirs; use crate::filetransfer::FileTransferProtocol; -use crate::fs::explorer::GroupDirs; // Ext use std::fs::{create_dir, remove_file, File, OpenOptions}; use std::io::Write; diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index c6a2dd35..7ee65f4b 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -28,8 +28,9 @@ // Locals use super::config_client::ConfigClient; // Ext +use remotefs::client::ssh::SshKeyStorage as SshKeyStorageT; use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; pub struct SshKeyStorage { hosts: HashMap, // Association between {user}@{host} and RSA key path @@ -74,14 +75,6 @@ impl SshKeyStorage { } } - /// ### resolve - /// - /// Return RSA key path from host and username - pub fn resolve(&self, host: &str, username: &str) -> Option<&PathBuf> { - let key: String = Self::make_mapkey(host, username); - self.hosts.get(&key) - } - /// ### make_mapkey /// /// Make mapkey from host and username @@ -100,6 +93,13 @@ impl SshKeyStorage { } } +impl SshKeyStorageT for SshKeyStorage { + fn resolve(&self, host: &str, username: &str) -> Option<&Path> { + let key: String = Self::make_mapkey(host, username); + self.hosts.get(&key).map(|x| x.as_path()) + } +} + #[cfg(test)] mod tests { diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index 908cd361..4791f4f1 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -52,19 +52,11 @@ use tuirealm::{Component, MockComponent}; // -- global listener -#[derive(MockComponent)] +#[derive(Default, MockComponent)] pub struct GlobalListener { component: Phantom, } -impl Default for GlobalListener { - fn default() -> Self { - Self { - component: Phantom::default(), - } - } -} - impl Component for GlobalListener { fn on(&mut self, ev: Event) -> Option { match ev { diff --git a/src/ui/activities/filetransfer/actions/change_dir.rs b/src/ui/activities/filetransfer/actions/change_dir.rs index ad7ad09b..024d5397 100644 --- a/src/ui/activities/filetransfer/actions/change_dir.rs +++ b/src/ui/activities/filetransfer/actions/change_dir.rs @@ -26,7 +26,9 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, FsEntry}; +use super::FileTransferActivity; + +use remotefs::Directory; use std::path::PathBuf; impl FileTransferActivity { @@ -34,70 +36,24 @@ impl FileTransferActivity { /// /// Enter a directory on local host from entry /// Return true whether the directory changed - pub(crate) 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, - } - } + pub(crate) fn action_enter_local_dir(&mut self, dir: Directory, block_sync: bool) -> bool { + self.local_changedir(dir.path.as_path(), true); + if self.browser.sync_browsing && !block_sync { + self.action_change_remote_dir(dir.name, true); } + true } /// ### action_enter_remote_dir /// /// Enter a directory on local host from entry /// Return true whether the directory changed - pub(crate) 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, - } - } + pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory, block_sync: bool) -> bool { + self.remote_changedir(dir.path.as_path(), true); + if self.browser.sync_browsing && !block_sync { + self.action_change_local_dir(dir.name, true); } + true } /// ### action_change_local_dir diff --git a/src/ui/activities/filetransfer/actions/copy.rs b/src/ui/activities/filetransfer/actions/copy.rs index 2d67963c..ccdd26a1 100644 --- a/src/ui/activities/filetransfer/actions/copy.rs +++ b/src/ui/activities/filetransfer/actions/copy.rs @@ -26,9 +26,9 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; -use crate::filetransfer::FileTransferErrorType; -use crate::fs::FsFile; +use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload}; + +use remotefs::{Entry, RemoteErrorType}; use std::path::{Path, PathBuf}; impl FileTransferActivity { @@ -49,7 +49,7 @@ impl FileTransferActivity { // Iter files for entry in entries.iter() { let mut dest_path: PathBuf = base_path.clone(); - dest_path.push(entry.get_name()); + dest_path.push(entry.name()); self.local_copy_file(entry, dest_path.as_path()); } // Reload entries @@ -76,7 +76,7 @@ impl FileTransferActivity { // Iter files for entry in entries.into_iter() { let mut dest_path: PathBuf = base_path.clone(); - dest_path.push(entry.get_name()); + dest_path.push(entry.name()); self.remote_copy_file(entry, dest_path.as_path()); } // Reload entries @@ -86,14 +86,14 @@ impl FileTransferActivity { } } - fn local_copy_file(&mut self, entry: &FsEntry, dest: &Path) { + fn local_copy_file(&mut self, entry: &Entry, dest: &Path) { match self.host.copy(entry, dest) { Ok(_) => { self.log( LogLevel::Info, format!( "Copied \"{}\" to \"{}\"", - entry.get_abs_path().display(), + entry.path().display(), dest.display() ), ); @@ -102,7 +102,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Could not copy \"{}\" to \"{}\": {}", - entry.get_abs_path().display(), + entry.path().display(), dest.display(), err ), @@ -110,20 +110,20 @@ impl FileTransferActivity { } } - fn remote_copy_file(&mut self, entry: FsEntry, dest: &Path) { - match self.client.as_mut().copy(&entry, dest) { + fn remote_copy_file(&mut self, entry: Entry, dest: &Path) { + match self.client.as_mut().copy(entry.path(), dest) { Ok(_) => { self.log( LogLevel::Info, format!( "Copied \"{}\" to \"{}\"", - entry.get_abs_path().display(), + entry.path().display(), dest.display() ), ); } - Err(err) => match err.kind() { - FileTransferErrorType::UnsupportedFeature => { + Err(err) => match err.kind { + RemoteErrorType::UnsupportedFeature => { // If copy is not supported, perform the tricky copy let _ = self.tricky_copy(entry, dest); } @@ -131,7 +131,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Could not copy \"{}\" to \"{}\": {}", - entry.get_abs_path().display(), + entry.path().display(), dest.display(), err ), @@ -143,12 +143,12 @@ impl FileTransferActivity { /// ### tricky_copy /// /// Tricky copy will be used whenever copy command is not available on remote host - pub(super) fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) -> Result<(), String> { + pub(super) fn tricky_copy(&mut self, entry: Entry, dest: &Path) -> Result<(), String> { // NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen self.umount_wait(); // match entry match entry { - FsEntry::File(entry) => { + Entry::File(entry) => { // Create tempfile let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() { Ok(f) => f, @@ -162,7 +162,7 @@ impl FileTransferActivity { }; // Download file let name = entry.name.clone(); - let entry_path = entry.abs_path.clone(); + let entry_path = entry.path.clone(); if let Err(err) = self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name)) { @@ -173,7 +173,7 @@ impl FileTransferActivity { return Err(err); } // Get local fs entry - let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) { + let tmpfile_entry = match self.host.stat(tmpfile.path()) { Ok(e) => e.unwrap_file(), Err(err) => { self.log_and_alert( @@ -206,7 +206,7 @@ impl FileTransferActivity { } Ok(()) } - FsEntry::Directory(_) => { + Entry::Directory(_) => { let tempdir: tempfile::TempDir = match tempfile::TempDir::new() { Ok(d) => d, Err(err) => { @@ -219,7 +219,7 @@ impl FileTransferActivity { }; // Get path of dest let mut tempdir_path: PathBuf = tempdir.path().to_path_buf(); - tempdir_path.push(entry.get_name()); + tempdir_path.push(entry.name()); // Download file if let Err(err) = self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None) @@ -231,7 +231,7 @@ impl FileTransferActivity { return Err(err); } // Stat dir - let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) { + let tempdir_entry = match self.host.stat(tempdir_path.as_path()) { Ok(e) => e, Err(err) => { self.log_and_alert( diff --git a/src/ui/activities/filetransfer/actions/delete.rs b/src/ui/activities/filetransfer/actions/delete.rs index 4c1bba5c..36eb06d5 100644 --- a/src/ui/activities/filetransfer/actions/delete.rs +++ b/src/ui/activities/filetransfer/actions/delete.rs @@ -26,7 +26,9 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry}; +use super::{FileTransferActivity, LogLevel, SelectedEntry}; + +use remotefs::Entry; impl FileTransferActivity { pub(crate) fn action_local_delete(&mut self) { @@ -71,13 +73,13 @@ impl FileTransferActivity { } } - pub(crate) fn local_remove_file(&mut self, entry: &FsEntry) { + pub(crate) fn local_remove_file(&mut self, entry: &Entry) { match self.host.remove(entry) { Ok(_) => { // Log self.log( LogLevel::Info, - format!("Removed file \"{}\"", entry.get_abs_path().display()), + format!("Removed file \"{}\"", entry.path().display()), ); } Err(err) => { @@ -85,7 +87,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Could not delete file \"{}\": {}", - entry.get_abs_path().display(), + entry.path().display(), err ), ); @@ -93,12 +95,12 @@ impl FileTransferActivity { } } - pub(crate) fn remote_remove_file(&mut self, entry: &FsEntry) { - match self.client.remove(entry) { + pub(crate) fn remote_remove_file(&mut self, entry: &Entry) { + match self.client.remove_dir_all(entry.path()) { Ok(_) => { self.log( LogLevel::Info, - format!("Removed file \"{}\"", entry.get_abs_path().display()), + format!("Removed file \"{}\"", entry.path().display()), ); } Err(err) => { @@ -106,7 +108,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Could not delete file \"{}\": {}", - entry.get_abs_path().display(), + entry.path().display(), err ), ); diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index c6545a5a..18771b73 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -26,9 +26,10 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; -use crate::fs::FsFile; +use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload}; + // ext +use remotefs::{Entry, File}; use std::fs::OpenOptions; use std::io::Read; use std::path::{Path, PathBuf}; @@ -36,7 +37,7 @@ use std::time::SystemTime; impl FileTransferActivity { pub(crate) fn action_edit_local_file(&mut self) { - let entries: Vec = match self.get_local_selected_entries() { + let entries: Vec = match self.get_local_selected_entries() { SelectedEntry::One(entry) => vec![entry], SelectedEntry::Many(entries) => entries, SelectedEntry::None => vec![], @@ -47,10 +48,10 @@ impl FileTransferActivity { if entry.is_file() { self.log( LogLevel::Info, - format!("Opening file \"{}\"…", entry.get_abs_path().display()), + format!("Opening file \"{}\"…", entry.path().display()), ); // Edit file - if let Err(err) = self.edit_local_file(entry.get_abs_path().as_path()) { + if let Err(err) = self.edit_local_file(entry.path()) { self.log_and_alert(LogLevel::Error, err); } } @@ -60,7 +61,7 @@ impl FileTransferActivity { } pub(crate) fn action_edit_remote_file(&mut self) { - let entries: Vec = match self.get_remote_selected_entries() { + let entries: Vec = match self.get_remote_selected_entries() { SelectedEntry::One(entry) => vec![entry], SelectedEntry::Many(entries) => entries, SelectedEntry::None => vec![], @@ -68,10 +69,10 @@ impl FileTransferActivity { // Edit all entries for entry in entries.into_iter() { // Check if file - if let FsEntry::File(file) = entry { + if let Entry::File(file) = entry { self.log( LogLevel::Info, - format!("Opening file \"{}\"…", file.abs_path.display()), + format!("Opening file \"{}\"…", file.path.display()), ); // Edit file if let Err(err) = self.edit_remote_file(file) { @@ -149,7 +150,7 @@ impl FileTransferActivity { /// ### edit_remote_file /// /// Edit file on remote host - fn edit_remote_file(&mut self, file: FsFile) -> Result<(), String> { + fn edit_remote_file(&mut self, file: File) -> Result<(), String> { // Create temp file let tmpfile: PathBuf = match self.download_file_as_temp(&file) { Ok(p) => p, @@ -157,7 +158,7 @@ impl FileTransferActivity { }; // Download file let file_name = file.name.clone(); - let file_path = file.abs_path.clone(); + let file_path = file.path.clone(); if let Err(err) = self.filetransfer_recv( TransferPayload::File(file), tmpfile.as_path(), @@ -167,7 +168,7 @@ impl FileTransferActivity { } // Get current file modification time let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) { - Ok(e) => e.get_last_change_time(), + Ok(e) => e.metadata().mtime, Err(err) => { return Err(format!( "Could not stat \"{}\": {}", @@ -181,7 +182,7 @@ impl FileTransferActivity { return Err(err); } // Get local fs entry - let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) { + let tmpfile_entry: Entry = match self.host.stat(tmpfile.as_path()) { Ok(e) => e, Err(err) => { return Err(format!( @@ -192,7 +193,7 @@ impl FileTransferActivity { } }; // Check if file has changed - match prev_mtime != tmpfile_entry.get_last_change_time() { + match prev_mtime != tmpfile_entry.metadata().mtime { true => { self.log( LogLevel::Info, @@ -202,7 +203,7 @@ impl FileTransferActivity { ), ); // Get local fs entry - let tmpfile_entry: FsFile = match self.host.stat(tmpfile.as_path()) { + let tmpfile_entry = match self.host.stat(tmpfile.as_path()) { Ok(e) => e.unwrap_file(), Err(err) => { return Err(format!( diff --git a/src/ui/activities/filetransfer/actions/exec.rs b/src/ui/activities/filetransfer/actions/exec.rs index ba897c26..089943c9 100644 --- a/src/ui/activities/filetransfer/actions/exec.rs +++ b/src/ui/activities/filetransfer/actions/exec.rs @@ -49,9 +49,12 @@ impl FileTransferActivity { pub(crate) fn action_remote_exec(&mut self, input: String) { match self.client.as_mut().exec(input.as_str()) { - Ok(output) => { + Ok((rc, output)) => { // Reload files - self.log(LogLevel::Info, format!("\"{}\": {}", input, output)); + self.log( + LogLevel::Info, + format!("\"{}\" (exitcode: {}): {}", input, rc, output), + ); self.reload_remote_dir(); } Err(err) => { diff --git a/src/ui/activities/filetransfer/actions/find.rs b/src/ui/activities/filetransfer/actions/find.rs index 26e83269..d9b844b2 100644 --- a/src/ui/activities/filetransfer/actions/find.rs +++ b/src/ui/activities/filetransfer/actions/find.rs @@ -27,21 +27,19 @@ */ // locals use super::super::browser::FileExplorerTab; -use super::{ - FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferOpts, TransferPayload, -}; +use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferOpts, TransferPayload}; use std::path::PathBuf; impl FileTransferActivity { - pub(crate) fn action_local_find(&mut self, input: String) -> Result, String> { + pub(crate) fn action_local_find(&mut self, input: String) -> Result, String> { match self.host.find(input.as_str()) { Ok(entries) => Ok(entries), Err(err) => Err(format!("Could not search for files: {}", err)), } } - pub(crate) fn action_remote_find(&mut self, input: String) -> Result, String> { + pub(crate) fn action_remote_find(&mut self, input: String) -> Result, String> { match self.client.as_mut().find(input.as_str()) { Ok(entries) => Ok(entries), Err(err) => Err(format!("Could not search for files: {}", err)), @@ -53,8 +51,8 @@ impl FileTransferActivity { if let SelectedEntry::One(entry) = self.get_found_selected_entries() { // Get path: if a directory, use directory path; if it is a File, get parent path let path: PathBuf = match entry { - FsEntry::Directory(dir) => dir.abs_path, - FsEntry::File(file) => match file.abs_path.parent() { + Entry::Directory(dir) => dir.path, + Entry::File(file) => match file.path.parent() { None => PathBuf::from("."), Some(p) => p.to_path_buf(), }, @@ -86,10 +84,10 @@ impl FileTransferActivity { { // Save pending transfer self.set_pending_transfer( - opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), + opts.save_as.as_deref().unwrap_or_else(|| entry.name()), ); } else if let Err(err) = self.filetransfer_send( - TransferPayload::Any(entry.get_realfile()), + TransferPayload::Any(entry), wrkdir.as_path(), opts.save_as, ) { @@ -107,10 +105,10 @@ impl FileTransferActivity { { // Save pending transfer self.set_pending_transfer( - opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), + opts.save_as.as_deref().unwrap_or_else(|| entry.name()), ); } else if let Err(err) = self.filetransfer_recv( - TransferPayload::Any(entry.get_realfile()), + TransferPayload::Any(entry), wrkdir.as_path(), opts.save_as, ) { @@ -128,12 +126,11 @@ impl FileTransferActivity { dest_path.push(save_as); } // Iter files - let entries: Vec = entries.iter().map(|x| x.get_realfile()).collect(); match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { if opts.check_replace && self.config().get_prompt_on_file_replace() { // Check which file would be replaced - let existing_files: Vec<&FsEntry> = entries + let existing_files: Vec<&Entry> = entries .iter() .filter(|x| { self.remote_file_exists( @@ -166,7 +163,7 @@ impl FileTransferActivity { FileExplorerTab::FindRemote | FileExplorerTab::Remote => { if opts.check_replace && self.config().get_prompt_on_file_replace() { // Check which file would be replaced - let existing_files: Vec<&FsEntry> = entries + let existing_files: Vec<&Entry> = entries .iter() .filter(|x| { self.local_file_exists( @@ -218,7 +215,7 @@ impl FileTransferActivity { } } - fn remove_found_file(&mut self, entry: &FsEntry) { + fn remove_found_file(&mut self, entry: &Entry) { match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { self.local_remove_file(entry); @@ -263,7 +260,7 @@ impl FileTransferActivity { } } - fn open_found_file(&mut self, entry: &FsEntry, with: Option<&str>) { + fn open_found_file(&mut self, entry: &Entry, with: Option<&str>) { match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { self.action_open_local_file(entry, with); diff --git a/src/ui/activities/filetransfer/actions/mkdir.rs b/src/ui/activities/filetransfer/actions/mkdir.rs index 3664920a..5a9b1c8a 100644 --- a/src/ui/activities/filetransfer/actions/mkdir.rs +++ b/src/ui/activities/filetransfer/actions/mkdir.rs @@ -27,6 +27,7 @@ */ // locals use super::{FileTransferActivity, LogLevel}; +use remotefs::fs::UnixPex; use std::path::PathBuf; impl FileTransferActivity { @@ -48,11 +49,10 @@ impl FileTransferActivity { } } pub(crate) fn action_remote_mkdir(&mut self, input: String) { - match self - .client - .as_mut() - .mkdir(PathBuf::from(input.as_str()).as_path()) - { + match self.client.as_mut().create_dir( + PathBuf::from(input.as_str()).as_path(), + UnixPex::from(0o755), + ) { Ok(_) => { // Reload files self.log(LogLevel::Info, format!("Created directory \"{}\"", input)); diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index 2e4fad8d..e7805e33 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -26,9 +26,9 @@ * SOFTWARE. */ pub(self) use super::{ - browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts, - TransferPayload, + browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, TransferOpts, TransferPayload, }; +pub(self) use remotefs::Entry; use tuirealm::{State, StateValue}; // actions @@ -47,8 +47,8 @@ pub(crate) mod submit; #[derive(Debug)] pub(crate) enum SelectedEntry { - One(FsEntry), - Many(Vec), + One(Entry), + Many(Vec), None, } @@ -59,8 +59,8 @@ enum SelectedEntryIndex { None, } -impl From> for SelectedEntry { - fn from(opt: Option<&FsEntry>) -> Self { +impl From> for SelectedEntry { + fn from(opt: Option<&Entry>) -> Self { match opt { Some(e) => SelectedEntry::One(e.clone()), None => SelectedEntry::None, @@ -68,8 +68,8 @@ impl From> for SelectedEntry { } } -impl From> for SelectedEntry { - fn from(files: Vec<&FsEntry>) -> Self { +impl From> for SelectedEntry { + fn from(files: Vec<&Entry>) -> Self { SelectedEntry::Many(files.into_iter().cloned().collect()) } } @@ -82,9 +82,9 @@ impl FileTransferActivity { match self.get_selected_index(&Id::ExplorerLocal) { SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)), SelectedEntryIndex::Many(files) => { - let files: Vec<&FsEntry> = files + let files: Vec<&Entry> = files .iter() - .map(|x| self.local().get(*x)) // Usize to Option + .map(|x| self.local().get(*x)) // Usize to Option .flatten() .collect(); SelectedEntry::from(files) @@ -100,9 +100,9 @@ impl FileTransferActivity { match self.get_selected_index(&Id::ExplorerRemote) { SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)), SelectedEntryIndex::Many(files) => { - let files: Vec<&FsEntry> = files + let files: Vec<&Entry> = files .iter() - .map(|x| self.remote().get(*x)) // Usize to Option + .map(|x| self.remote().get(*x)) // Usize to Option .flatten() .collect(); SelectedEntry::from(files) @@ -120,9 +120,9 @@ impl FileTransferActivity { SelectedEntry::from(self.found().as_ref().unwrap().get(idx)) } SelectedEntryIndex::Many(files) => { - let files: Vec<&FsEntry> = files + let files: Vec<&Entry> = files .iter() - .map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option + .map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option .flatten() .collect(); SelectedEntry::from(files) diff --git a/src/ui/activities/filetransfer/actions/newfile.rs b/src/ui/activities/filetransfer/actions/newfile.rs index fe683500..18bfee02 100644 --- a/src/ui/activities/filetransfer/actions/newfile.rs +++ b/src/ui/activities/filetransfer/actions/newfile.rs @@ -26,7 +26,7 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, FsEntry, LogLevel}; +use super::{Entry, FileTransferActivity, LogLevel}; use std::fs::File; use std::path::PathBuf; @@ -35,7 +35,7 @@ impl FileTransferActivity { // Check if file exists let mut file_exists: bool = false; for file in self.local().iter_files_all() { - if input == file.get_name() { + if input == file.name() { file_exists = true; } } @@ -67,7 +67,7 @@ impl FileTransferActivity { // Check if file exists let mut file_exists: bool = false; for file in self.remote().iter_files_all() { - if input == file.get_name() { + if input == file.name() { file_exists = true; } } @@ -88,7 +88,7 @@ impl FileTransferActivity { ), Ok(tfile) => { // Stat tempfile - let local_file: FsEntry = match self.host.stat(tfile.path()) { + let local_file: Entry = match self.host.stat(tfile.path()) { Err(err) => { self.log_and_alert( LogLevel::Error, @@ -98,7 +98,7 @@ impl FileTransferActivity { } Ok(f) => f, }; - if let FsEntry::File(local_file) = local_file { + if let Entry::File(local_file) = local_file { // Create file let reader = Box::new(match File::open(tfile.path()) { Ok(f) => f, @@ -112,7 +112,7 @@ impl FileTransferActivity { }); match self .client - .send_file_wno_stream(&local_file, file_path.as_path(), reader) + .create_file(file_path.as_path(), &local_file.metadata, reader) { Err(err) => self.log_and_alert( LogLevel::Error, diff --git a/src/ui/activities/filetransfer/actions/open.rs b/src/ui/activities/filetransfer/actions/open.rs index 86f056e5..2bd6c3d1 100644 --- a/src/ui/activities/filetransfer/actions/open.rs +++ b/src/ui/activities/filetransfer/actions/open.rs @@ -26,7 +26,7 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; +use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferPayload}; // ext use std::path::{Path, PathBuf}; @@ -35,7 +35,7 @@ impl FileTransferActivity { /// /// Open local file pub(crate) fn action_open_local(&mut self) { - let entries: Vec = match self.get_local_selected_entries() { + let entries: Vec = match self.get_local_selected_entries() { SelectedEntry::One(entry) => vec![entry], SelectedEntry::Many(entries) => entries, SelectedEntry::None => vec![], @@ -49,7 +49,7 @@ impl FileTransferActivity { /// /// Open local file pub(crate) fn action_open_remote(&mut self) { - let entries: Vec = match self.get_remote_selected_entries() { + let entries: Vec = match self.get_remote_selected_entries() { SelectedEntry::One(entry) => vec![entry], SelectedEntry::Many(entries) => entries, SelectedEntry::None => vec![], @@ -62,25 +62,22 @@ impl FileTransferActivity { /// ### action_open_local_file /// /// Perform open lopcal file - pub(crate) fn action_open_local_file(&mut self, entry: &FsEntry, open_with: Option<&str>) { - let entry: FsEntry = entry.get_realfile(); - self.open_path_with(entry.get_abs_path().as_path(), open_with); + pub(crate) fn action_open_local_file(&mut self, entry: &Entry, open_with: Option<&str>) { + self.open_path_with(entry.path(), open_with); } /// ### action_open_local /// /// Open remote file. The file is first downloaded to a temporary directory on localhost - pub(crate) fn action_open_remote_file(&mut self, entry: &FsEntry, open_with: Option<&str>) { - let entry: FsEntry = entry.get_realfile(); + pub(crate) fn action_open_remote_file(&mut self, entry: &Entry, open_with: Option<&str>) { // Download file - let tmpfile: String = - match self.get_cache_tmp_name(entry.get_name(), entry.get_ftype().as_deref()) { - None => { - self.log(LogLevel::Error, String::from("Could not create tempdir")); - return; - } - Some(p) => p, - }; + let tmpfile: String = match self.get_cache_tmp_name(entry.name(), entry.extension()) { + None => { + self.log(LogLevel::Error, String::from("Could not create tempdir")); + return; + } + Some(p) => p, + }; let cache: PathBuf = match self.cache.as_ref() { None => { self.log(LogLevel::Error, String::from("Could not create tempdir")); @@ -89,7 +86,7 @@ impl FileTransferActivity { Some(p) => p.path().to_path_buf(), }; match self.filetransfer_recv( - TransferPayload::Any(entry), + TransferPayload::Any(entry.clone()), cache.as_path(), Some(tmpfile.clone()), ) { @@ -114,7 +111,7 @@ impl FileTransferActivity { /// /// Open selected file with provided application pub(crate) fn action_local_open_with(&mut self, with: &str) { - let entries: Vec = match self.get_local_selected_entries() { + let entries: Vec = match self.get_local_selected_entries() { SelectedEntry::One(entry) => vec![entry], SelectedEntry::Many(entries) => entries, SelectedEntry::None => vec![], @@ -129,7 +126,7 @@ impl FileTransferActivity { /// /// Open selected file with provided application pub(crate) fn action_remote_open_with(&mut self, with: &str) { - let entries: Vec = match self.get_remote_selected_entries() { + let entries: Vec = match self.get_remote_selected_entries() { SelectedEntry::One(entry) => vec![entry], SelectedEntry::Many(entries) => entries, SelectedEntry::None => vec![], diff --git a/src/ui/activities/filetransfer/actions/rename.rs b/src/ui/activities/filetransfer/actions/rename.rs index 31aaad5a..57c004b5 100644 --- a/src/ui/activities/filetransfer/actions/rename.rs +++ b/src/ui/activities/filetransfer/actions/rename.rs @@ -26,8 +26,9 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry}; -use crate::filetransfer::FileTransferErrorType; +use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry}; + +use remotefs::RemoteErrorType; use std::path::{Path, PathBuf}; impl FileTransferActivity { @@ -45,7 +46,7 @@ impl FileTransferActivity { // Iter files for entry in entries.iter() { let mut dest_path: PathBuf = base_path.clone(); - dest_path.push(entry.get_name()); + dest_path.push(entry.name()); self.local_rename_file(entry, dest_path.as_path()); } // Reload entries @@ -69,7 +70,7 @@ impl FileTransferActivity { // Iter files for entry in entries.iter() { let mut dest_path: PathBuf = base_path.clone(); - dest_path.push(entry.get_name()); + dest_path.push(entry.name()); self.remote_rename_file(entry, dest_path.as_path()); } // Reload entries @@ -79,14 +80,14 @@ impl FileTransferActivity { } } - fn local_rename_file(&mut self, entry: &FsEntry, dest: &Path) { + fn local_rename_file(&mut self, entry: &Entry, dest: &Path) { match self.host.rename(entry, dest) { Ok(_) => { self.log( LogLevel::Info, format!( "Moved \"{}\" to \"{}\"", - entry.get_abs_path().display(), + entry.path().display(), dest.display() ), ); @@ -95,7 +96,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Could not move \"{}\" to \"{}\": {}", - entry.get_abs_path().display(), + entry.path().display(), dest.display(), err ), @@ -103,26 +104,26 @@ impl FileTransferActivity { } } - fn remote_rename_file(&mut self, entry: &FsEntry, dest: &Path) { - match self.client.as_mut().rename(entry, dest) { + fn remote_rename_file(&mut self, entry: &Entry, dest: &Path) { + match self.client.as_mut().mov(entry.path(), dest) { Ok(_) => { self.log( LogLevel::Info, format!( "Moved \"{}\" to \"{}\"", - entry.get_abs_path().display(), + entry.path().display(), dest.display() ), ); } - Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => { + Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { self.tricky_move(entry, dest); } Err(err) => self.log_and_alert( LogLevel::Error, format!( "Could not move \"{}\" to \"{}\": {}", - entry.get_abs_path().display(), + entry.path().display(), dest.display(), err ), @@ -134,21 +135,21 @@ impl FileTransferActivity { /// /// Tricky move will be used whenever copy command is not available on remote host. /// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`) - fn tricky_move(&mut self, entry: &FsEntry, dest: &Path) { + fn tricky_move(&mut self, entry: &Entry, dest: &Path) { debug!( "Using tricky-move to move entry {} to {}", - entry.get_abs_path().display(), + entry.path().display(), dest.display() ); if self.tricky_copy(entry.clone(), dest).is_ok() { // Delete remote existing entry debug!("Tricky-copy worked; removing existing remote entry"); - match self.client.remove(entry) { + match self.client.remove_dir_all(entry.path()) { Ok(_) => self.log( LogLevel::Info, format!( "Moved \"{}\" to \"{}\"", - entry.get_abs_path().display(), + entry.path().display(), dest.display() ), ), @@ -156,7 +157,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Copied \"{}\" to \"{}\"; but failed to remove src: {}", - entry.get_abs_path().display(), + entry.path().display(), dest.display(), err ), diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index bffcbc3f..6ba0741a 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -27,7 +27,7 @@ */ // locals use super::{ - super::STORAGE_PENDING_TRANSFER, FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, + super::STORAGE_PENDING_TRANSFER, Entry, FileExplorerTab, FileTransferActivity, LogLevel, SelectedEntry, TransferOpts, TransferPayload, }; use std::path::{Path, PathBuf}; @@ -101,10 +101,10 @@ impl FileTransferActivity { { // Save pending transfer self.set_pending_transfer( - opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), + opts.save_as.as_deref().unwrap_or_else(|| entry.name()), ); } else if let Err(err) = self.filetransfer_send( - TransferPayload::Any(entry.get_realfile()), + TransferPayload::Any(entry.clone()), wrkdir.as_path(), opts.save_as, ) { @@ -123,10 +123,9 @@ impl FileTransferActivity { dest_path.push(save_as); } // Iter files - let entries: Vec = entries.iter().map(|x| x.get_realfile()).collect(); if opts.check_replace && self.config().get_prompt_on_file_replace() { // Check which file would be replaced - let existing_files: Vec<&FsEntry> = entries + let existing_files: Vec<&Entry> = entries .iter() .filter(|x| { self.remote_file_exists( @@ -171,10 +170,10 @@ impl FileTransferActivity { { // Save pending transfer self.set_pending_transfer( - opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), + opts.save_as.as_deref().unwrap_or_else(|| entry.name()), ); } else if let Err(err) = self.filetransfer_recv( - TransferPayload::Any(entry.get_realfile()), + TransferPayload::Any(entry.clone()), wrkdir.as_path(), opts.save_as, ) { @@ -193,10 +192,9 @@ impl FileTransferActivity { dest_path.push(save_as); } // Iter files - let entries: Vec = entries.iter().map(|x| x.get_realfile()).collect(); if opts.check_replace && self.config().get_prompt_on_file_replace() { // Check which file would be replaced - let existing_files: Vec<&FsEntry> = entries + let existing_files: Vec<&Entry> = entries .iter() .filter(|x| { self.local_file_exists( @@ -244,8 +242,8 @@ impl FileTransferActivity { /// ### set_pending_transfer_many /// /// Set pending transfer for many files into storage and mount radio - pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&FsEntry>, dest_path: &str) { - let file_names: Vec<&str> = files.iter().map(|x| x.get_name()).collect(); + pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&Entry>, dest_path: &str) { + let file_names: Vec<&str> = files.iter().map(|x| x.name()).collect(); self.mount_radio_replace_many(file_names.as_slice()); self.context_mut() .store_mut() @@ -255,16 +253,16 @@ impl FileTransferActivity { /// ### file_to_check /// /// Get file to check for path - pub(crate) fn file_to_check(e: &FsEntry, alt: Option<&String>) -> PathBuf { + pub(crate) fn file_to_check(e: &Entry, alt: Option<&String>) -> PathBuf { match alt { Some(s) => PathBuf::from(s), - None => PathBuf::from(e.get_name()), + None => PathBuf::from(e.name()), } } - pub(crate) fn file_to_check_many(e: &FsEntry, wrkdir: &Path) -> PathBuf { + pub(crate) fn file_to_check_many(e: &Entry, wrkdir: &Path) -> PathBuf { let mut p = wrkdir.to_path_buf(); - p.push(e.get_name()); + p.push(e.name()); p } } diff --git a/src/ui/activities/filetransfer/actions/submit.rs b/src/ui/activities/filetransfer/actions/submit.rs index ea034a7f..ae227f7b 100644 --- a/src/ui/activities/filetransfer/actions/submit.rs +++ b/src/ui/activities/filetransfer/actions/submit.rs @@ -26,7 +26,9 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, FsEntry}; +use super::{Entry, FileTransferActivity}; + +use remotefs::fs::{File, Metadata}; enum SubmitAction { ChangeDir, @@ -38,25 +40,41 @@ impl FileTransferActivity { /// /// Decides which action to perform on submit for local explorer /// Return true whether the directory changed - pub(crate) fn action_submit_local(&mut self, entry: FsEntry) -> bool { - let action: SubmitAction = match &entry { - FsEntry::Directory(_) => SubmitAction::ChangeDir, - FsEntry::File(file) => { - match &file.symlink { - Some(symlink_entry) => { - // If symlink and is directory, point to symlink - match &**symlink_entry { - FsEntry::Directory(_) => SubmitAction::ChangeDir, - _ => SubmitAction::None, - } + pub(crate) fn action_submit_local(&mut self, entry: Entry) -> bool { + let (action, entry) = match &entry { + Entry::Directory(_) => (SubmitAction::ChangeDir, entry), + Entry::File(File { + path, + metadata: + Metadata { + symlink: Some(symlink), + .. + }, + .. + }) => { + // Stat file + let stat_file = match self.host.stat(symlink.as_path()) { + Ok(e) => e, + Err(err) => { + warn!( + "Could not stat file pointed by {} ({}): {}", + path.display(), + symlink.display(), + err + ); + entry } - None => SubmitAction::None, - } + }; + (SubmitAction::ChangeDir, stat_file) } + Entry::File(_) => (SubmitAction::None, entry), }; - match action { - SubmitAction::ChangeDir => self.action_enter_local_dir(entry, false), - SubmitAction::None => false, + match (action, entry) { + (SubmitAction::ChangeDir, Entry::Directory(dir)) => { + self.action_enter_local_dir(dir, false) + } + (SubmitAction::ChangeDir, _) => false, + (SubmitAction::None, _) => false, } } @@ -64,25 +82,41 @@ impl FileTransferActivity { /// /// Decides which action to perform on submit for remote explorer /// Return true whether the directory changed - pub(crate) fn action_submit_remote(&mut self, entry: FsEntry) -> bool { - let action: SubmitAction = match &entry { - FsEntry::Directory(_) => SubmitAction::ChangeDir, - FsEntry::File(file) => { - match &file.symlink { - Some(symlink_entry) => { - // If symlink and is directory, point to symlink - match &**symlink_entry { - FsEntry::Directory(_) => SubmitAction::ChangeDir, - _ => SubmitAction::None, - } + pub(crate) fn action_submit_remote(&mut self, entry: Entry) -> bool { + let (action, entry) = match &entry { + Entry::Directory(_) => (SubmitAction::ChangeDir, entry), + Entry::File(File { + path, + metadata: + Metadata { + symlink: Some(symlink), + .. + }, + .. + }) => { + // Stat file + let stat_file = match self.client.stat(symlink.as_path()) { + Ok(e) => e, + Err(err) => { + warn!( + "Could not stat file pointed by {} ({}): {}", + path.display(), + symlink.display(), + err + ); + entry } - None => SubmitAction::None, - } + }; + (SubmitAction::ChangeDir, stat_file) } + Entry::File(_) => (SubmitAction::None, entry), }; - match action { - SubmitAction::ChangeDir => self.action_enter_remote_dir(entry, false), - SubmitAction::None => false, + match (action, entry) { + (SubmitAction::ChangeDir, Entry::Directory(dir)) => { + self.action_enter_remote_dir(dir, false) + } + (SubmitAction::ChangeDir, _) => false, + (SubmitAction::None, _) => false, } } } diff --git a/src/ui/activities/filetransfer/components/log.rs b/src/ui/activities/filetransfer/components/log.rs index 9d4fd0ba..87fe4d3d 100644 --- a/src/ui/activities/filetransfer/components/log.rs +++ b/src/ui/activities/filetransfer/components/log.rs @@ -225,21 +225,12 @@ impl Component for Log { /// ## OwnStates /// /// OwnStates contains states for this component -#[derive(Clone)] +#[derive(Clone, Default)] struct OwnStates { list_index: usize, // Index of selected element in list list_len: usize, // Length of file list } -impl Default for OwnStates { - fn default() -> Self { - OwnStates { - list_index: 0, - list_len: 0, - } - } -} - impl OwnStates { /// ### set_list_len /// diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index a0318172..b21a2569 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -27,12 +27,11 @@ */ use super::super::Browser; use super::{Msg, TransferMsg, UiMsg}; -use crate::fs::explorer::FileSorting; -use crate::fs::FsEntry; +use crate::explorer::FileSorting; use crate::utils::fmt::fmt_time; use bytesize::ByteSize; -use std::path::PathBuf; +use remotefs::Entry; use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span}; use tuirealm::command::{Cmd, CmdResult, Direction, Position}; @@ -400,38 +399,32 @@ pub struct FileInfoPopup { } impl FileInfoPopup { - pub fn new(file: &FsEntry) -> Self { + pub fn new(file: &Entry) -> Self { let mut texts: TableBuilder = TableBuilder::default(); // Abs path - let real_path: Option = { - let real_file: FsEntry = file.get_realfile(); - match real_file.get_abs_path() != file.get_abs_path() { - true => Some(real_file.get_abs_path()), - false => None, - } - }; + let real_path = file.metadata().symlink.as_deref(); let path: String = match real_path { - Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()), - None => format!("{}", file.get_abs_path().display()), + Some(symlink) => format!("{} -> {}", file.path().display(), symlink.display()), + None => format!("{}", file.path().display()), }; // Make texts texts .add_col(TextSpan::from("Path: ")) .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); - if let Some(filetype) = file.get_ftype() { + if let Some(filetype) = file.extension() { texts .add_row() .add_col(TextSpan::from("File type: ")) - .add_col(TextSpan::new(filetype.as_str()).fg(Color::LightGreen)); + .add_col(TextSpan::new(filetype).fg(Color::LightGreen)); } - let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size()); + let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size); texts .add_row() .add_col(TextSpan::from("Size: ")) .add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan)); - 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"); + let atime: String = fmt_time(file.metadata().atime, "%b %d %Y %H:%M:%S"); + let ctime: String = fmt_time(file.metadata().ctime, "%b %d %Y %H:%M:%S"); + let mtime: String = fmt_time(file.metadata().mtime, "%b %d %Y %H:%M:%S"); texts .add_row() .add_col(TextSpan::from("Creation time: ")) @@ -446,7 +439,7 @@ impl FileInfoPopup { .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); // User #[cfg(target_family = "unix")] - let username: String = match file.get_user() { + let username: String = match file.metadata().uid { Some(uid) => match get_user_by_uid(uid) { Some(user) => user.name().to_string_lossy().to_string(), None => uid.to_string(), @@ -454,10 +447,10 @@ impl FileInfoPopup { None => String::from("0"), }; #[cfg(target_os = "windows")] - let username: String = format!("{}", file.get_user().unwrap_or(0)); + let username: String = format!("{}", file.metadata().uid.unwrap_or(0)); // Group #[cfg(target_family = "unix")] - let group: String = match file.get_group() { + let group: String = match file.metadata().gid { Some(gid) => match get_group_by_gid(gid) { Some(group) => group.name().to_string_lossy().to_string(), None => gid.to_string(), @@ -465,7 +458,7 @@ impl FileInfoPopup { None => String::from("0"), }; #[cfg(target_os = "windows")] - let group: String = format!("{}", file.get_group().unwrap_or(0)); + let group: String = format!("{}", file.metadata().gid.unwrap_or(0)); texts .add_row() .add_col(TextSpan::from("User: ")) @@ -478,7 +471,7 @@ impl FileInfoPopup { component: List::default() .borders(Borders::default().modifiers(BorderType::Rounded)) .scroll(false) - .title(file.get_name(), Alignment::Left) + .title(file.name(), Alignment::Left) .rows(texts.build()), } } diff --git a/src/ui/activities/filetransfer/components/transfer/file_list.rs b/src/ui/activities/filetransfer/components/transfer/file_list.rs index 27ca9c07..d05ba2ca 100644 --- a/src/ui/activities/filetransfer/components/transfer/file_list.rs +++ b/src/ui/activities/filetransfer/components/transfer/file_list.rs @@ -39,21 +39,12 @@ pub const FILE_LIST_CMD_SELECT_ALL: &str = "A"; /// ## OwnStates /// /// OwnStates contains states for this component -#[derive(Clone)] +#[derive(Clone, Default)] struct OwnStates { list_index: usize, // Index of selected element in list selected: Vec, // Selected files } -impl Default for OwnStates { - fn default() -> Self { - OwnStates { - list_index: 0, - selected: Vec::new(), - } - } -} - impl OwnStates { /// ### init_list_states /// diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index ecd948ef..dabb489a 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -25,10 +25,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs}; -use crate::fs::FsEntry; +use crate::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs}; use crate::system::config_client::ConfigClient; +use remotefs::Entry; use std::path::Path; /// ## FileExplorerTab @@ -100,7 +100,7 @@ impl Browser { self.found.as_mut().map(|x| &mut x.1) } - pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec, wrkdir: &Path) { + pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec, wrkdir: &Path) { let mut explorer = Self::build_found_explorer(wrkdir); explorer.set_files(files); self.found = Some((tab, explorer)); diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 60e98c27..51a5be8e 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -29,7 +29,6 @@ use super::{ use crate::filetransfer::ProtocolParams; use crate::system::environment; use crate::system::notifications::Notification; -use crate::system::sshkey_storage::SshKeyStorage; use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex}; use crate::utils::path; // Ext @@ -120,13 +119,6 @@ impl FileTransferActivity { } } - /// ### make_ssh_storage - /// - /// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded) - pub(super) fn make_ssh_storage(cli: &ConfigClient) -> SshKeyStorage { - SshKeyStorage::storage_from_config(cli) - } - /// ### setup_text_editor /// /// Set text editor to use @@ -227,7 +219,7 @@ impl FileTransferActivity { TransferPayload::Any(entry) => { format!( "\"{}\" has been successfully transferred ({})", - entry.get_name(), + entry.name(), transfer_stats ) } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 905da934..ae8b18a2 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -37,10 +37,8 @@ mod view; // locals use super::{Activity, Context, ExitReason}; use crate::config::themes::Theme; -use crate::filetransfer::{FileTransfer, FileTransferProtocol}; -use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer}; -use crate::fs::explorer::{FileExplorer, FileSorting}; -use crate::fs::FsEntry; +use crate::explorer::{FileExplorer, FileSorting}; +use crate::filetransfer::{Builder, FileTransferParams}; use crate::host::Localhost; use crate::system::config_client::ConfigClient; pub(self) use lib::browser; @@ -50,6 +48,7 @@ pub(self) use session::TransferPayload; // Includes use chrono::{DateTime, Local}; +use remotefs::RemoteFs; use std::collections::VecDeque; use std::time::Duration; use tempfile::TempDir; @@ -217,8 +216,8 @@ pub struct FileTransferActivity { redraw: bool, /// Localhost bridge host: Localhost, - /// Remote host - client: Box, + /// Remote host client + client: Box, /// Browser browser: Browser, /// Current log lines @@ -232,7 +231,7 @@ impl FileTransferActivity { /// ### new /// /// Instantiates a new FileTransferActivity - pub fn new(host: Localhost, protocol: FileTransferProtocol, ticks: Duration) -> Self { + pub fn new(host: Localhost, params: &FileTransferParams, ticks: Duration) -> Self { // Get config client let config_client: ConfigClient = Self::init_config_client(); Self { @@ -245,16 +244,7 @@ impl FileTransferActivity { ), redraw: true, host, - client: match protocol { - FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new( - Self::make_ssh_storage(&config_client), - )), - FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)), - FileTransferProtocol::Scp => { - Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client))) - } - FileTransferProtocol::AwsS3 => Box::new(S3FileTransfer::default()), - }, + client: Builder::build(params.protocol, params.params.clone(), &config_client), browser: Browser::new(&config_client), log_records: VecDeque::with_capacity(256), // 256 events is enough I guess transfer: TransferStates::default(), diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 4d90943a..41bccec5 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -27,14 +27,14 @@ */ // Locals use super::{FileTransferActivity, LogLevel}; -use crate::filetransfer::{FileTransferError, FileTransferErrorType}; -use crate::fs::{FsEntry, FsFile}; use crate::host::HostError; use crate::utils::fmt::fmt_millis; // Ext use bytesize::ByteSize; -use std::fs::File; +use remotefs::fs::{Entry, File, UnixPex, Welcome}; +use remotefs::{RemoteError, RemoteErrorType}; +use std::fs::File as StdFile; use std::io::{Read, Seek, Write}; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -56,20 +56,20 @@ enum TransferErrorReason { #[error("I/O error on remote: {0}")] RemoteIoError(std::io::Error), #[error("File transfer error: {0}")] - FileTransferError(FileTransferError), + FileTransferError(RemoteError), } /// ## TransferPayload /// /// Represents the entity to send or receive during a transfer. -/// - File: describes an individual `FsFile` to send -/// - Any: Can be any kind of `FsEntry`, but just one -/// - Many: a list of `FsEntry` +/// - File: describes an individual `File` to send +/// - Any: Can be any kind of `Entry`, but just one +/// - Many: a list of `Entry` #[derive(Debug)] pub(super) enum TransferPayload { - File(FsFile), - Any(FsEntry), - Many(Vec), + File(File), + Any(Entry), + Many(Vec), } impl FileTransferActivity { @@ -78,11 +78,11 @@ impl FileTransferActivity { /// Connect to remote pub(super) fn connect(&mut self) { let ft_params = self.context().ft_params().unwrap().clone(); - let entry_dir: Option = ft_params.entry_directory.clone(); + let entry_dir: Option = ft_params.entry_directory; // Connect to remote - match self.client.connect(&ft_params.params) { - Ok(welcome) => { - if let Some(banner) = welcome { + match self.client.connect() { + Ok(Welcome { banner, .. }) => { + if let Some(banner) = banner { // Log welcome self.log( LogLevel::Info, @@ -234,17 +234,17 @@ impl FileTransferActivity { /// Send one file to remote at specified path. fn filetransfer_send_file( &mut self, - file: &FsFile, + file: &File, curr_remote_path: &Path, dst_name: Option, ) -> Result<(), String> { // Reset states self.transfer.reset(); // Calculate total size of transfer - let total_transfer_size: usize = file.size; + let total_transfer_size: usize = file.metadata.size as usize; self.transfer.full.init(total_transfer_size); // Mount progress bar - self.mount_progress_bar(format!("Uploading {}…", file.abs_path.display())); + self.mount_progress_bar(format!("Uploading {}…", file.path.display())); // Get remote path let file_name: String = file.name.clone(); let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); @@ -266,7 +266,7 @@ impl FileTransferActivity { /// Send a `TransferPayload` of type `Any` fn filetransfer_send_any( &mut self, - entry: &FsEntry, + entry: &Entry, curr_remote_path: &Path, dst_name: Option, ) -> Result<(), String> { @@ -276,7 +276,7 @@ impl FileTransferActivity { let total_transfer_size: usize = self.get_total_transfer_size_local(entry); self.transfer.full.init(total_transfer_size); // Mount progress bar - self.mount_progress_bar(format!("Uploading {}…", entry.get_abs_path().display())); + self.mount_progress_bar(format!("Uploading {}…", entry.path().display())); // Send recurse let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name); // Umount progress bar @@ -289,7 +289,7 @@ impl FileTransferActivity { /// Send many entries to remote fn filetransfer_send_many( &mut self, - entries: &[FsEntry], + entries: &[Entry], curr_remote_path: &Path, ) -> Result<(), String> { // Reset states @@ -315,14 +315,14 @@ impl FileTransferActivity { fn filetransfer_send_recurse( &mut self, - entry: &FsEntry, + entry: &Entry, curr_remote_path: &Path, dst_name: Option, ) -> Result<(), String> { // Write popup let file_name: String = match entry { - FsEntry::Directory(dir) => dir.name.clone(), - FsEntry::File(file) => file.name.clone(), + Entry::Directory(dir) => dir.name.clone(), + Entry::File(file) => file.name.clone(), }; // Get remote path let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); @@ -333,7 +333,7 @@ impl FileTransferActivity { remote_path.push(remote_file_name); // Match entry let result: Result<(), String> = match entry { - FsEntry::File(file) => { + Entry::File(file) => { match self.filetransfer_send_one(file, remote_path.as_path(), file_name) { Err(err) => { // If transfer was abrupted or there was an IO error on remote, remove file @@ -352,7 +352,7 @@ impl FileTransferActivity { ), ), Ok(entry) => { - if let Err(err) = self.client.remove(&entry) { + if let Err(err) = self.client.remove_file(entry.path()) { self.log( LogLevel::Error, format!( @@ -370,16 +370,19 @@ impl FileTransferActivity { Ok(_) => Ok(()), } } - FsEntry::Directory(dir) => { + Entry::Directory(dir) => { // Create directory on remote first - match self.client.mkdir(remote_path.as_path()) { + match self + .client + .create_dir(remote_path.as_path(), UnixPex::from(0o755)) + { Ok(_) => { self.log( LogLevel::Info, format!("Created directory \"{}\"", remote_path.display()), ); } - Err(err) if err.kind() == FileTransferErrorType::DirectoryAlreadyExists => { + Err(err) if err.kind == RemoteErrorType::DirectoryAlreadyExists => { self.log( LogLevel::Info, format!( @@ -401,7 +404,7 @@ impl FileTransferActivity { } } // Get files in dir - match self.host.scan_dir(dir.abs_path.as_path()) { + match self.host.scan_dir(dir.path.as_path()) { Ok(entries) => { // Iterate over files for entry in entries.iter() { @@ -423,7 +426,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Could not scan directory \"{}\": {}", - dir.abs_path.display(), + dir.path.display(), err ), ); @@ -439,7 +442,7 @@ impl FileTransferActivity { // Log abort self.log_and_alert( LogLevel::Warn, - format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()), + format!("Upload aborted for \"{}\"!", entry.path().display()), ); } result @@ -450,18 +453,24 @@ impl FileTransferActivity { /// Send local file and write it to remote path fn filetransfer_send_one( &mut self, - local: &FsFile, + local: &File, remote: &Path, file_name: String, ) -> Result<(), TransferErrorReason> { + // Sync file size and attributes before transfer + let metadata = self + .host + .stat(local.path.as_path()) + .map_err(TransferErrorReason::HostError) + .map(|x| x.metadata().clone())?; // Upload file // Try to open local file - match self.host.open_file_read(local.abs_path.as_path()) { - Ok(fhnd) => match self.client.send_file(local, remote) { + match self.host.open_file_read(local.path.as_path()) { + Ok(fhnd) => match self.client.create(remote, &metadata) { Ok(rhnd) => { self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd) } - Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => { + Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd) } Err(err) => Err(TransferErrorReason::FileTransferError(err)), @@ -475,10 +484,10 @@ impl FileTransferActivity { /// Send file to remote using stream fn filetransfer_send_one_with_stream( &mut self, - local: &FsFile, + local: &File, remote: &Path, file_name: String, - mut reader: File, + mut reader: StdFile, mut writer: Box, ) -> Result<(), TransferErrorReason> { // Write file @@ -548,7 +557,7 @@ impl FileTransferActivity { } } // Finalize stream - if let Err(err) = self.client.on_sent(writer) { + if let Err(err) = self.client.on_written(writer) { self.log( LogLevel::Warn, format!("Could not finalize remote stream: \"{}\"", err), @@ -562,7 +571,7 @@ impl FileTransferActivity { LogLevel::Info, format!( "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - local.abs_path.display(), + local.path.display(), remote.display(), fmt_millis(self.transfer.partial.started().elapsed()), ByteSize(self.transfer.partial.calc_bytes_per_second()), @@ -573,14 +582,20 @@ impl FileTransferActivity { /// ### filetransfer_send_one_wno_stream /// - /// Send an `FsFile` to remote without using streams. + /// Send an `File` to remote without using streams. fn filetransfer_send_one_wno_stream( &mut self, - local: &FsFile, + local: &File, remote: &Path, file_name: String, - mut reader: File, + mut reader: StdFile, ) -> Result<(), TransferErrorReason> { + // Sync file size and attributes before transfer + let metadata = self + .host + .stat(local.path.as_path()) + .map_err(TransferErrorReason::HostError) + .map(|x| x.metadata().clone())?; // Write file let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; // Init transfer @@ -593,10 +608,7 @@ impl FileTransferActivity { self.update_progress_bar(format!("Uploading \"{}\"…", file_name)); self.view(); // Send file - if let Err(err) = self - .client - .send_file_wno_stream(local, remote, Box::new(reader)) - { + if let Err(err) = self.client.create_file(remote, &metadata, Box::new(reader)) { return Err(TransferErrorReason::FileTransferError(err)); } // Set transfer size ok @@ -610,7 +622,7 @@ impl FileTransferActivity { LogLevel::Info, format!( "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - local.abs_path.display(), + local.path.display(), remote.display(), fmt_millis(self.transfer.partial.started().elapsed()), ByteSize(self.transfer.partial.calc_bytes_per_second()), @@ -656,7 +668,7 @@ impl FileTransferActivity { /// If entry is a directory, this applies to directory only fn filetransfer_recv_any( &mut self, - entry: &FsEntry, + entry: &Entry, local_path: &Path, dst_name: Option, ) -> Result<(), String> { @@ -666,7 +678,7 @@ impl FileTransferActivity { let total_transfer_size: usize = self.get_total_transfer_size_remote(entry); self.transfer.full.init(total_transfer_size); // Mount progress bar - self.mount_progress_bar(format!("Downloading {}…", entry.get_abs_path().display())); + self.mount_progress_bar(format!("Downloading {}…", entry.path().display())); // Receive let result = self.filetransfer_recv_recurse(entry, local_path, dst_name); // Umount progress bar @@ -677,14 +689,14 @@ impl FileTransferActivity { /// ### filetransfer_recv_file /// /// Receive a single file from remote. - fn filetransfer_recv_file(&mut self, entry: &FsFile, local_path: &Path) -> Result<(), String> { + fn filetransfer_recv_file(&mut self, entry: &File, local_path: &Path) -> Result<(), String> { // Reset states self.transfer.reset(); // Calculate total transfer size - let total_transfer_size: usize = entry.size; + let total_transfer_size: usize = entry.metadata.size as usize; self.transfer.full.init(total_transfer_size); // Mount progress bar - self.mount_progress_bar(format!("Downloading {}…", entry.abs_path.display())); + self.mount_progress_bar(format!("Downloading {}…", entry.path.display())); // Receive let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone()); // Umount progress bar @@ -698,7 +710,7 @@ impl FileTransferActivity { /// Send many entries to remote fn filetransfer_recv_many( &mut self, - entries: &[FsEntry], + entries: &[Entry], curr_remote_path: &Path, ) -> Result<(), String> { // Reset states @@ -724,18 +736,18 @@ impl FileTransferActivity { fn filetransfer_recv_recurse( &mut self, - entry: &FsEntry, + entry: &Entry, local_path: &Path, dst_name: Option, ) -> Result<(), String> { // Write popup let file_name: String = match entry { - FsEntry::Directory(dir) => dir.name.clone(), - FsEntry::File(file) => file.name.clone(), + Entry::Directory(dir) => dir.name.clone(), + Entry::File(file) => file.name.clone(), }; // Match entry let result: Result<(), String> = match entry { - FsEntry::File(file) => { + Entry::File(file) => { // Get local file let mut local_file_path: PathBuf = PathBuf::from(local_path); let local_file_name: String = match dst_name { @@ -781,7 +793,7 @@ impl FileTransferActivity { Ok(()) } } - FsEntry::Directory(dir) => { + Entry::Directory(dir) => { // Get dir name let mut local_dir_path: PathBuf = PathBuf::from(local_path); match dst_name { @@ -797,16 +809,13 @@ impl FileTransferActivity { target_os = "macos", target_os = "linux" ))] - if let Some((owner, group, others)) = dir.unix_pex { - if let Err(err) = self.host.chmod( - local_dir_path.as_path(), - (owner.as_byte(), group.as_byte(), others.as_byte()), - ) { + if let Some(mode) = dir.metadata.mode { + if let Err(err) = self.host.chmod(local_dir_path.as_path(), mode) { self.log( LogLevel::Error, format!( - "Could not apply file mode {:?} to \"{}\": {}", - (owner.as_byte(), group.as_byte(), others.as_byte()), + "Could not apply file mode {:o} to \"{}\": {}", + u32::from(mode), local_dir_path.display(), err ), @@ -818,7 +827,7 @@ impl FileTransferActivity { format!("Created directory \"{}\"", local_dir_path.display()), ); // Get files in dir - match self.client.list_dir(dir.abs_path.as_path()) { + match self.client.list_dir(dir.path.as_path()) { Ok(entries) => { // Iterate over files for entry in entries.iter() { @@ -843,7 +852,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Could not scan directory \"{}\": {}", - dir.abs_path.display(), + dir.path.display(), err ), ); @@ -872,10 +881,7 @@ impl FileTransferActivity { // Log abort self.log_and_alert( LogLevel::Warn, - format!( - "Download aborted for \"{}\"!", - entry.get_abs_path().display() - ), + format!("Download aborted for \"{}\"!", entry.path().display()), ); } result @@ -887,18 +893,18 @@ impl FileTransferActivity { fn filetransfer_recv_one( &mut self, local: &Path, - remote: &FsFile, + remote: &File, file_name: String, ) -> Result<(), TransferErrorReason> { // Try to open local file match self.host.open_file_write(local) { Ok(local_file) => { // Download file from remote - match self.client.recv_file(remote) { + match self.client.open(remote.path.as_path()) { Ok(rhnd) => self.filetransfer_recv_one_with_stream( local, remote, file_name, rhnd, local_file, ), - Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => { + Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { self.filetransfer_recv_one_wno_stream(local, remote, file_name) } Err(err) => Err(TransferErrorReason::FileTransferError(err)), @@ -910,24 +916,24 @@ impl FileTransferActivity { /// ### filetransfer_recv_one_with_stream /// - /// Receive an `FsEntry` from remote using stream + /// Receive an `Entry` from remote using stream fn filetransfer_recv_one_with_stream( &mut self, local: &Path, - remote: &FsFile, + remote: &File, file_name: String, mut reader: Box, - mut writer: File, + mut writer: StdFile, ) -> Result<(), TransferErrorReason> { let mut total_bytes_written: usize = 0; // Init transfer - self.transfer.partial.init(remote.size); + self.transfer.partial.init(remote.metadata.size as usize); // Write local file let mut last_progress_val: f64 = 0.0; let mut last_input_event_fetch: Option = None; // While the entire file hasn't been completely read, // Or filetransfer has been aborted - while total_bytes_written < remote.size && !self.transfer.aborted() { + while total_bytes_written < remote.metadata.size as usize && !self.transfer.aborted() { // Handle input events (each 500 ms) or is None if last_input_event_fetch.is_none() || last_input_event_fetch @@ -978,7 +984,7 @@ impl FileTransferActivity { } } // Finalize stream - if let Err(err) = self.client.on_recv(reader) { + if let Err(err) = self.client.on_read(reader) { self.log( LogLevel::Warn, format!("Could not finalize remote stream: \"{}\"", err), @@ -990,16 +996,13 @@ impl FileTransferActivity { } // Apply file mode to file #[cfg(target_family = "unix")] - if let Some((owner, group, others)) = remote.unix_pex { - if let Err(err) = self - .host - .chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte())) - { + if let Some(mode) = remote.metadata.mode { + if let Err(err) = self.host.chmod(local, mode) { self.log( LogLevel::Error, format!( - "Could not apply file mode {:?} to \"{}\": {}", - (owner.as_byte(), group.as_byte(), others.as_byte()), + "Could not apply file mode {:o} to \"{}\": {}", + u32::from(mode), local.display(), err ), @@ -1011,7 +1014,7 @@ impl FileTransferActivity { LogLevel::Info, format!( "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - remote.abs_path.display(), + remote.path.display(), local.display(), fmt_millis(self.transfer.partial.started().elapsed()), ByteSize(self.transfer.partial.calc_bytes_per_second()), @@ -1022,40 +1025,47 @@ impl FileTransferActivity { /// ### filetransfer_recv_one_with_stream /// - /// Receive an `FsEntry` from remote without using stream + /// Receive an `Entry` from remote without using stream fn filetransfer_recv_one_wno_stream( &mut self, local: &Path, - remote: &FsFile, + remote: &File, file_name: String, ) -> Result<(), TransferErrorReason> { + // Open local file + let reader = self + .host + .open_file_write(local) + .map_err(TransferErrorReason::HostError) + .map(Box::new)?; // Init transfer - self.transfer.partial.init(remote.size); + self.transfer.partial.init(remote.metadata.size as usize); // Draw before transfer self.update_progress_bar(format!("Downloading \"{}\"", file_name)); self.view(); // recv wno stream - if let Err(err) = self.client.recv_file_wno_stream(remote, local) { + if let Err(err) = self.client.open_file(remote.path.as_path(), reader) { return Err(TransferErrorReason::FileTransferError(err)); } // Update progress at the end - self.transfer.partial.update_progress(remote.size); - self.transfer.full.update_progress(remote.size); + self.transfer + .partial + .update_progress(remote.metadata.size as usize); + self.transfer + .full + .update_progress(remote.metadata.size as usize); // Draw after transfer self.update_progress_bar(format!("Downloading \"{}\"", file_name)); self.view(); // Apply file mode to file #[cfg(target_family = "unix")] - if let Some((owner, group, others)) = remote.unix_pex { - if let Err(err) = self - .host - .chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte())) - { + if let Some(mode) = remote.metadata.mode { + if let Err(err) = self.host.chmod(local, mode) { self.log( LogLevel::Error, format!( - "Could not apply file mode {:?} to \"{}\": {}", - (owner.as_byte(), group.as_byte(), others.as_byte()), + "Could not apply file mode {:o} to \"{}\": {}", + u32::from(mode), local.display(), err ), @@ -1067,7 +1077,7 @@ impl FileTransferActivity { LogLevel::Info, format!( "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - remote.abs_path.display(), + remote.path.display(), local.display(), fmt_millis(self.transfer.partial.started().elapsed()), ByteSize(self.transfer.partial.calc_bytes_per_second()), @@ -1136,7 +1146,7 @@ impl FileTransferActivity { /// ### download_file_as_temp /// /// Download provided file as a temporary file - pub(super) fn download_file_as_temp(&mut self, file: &FsFile) -> Result { + pub(super) fn download_file_as_temp(&mut self, file: &File) -> Result { let tmpfile: PathBuf = match self.cache.as_ref() { Some(cache) => { let mut p: PathBuf = cache.path().to_path_buf(); @@ -1157,7 +1167,7 @@ impl FileTransferActivity { ) { Err(err) => Err(format!( "Could not download {} to temporary file: {}", - file.abs_path.display(), + file.path.display(), err )), Ok(()) => Ok(tmpfile), @@ -1169,12 +1179,12 @@ impl FileTransferActivity { /// ### get_total_transfer_size_local /// /// Get total size of transfer for localhost - fn get_total_transfer_size_local(&mut self, entry: &FsEntry) -> usize { + fn get_total_transfer_size_local(&mut self, entry: &Entry) -> usize { match entry { - FsEntry::File(file) => file.size, - FsEntry::Directory(dir) => { + Entry::File(file) => file.metadata.size as usize, + Entry::Directory(dir) => { // List dir - match self.host.scan_dir(dir.abs_path.as_path()) { + match self.host.scan_dir(dir.path.as_path()) { Ok(files) => files .iter() .map(|x| self.get_total_transfer_size_local(x)) @@ -1182,11 +1192,7 @@ impl FileTransferActivity { Err(err) => { self.log( LogLevel::Error, - format!( - "Could not list directory {}: {}", - dir.abs_path.display(), - err - ), + format!("Could not list directory {}: {}", dir.path.display(), err), ); 0 } @@ -1198,12 +1204,12 @@ impl FileTransferActivity { /// ### get_total_transfer_size_remote /// /// Get total size of transfer for remote host - fn get_total_transfer_size_remote(&mut self, entry: &FsEntry) -> usize { + fn get_total_transfer_size_remote(&mut self, entry: &Entry) -> usize { match entry { - FsEntry::File(file) => file.size, - FsEntry::Directory(dir) => { + Entry::File(file) => file.metadata.size as usize, + Entry::Directory(dir) => { // List directory - match self.client.list_dir(dir.abs_path.as_path()) { + match self.client.list_dir(dir.path.as_path()) { Ok(files) => files .iter() .map(|x| self.get_total_transfer_size_remote(x)) @@ -1211,11 +1217,7 @@ impl FileTransferActivity { Err(err) => { self.log( LogLevel::Error, - format!( - "Could not list directory {}: {}", - dir.abs_path.display(), - err - ), + format!("Could not list directory {}: {}", dir.path.display(), err), ); 0 } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index af5e54b4..75c8c093 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -31,8 +31,8 @@ use super::{ browser::{FileExplorerTab, FoundExplorerTab}, ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg, }; -use crate::fs::FsEntry; // externals +use remotefs::fs::Entry; use tuirealm::{ props::{AttrValue, Attribute}, State, StateValue, Update, @@ -282,7 +282,7 @@ impl FileTransferActivity { // Mount wait self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str()); // Find - let res: Result, String> = match self.browser.tab() { + let res: Result, String> = match self.browser.tab() { FileExplorerTab::Local => self.action_local_find(search.clone()), FileExplorerTab::Remote => self.action_remote_find(search.clone()), _ => panic!("Trying to search for files, while already in a find result"), diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 8a03f34c..96624a82 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -30,11 +30,11 @@ use super::{ browser::{FileExplorerTab, FoundExplorerTab}, components, Context, FileTransferActivity, Id, }; -use crate::fs::explorer::FileSorting; -use crate::fs::FsEntry; +use crate::explorer::FileSorting; use crate::ui::store::Store; use crate::utils::ui::draw_area_in; // Ext +use remotefs::fs::Entry; use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::tui::widgets::Clear; @@ -747,7 +747,7 @@ impl FileTransferActivity { let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway } - pub(super) fn mount_file_info(&mut self, file: &FsEntry) { + pub(super) fn mount_file_info(&mut self, file: &Entry) { assert!(self .app .remount( diff --git a/src/ui/activities/setup/components/config.rs b/src/ui/activities/setup/components/config.rs index 09bb4b70..cc7ffa9b 100644 --- a/src/ui/activities/setup/components/config.rs +++ b/src/ui/activities/setup/components/config.rs @@ -26,8 +26,8 @@ * SOFTWARE. */ use super::{ConfigMsg, Msg}; +use crate::explorer::GroupDirs as GroupDirsEnum; use crate::filetransfer::FileTransferProtocol; -use crate::fs::explorer::GroupDirs as GroupDirsEnum; use crate::utils::parser::parse_bytesize; use tui_realm_stdlib::{Input, Radio}; diff --git a/src/ui/activities/setup/components/mod.rs b/src/ui/activities/setup/components/mod.rs index b1e3794d..e9b58a51 100644 --- a/src/ui/activities/setup/components/mod.rs +++ b/src/ui/activities/setup/components/mod.rs @@ -46,19 +46,11 @@ use tuirealm::{Component, MockComponent}; // -- global listener -#[derive(MockComponent)] +#[derive(Default, MockComponent)] pub struct GlobalListener { component: Phantom, } -impl Default for GlobalListener { - fn default() -> Self { - Self { - component: Phantom::default(), - } - } -} - impl Component for GlobalListener { fn on(&mut self, ev: Event) -> Option { match ev { diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index b80ea25e..0ecee34e 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -28,8 +28,8 @@ */ // Locals use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout}; +use crate::explorer::GroupDirs; use crate::filetransfer::FileTransferProtocol; -use crate::fs::explorer::GroupDirs; use crate::utils::fmt::fmt_bytes; // Ext diff --git a/src/utils/fmt.rs b/src/utils/fmt.rs index 1fa714f1..637e4a38 100644 --- a/src/utils/fmt.rs +++ b/src/utils/fmt.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use crate::fs::UnixPex; +use remotefs::fs::UnixPexClass; use chrono::prelude::*; use std::path::{Path, PathBuf}; @@ -35,18 +35,18 @@ use tuirealm::tui::style::Color; /// ### fmt_pex /// /// Convert permissions bytes of permissions value into ls notation (e.g. rwx,-wx,--x) -pub fn fmt_pex(pex: UnixPex) -> String { +pub fn fmt_pex(pex: UnixPexClass) -> String { format!( "{}{}{}", - match pex.can_read() { + match pex.read() { true => 'r', false => '-', }, - match pex.can_write() { + match pex.write() { true => 'w', false => '-', }, - match pex.can_execute() { + match pex.execute() { true => 'x', false => '-', } @@ -315,9 +315,9 @@ mod tests { #[test] fn test_utils_fmt_pex() { - assert_eq!(fmt_pex(UnixPex::from(7)), String::from("rwx")); - assert_eq!(fmt_pex(UnixPex::from(5)), String::from("r-x")); - assert_eq!(fmt_pex(UnixPex::from(6)), String::from("rw-")); + assert_eq!(fmt_pex(UnixPexClass::from(7)), String::from("rwx")); + assert_eq!(fmt_pex(UnixPexClass::from(5)), String::from("r-x")); + assert_eq!(fmt_pex(UnixPexClass::from(6)), String::from("rw-")); } #[test] diff --git a/src/utils/parser.rs b/src/utils/parser.rs index d640a8e0..3b550227 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -37,12 +37,9 @@ use crate::system::environment; // Ext use bytesize::ByteSize; -use chrono::format::ParseError; -use chrono::prelude::*; use regex::Regex; use std::path::PathBuf; use std::str::FromStr; -use std::time::{Duration, SystemTime}; use tuirealm::tui::style::Color; // Regex @@ -267,54 +264,6 @@ fn parse_s3_remote_opt(s: &str) -> Result { } } -/// ### parse_lstime -/// -/// Convert ls syntax time to System Time -/// ls time has two possible syntax: -/// 1. if year is current: %b %d %H:%M (e.g. Nov 5 13:46) -/// 2. else: %b %d %Y (e.g. Nov 5 2019) -pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result { - let datetime: NaiveDateTime = match NaiveDate::parse_from_str(tm, fmt_year) { - Ok(date) => { - // Case 2. - // Return NaiveDateTime from NaiveDate with time 00:00:00 - date.and_hms(0, 0, 0) - } - Err(_) => { - // Might be case 1. - // We need to add Current Year at the end of the string - let this_year: i32 = Utc::now().year(); - let date_time_str: String = format!("{} {}", tm, this_year); - // Now parse - NaiveDateTime::parse_from_str( - date_time_str.as_ref(), - format!("{} %Y", fmt_hours).as_ref(), - )? - } - }; - // Convert datetime to system time - let sys_time: SystemTime = SystemTime::UNIX_EPOCH; - Ok(sys_time - .checked_add(Duration::from_secs(datetime.timestamp() as u64)) - .unwrap_or(SystemTime::UNIX_EPOCH)) -} - -/// ### parse_datetime -/// -/// Parse date time string representation and transform it into `SystemTime` -#[allow(dead_code)] -pub fn parse_datetime(tm: &str, fmt: &str) -> Result { - match NaiveDateTime::parse_from_str(tm, fmt) { - Ok(dt) => { - let sys_time: SystemTime = SystemTime::UNIX_EPOCH; - Ok(sys_time - .checked_add(Duration::from_secs(dt.timestamp() as u64)) - .unwrap_or(SystemTime::UNIX_EPOCH)) - } - Err(err) => Err(err), - } -} - /// ### parse_semver /// /// Parse semver string @@ -611,7 +560,6 @@ pub fn parse_bytesize>(bytes: S) -> Option { mod tests { use super::*; - use crate::utils::fmt::fmt_time; use pretty_assertions::assert_eq; @@ -800,68 +748,6 @@ mod tests { assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err()); } - #[test] - fn test_utils_parse_lstime() { - // Good cases - assert_eq!( - fmt_time( - parse_lstime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M") - .ok() - .unwrap(), - "%m %d %M" - ) - .as_str(), - "11 05 32" - ); - assert_eq!( - fmt_time( - parse_lstime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M") - .ok() - .unwrap(), - "%m %d %M" - ) - .as_str(), - "12 02 32" - ); - assert_eq!( - parse_lstime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M") - .ok() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!( - parse_lstime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M") - .ok() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1521331200) - ); - // bad cases - assert!(parse_lstime("Oma 31 2018", "%b %d %Y", "%b %d %H:%M").is_err()); - assert!(parse_lstime("Feb 31 2018", "%b %d %Y", "%b %d %H:%M").is_err()); - assert!(parse_lstime("Feb 15 25:32", "%b %d %Y", "%b %d %H:%M").is_err()); - } - - #[test] - fn test_utils_parse_datetime() { - assert_eq!( - parse_datetime("04-08-14 03:09PM", "%d-%m-%y %I:%M%p") - .ok() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - // Not enough argument for datetime - assert!(parse_datetime("04-08-14", "%d-%m-%y").is_err()); - } - #[test] fn test_utils_parse_semver() { assert_eq!( diff --git a/src/utils/test_helpers.rs b/src/utils/test_helpers.rs index 534a768c..52ca22cb 100644 --- a/src/utils/test_helpers.rs +++ b/src/utils/test_helpers.rs @@ -25,39 +25,27 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; +use remotefs::fs::{Directory, Entry, File, Metadata}; // ext -use std::fs::File; -#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))] -use std::fs::OpenOptions; -#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))] -use std::io::Read; +use std::fs::File as StdFile; use std::io::Write; use std::path::{Path, PathBuf}; -use std::time::SystemTime; use tempfile::NamedTempFile; -pub fn create_sample_file_entry() -> (FsFile, NamedTempFile) { +pub fn create_sample_file_entry() -> (File, NamedTempFile) { // Write let tmpfile = create_sample_file(); ( - FsFile { + File { name: tmpfile .path() .file_name() .unwrap() .to_string_lossy() .to_string(), - abs_path: tmpfile.path().to_path_buf(), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, - size: 127, - ftype: None, // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path: tmpfile.path().to_path_buf(), + extension: None, + metadata: Metadata::default(), }, tmpfile, ) @@ -80,7 +68,7 @@ pub fn create_sample_file() -> NamedTempFile { pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> { let mut p: PathBuf = PathBuf::from(dir); p.push(filename); - let mut file: File = File::create(p.as_path())?; + let mut file = StdFile::create(p.as_path())?; writeln!( file, "Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus." @@ -97,88 +85,20 @@ pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> { std::fs::create_dir(p.as_path()) } -#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))] -pub fn write_file(file: &NamedTempFile, writable: &mut Box) { - let mut fhnd = OpenOptions::new() - .create(false) - .read(true) - .write(false) - .open(file.path()) - .ok() - .unwrap(); - // Read file - let mut buffer: [u8; 65536] = [0; 65536]; - assert!(fhnd.read(&mut buffer).is_ok()); - // Write file - assert!(writable.write(&buffer).is_ok()); -} - -#[cfg(feature = "with-containers")] -pub fn write_ssh_key() -> NamedTempFile { - let mut tmpfile: NamedTempFile = NamedTempFile::new().unwrap(); - writeln!( - tmpfile, - r"-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEAxKyYUMRCNPlb4ZV1VMofrzApu2l3wgP4Ot9wBvHsw/+RMpcHIbQK -9iQqAVp8Z+M1fJyPXTKjoJtIzuCLF6Sjo0KI7/tFTh+yPnA5QYNLZOIRZb8skumL4gwHww -5Z942FDPuUDQ30C2mZR9lr3Cd5pA8S1ZSPTAV9QQHkpgoS8cAL8QC6dp3CJjUC8wzvXh3I -oN3bTKxCpM10KMEVuWO3lM4Nvr71auB9gzo1sFJ3bwebCZIRH01FROyA/GXRiaOtJFG/9N -nWWI/iG5AJzArKpLZNHIP+FxV/NoRH0WBXm9Wq5MrBYrD1NQzm+kInpS/2sXk3m1aZWqLm -HF2NKRXSbQAAA8iI+KSniPikpwAAAAdzc2gtcnNhAAABAQDErJhQxEI0+VvhlXVUyh+vMC -m7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VO -H7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAe -SmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndv -B5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkys -FisPU1DOb6QielL/axeTebVplaouYcXY0pFdJtAAAAAwEAAQAAAP8u3PFuTVV5SfGazwIm -MgNaux82iOsAT/HWFWecQAkqqrruUw5f+YajH/riV61NE9aq2qNOkcJrgpTWtqpt980GGd -SHWlgpRWQzfIooEiDk6Pk8RVFZsEykkDlJQSIu2onZjhi5A5ojHgZoGGabDsztSqoyOjPq -6WPvGYRiDAR3leBMyp1WufBCJqAsC4L8CjPJSmnZhc5a0zXkC9Syz74Fa08tdM7bGhtvP1 -GmzuYxkgxHH2IFeoumUSBHRiTZayGuRUDel6jgEiUMxenaDKXe7FpYzMm9tQZA10Mm4LhK -5rP9nd2/KRTFRnfZMnKvtIRC9vtlSLBe14qw+4ZCl60AAACAf1kghlO3+HIWplOmk/lCL0 -w75Zz+RdvueL9UuoyNN1QrUEY420LsixgWSeRPby+Rb/hW+XSAZJQHowQ8acFJhU85So7f -4O4wcDuE4f6hpsW9tTfkCEUdLCQJ7EKLCrod6jIV7hvI6rvXiVucRpeAzdOaq4uzj2cwDd -tOdYVsnmQAAACBAOVxBsvO/Sr3rZUbNtA6KewZh/09HNGoKNaCeiD7vaSn2UJbbPRByF/o -Oo5zv8ee8r3882NnmG808XfSn7pPZAzbbTmOaJt0fmyZhivCghSNzV6njW3o0PdnC0fGZQ -ruVXgkd7RJFbsIiD4dDcF4VCjwWHfTK21EOgJUA5pN6TNvAAAAgQDbcJWRx8Uyhkj2+srb -3n2Rt6CR7kEl9cw17ItFjMn+pO81/5U2aGw0iLlX7E06TAMQC+dyW/WaxQRey8RRdtbJ1e -TNKCN34QCWkyuYRHGhcNc0quEDayPw5QWGXlP4BzjfRUcPxY9cCXLe5wDLYsX33HwOAc59 -RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw== ------END OPENSSH PRIVATE KEY-----" - ) - .unwrap(); - tmpfile -} - -/// ### make_fsentry -/// -/// Create a FsEntry at specified path -pub fn make_fsentry>(path: P, is_dir: bool) -> FsEntry { +/// Create a Entry at specified path +pub fn make_fsentry>(path: P, is_dir: bool) -> Entry { let path: PathBuf = path.as_ref().to_path_buf(); match is_dir { - true => FsEntry::Directory(FsDirectory { + true => Entry::Directory(Directory { name: path.file_name().unwrap().to_string_lossy().to_string(), - abs_path: path, - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path, + metadata: Metadata::default(), }), - false => FsEntry::File(FsFile { + false => Entry::File(File { name: path.file_name().unwrap().to_string_lossy().to_string(), - abs_path: path, - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, - size: 127, - ftype: None, // File type - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only + path, + extension: None, + metadata: Metadata::default(), }), } } @@ -186,8 +106,11 @@ pub fn make_fsentry>(path: P, is_dir: bool) -> FsEntry { /// ### create_file_ioers /// /// Open a file with two handlers, the first is to read, the second is to write -pub fn create_file_ioers(p: &Path) -> (File, File) { - (File::open(p).ok().unwrap(), File::create(p).ok().unwrap()) +pub fn create_file_ioers(p: &Path) -> (StdFile, StdFile) { + ( + StdFile::open(p).ok().unwrap(), + StdFile::create(p).ok().unwrap(), + ) } mod test { @@ -197,31 +120,7 @@ mod test { #[test] fn test_utils_test_helpers_sample_file() { - let (file, _) = create_sample_file_entry(); - assert!(file.symlink.is_none()); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_utils_test_helpers_write_file() { - let (_, temp) = create_sample_file_entry(); - let tempdest = NamedTempFile::new().unwrap(); - let mut dest: Box = Box::new( - OpenOptions::new() - .create(true) - .read(false) - .write(true) - .open(tempdest.path()) - .ok() - .unwrap(), - ); - write_file(&temp, &mut dest); - } - - #[test] - #[cfg(feature = "with-containers")] - fn test_utils_test_helpers_write_ssh_key() { - let _ = write_ssh_key(); + let _ = create_sample_file_entry(); } #[test] diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml deleted file mode 100644 index d68fed68..00000000 --- a/tests/docker-compose.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: "3" -services: - openssh-server: - image: ghcr.io/linuxserver/openssh-server - environment: - - PUID=1000 - - PGID=1000 - - TZ=Europe/London - - SUDO_ACCESS=false - - PUBLIC_KEY=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a - - PASSWORD_ACCESS=true - - USER_PASSWORD=password - - USER_NAME=sftp - ports: - - "10022:2222" - openssh-server-scp: - image: ghcr.io/linuxserver/openssh-server - environment: - - PUID=1000 - - PGID=1000 - - TZ=Europe/London - - SUDO_ACCESS=false - - PASSWORD_ACCESS=true - - PUBLIC_KEY=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a - - USER_PASSWORD=password - - USER_NAME=sftp - ports: - - "10222:2222" - ftp-server: - image: afharo/pure-ftp - ports: - - "10021:21" - - "30000-30009:30000-30009" - environment: - - PUBLICHOST=localhost diff --git a/tests/test.sh b/tests/test.sh deleted file mode 100755 index daecb008..00000000 --- a/tests/test.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env sh - -if [ ! -f docker-compose.yml ]; then - set -e - cd tests/ - set +e -fi - -echo "Prepare volume..." -rm -rf /tmp/termscp-test-ftp -mkdir -p /tmp/termscp-test-ftp -echo "Building docker image..." -docker compose build -set -e -docker compose up -d -set +e - -# Go back to src root -cd .. -# Run tests -echo "Running tests" -cargo test --features with-containers -- --test-threads 1 -TEST_RESULT=$? -# Stop container -cd tests/ -echo "Stopping container..." -docker compose stop - -exit $TEST_RESULT From e101804ed8044530c4227b0876c0c6170b4833e8 Mon Sep 17 00:00:00 2001 From: veeso Date: Thu, 9 Dec 2021 18:14:15 +0100 Subject: [PATCH 17/45] dir size is 0 on Windows --- src/host/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/host/mod.rs b/src/host/mod.rs index c3147150..ee5805e0 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -514,11 +514,7 @@ impl Localhost { atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), - size: if path.is_dir() { - attr.blksize() - } else { - attr.len() - }, + size: if path.is_dir() { 0 } else { attr.len() }, symlink: fs::read_link(path.as_path()).ok(), uid: None, gid: None, From bc57b9a6de6a837bd089b7b2a59422938f8d58b8 Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 11 Dec 2021 10:19:29 +0100 Subject: [PATCH 18/45] Removed docs headers --- src/activity_manager.rs | 14 ---- src/config/bookmarks.rs | 6 -- src/config/params.rs | 6 -- src/config/serialization.rs | 8 --- src/explorer/builder.rs | 16 ----- src/explorer/formatter.rs | 42 ------------ src/explorer/mod.rs | 48 -------------- src/filetransfer/mod.rs | 2 - src/filetransfer/params.rs | 20 ------ src/host/mod.rs | 46 ------------- src/system/auto_update.rs | 12 ---- src/system/bookmarks_client.rs | 34 ---------- src/system/config_client.rs | 66 ------------------- src/system/keys/filestorage.rs | 10 --- src/system/keys/keyringstorage.rs | 8 --- src/system/keys/mod.rs | 8 --- src/system/notifications.rs | 14 ---- src/system/sshkey_storage.rs | 10 --- src/system/theme_provider.rs | 16 ----- src/ui/activities/auth/bookmarks.rs | 22 ------- src/ui/activities/auth/components/form.rs | 4 -- src/ui/activities/auth/misc.rs | 16 ----- src/ui/activities/auth/mod.rs | 22 ------- src/ui/activities/auth/view.rs | 62 ----------------- .../filetransfer/actions/change_dir.rs | 14 ---- .../activities/filetransfer/actions/copy.rs | 6 -- .../activities/filetransfer/actions/edit.rs | 4 -- src/ui/activities/filetransfer/actions/mod.rs | 6 -- .../activities/filetransfer/actions/open.rs | 14 ---- .../activities/filetransfer/actions/rename.rs | 2 - .../activities/filetransfer/actions/save.rs | 8 --- .../activities/filetransfer/actions/submit.rs | 4 -- .../activities/filetransfer/components/log.rs | 14 ---- .../components/transfer/file_list.rs | 28 -------- src/ui/activities/filetransfer/lib/browser.rs | 22 ------- .../activities/filetransfer/lib/transfer.rs | 30 --------- src/ui/activities/filetransfer/misc.rs | 30 --------- src/ui/activities/filetransfer/mod.rs | 28 -------- src/ui/activities/filetransfer/session.rs | 54 --------------- src/ui/activities/filetransfer/view.rs | 26 -------- src/ui/activities/mod.rs | 8 --- src/ui/activities/setup/actions.rs | 24 ------- src/ui/activities/setup/config.rs | 14 ---- src/ui/activities/setup/mod.rs | 18 ----- src/ui/activities/setup/update.rs | 2 - src/ui/activities/setup/view/mod.rs | 26 -------- src/ui/activities/setup/view/setup.rs | 6 -- src/ui/activities/setup/view/ssh_keys.rs | 12 ---- src/ui/activities/setup/view/theme.rs | 4 -- src/ui/context.rs | 8 --- src/ui/store.rs | 40 ----------- src/utils/random.rs | 2 - 52 files changed, 966 deletions(-) diff --git a/src/activity_manager.rs b/src/activity_manager.rs index fa68e898..224fd537 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -60,8 +60,6 @@ pub struct ActivityManager { } impl ActivityManager { - /// ### new - /// /// Initializes a new Activity Manager pub fn new(local_dir: &Path, ticks: Duration) -> Result { // Prepare Context @@ -83,16 +81,12 @@ impl ActivityManager { }) } - /// ### set_filetransfer_params - /// /// Set file transfer params pub fn set_filetransfer_params(&mut self, params: FileTransferParams) { // Put params into the context self.context.as_mut().unwrap().set_ftparams(params); } - /// ### run - /// /// /// Loop for activity manager. You need to provide the activity to start with /// Returns the exitcode @@ -114,8 +108,6 @@ impl ActivityManager { // -- Activity Loops - /// ### run_authentication - /// /// Loop for Authentication activity. /// Returns when activity terminates. /// Returns the next activity to run @@ -168,8 +160,6 @@ impl ActivityManager { result } - /// ### run_filetransfer - /// /// Loop for FileTransfer activity. /// Returns when activity terminates. /// Returns the next activity to run @@ -233,8 +223,6 @@ impl ActivityManager { result } - /// ### run_setup - /// /// `SetupActivity` run loop. /// Returns when activity terminates. /// Returns the next activity to run @@ -268,8 +256,6 @@ impl ActivityManager { // -- misc - /// ### init_config_client - /// /// Initialize configuration client fn init_config_client() -> Result { // Get config dir diff --git a/src/config/bookmarks.rs b/src/config/bookmarks.rs index 05b35b7b..86a01b71 100644 --- a/src/config/bookmarks.rs +++ b/src/config/bookmarks.rs @@ -32,8 +32,6 @@ use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializ use std::collections::HashMap; use std::str::FromStr; -/// ## UserHosts -/// /// UserHosts contains all the hosts saved by the user in the data storage /// It contains both `Bookmark` #[derive(Deserialize, Serialize, Debug, Default)] @@ -42,8 +40,6 @@ pub struct UserHosts { pub recents: HashMap, } -/// ## Bookmark -/// /// Bookmark describes a single bookmark entry in the user hosts storage #[derive(Clone, Deserialize, Serialize, Debug, PartialEq)] pub struct Bookmark { @@ -64,8 +60,6 @@ pub struct Bookmark { pub s3: Option, } -/// ## S3Params -/// /// Connection parameters for Aws s3 protocol #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Default)] pub struct S3Params { diff --git a/src/config/params.rs b/src/config/params.rs index 30a483c4..3c8b5c1a 100644 --- a/src/config/params.rs +++ b/src/config/params.rs @@ -36,8 +36,6 @@ use std::path::PathBuf; pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB #[derive(Deserialize, Serialize, Debug, Default)] -/// ## UserConfig -/// /// UserConfig contains all the configurations for the user, /// supported by termscp pub struct UserConfig { @@ -46,8 +44,6 @@ pub struct UserConfig { } #[derive(Deserialize, Serialize, Debug)] -/// ## UserInterfaceConfig -/// /// UserInterfaceConfig provides all the keys to configure the user interface pub struct UserInterfaceConfig { pub text_editor: PathBuf, @@ -63,8 +59,6 @@ pub struct UserInterfaceConfig { } #[derive(Deserialize, Serialize, Debug, Default)] -/// ## RemoteConfig -/// /// Contains configuratio related to remote hosts pub struct RemoteConfig { pub ssh_keys: HashMap, // Association between host name and path to private key diff --git a/src/config/serialization.rs b/src/config/serialization.rs index 19263c15..2ddb8eda 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -29,8 +29,6 @@ use serde::{de::DeserializeOwned, Serialize}; use std::io::{Read, Write}; use thiserror::Error; -/// ## SerializerError -/// /// Contains the error for serializer/deserializer #[derive(std::fmt::Debug)] pub struct SerializerError { @@ -38,8 +36,6 @@ pub struct SerializerError { msg: Option, } -/// ## SerializerErrorKind -/// /// Describes the kind of error for the serializer/deserializer #[derive(Error, Debug)] pub enum SerializerErrorKind { @@ -54,15 +50,11 @@ pub enum SerializerErrorKind { } impl SerializerError { - /// ### new - /// /// Instantiate a new `SerializerError` pub fn new(kind: SerializerErrorKind) -> SerializerError { SerializerError { kind, msg: None } } - /// ### new_ex - /// /// Instantiates a new `SerializerError` with description message pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError { let mut err: SerializerError = SerializerError::new(kind); diff --git a/src/explorer/builder.rs b/src/explorer/builder.rs index 4bb21855..5fe82801 100644 --- a/src/explorer/builder.rs +++ b/src/explorer/builder.rs @@ -31,16 +31,12 @@ use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs}; // Ext use std::collections::VecDeque; -/// ## FileExplorerBuilder -/// /// Struct used to create a `FileExplorer` pub struct FileExplorerBuilder { explorer: Option, } impl FileExplorerBuilder { - /// ### new - /// /// Build a new `FileExplorerBuilder` pub fn new() -> Self { FileExplorerBuilder { @@ -48,15 +44,11 @@ impl FileExplorerBuilder { } } - /// ### build - /// /// Take FileExplorer out of builder pub fn build(&mut self) -> FileExplorer { self.explorer.take().unwrap() } - /// ### with_hidden_files - /// /// Enable HIDDEN_FILES option pub fn with_hidden_files(&mut self, val: bool) -> &mut FileExplorerBuilder { if let Some(e) = self.explorer.as_mut() { @@ -68,8 +60,6 @@ impl FileExplorerBuilder { self } - /// ### with_file_sorting - /// /// Set sorting method pub fn with_file_sorting(&mut self, sorting: FileSorting) -> &mut FileExplorerBuilder { if let Some(e) = self.explorer.as_mut() { @@ -78,8 +68,6 @@ impl FileExplorerBuilder { self } - /// ### with_dirs_first - /// /// Enable DIRS_FIRST option pub fn with_group_dirs(&mut self, group_dirs: Option) -> &mut FileExplorerBuilder { if let Some(e) = self.explorer.as_mut() { @@ -88,8 +76,6 @@ impl FileExplorerBuilder { self } - /// ### with_stack_size - /// /// Set stack size for FileExplorer pub fn with_stack_size(&mut self, sz: usize) -> &mut FileExplorerBuilder { if let Some(e) = self.explorer.as_mut() { @@ -99,8 +85,6 @@ impl FileExplorerBuilder { self } - /// ### with_formatter - /// /// Set formatter for FileExplorer pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder { if let Some(e) = self.explorer.as_mut() { diff --git a/src/explorer/formatter.rs b/src/explorer/formatter.rs index e63b3337..e2f61d33 100644 --- a/src/explorer/formatter.rs +++ b/src/explorer/formatter.rs @@ -64,8 +64,6 @@ lazy_static! { static ref FMT_ATTR_REGEX: Regex = Regex::new(r"(?:([A-Z]+))(:?([0-9]+))?(:?(.+))?").ok().unwrap(); } -/// ## CallChainBlock -/// /// Call Chain block is a block in a chain of functions which are called in order to format the Entry. /// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found /// a chain of function is made using the Formatters method. @@ -84,8 +82,6 @@ struct CallChainBlock { } impl CallChainBlock { - /// ### new - /// /// Create a new `CallChainBlock` pub fn new( func: FmtCallback, @@ -102,8 +98,6 @@ impl CallChainBlock { } } - /// ### next - /// /// Call next callback in the CallChain pub fn next(&self, fmt: &Formatter, fsentry: &Entry, cur_str: &str) -> String { // Call func @@ -122,8 +116,6 @@ impl CallChainBlock { } } - /// ### push - /// /// Push func to the last element in the Call chain pub fn push( &mut self, @@ -144,8 +136,6 @@ impl CallChainBlock { } } -/// ## Formatter -/// /// Formatter takes care of formatting FsEntries according to the provided keys. /// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups /// at each fmt call. @@ -154,8 +144,6 @@ pub struct Formatter { } impl Default for Formatter { - /// ### default - /// /// Instantiates a Formatter with the default fmt syntax fn default() -> Self { Formatter { @@ -165,8 +153,6 @@ impl Default for Formatter { } impl Formatter { - /// ### new - /// /// Instantiates a new `Formatter` with the provided format string pub fn new(fmt_str: &str) -> Self { Formatter { @@ -174,8 +160,6 @@ impl Formatter { } } - /// ### fmt - /// /// Format fsentry pub fn fmt(&self, fsentry: &Entry) -> String { // Execute callchain blocks @@ -184,8 +168,6 @@ impl Formatter { // Fmt methods - /// ### fmt_atime - /// /// Format last access time fn fmt_atime( &self, @@ -213,8 +195,6 @@ impl Formatter { ) } - /// ### fmt_ctime - /// /// Format creation time fn fmt_ctime( &self, @@ -242,8 +222,6 @@ impl Formatter { ) } - /// ### fmt_group - /// /// Format owner group fn fmt_group( &self, @@ -277,8 +255,6 @@ impl Formatter { ) } - /// ### fmt_mtime - /// /// Format last change time fn fmt_mtime( &self, @@ -306,8 +282,6 @@ impl Formatter { ) } - /// ### fmt_name - /// /// Format file name fn fmt_name( &self, @@ -339,8 +313,6 @@ impl Formatter { format!("{}{}{:0width$}", cur_str, prefix, name, width = file_len) } - /// ### fmt_path - /// /// Format path fn fmt_path( &self, @@ -366,8 +338,6 @@ impl Formatter { ) } - /// ### fmt_pex - /// /// Format file permissions fn fmt_pex( &self, @@ -403,8 +373,6 @@ impl Formatter { format!("{}{}{:10}", cur_str, prefix, pex) } - /// ### fmt_size - /// /// Format file size fn fmt_size( &self, @@ -425,8 +393,6 @@ impl Formatter { } } - /// ### fmt_symlink - /// /// Format file symlink (if any) fn fmt_symlink( &self, @@ -454,8 +420,6 @@ impl Formatter { } } - /// ### fmt_user - /// /// Format owner user fn fmt_user( &self, @@ -483,8 +447,6 @@ impl Formatter { format!("{}{}{:12}", cur_str, prefix, username) } - /// ### fmt_fallback - /// /// Fallback function in case the format key is unknown /// It does nothing, just returns cur_str fn fmt_fallback( @@ -501,8 +463,6 @@ impl Formatter { // Static - /// ### make_callchain - /// /// Make a callchain starting from the fmt str fn make_callchain(fmt_str: &str) -> CallChainBlock { // Init chain block @@ -952,8 +912,6 @@ mod tests { assert_eq!(formatter.fmt(&entry).as_str(), "File path: c/bar.txt"); } - /// ### dummy_fmt - /// /// Dummy formatter, just yelds an 'A' at the end of the current string fn dummy_fmt( _fmt: &Formatter, diff --git a/src/explorer/mod.rs b/src/explorer/mod.rs index 9dbc4252..ff183fda 100644 --- a/src/explorer/mod.rs +++ b/src/explorer/mod.rs @@ -47,8 +47,6 @@ bitflags! { } } -/// ## FileSorting -/// /// FileSorting defines the criteria for sorting files #[derive(Copy, Clone, PartialEq, std::fmt::Debug)] pub enum FileSorting { @@ -58,8 +56,6 @@ pub enum FileSorting { Size, } -/// ## GroupDirs -/// /// GroupDirs defines how directories should be grouped in sorting files #[derive(PartialEq, std::fmt::Debug)] pub enum GroupDirs { @@ -67,8 +63,6 @@ pub enum GroupDirs { Last, } -/// ## FileExplorer -/// /// File explorer states pub struct FileExplorer { pub wrkdir: PathBuf, // Current directory @@ -97,8 +91,6 @@ impl Default for FileExplorer { } impl FileExplorer { - /// ### pushd - /// /// push directory to stack pub fn pushd(&mut self, dir: &Path) { // Check if stack would overflow the size @@ -109,15 +101,11 @@ impl FileExplorer { self.dirstack.push_back(PathBuf::from(dir)); } - /// ### popd - /// /// Pop directory from the stack and return the directory pub fn popd(&mut self) -> Option { self.dirstack.pop_back() } - /// ### set_files - /// /// Set Explorer files /// This method will also sort entries based on current options /// Once all sorting have been performed, index is moved to first valid entry. @@ -127,8 +115,6 @@ impl FileExplorer { self.sort(); } - /// ### del_entry - /// /// Delete file at provided index pub fn del_entry(&mut self, idx: usize) { if self.files.len() > idx { @@ -137,16 +123,12 @@ impl FileExplorer { } /* - /// ### count - /// /// Return amount of files pub fn count(&self) -> usize { self.files.len() } */ - /// ### iter_files - /// /// Iterate over files /// Filters are applied based on current options (e.g. hidden files not returned) pub fn iter_files(&self) -> impl Iterator + '_ { @@ -163,15 +145,11 @@ impl FileExplorer { })) } - /// ### iter_files_all - /// /// Iterate all files; doesn't care about options pub fn iter_files_all(&self) -> impl Iterator + '_ { Box::new(self.files.iter()) } - /// ### get - /// /// Get file at relative index pub fn get(&self, idx: usize) -> Option<&Entry> { let opts: ExplorerOpts = self.opts; @@ -193,8 +171,6 @@ impl FileExplorer { // Formatting - /// ### fmt_file - /// /// Format a file entry pub fn fmt_file(&self, entry: &Entry) -> String { self.fmt.fmt(entry) @@ -202,8 +178,6 @@ impl FileExplorer { // Sorting - /// ### sort_by - /// /// Choose sorting method; then sort files pub fn sort_by(&mut self, sorting: FileSorting) { // If method HAS ACTUALLY CHANGED, sort (performance!) @@ -213,15 +187,11 @@ impl FileExplorer { } } - /// ### get_file_sorting - /// /// Get current file sorting method pub fn get_file_sorting(&self) -> FileSorting { self.file_sorting } - /// ### group_dirs_by - /// /// Choose group dirs method; then sort files pub fn group_dirs_by(&mut self, group_dirs: Option) { // If method HAS ACTUALLY CHANGED, sort (performance!) @@ -231,8 +201,6 @@ impl FileExplorer { } } - /// ### sort - /// /// Sort files based on Explorer options. fn sort(&mut self) { // Choose sorting method @@ -252,60 +220,44 @@ impl FileExplorer { } } - /// ### sort_files_by_name - /// /// Sort explorer files by their name. All names are converted to lowercase fn sort_files_by_name(&mut self) { self.files.sort_by_key(|x: &Entry| x.name().to_lowercase()); } - /// ### sort_files_by_mtime - /// /// Sort files by mtime; the newest comes first fn sort_files_by_mtime(&mut self) { self.files .sort_by(|a: &Entry, b: &Entry| b.metadata().mtime.cmp(&a.metadata().mtime)); } - /// ### sort_files_by_creation_time - /// /// Sort files by creation time; the newest comes first fn sort_files_by_creation_time(&mut self) { self.files .sort_by_key(|b: &Entry| Reverse(b.metadata().ctime)); } - /// ### sort_files_by_size - /// /// Sort files by size fn sort_files_by_size(&mut self) { self.files .sort_by_key(|b: &Entry| Reverse(b.metadata().size)); } - /// ### sort_files_directories_first - /// /// Sort files; directories come first fn sort_files_directories_first(&mut self) { self.files.sort_by_key(|x: &Entry| x.is_file()); } - /// ### sort_files_directories_last - /// /// Sort files; directories come last fn sort_files_directories_last(&mut self) { self.files.sort_by_key(|x: &Entry| x.is_dir()); } - /// ### toggle_hidden_files - /// /// Enable/disable hidden files pub fn toggle_hidden_files(&mut self) { self.opts.toggle(ExplorerOpts::SHOW_HIDDEN_FILES); } - /// ### hidden_files_visible - /// /// Returns whether hidden files are visible pub fn hidden_files_visible(&self) -> bool { self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 6e201cbd..e9af48e1 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -32,8 +32,6 @@ pub mod params; pub use builder::Builder; pub use params::{FileTransferParams, ProtocolParams}; -/// ## FileTransferProtocol -/// /// This enum defines the different transfer protocol available in termscp #[derive(PartialEq, Debug, Clone, Copy)] diff --git a/src/filetransfer/params.rs b/src/filetransfer/params.rs index 74066e8b..f60c4358 100644 --- a/src/filetransfer/params.rs +++ b/src/filetransfer/params.rs @@ -39,8 +39,6 @@ pub struct FileTransferParams { pub entry_directory: Option, } -/// ## ProtocolParams -/// /// Container for protocol params #[derive(Debug, Clone)] pub enum ProtocolParams { @@ -48,8 +46,6 @@ pub enum ProtocolParams { AwsS3(AwsS3Params), } -/// ## GenericProtocolParams -/// /// Protocol params used by most common protocols #[derive(Debug, Clone)] pub struct GenericProtocolParams { @@ -59,8 +55,6 @@ pub struct GenericProtocolParams { pub password: Option, } -/// ## AwsS3Params -/// /// Connection parameters for AWS S3 protocol #[derive(Debug, Clone)] pub struct AwsS3Params { @@ -70,8 +64,6 @@ pub struct AwsS3Params { } impl FileTransferParams { - /// ### new - /// /// Instantiates a new `FileTransferParams` pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self { Self { @@ -81,8 +73,6 @@ impl FileTransferParams { } } - /// ### entry_directory - /// /// Set entry directory pub fn entry_directory>(mut self, dir: Option

) -> Self { self.entry_directory = dir.map(|x| x.as_ref().to_path_buf()); @@ -143,32 +133,24 @@ impl Default for GenericProtocolParams { } impl GenericProtocolParams { - /// ### address - /// /// Set address to params pub fn address>(mut self, address: S) -> Self { self.address = address.as_ref().to_string(); self } - /// ### port - /// /// Set port to params pub fn port(mut self, port: u16) -> Self { self.port = port; self } - /// ### username - /// /// Set username for params pub fn username>(mut self, username: Option) -> Self { self.username = username.map(|x| x.as_ref().to_string()); self } - /// ### password - /// /// Set password for params pub fn password>(mut self, password: Option) -> Self { self.password = password.map(|x| x.as_ref().to_string()); @@ -179,8 +161,6 @@ impl GenericProtocolParams { // -- S3 params impl AwsS3Params { - /// ### new - /// /// Instantiates a new `AwsS3Params` struct pub fn new>(bucket: S, region: S, profile: Option) -> Self { Self { diff --git a/src/host/mod.rs b/src/host/mod.rs index ee5805e0..b19a585e 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -43,8 +43,6 @@ use std::os::unix::fs::{MetadataExt, PermissionsExt}; // Locals use crate::utils::path; -/// ## HostErrorType -/// /// HostErrorType provides an overview of the specific host error #[derive(Error, Debug)] pub enum HostErrorType { @@ -77,8 +75,6 @@ pub struct HostError { } impl HostError { - /// ### new - /// /// Instantiates a new HostError pub(crate) fn new(error: HostErrorType, errno: Option, p: &Path) -> Self { HostError { @@ -112,8 +108,6 @@ impl std::fmt::Display for HostError { } } -/// ## Localhost -/// /// Localhost is the entity which holds the information about the current directory and host. /// It provides functions to navigate across the local host file system pub struct Localhost { @@ -122,8 +116,6 @@ pub struct Localhost { } impl Localhost { - /// ### new - /// /// Instantiates a new Localhost struct pub fn new(wrkdir: PathBuf) -> Result { debug!("Initializing localhost at {}", wrkdir.display()); @@ -158,23 +150,17 @@ impl Localhost { Ok(host) } - /// ### pwd - /// /// Print working directory pub fn pwd(&self) -> PathBuf { self.wrkdir.clone() } - /// ### list_dir - /// /// List files in current directory #[allow(dead_code)] pub fn list_dir(&self) -> Vec { self.files.clone() } - /// ### change_wrkdir - /// /// Change working directory with the new provided directory pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result { let new_dir: PathBuf = self.to_path(new_dir); @@ -215,15 +201,11 @@ impl Localhost { Ok(self.wrkdir.clone()) } - /// ### mkdir - /// /// Make a directory at path and update the file list (only if relative) pub fn mkdir(&mut self, dir_name: &Path) -> Result<(), HostError> { self.mkdir_ex(dir_name, false) } - /// ### mkdir_ex - /// /// Extended option version of makedir. /// ignex: don't report error if directory already exists pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> { @@ -262,8 +244,6 @@ impl Localhost { } } - /// ### remove - /// /// Remove file entry pub fn remove(&mut self, entry: &Entry) -> Result<(), HostError> { match entry { @@ -328,8 +308,6 @@ impl Localhost { } } - /// ### rename - /// /// Rename file or directory to new name pub fn rename(&mut self, entry: &Entry, dst_path: &Path) -> Result<(), HostError> { match std::fs::rename(entry.path(), dst_path) { @@ -359,8 +337,6 @@ impl Localhost { } } - /// ### copy - /// /// Copy file to destination path pub fn copy(&mut self, entry: &Entry, dst: &Path) -> Result<(), HostError> { // Get absolute path of dest @@ -436,8 +412,6 @@ impl Localhost { Ok(()) } - /// ### stat - /// /// Stat file and create a Entry #[cfg(target_family = "unix")] pub fn stat(&self, path: &Path) -> Result { @@ -491,8 +465,6 @@ impl Localhost { }) } - /// ### stat - /// /// Stat file and create a Entry #[cfg(target_os = "windows")] pub fn stat(&self, path: &Path) -> Result { @@ -542,8 +514,6 @@ impl Localhost { }) } - /// ### exec - /// /// Execute a command on localhost pub fn exec(&self, cmd: &str) -> Result { // Make command @@ -570,8 +540,6 @@ impl Localhost { } } - /// ### chmod - /// /// Change file mode to file, according to UNIX permissions #[cfg(target_family = "unix")] pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> { @@ -611,8 +579,6 @@ impl Localhost { } } - /// ### open_file_read - /// /// Open file for read pub fn open_file_read(&self, file: &Path) -> Result { let file: PathBuf = self.to_path(file); @@ -643,8 +609,6 @@ impl Localhost { } } - /// ### open_file_write - /// /// Open file for write pub fn open_file_write(&self, file: &Path) -> Result { let file: PathBuf = self.to_path(file); @@ -674,15 +638,11 @@ impl Localhost { } } - /// ### file_exists - /// /// Returns whether provided file path exists pub fn file_exists(&self, path: &Path) -> bool { path.exists() } - /// ### scan_dir - /// /// Get content of the current directory as a list of fs entry pub fn scan_dir(&self, dir: &Path) -> Result, HostError> { info!("Reading directory {}", dir.display()); @@ -706,8 +666,6 @@ impl Localhost { } } - /// ### find - /// /// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course. /// The `search` argument supports wilcards ('*', '?') pub fn find(&self, search: &str) -> Result, HostError> { @@ -716,8 +674,6 @@ impl Localhost { // -- privates - /// ### iter_search - /// /// Recursive call for `find` method. /// Search in current directory for files which match `filter`. /// If a directory is found in current directory, `iter_search` will be called using that dir as argument. @@ -755,8 +711,6 @@ impl Localhost { } } - /// ### to_path - /// /// Convert path to absolute path fn to_path(&self, p: &Path) -> PathBuf { path::absolutize(self.wrkdir.as_path(), p) diff --git a/src/system/auto_update.rs b/src/system/auto_update.rs index 57dec380..ea6da0df 100644 --- a/src/system/auto_update.rs +++ b/src/system/auto_update.rs @@ -44,8 +44,6 @@ pub enum UpdateStatus { UpdateInstalled(String), } -/// ## Release -/// /// Info related to a github release #[derive(Debug)] pub struct Release { @@ -53,8 +51,6 @@ pub struct Release { pub body: String, } -/// ## Update -/// /// The update structure defines the options used to install the update. /// Once you're fine with the options, just call the `upgrade()` method to upgrade termscp. #[derive(Debug, Default)] @@ -64,16 +60,12 @@ pub struct Update { } impl Update { - /// ### show_progress - /// /// Set whether to show or not the progress bar pub fn show_progress(mut self, opt: bool) -> Self { self.progress = opt; self } - /// ### ask_confirm - /// /// Set whether to ask for confirm when updating pub fn ask_confirm(mut self, opt: bool) -> Self { self.ask_confirm = opt; @@ -96,8 +88,6 @@ impl Update { .map(UpdateStatus::from) } - /// ### is_new_version_available - /// /// Returns whether a new version of termscp is available /// In case of success returns Ok(Option), where the Option is Some(new_version); /// otherwise if no version is available, return None @@ -119,8 +109,6 @@ impl Update { .map(Self::check_version) } - /// ### check_version - /// /// In case received version is newer than current one, version as Some is returned; otherwise None fn check_version(r: Release) -> Option { match parse_semver(r.version.as_str()) { diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index bac7ee1f..9b9f0d94 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -44,8 +44,6 @@ use std::path::{Path, PathBuf}; use std::string::ToString; use std::time::SystemTime; -/// ## BookmarksClient -/// /// BookmarksClient provides a layer between the host system and the bookmarks module pub struct BookmarksClient { hosts: UserHosts, @@ -55,8 +53,6 @@ pub struct BookmarksClient { } impl BookmarksClient { - /// ### BookmarksClient - /// /// Instantiates a new BookmarksClient /// Bookmarks file path must be provided /// Storage path for file provider must be provided @@ -155,15 +151,11 @@ impl BookmarksClient { Ok(client) } - /// ### iter_bookmarks - /// /// Iterate over bookmarks keys pub fn iter_bookmarks(&self) -> impl Iterator + '_ { Box::new(self.hosts.bookmarks.keys()) } - /// ### get_bookmark - /// /// Get bookmark associated to key pub fn get_bookmark(&self, key: &str) -> Option { debug!("Getting bookmark {}", key); @@ -183,8 +175,6 @@ impl BookmarksClient { Some(FileTransferParams::from(entry)) } - /// ### add_recent - /// /// Add a new recent to bookmarks pub fn add_bookmark>( &mut self, @@ -207,22 +197,16 @@ impl BookmarksClient { self.hosts.bookmarks.insert(name, host); } - /// ### del_bookmark - /// /// Delete entry from bookmarks pub fn del_bookmark(&mut self, name: &str) { let _ = self.hosts.bookmarks.remove(name); info!("Removed bookmark {}", name); } - /// ### iter_recents - /// /// Iterate over recents keys pub fn iter_recents(&self) -> impl Iterator + '_ { Box::new(self.hosts.recents.keys()) } - /// ### get_recent - /// /// Get recent associated to key pub fn get_recent(&self, key: &str) -> Option { // NOTE: password is not decrypted; recents will never have password @@ -231,8 +215,6 @@ impl BookmarksClient { Some(FileTransferParams::from(entry)) } - /// ### add_recent - /// /// Add a new recent to bookmarks pub fn add_recent(&mut self, params: FileTransferParams) { // Make bookmark @@ -271,16 +253,12 @@ impl BookmarksClient { self.hosts.recents.insert(name, host); } - /// ### del_recent - /// /// Delete entry from recents pub fn del_recent(&mut self, name: &str) { let _ = self.hosts.recents.remove(name); info!("Removed recent host {}", name); } - /// ### write_bookmarks - /// /// Write bookmarks to file pub fn write_bookmarks(&self) -> Result<(), SerializerError> { // Open file @@ -302,8 +280,6 @@ impl BookmarksClient { } } - /// ### read_bookmarks - /// /// Read bookmarks from file fn read_bookmarks(&mut self) -> Result<(), SerializerError> { // Open bookmarks file for read @@ -332,16 +308,12 @@ impl BookmarksClient { } } - /// ### generate_key - /// /// Generate a new AES key fn generate_key() -> String { // Generate 256 bytes (2048 bits) key random_alphanumeric_with_len(256) } - /// ### make_bookmark - /// /// Make bookmark from credentials fn make_bookmark(&self, params: FileTransferParams) -> Bookmark { let mut bookmark: Bookmark = Bookmark::from(params); @@ -352,15 +324,11 @@ impl BookmarksClient { bookmark } - /// ### encrypt_str - /// /// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64 fn encrypt_str(&self, txt: &str) -> String { crypto::aes128_b64_crypt(self.key.as_str(), txt) } - /// ### decrypt_str - /// /// Decrypt provided string using AES-128 fn decrypt_str(&self, secret: &str) -> Result { match crypto::aes128_b64_decrypt(self.key.as_str(), secret) { @@ -741,8 +709,6 @@ mod tests { assert!(client.decrypt_str("bidoof").is_err()); } - /// ### get_paths - /// /// Get paths for configuration and key for bookmarks fn get_paths(dir: &Path) -> (PathBuf, PathBuf) { let k: PathBuf = PathBuf::from(dir); diff --git a/src/system/config_client.rs b/src/system/config_client.rs index 365db280..e87a03e0 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -42,8 +42,6 @@ use std::string::ToString; // Types pub type SshHost = (String, String, PathBuf); // 0: host, 1: username, 2: RSA key path -/// ## ConfigClient -/// /// ConfigClient provides a high level API to communicate with the termscp configuration pub struct ConfigClient { config: UserConfig, // Configuration loaded @@ -53,8 +51,6 @@ pub struct ConfigClient { } impl ConfigClient { - /// ### new - /// /// Instantiate a new `ConfigClient` with provided path pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result { // Initialize a default configuration @@ -104,8 +100,6 @@ impl ConfigClient { Ok(client) } - /// ### degraded - /// /// Instantiate a ConfigClient in degraded mode. /// When in degraded mode, the configuration in use will be the default configuration /// and the IO operation on configuration won't be available @@ -120,15 +114,11 @@ impl ConfigClient { // Text editor - /// ### get_text_editor - /// /// Get text editor from configuration pub fn get_text_editor(&self) -> PathBuf { self.config.user_interface.text_editor.clone() } - /// ### set_text_editor - /// /// Set text editor path pub fn set_text_editor(&mut self, path: PathBuf) { self.config.user_interface.text_editor = path; @@ -136,8 +126,6 @@ impl ConfigClient { // Default protocol - /// ### get_default_protocol - /// /// Get default protocol from configuration pub fn get_default_protocol(&self) -> FileTransferProtocol { match FileTransferProtocol::from_str(self.config.user_interface.default_protocol.as_str()) { @@ -146,43 +134,31 @@ impl ConfigClient { } } - /// ### set_default_protocol - /// /// Set default protocol to configuration pub fn set_default_protocol(&mut self, proto: FileTransferProtocol) { self.config.user_interface.default_protocol = proto.to_string(); } - /// ### get_show_hidden_files - /// /// Get value of `show_hidden_files` pub fn get_show_hidden_files(&self) -> bool { self.config.user_interface.show_hidden_files } - /// ### set_show_hidden_files - /// /// Set new value for `show_hidden_files` pub fn set_show_hidden_files(&mut self, value: bool) { self.config.user_interface.show_hidden_files = value; } - /// ### get_check_for_updates - /// /// Get value of `check_for_updates` pub fn get_check_for_updates(&self) -> bool { self.config.user_interface.check_for_updates.unwrap_or(true) } - /// ### set_check_for_updates - /// /// Set new value for `check_for_updates` pub fn set_check_for_updates(&mut self, value: bool) { self.config.user_interface.check_for_updates = Some(value); } - /// ### get_prompt_on_file_replace - /// /// Get value of `prompt_on_file_replace` pub fn get_prompt_on_file_replace(&self) -> bool { self.config @@ -191,15 +167,11 @@ impl ConfigClient { .unwrap_or(true) } - /// ### set_prompt_on_file_replace - /// /// Set new value for `prompt_on_file_replace` pub fn set_prompt_on_file_replace(&mut self, value: bool) { self.config.user_interface.prompt_on_file_replace = Some(value); } - /// ### get_group_dirs - /// /// Get GroupDirs value from configuration (will be converted from string) pub fn get_group_dirs(&self) -> Option { // Convert string to `GroupDirs` @@ -212,23 +184,17 @@ impl ConfigClient { } } - /// ### set_group_dirs - /// /// Set value for group_dir in configuration. /// Provided value, if `Some` will be converted to `GroupDirs` pub fn set_group_dirs(&mut self, val: Option) { self.config.user_interface.group_dirs = val.map(|val| val.to_string()); } - /// ### get_local_file_fmt - /// /// Get current file fmt for local host pub fn get_local_file_fmt(&self) -> Option { self.config.user_interface.file_fmt.clone() } - /// ### set_local_file_fmt - /// /// Set file fmt parameter for local host pub fn set_local_file_fmt(&mut self, s: String) { self.config.user_interface.file_fmt = match s.is_empty() { @@ -237,15 +203,11 @@ impl ConfigClient { }; } - /// ### get_remote_file_fmt - /// /// Get current file fmt for remote host pub fn get_remote_file_fmt(&self) -> Option { self.config.user_interface.remote_file_fmt.clone() } - /// ### set_remote_file_fmt - /// /// Set file fmt parameter for remote host pub fn set_remote_file_fmt(&mut self, s: String) { self.config.user_interface.remote_file_fmt = match s.is_empty() { @@ -254,22 +216,16 @@ impl ConfigClient { }; } - /// ### get_notifications - /// /// Get value of `notifications` pub fn get_notifications(&self) -> bool { self.config.user_interface.notifications.unwrap_or(true) } - /// ### set_notifications - /// /// Set new value for `notifications` pub fn set_notifications(&mut self, value: bool) { self.config.user_interface.notifications = Some(value); } - /// ### get_notification_threshold - /// /// Get value of `notification_threshold` pub fn get_notification_threshold(&self) -> u64 { self.config @@ -278,8 +234,6 @@ impl ConfigClient { .unwrap_or(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD) } - /// ### set_notification_threshold - /// /// Set new value for `notification_threshold` pub fn set_notification_threshold(&mut self, value: u64) { self.config.user_interface.notification_threshold = Some(value); @@ -287,8 +241,6 @@ impl ConfigClient { // SSH Keys - /// ### save_ssh_key - /// /// Save a SSH key into configuration. /// This operation also creates the key file in `ssh_key_dir` /// and also commits changes to configuration, to prevent incoerent data @@ -331,8 +283,6 @@ impl ConfigClient { self.write_config() } - /// ### del_ssh_key - /// /// Delete a ssh key from configuration, using host as key. /// This operation also unlinks the key file in `ssh_key_dir` /// and also commits changes to configuration, to prevent incoerent data @@ -363,8 +313,6 @@ impl ConfigClient { self.write_config() } - /// ### get_ssh_key - /// /// Get ssh key from host. /// None is returned if key doesn't exist /// `std::io::Error` is returned in case it was not possible to read the key file @@ -384,8 +332,6 @@ impl ConfigClient { } } - /// ### iter_ssh_keys - /// /// Get an iterator through hosts in the ssh key storage pub fn iter_ssh_keys(&self) -> impl Iterator + '_ { Box::new(self.config.remote.ssh_keys.keys()) @@ -393,8 +339,6 @@ impl ConfigClient { // I/O - /// ### write_config - /// /// Write configuration to file pub fn write_config(&self) -> Result<(), SerializerError> { if self.degraded { @@ -421,8 +365,6 @@ impl ConfigClient { } } - /// ### read_config - /// /// Read configuration from file (or reload it if already read) pub fn read_config(&mut self) -> Result<(), SerializerError> { if self.degraded { @@ -456,16 +398,12 @@ impl ConfigClient { } } - /// ### make_ssh_host_key - /// /// Hosts are saved as `username@host` into configuration. /// This method creates the key name, starting from host and username fn make_ssh_host_key(host: &str, username: &str) -> String { format!("{}@{}", username, host) } - /// ### get_ssh_tokens - /// /// Get ssh tokens starting from ssh host key /// Panics if key has invalid syntax /// Returns: (host, username) @@ -475,8 +413,6 @@ impl ConfigClient { (String::from(tokens[1]), String::from(tokens[0])) } - /// ### make_io_err - /// /// Make serializer error from `std::io::Error` fn make_io_err(err: std::io::Error) -> Result<(), SerializerError> { Err(SerializerError::new_ex( @@ -774,8 +710,6 @@ mod tests { assert_eq!(err.to_string(), "IO error (permission denied)"); } - /// ### get_paths - /// /// Get paths for configuration and keys directory fn get_paths(dir: &Path) -> (PathBuf, PathBuf) { let mut k: PathBuf = PathBuf::from(dir); diff --git a/src/system/keys/filestorage.rs b/src/system/keys/filestorage.rs index 074ffa33..9d6379df 100644 --- a/src/system/keys/filestorage.rs +++ b/src/system/keys/filestorage.rs @@ -32,16 +32,12 @@ use std::fs::{OpenOptions, Permissions}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -/// ## FileStorage -/// /// File storage is an implementation o the `KeyStorage` which uses a file to store the key pub struct FileStorage { dir_path: PathBuf, } impl FileStorage { - /// ### new - /// /// Instantiates a new `FileStorage` pub fn new(dir_path: &Path) -> Self { FileStorage { @@ -49,8 +45,6 @@ impl FileStorage { } } - /// ### make_file_path - /// /// Make file path for key file from `dir_path` and the application id fn make_file_path(&self, storage_id: &str) -> PathBuf { let mut p: PathBuf = self.dir_path.clone(); @@ -61,8 +55,6 @@ impl FileStorage { } impl KeyStorage for FileStorage { - /// ### get_key - /// /// Retrieve key from the key storage. /// The key might be acccess through an identifier, which identifies /// the key in the storage @@ -85,8 +77,6 @@ impl KeyStorage for FileStorage { } } - /// ### set_key - /// /// Set the key into the key storage fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> { let key_file: PathBuf = self.make_file_path(storage_id); diff --git a/src/system/keys/keyringstorage.rs b/src/system/keys/keyringstorage.rs index 1e596475..5226d90e 100644 --- a/src/system/keys/keyringstorage.rs +++ b/src/system/keys/keyringstorage.rs @@ -30,16 +30,12 @@ use super::{KeyStorage, KeyStorageError}; // Ext use keyring::{Keyring, KeyringError}; -/// ## KeyringStorage -/// /// provides a `KeyStorage` implementation using the keyring crate pub struct KeyringStorage { username: String, } impl KeyringStorage { - /// ### new - /// /// Instantiates a new KeyringStorage pub fn new(username: &str) -> Self { KeyringStorage { @@ -49,8 +45,6 @@ impl KeyringStorage { } impl KeyStorage for KeyringStorage { - /// ### get_key - /// /// Retrieve key from the key storage. /// The key might be acccess through an identifier, which identifies /// the key in the storage @@ -72,8 +66,6 @@ impl KeyStorage for KeyringStorage { } } - /// ### set_key - /// /// Set the key into the key storage fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> { let storage: Keyring = Keyring::new(storage_id, self.username.as_str()); diff --git a/src/system/keys/mod.rs b/src/system/keys/mod.rs index 66ac480a..7f10e75b 100644 --- a/src/system/keys/mod.rs +++ b/src/system/keys/mod.rs @@ -32,8 +32,6 @@ pub mod keyringstorage; // ext use thiserror::Error; -/// ## KeyStorageError -/// /// defines the error type for the `KeyStorage` #[derive(Debug, Error, PartialEq)] pub enum KeyStorageError { @@ -46,19 +44,13 @@ pub enum KeyStorageError { NoSuchKey, } -/// ## KeyStorage -/// /// this traits provides the methods to communicate and interact with the key storage. pub trait KeyStorage { - /// ### get_key - /// /// Retrieve key from the key storage. /// The key might be acccess through an identifier, which identifies /// the key in the storage fn get_key(&self, storage_id: &str) -> Result; - /// ### set_key - /// /// Set the key into the key storage fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError>; diff --git a/src/system/notifications.rs b/src/system/notifications.rs index e375c050..dab5800d 100644 --- a/src/system/notifications.rs +++ b/src/system/notifications.rs @@ -6,14 +6,10 @@ use notify_rust::Hint; use notify_rust::{Notification as OsNotification, Timeout}; -/// ## Notification -/// /// A notification helper which provides all the functions to send the available notifications for termscp pub struct Notification; impl Notification { - /// ### transfer_completed - /// /// Notify a transfer has been completed with success pub fn transfer_completed>(body: S) { Self::notify( @@ -23,15 +19,11 @@ impl Notification { ); } - /// ### transfer_error - /// /// Notify a transfer has failed pub fn transfer_error>(body: S) { Self::notify("Transfer failed ❌", body.as_ref(), Some("transfer.error")); } - /// ### update_available - /// /// Notify a new version of termscp is available for download pub fn update_available>(version: S) { Self::notify( @@ -41,8 +33,6 @@ impl Notification { ); } - /// ### update_installed - /// /// Notify the update has been correctly installed pub fn update_installed>(version: S) { Self::notify( @@ -52,15 +42,11 @@ impl Notification { ); } - /// ### update_failed - /// /// Notify the update installation has failed pub fn update_failed>(err: S) { Self::notify("Update installation failed ❌", err.as_ref(), None); } - /// ### notify - /// /// Notify guest OS with provided Summary, body and optional category /// e.g. Category is supported on FreeBSD/Linux only #[allow(unused_variables)] diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index 7ee65f4b..a67e244e 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -37,8 +37,6 @@ pub struct SshKeyStorage { } impl SshKeyStorage { - /// ### storage_from_config - /// /// Create a `SshKeyStorage` starting from a `ConfigClient` pub fn storage_from_config(cfg_client: &ConfigClient) -> Self { let mut hosts: HashMap = @@ -65,8 +63,6 @@ impl SshKeyStorage { SshKeyStorage { hosts } } - /// ### empty - /// /// Create an empty ssh key storage; used in case `ConfigClient` is not available #[cfg(test)] pub fn empty() -> Self { @@ -75,16 +71,12 @@ impl SshKeyStorage { } } - /// ### make_mapkey - /// /// Make mapkey from host and username fn make_mapkey(host: &str, username: &str) -> String { format!("{}@{}", username, host) } #[cfg(test)] - /// ### add_key - /// /// Add a key to storage /// NOTE: available only for tests pub fn add_key(&mut self, host: &str, username: &str, p: PathBuf) { @@ -149,8 +141,6 @@ mod tests { ); } - /// ### get_paths - /// /// Get paths for configuration and keys directory fn get_paths(dir: &Path) -> (PathBuf, PathBuf) { let mut k: PathBuf = PathBuf::from(dir); diff --git a/src/system/theme_provider.rs b/src/system/theme_provider.rs index 643687bd..bba67650 100644 --- a/src/system/theme_provider.rs +++ b/src/system/theme_provider.rs @@ -35,8 +35,6 @@ use std::fs::OpenOptions; use std::path::{Path, PathBuf}; use std::string::ToString; -/// ## ThemeProvider -/// /// ThemeProvider provides a high level API to communicate with the termscp theme pub struct ThemeProvider { theme: Theme, // Theme loaded @@ -45,8 +43,6 @@ pub struct ThemeProvider { } impl ThemeProvider { - /// ### new - /// /// Instantiates a new `ThemeProvider` pub fn new(theme_path: &Path) -> Result { let default_theme: Theme = Theme::default(); @@ -78,8 +74,6 @@ impl ThemeProvider { Ok(provider) } - /// ### degraded - /// /// Create a new theme provider which won't work with file system. /// This is done in order to prevent a lot of `unwrap_or` on Ui pub fn degraded() -> Self { @@ -92,15 +86,11 @@ impl ThemeProvider { // -- getters - /// ### theme - /// /// Returns theme as reference pub fn theme(&self) -> &Theme { &self.theme } - /// ### theme_mut - /// /// Returns a mutable reference to the theme pub fn theme_mut(&mut self) -> &mut Theme { &mut self.theme @@ -108,8 +98,6 @@ impl ThemeProvider { // -- io - /// ### load - /// /// Load theme from file pub fn load(&mut self) -> Result<(), SerializerError> { if self.degraded { @@ -146,8 +134,6 @@ impl ThemeProvider { } } - /// ### save - /// /// Save theme to file pub fn save(&self) -> Result<(), SerializerError> { if self.degraded { @@ -235,8 +221,6 @@ mod test { assert!(ThemeProvider::new(Path::new("/tmp/oifoif/omar")).is_err()); } - /// ### get_theme_path - /// /// Get paths for theme file fn get_theme_path(dir: &Path) -> PathBuf { let mut p: PathBuf = PathBuf::from(dir); diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index f1634b3c..d3ac9bb7 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -35,8 +35,6 @@ use crate::system::environment; use std::path::PathBuf; impl AuthActivity { - /// ### del_bookmark - /// /// Delete bookmark pub(super) fn del_bookmark(&mut self, idx: usize) { if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() { @@ -52,8 +50,6 @@ impl AuthActivity { } } - /// ### load_bookmark - /// /// Load selected bookmark (at index) to input fields pub(super) fn load_bookmark(&mut self, idx: usize) { if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() { @@ -67,8 +63,6 @@ impl AuthActivity { } } - /// ### save_bookmark - /// /// Save current input fields as a bookmark pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) { let params = match self.collect_host_params() { @@ -89,8 +83,6 @@ impl AuthActivity { self.sort_bookmarks(); } } - /// ### del_recent - /// /// Delete recent pub(super) fn del_recent(&mut self, idx: usize) { if let Some(client) = self.bookmarks_client.as_mut() { @@ -105,8 +97,6 @@ impl AuthActivity { } } - /// ### load_recent - /// /// Load selected recent (at index) to input fields pub(super) fn load_recent(&mut self, idx: usize) { if let Some(client) = self.bookmarks_client.as_ref() { @@ -120,8 +110,6 @@ impl AuthActivity { } } - /// ### save_recent - /// /// Save current input fields as a "recent" pub(super) fn save_recent(&mut self) { let params = match self.collect_host_params() { @@ -138,8 +126,6 @@ impl AuthActivity { } } - /// ### write_bookmarks - /// /// Write bookmarks to file fn write_bookmarks(&mut self) { if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() { @@ -149,8 +135,6 @@ impl AuthActivity { } } - /// ### init_bookmarks_client - /// /// Initialize bookmarks client pub(super) fn init_bookmarks_client(&mut self) { // Get config dir @@ -210,8 +194,6 @@ impl AuthActivity { // -- privates - /// ### sort_bookmarks - /// /// Sort bookmarks in list fn sort_bookmarks(&mut self) { // Conver to lowercase when sorting @@ -219,16 +201,12 @@ impl AuthActivity { .sort_by(|a, b| a.to_lowercase().as_str().cmp(b.to_lowercase().as_str())); } - /// ### sort_recents - /// /// Sort recents in list fn sort_recents(&mut self) { // Reverse order self.recents_list.sort_by(|a, b| b.cmp(a)); } - /// ### load_bookmark_into_gui - /// /// Load bookmark data into the gui components fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) { // Load parameters into components diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs index 29a5c72e..c6950621 100644 --- a/src/ui/activities/auth/components/form.rs +++ b/src/ui/activities/auth/components/form.rs @@ -57,8 +57,6 @@ impl ProtocolRadio { } } - /// ### protocol_opt_to_enum - /// /// Convert radio index for protocol into a `FileTransferProtocol` fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol { match protocol { @@ -70,8 +68,6 @@ impl ProtocolRadio { } } - /// ### protocol_enum_to_opt - /// /// Convert `FileTransferProtocol` enum into radio group index fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize { match protocol { diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index 5e831180..29b2164f 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -31,8 +31,6 @@ use crate::system::auto_update::{Release, Update, UpdateStatus}; use crate::system::notifications::Notification; impl AuthActivity { - /// ### get_default_port_for_protocol - /// /// Get the default port for protocol pub(super) fn get_default_port_for_protocol(protocol: FileTransferProtocol) -> u16 { match protocol { @@ -42,15 +40,11 @@ impl AuthActivity { } } - /// ### is_port_standard - /// /// Returns whether the port is standard or not pub(super) fn is_port_standard(port: u16) -> bool { port < 1024 } - /// ### check_minimum_window_size - /// /// Check minimum window size window pub(super) fn check_minimum_window_size(&mut self, height: u16) { if height < 25 { @@ -61,8 +55,6 @@ impl AuthActivity { } } - /// ### collect_host_params - /// /// Collect host params as `FileTransferParams` pub(super) fn collect_host_params(&self) -> Result { match self.protocol { @@ -71,8 +63,6 @@ impl AuthActivity { } } - /// ### collect_generic_host_params - /// /// Get input values from fields or return an error if fields are invalid to work as generic pub(super) fn collect_generic_host_params( &self, @@ -105,8 +95,6 @@ impl AuthActivity { }) } - /// ### collect_s3_host_params - /// /// Get input values from fields or return an error if fields are invalid to work as aws s3 pub(super) fn collect_s3_host_params(&self) -> Result { let (bucket, region, profile): (String, String, Option) = @@ -126,8 +114,6 @@ impl AuthActivity { // -- update install - /// ### check_for_updates - /// /// If enabled in configuration, check for updates from Github pub(super) fn check_for_updates(&mut self) { debug!("Check for updates..."); @@ -171,8 +157,6 @@ impl AuthActivity { } } - /// ### install_update - /// /// Install latest termscp version via GUI pub(super) fn install_update(&mut self) { // Umount release notes diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 2d9da506..2c8fd280 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -127,8 +127,6 @@ pub enum Msg { None, } -/// ## InputMask -/// /// Auth form input mask #[derive(Eq, PartialEq)] enum InputMask { @@ -160,8 +158,6 @@ pub struct AuthActivity { } impl AuthActivity { - /// ### new - /// /// Instantiates a new AuthActivity pub fn new(ticks: Duration) -> AuthActivity { AuthActivity { @@ -180,36 +176,26 @@ impl AuthActivity { } } - /// ### context - /// /// Returns a reference to context fn context(&self) -> &Context { self.context.as_ref().unwrap() } - /// ### context_mut - /// /// Returns a mutable reference to context fn context_mut(&mut self) -> &mut Context { self.context.as_mut().unwrap() } - /// ### config - /// /// Returns config client reference fn config(&self) -> &ConfigClient { self.context().config() } - /// ### theme - /// /// Returns a reference to theme fn theme(&self) -> &Theme { self.context().theme_provider().theme() } - /// ### input_mask - /// /// Get current input mask to show fn input_mask(&self) -> InputMask { match self.protocol { @@ -222,8 +208,6 @@ impl AuthActivity { } impl Activity for AuthActivity { - /// ### on_create - /// /// `on_create` is the function which must be called to initialize the activity. /// `on_create` must initialize all the data structures used by the activity /// Context is taken from activity manager and will be released only when activity is destroyed @@ -259,8 +243,6 @@ impl Activity for AuthActivity { info!("Activity initialized"); } - /// ### on_draw - /// /// `on_draw` is the function which draws the graphical interface. /// This function must be called at each tick to refresh the interface fn on_draw(&mut self) { @@ -288,8 +270,6 @@ impl Activity for AuthActivity { } } - /// ### will_umount - /// /// `will_umount` is the method which must be able to report to the activity manager, whether /// the activity should be terminated or not. /// If not, the call will return `None`, otherwise return`Some(ExitReason)` @@ -297,8 +277,6 @@ impl Activity for AuthActivity { self.exit_reason.as_ref() } - /// ### on_destroy - /// /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity. /// This function must be called once before terminating the activity. /// This function finally releases the context diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index ec81b0b7..e5a979af 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -37,8 +37,6 @@ use tuirealm::tui::widgets::Clear; use tuirealm::{State, StateValue, Sub, SubClause, SubEventClause}; impl AuthActivity { - /// ### init - /// /// Initialize view, mounting all startup components inside the view pub(super) fn init(&mut self) { let key_color = self.theme().misc_keys; @@ -104,8 +102,6 @@ impl AuthActivity { assert!(self.app.active(&Id::Protocol).is_ok()); } - /// ### view - /// /// Display view on canvas pub(super) fn view(&mut self) { self.redraw = false; @@ -276,8 +272,6 @@ impl AuthActivity { // -- partials - /// ### view_bookmarks - /// /// Make text span from bookmarks pub(super) fn view_bookmarks(&mut self) { let bookmarks: Vec = self @@ -305,8 +299,6 @@ impl AuthActivity { .is_ok()); } - /// ### view_recent_connections - /// /// View recent connections pub(super) fn view_recent_connections(&mut self) { let bookmarks: Vec = self @@ -335,8 +327,6 @@ impl AuthActivity { // -- mount - /// ### mount_error - /// /// Mount error box pub(super) fn mount_error>(&mut self, text: S) { let err_color = self.theme().misc_error_dialog; @@ -351,15 +341,11 @@ impl AuthActivity { assert!(self.app.active(&Id::ErrorPopup).is_ok()); } - /// ### umount_error - /// /// Umount error message pub(super) fn umount_error(&mut self) { let _ = self.app.umount(&Id::ErrorPopup); } - /// ### mount_info - /// /// Mount info box pub(super) fn mount_info>(&mut self, text: S) { let color = self.theme().misc_info_dialog; @@ -374,15 +360,11 @@ impl AuthActivity { assert!(self.app.active(&Id::InfoPopup).is_ok()); } - /// ### umount_info - /// /// Umount info message pub(super) fn umount_info(&mut self) { let _ = self.app.umount(&Id::InfoPopup); } - /// ### mount_error - /// /// Mount wait box pub(super) fn mount_wait(&mut self, text: &str) { let wait_color = self.theme().misc_info_dialog; @@ -397,15 +379,11 @@ impl AuthActivity { assert!(self.app.active(&Id::WaitPopup).is_ok()); } - /// ### umount_wait - /// /// Umount wait message pub(super) fn umount_wait(&mut self) { let _ = self.app.umount(&Id::WaitPopup); } - /// ### mount_size_err - /// /// Mount size error pub(super) fn mount_size_err(&mut self) { // Mount @@ -421,15 +399,11 @@ impl AuthActivity { assert!(self.app.active(&Id::WindowSizeError).is_ok()); } - /// ### umount_size_err - /// /// Umount error size error pub(super) fn umount_size_err(&mut self) { let _ = self.app.umount(&Id::WindowSizeError); } - /// ### mount_quit - /// /// Mount quit popup pub(super) fn mount_quit(&mut self) { // Protocol @@ -445,15 +419,11 @@ impl AuthActivity { assert!(self.app.active(&Id::QuitPopup).is_ok()); } - /// ### umount_quit - /// /// Umount quit popup pub(super) fn umount_quit(&mut self) { let _ = self.app.umount(&Id::QuitPopup); } - /// ### mount_bookmark_del_dialog - /// /// Mount bookmark delete dialog pub(super) fn mount_bookmark_del_dialog(&mut self) { let warn_color = self.theme().misc_warn_dialog; @@ -468,15 +438,11 @@ impl AuthActivity { assert!(self.app.active(&Id::DeleteBookmarkPopup).is_ok()); } - /// ### umount_bookmark_del_dialog - /// /// umount delete bookmark dialog pub(super) fn umount_bookmark_del_dialog(&mut self) { let _ = self.app.umount(&Id::DeleteBookmarkPopup); } - /// ### mount_bookmark_del_dialog - /// /// Mount recent delete dialog pub(super) fn mount_recent_del_dialog(&mut self) { let warn_color = self.theme().misc_warn_dialog; @@ -491,15 +457,11 @@ impl AuthActivity { assert!(self.app.active(&Id::DeleteRecentPopup).is_ok()); } - /// ### umount_recent_del_dialog - /// /// umount delete recent dialog pub(super) fn umount_recent_del_dialog(&mut self) { let _ = self.app.umount(&Id::DeleteRecentPopup); } - /// ### mount_bookmark_save_dialog - /// /// Mount bookmark save dialog pub(super) fn mount_bookmark_save_dialog(&mut self) { let save_color = self.theme().misc_save_dialog; @@ -524,16 +486,12 @@ impl AuthActivity { assert!(self.app.active(&Id::BookmarkName).is_ok()); } - /// ### umount_bookmark_save_dialog - /// /// Umount bookmark save dialog pub(super) fn umount_bookmark_save_dialog(&mut self) { let _ = self.app.umount(&Id::BookmarkName); let _ = self.app.umount(&Id::BookmarkSavePassword); } - /// ### mount_keybindings - /// /// Mount keybindings pub(super) fn mount_keybindings(&mut self) { let key_color = self.theme().misc_keys; @@ -549,15 +507,11 @@ impl AuthActivity { assert!(self.app.active(&Id::Keybindings).is_ok()); } - /// ### umount_help - /// /// Umount help pub(super) fn umount_help(&mut self) { let _ = self.app.umount(&Id::Keybindings); } - /// ### mount_release_notes - /// /// mount release notes text area pub(super) fn mount_release_notes(&mut self) { if let Some(ctx) = self.context.as_ref() { @@ -585,8 +539,6 @@ impl AuthActivity { } } - /// ### umount_release_notes - /// /// Umount release notes text area pub(super) fn umount_release_notes(&mut self) { let _ = self.app.umount(&Id::NewVersionChangelog); @@ -691,8 +643,6 @@ impl AuthActivity { // -- query - /// ### get_generic_params - /// /// Collect input values from view pub(super) fn get_generic_params_input(&self) -> (String, u16, String, String) { let addr: String = self.get_input_addr(); @@ -702,8 +652,6 @@ impl AuthActivity { (addr, port, username, password) } - /// ### get_s3_params_input - /// /// Collect s3 input values from view pub(super) fn get_s3_params_input(&self) -> (String, String, Option) { let bucket: String = self.get_input_s3_bucket(); @@ -764,8 +712,6 @@ impl AuthActivity { } } - /// ### get_new_bookmark - /// /// Get new bookmark params pub(super) fn get_new_bookmark(&self) -> (String, bool) { let name = match self.app.state(&Id::BookmarkName) { @@ -784,8 +730,6 @@ impl AuthActivity { // -- len - /// ### input_mask_size - /// /// Returns the input mask size based on current input mask pub(super) fn input_mask_size(&self) -> u16 { match self.input_mask() { @@ -796,16 +740,12 @@ impl AuthActivity { // -- fmt - /// ### fmt_bookmark - /// /// Format bookmark to display on ui fn fmt_bookmark(name: &str, b: FileTransferParams) -> String { let addr: String = Self::fmt_recent(b); format!("{} ({})", name, addr) } - /// ### fmt_recent - /// /// Format recent connection to display on ui fn fmt_recent(b: FileTransferParams) -> String { let protocol: String = b.protocol.to_string().to_lowercase(); @@ -881,8 +821,6 @@ impl AuthActivity { .is_ok()); } - /// ### no_popup_mounted_clause - /// /// Returns a sub clause which requires that no popup is mounted in order to be satisfied fn no_popup_mounted_clause() -> SubClause { SubClause::And( diff --git a/src/ui/activities/filetransfer/actions/change_dir.rs b/src/ui/activities/filetransfer/actions/change_dir.rs index 024d5397..10ffa4d0 100644 --- a/src/ui/activities/filetransfer/actions/change_dir.rs +++ b/src/ui/activities/filetransfer/actions/change_dir.rs @@ -32,8 +32,6 @@ use remotefs::Directory; 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(crate) fn action_enter_local_dir(&mut self, dir: Directory, block_sync: bool) -> bool { @@ -44,8 +42,6 @@ impl FileTransferActivity { true } - /// ### action_enter_remote_dir - /// /// Enter a directory on local host from entry /// Return true whether the directory changed pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory, block_sync: bool) -> bool { @@ -56,8 +52,6 @@ impl FileTransferActivity { true } - /// ### action_change_local_dir - /// /// Change local directory reading value from input pub(crate) 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()); @@ -68,8 +62,6 @@ impl FileTransferActivity { } } - /// ### action_change_remote_dir - /// /// Change remote directory reading value from input pub(crate) 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()); @@ -80,8 +72,6 @@ impl FileTransferActivity { } } - /// ### action_go_to_previous_local_dir - /// /// Go to previous directory from localhost pub(crate) fn action_go_to_previous_local_dir(&mut self, block_sync: bool) { if let Some(d) = self.local_mut().popd() { @@ -93,8 +83,6 @@ impl FileTransferActivity { } } - /// ### action_go_to_previous_remote_dir - /// /// Go to previous directory from remote host pub(crate) fn action_go_to_previous_remote_dir(&mut self, block_sync: bool) { if let Some(d) = self.remote_mut().popd() { @@ -106,8 +94,6 @@ impl FileTransferActivity { } } - /// ### action_go_to_local_upper_dir - /// /// Go to upper directory on local host pub(crate) fn action_go_to_local_upper_dir(&mut self, block_sync: bool) { // Get pwd diff --git a/src/ui/activities/filetransfer/actions/copy.rs b/src/ui/activities/filetransfer/actions/copy.rs index ccdd26a1..15b7dead 100644 --- a/src/ui/activities/filetransfer/actions/copy.rs +++ b/src/ui/activities/filetransfer/actions/copy.rs @@ -32,8 +32,6 @@ use remotefs::{Entry, RemoteErrorType}; use std::path::{Path, PathBuf}; impl FileTransferActivity { - /// ### action_local_copy - /// /// Copy file on local pub(crate) fn action_local_copy(&mut self, input: String) { match self.get_local_selected_entries() { @@ -59,8 +57,6 @@ impl FileTransferActivity { } } - /// ### action_remote_copy - /// /// Copy file on remote pub(crate) fn action_remote_copy(&mut self, input: String) { match self.get_remote_selected_entries() { @@ -140,8 +136,6 @@ impl FileTransferActivity { } } - /// ### tricky_copy - /// /// Tricky copy will be used whenever copy command is not available on remote host pub(super) fn tricky_copy(&mut self, entry: Entry, dest: &Path) -> Result<(), String> { // NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index 18771b73..355f8ff1 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -84,8 +84,6 @@ impl FileTransferActivity { self.reload_remote_dir(); } - /// ### edit_local_file - /// /// Edit a file on localhost fn edit_local_file(&mut self, path: &Path) -> Result<(), String> { // Read first 2048 bytes or less from file to check if it is textual @@ -147,8 +145,6 @@ impl FileTransferActivity { Ok(()) } - /// ### edit_remote_file - /// /// Edit file on remote host fn edit_remote_file(&mut self, file: File) -> Result<(), String> { // Create temp file diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index e7805e33..5b51a46f 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -75,8 +75,6 @@ impl From> for SelectedEntry { } impl FileTransferActivity { - /// ### get_local_selected_entries - /// /// Get local file entry pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry { match self.get_selected_index(&Id::ExplorerLocal) { @@ -93,8 +91,6 @@ impl FileTransferActivity { } } - /// ### get_remote_selected_entries - /// /// Get remote file entry pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry { match self.get_selected_index(&Id::ExplorerRemote) { @@ -111,8 +107,6 @@ impl FileTransferActivity { } } - /// ### get_remote_selected_entries - /// /// Get remote file entry pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry { match self.get_selected_index(&Id::ExplorerFind) { diff --git a/src/ui/activities/filetransfer/actions/open.rs b/src/ui/activities/filetransfer/actions/open.rs index 2bd6c3d1..4366c4f2 100644 --- a/src/ui/activities/filetransfer/actions/open.rs +++ b/src/ui/activities/filetransfer/actions/open.rs @@ -31,8 +31,6 @@ use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferPayloa use std::path::{Path, PathBuf}; impl FileTransferActivity { - /// ### action_open_local - /// /// Open local file pub(crate) fn action_open_local(&mut self) { let entries: Vec = match self.get_local_selected_entries() { @@ -45,8 +43,6 @@ impl FileTransferActivity { .for_each(|x| self.action_open_local_file(x, None)); } - /// ### action_open_remote - /// /// Open local file pub(crate) fn action_open_remote(&mut self) { let entries: Vec = match self.get_remote_selected_entries() { @@ -59,15 +55,11 @@ impl FileTransferActivity { .for_each(|x| self.action_open_remote_file(x, None)); } - /// ### action_open_local_file - /// /// Perform open lopcal file pub(crate) fn action_open_local_file(&mut self, entry: &Entry, open_with: Option<&str>) { self.open_path_with(entry.path(), open_with); } - /// ### action_open_local - /// /// Open remote file. The file is first downloaded to a temporary directory on localhost pub(crate) fn action_open_remote_file(&mut self, entry: &Entry, open_with: Option<&str>) { // Download file @@ -107,8 +99,6 @@ impl FileTransferActivity { } } - /// ### action_local_open_with - /// /// Open selected file with provided application pub(crate) fn action_local_open_with(&mut self, with: &str) { let entries: Vec = match self.get_local_selected_entries() { @@ -122,8 +112,6 @@ impl FileTransferActivity { .for_each(|x| self.action_open_local_file(x, Some(with))); } - /// ### action_remote_open_with - /// /// Open selected file with provided application pub(crate) fn action_remote_open_with(&mut self, with: &str) { let entries: Vec = match self.get_remote_selected_entries() { @@ -137,8 +125,6 @@ impl FileTransferActivity { .for_each(|x| self.action_open_remote_file(x, Some(with))); } - /// ### open_path_with - /// /// Common function which opens a path with default or specified program. fn open_path_with(&mut self, p: &Path, with: Option<&str>) { // Open file diff --git a/src/ui/activities/filetransfer/actions/rename.rs b/src/ui/activities/filetransfer/actions/rename.rs index 57c004b5..2046a4f2 100644 --- a/src/ui/activities/filetransfer/actions/rename.rs +++ b/src/ui/activities/filetransfer/actions/rename.rs @@ -131,8 +131,6 @@ impl FileTransferActivity { } } - /// ### tricky_move - /// /// Tricky move will be used whenever copy command is not available on remote host. /// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`) fn tricky_move(&mut self, entry: &Entry, dest: &Path) { diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index 6ba0741a..f71ef837 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -49,8 +49,6 @@ impl FileTransferActivity { self.remote_recv_file(TransferOpts::default()); } - /// ### action_finalize_pending_transfer - /// /// Finalize "pending" transfer. /// The pending transfer is created after a transfer which required a user action to be completed first. /// The name of the file to transfer, is contained in the storage at `STORAGE_PENDING_TRANSFER`. @@ -228,8 +226,6 @@ impl FileTransferActivity { } } - /// ### set_pending_transfer - /// /// Set pending transfer into storage pub(crate) fn set_pending_transfer(&mut self, file_name: &str) { self.mount_radio_replace(file_name); @@ -239,8 +235,6 @@ impl FileTransferActivity { .set_string(STORAGE_PENDING_TRANSFER, file_name.to_string()); } - /// ### set_pending_transfer_many - /// /// Set pending transfer for many files into storage and mount radio pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&Entry>, dest_path: &str) { let file_names: Vec<&str> = files.iter().map(|x| x.name()).collect(); @@ -250,8 +244,6 @@ impl FileTransferActivity { .set_string(STORAGE_PENDING_TRANSFER, dest_path.to_string()); } - /// ### file_to_check - /// /// Get file to check for path pub(crate) fn file_to_check(e: &Entry, alt: Option<&String>) -> PathBuf { match alt { diff --git a/src/ui/activities/filetransfer/actions/submit.rs b/src/ui/activities/filetransfer/actions/submit.rs index ae227f7b..692ff987 100644 --- a/src/ui/activities/filetransfer/actions/submit.rs +++ b/src/ui/activities/filetransfer/actions/submit.rs @@ -36,8 +36,6 @@ enum SubmitAction { } impl FileTransferActivity { - /// ### action_submit_local - /// /// Decides which action to perform on submit for local explorer /// Return true whether the directory changed pub(crate) fn action_submit_local(&mut self, entry: Entry) -> bool { @@ -78,8 +76,6 @@ impl FileTransferActivity { } } - /// ### action_submit_remote - /// /// Decides which action to perform on submit for remote explorer /// Return true whether the directory changed pub(crate) fn action_submit_remote(&mut self, entry: Entry) -> bool { diff --git a/src/ui/activities/filetransfer/components/log.rs b/src/ui/activities/filetransfer/components/log.rs index 87fe4d3d..540261f2 100644 --- a/src/ui/activities/filetransfer/components/log.rs +++ b/src/ui/activities/filetransfer/components/log.rs @@ -222,8 +222,6 @@ impl Component for Log { // -- states -/// ## OwnStates -/// /// OwnStates contains states for this component #[derive(Clone, Default)] struct OwnStates { @@ -232,22 +230,16 @@ struct OwnStates { } impl OwnStates { - /// ### set_list_len - /// /// Set list length pub fn set_list_len(&mut self, len: usize) { self.list_len = len; } - /// ### get_list_index - /// /// Return current value for list index pub fn get_list_index(&self) -> usize { self.list_index } - /// ### incr_list_index - /// /// Incremenet list index pub fn incr_list_index(&mut self) { // Check if index is at last element @@ -256,8 +248,6 @@ impl OwnStates { } } - /// ### decr_list_index - /// /// Decrement list index pub fn decr_list_index(&mut self) { // Check if index is bigger than 0 @@ -266,8 +256,6 @@ impl OwnStates { } } - /// ### list_index_at_last - /// /// Set list index at last item pub fn list_index_at_last(&mut self) { self.list_index = match self.list_len { @@ -276,8 +264,6 @@ impl OwnStates { }; } - /// ### reset_list_index - /// /// Reset list index to last element pub fn reset_list_index(&mut self) { self.list_index = 0; // Last element is always 0 diff --git a/src/ui/activities/filetransfer/components/transfer/file_list.rs b/src/ui/activities/filetransfer/components/transfer/file_list.rs index d05ba2ca..f62316a9 100644 --- a/src/ui/activities/filetransfer/components/transfer/file_list.rs +++ b/src/ui/activities/filetransfer/components/transfer/file_list.rs @@ -36,8 +36,6 @@ use tuirealm::{MockComponent, Props, State, StateValue}; pub const FILE_LIST_CMD_SELECT_ALL: &str = "A"; -/// ## OwnStates -/// /// OwnStates contains states for this component #[derive(Clone, Default)] struct OwnStates { @@ -46,23 +44,17 @@ struct OwnStates { } impl OwnStates { - /// ### init_list_states - /// /// Initialize list states pub fn init_list_states(&mut self, len: usize) { self.selected = Vec::with_capacity(len); self.fix_list_index(); } - /// ### list_index - /// /// Return current value for list index pub fn list_index(&self) -> usize { self.list_index } - /// ### incr_list_index - /// /// Incremenet list index. /// If `can_rewind` is `true` the index rewinds when boundary is reached pub fn incr_list_index(&mut self, can_rewind: bool) { @@ -74,8 +66,6 @@ impl OwnStates { } } - /// ### decr_list_index - /// /// Decrement list index /// If `can_rewind` is `true` the index rewinds when boundary is reached pub fn decr_list_index(&mut self, can_rewind: bool) { @@ -98,36 +88,26 @@ impl OwnStates { }; } - /// ### list_len - /// /// Returns the length of the file list, which is actually the capacity of the selection vector pub fn list_len(&self) -> usize { self.selected.capacity() } - /// ### is_selected - /// /// Returns whether the file with index `entry` is selected pub fn is_selected(&self, entry: usize) -> bool { self.selected.contains(&entry) } - /// ### is_selection_empty - /// /// Returns whether the selection is currently empty pub fn is_selection_empty(&self) -> bool { self.selected.is_empty() } - /// ### get_selection - /// /// Returns current file selection pub fn get_selection(&self) -> Vec { self.selected.clone() } - /// ### fix_list_index - /// /// Keep index if possible, otherwise set to lenght - 1 fn fix_list_index(&mut self) { if self.list_index >= self.list_len() && self.list_len() > 0 { @@ -139,8 +119,6 @@ impl OwnStates { // -- select manipulation - /// ### toggle_file - /// /// Select or deselect file with provided entry index pub fn toggle_file(&mut self, entry: usize) { match self.is_selected(entry) { @@ -149,8 +127,6 @@ impl OwnStates { } } - /// ### select_all - /// /// Select all files pub fn select_all(&mut self) { for i in 0..self.list_len() { @@ -158,8 +134,6 @@ impl OwnStates { } } - /// ### select - /// /// Select provided index if not selected yet fn select(&mut self, entry: usize) { if !self.is_selected(entry) { @@ -167,8 +141,6 @@ impl OwnStates { } } - /// ### deselect - /// /// Remove element file with associated index fn deselect(&mut self, entry: usize) { if self.is_selected(entry) { diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index dabb489a..3661d6b8 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -31,8 +31,6 @@ use crate::system::config_client::ConfigClient; use remotefs::Entry; use std::path::Path; -/// ## FileExplorerTab -/// /// File explorer tab #[derive(Clone, Copy, PartialEq, Eq)] pub enum FileExplorerTab { @@ -42,8 +40,6 @@ pub enum FileExplorerTab { FindRemote, // Find result tab } -/// ## FoundExplorerTab -/// /// Describes the explorer tab type #[derive(Copy, Clone, Debug)] pub enum FoundExplorerTab { @@ -51,8 +47,6 @@ pub enum FoundExplorerTab { Remote, } -/// ## Browser -/// /// Browser contains the browser options pub struct Browser { local: FileExplorer, // Local File explorer state @@ -63,8 +57,6 @@ pub struct Browser { } impl Browser { - /// ### new - /// /// Build a new `Browser` struct pub fn new(cli: &ConfigClient) -> Self { Self { @@ -110,8 +102,6 @@ impl Browser { self.found = None; } - /// ### found_tab - /// /// Returns found tab if any pub fn found_tab(&self) -> Option { self.found.as_ref().map(|x| x.0) @@ -121,22 +111,16 @@ impl Browser { self.tab } - /// ### change_tab - /// /// Update tab value pub fn change_tab(&mut self, tab: FileExplorerTab) { self.tab = tab; } - /// ### toggle_sync_browsing - /// /// Invert the current state for the sync browsing pub fn toggle_sync_browsing(&mut self) { self.sync_browsing = !self.sync_browsing; } - /// ### build_local_explorer - /// /// Build a file explorer with local host setup pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer { let mut builder = Self::build_explorer(cli); @@ -144,8 +128,6 @@ impl Browser { builder.build() } - /// ### build_remote_explorer - /// /// Build a file explorer with remote host setup pub fn build_remote_explorer(cli: &ConfigClient) -> FileExplorer { let mut builder = Self::build_explorer(cli); @@ -153,8 +135,6 @@ impl Browser { builder.build() } - /// ### build_explorer - /// /// Build explorer reading configuration from `ConfigClient` fn build_explorer(cli: &ConfigClient) -> FileExplorerBuilder { let mut builder: FileExplorerBuilder = FileExplorerBuilder::new(); @@ -167,8 +147,6 @@ impl Browser { builder } - /// ### build_found_explorer - /// /// Build explorer reading from `ConfigClient`, for found result (has some differences) fn build_found_explorer(wrkdir: &Path) -> FileExplorer { FileExplorerBuilder::new() diff --git a/src/ui/activities/filetransfer/lib/transfer.rs b/src/ui/activities/filetransfer/lib/transfer.rs index 3a2e8394..6b1659f5 100644 --- a/src/ui/activities/filetransfer/lib/transfer.rs +++ b/src/ui/activities/filetransfer/lib/transfer.rs @@ -56,8 +56,6 @@ impl Default for TransferStates { } impl TransferStates { - /// ### new - /// /// Instantiates a new transfer states pub fn new() -> TransferStates { TransferStates { @@ -67,29 +65,21 @@ impl TransferStates { } } - /// ### reset - /// /// Re-intiialize transfer states pub fn reset(&mut self) { self.aborted = false; } - /// ### abort - /// /// Set aborted to true pub fn abort(&mut self) { self.aborted = true; } - /// ### aborted - /// /// Returns whether transfer has been aborted pub fn aborted(&self) -> bool { self.aborted } - /// ### full_size - /// /// Returns the size of the entire transfer pub fn full_size(&self) -> usize { self.full.total @@ -128,8 +118,6 @@ impl fmt::Display for ProgressStates { } impl ProgressStates { - /// ### init - /// /// Initialize a new Progress State pub fn init(&mut self, sz: usize) { self.started = Instant::now(); @@ -137,16 +125,12 @@ impl ProgressStates { self.written = 0; } - /// ### update_progress - /// /// Update progress state pub fn update_progress(&mut self, delta: usize) -> f64 { self.written += delta; self.calc_progress_percentage() } - /// ### calc_progress - /// /// Calculate progress in a range between 0.0 to 1.0 pub fn calc_progress(&self) -> f64 { // Prevent dividing by 0 @@ -160,22 +144,16 @@ impl ProgressStates { } } - /// ### started - /// /// Get started pub fn started(&self) -> Instant { self.started } - /// ### calc_progress_percentage - /// /// Calculate the current transfer progress as percentage fn calc_progress_percentage(&self) -> f64 { self.calc_progress() * 100.0 } - /// ### calc_bytes_per_second - /// /// Generic function to calculate bytes per second using elapsed time since transfer started and the bytes written /// and the total amount of bytes to write pub fn calc_bytes_per_second(&self) -> u64 { @@ -191,8 +169,6 @@ impl ProgressStates { } } - /// ### calc_eta - /// /// Calculate ETA for current transfer as seconds fn calc_eta(&self) -> u64 { let elapsed_secs: u64 = self.started.elapsed().as_secs(); @@ -206,8 +182,6 @@ impl ProgressStates { // -- Options -/// ## TransferOpts -/// /// Defines the transfer options for transfer actions pub struct TransferOpts { /// Save file as @@ -226,16 +200,12 @@ impl Default for TransferOpts { } impl TransferOpts { - /// ### save_as - /// /// Define the name of the file to be saved pub fn save_as>(mut self, n: Option) -> Self { self.save_as = n.map(|x| x.as_ref().to_string()); self } - /// ### check_replace - /// /// Set whether to check if the file being transferred will "replace" an existing one pub fn check_replace(mut self, opt: bool) -> Self { self.check_replace = opt; diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 51a5be8e..eebea5cb 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -43,8 +43,6 @@ use tuirealm::{PollStrategy, Update}; const LOG_CAPACITY: usize = 256; impl FileTransferActivity { - /// ### tick - /// /// Call `Application::tick()` and process messages in `Update` pub(super) fn tick(&mut self) { match self.app.tick(PollStrategy::UpTo(3)) { @@ -65,8 +63,6 @@ impl FileTransferActivity { } } - /// ### log - /// /// Add message to log events pub(super) fn log(&mut self, level: LogLevel, msg: String) { // Log to file @@ -87,8 +83,6 @@ impl FileTransferActivity { self.update_logbox(); } - /// ### 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) { self.mount_error(msg.as_str()); @@ -97,8 +91,6 @@ impl FileTransferActivity { self.update_logbox(); } - /// ### init_config_client - /// /// Initialize configuration client if possible. /// This function doesn't return errors. pub(super) fn init_config_client() -> ConfigClient { @@ -119,29 +111,21 @@ impl FileTransferActivity { } } - /// ### setup_text_editor - /// /// Set text editor to use pub(super) fn setup_text_editor(&self) { env::set_var("EDITOR", self.config().get_text_editor()); } - /// ### local_to_abs_path - /// /// Convert a path to absolute according to local explorer pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf { path::absolutize(self.local().wrkdir.as_path(), path) } - /// ### remote_to_abs_path - /// /// Convert a path to absolute according to remote explorer pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf { path::absolutize(self.remote().wrkdir.as_path(), path) } - /// ### get_remote_hostname - /// /// Get remote hostname pub(super) fn get_remote_hostname(&self) -> String { let ft_params = self.context().ft_params().unwrap(); @@ -151,8 +135,6 @@ impl FileTransferActivity { } } - /// ### get_connection_msg - /// /// Get connection message to show to client pub(super) fn get_connection_msg(params: &ProtocolParams) -> String { match params { @@ -173,8 +155,6 @@ impl FileTransferActivity { } } - /// ### notify_transfer_completed - /// /// Send notification regarding transfer completed /// The notification is sent only when these conditions are satisfied: /// @@ -188,8 +168,6 @@ impl FileTransferActivity { } } - /// ### notify_transfer_error - /// /// Send notification regarding transfer error /// The notification is sent only when these conditions are satisfied: /// @@ -233,8 +211,6 @@ impl FileTransferActivity { } } - /// ### update_local_filelist - /// /// Update local file list pub(super) fn update_local_filelist(&mut self) { // Get width @@ -280,8 +256,6 @@ impl FileTransferActivity { .is_ok()); } - /// ### update_remote_filelist - /// /// Update remote file list pub(super) fn update_remote_filelist(&mut self) { let width: usize = self @@ -323,8 +297,6 @@ impl FileTransferActivity { .is_ok()); } - /// ### update_logbox - /// /// Update log box pub(super) fn update_logbox(&mut self) { let mut table: TableBuilder = TableBuilder::default(); @@ -418,8 +390,6 @@ impl FileTransferActivity { .is_ok()); } - /// ### finalize_find - /// /// Finalize find process pub(super) fn finalize_find(&mut self) { // Set found to none diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index ae8b18a2..7f526cac 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -171,8 +171,6 @@ enum UiMsg { ToggleSyncBrowsing, } -/// ## LogLevel -/// /// Log level type enum LogLevel { Error, @@ -180,8 +178,6 @@ enum LogLevel { Info, } -/// ## LogRecord -/// /// Log record entry struct LogRecord { pub time: DateTime, @@ -190,8 +186,6 @@ struct LogRecord { } impl LogRecord { - /// ### new - /// /// Instantiates a new LogRecord pub fn new(level: LogLevel, msg: String) -> LogRecord { LogRecord { @@ -202,8 +196,6 @@ impl LogRecord { } } -/// ## FileTransferActivity -/// /// FileTransferActivity is the data holder for the file transfer activity pub struct FileTransferActivity { /// Exit reason @@ -228,8 +220,6 @@ pub struct FileTransferActivity { } impl FileTransferActivity { - /// ### new - /// /// Instantiates a new FileTransferActivity pub fn new(host: Localhost, params: &FileTransferParams, ticks: Duration) -> Self { // Get config client @@ -279,8 +269,6 @@ impl FileTransferActivity { self.browser.found_mut() } - /// ### get_cache_tmp_name - /// /// Get file name for a file in cache fn get_cache_tmp_name(&self, name: &str, file_type: Option<&str>) -> Option { self.cache.as_ref().map(|_| { @@ -299,29 +287,21 @@ impl FileTransferActivity { }) } - /// ### context - /// /// Returns a reference to context fn context(&self) -> &Context { self.context.as_ref().unwrap() } - /// ### context_mut - /// /// Returns a mutable reference to context fn context_mut(&mut self) -> &mut Context { self.context.as_mut().unwrap() } - /// ### config - /// /// Returns config client reference fn config(&self) -> &ConfigClient { self.context().config() } - /// ### theme - /// /// Get a reference to `Theme` fn theme(&self) -> &Theme { self.context().theme_provider().theme() @@ -335,8 +315,6 @@ impl FileTransferActivity { */ impl Activity for FileTransferActivity { - /// ### on_create - /// /// `on_create` is the function which must be called to initialize the activity. /// `on_create` must initialize all the data structures used by the activity fn on_create(&mut self, context: Context) { @@ -368,8 +346,6 @@ impl Activity for FileTransferActivity { info!("Created FileTransferActivity"); } - /// ### on_draw - /// /// `on_draw` is the function which draws the graphical interface. /// This function must be called at each tick to refresh the interface fn on_draw(&mut self) { @@ -398,8 +374,6 @@ impl Activity for FileTransferActivity { } } - /// ### will_umount - /// /// `will_umount` is the method which must be able to report to the activity manager, whether /// the activity should be terminated or not. /// If not, the call will return `None`, otherwise return`Some(ExitReason)` @@ -407,8 +381,6 @@ impl Activity for FileTransferActivity { self.exit_reason.as_ref() } - /// ### on_destroy - /// /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity. /// This function must be called once before terminating the activity. fn on_destroy(&mut self) -> Option { diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 41bccec5..5eba2349 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -40,8 +40,6 @@ use std::path::{Path, PathBuf}; use std::time::Instant; use thiserror::Error; -/// ## TransferErrorReason -/// /// Describes the reason that caused an error during a file transfer #[derive(Error, Debug)] enum TransferErrorReason { @@ -59,8 +57,6 @@ enum TransferErrorReason { FileTransferError(RemoteError), } -/// ## TransferPayload -/// /// Represents the entity to send or receive during a transfer. /// - File: describes an individual `File` to send /// - Any: Can be any kind of `Entry`, but just one @@ -73,8 +69,6 @@ pub(super) enum TransferPayload { } impl FileTransferActivity { - /// ### connect - /// /// Connect to remote pub(super) fn connect(&mut self) { let ft_params = self.context().ft_params().unwrap().clone(); @@ -116,8 +110,6 @@ impl FileTransferActivity { } } - /// ### disconnect - /// /// disconnect from remote pub(super) fn disconnect(&mut self) { let msg: String = format!("Disconnecting from {}…", self.get_remote_hostname()); @@ -129,16 +121,12 @@ impl FileTransferActivity { self.exit_reason = Some(super::ExitReason::Disconnect); } - /// ### disconnect_and_quit - /// /// disconnect from remote and then quit pub(super) fn disconnect_and_quit(&mut self) { self.disconnect(); self.exit_reason = Some(super::ExitReason::Quit); } - /// ### reload_remote_dir - /// /// Reload remote directory entries and update browser pub(super) fn reload_remote_dir(&mut self) { // Get current entries @@ -149,8 +137,6 @@ impl FileTransferActivity { } } - /// ### reload_local_dir - /// /// Reload local directory entries and update browser pub(super) fn reload_local_dir(&mut self) { let wrkdir: PathBuf = self.host.pwd(); @@ -158,8 +144,6 @@ impl FileTransferActivity { self.local_mut().wrkdir = wrkdir; } - /// ### local_scan - /// /// Scan current local directory fn local_scan(&mut self, path: &Path) { match self.host.scan_dir(path) { @@ -176,8 +160,6 @@ impl FileTransferActivity { } } - /// ### remote_scan - /// /// Scan current remote directory fn remote_scan(&mut self, path: &Path) { match self.client.list_dir(path) { @@ -194,8 +176,6 @@ impl FileTransferActivity { } } - /// ### filetransfer_send - /// /// Send fs entry to remote. /// If dst_name is Some, entry will be saved with a different name. /// If entry is a directory, this applies to directory only @@ -229,8 +209,6 @@ impl FileTransferActivity { result } - /// ### filetransfer_send_file - /// /// Send one file to remote at specified path. fn filetransfer_send_file( &mut self, @@ -261,8 +239,6 @@ impl FileTransferActivity { result.map_err(|x| x.to_string()) } - /// ### filetransfer_send_any - /// /// Send a `TransferPayload` of type `Any` fn filetransfer_send_any( &mut self, @@ -284,8 +260,6 @@ impl FileTransferActivity { result } - /// ### filetransfer_send_many - /// /// Send many entries to remote fn filetransfer_send_many( &mut self, @@ -448,8 +422,6 @@ impl FileTransferActivity { result } - /// ### filetransfer_send_file - /// /// Send local file and write it to remote path fn filetransfer_send_one( &mut self, @@ -479,8 +451,6 @@ impl FileTransferActivity { } } - /// ### filetransfer_send_one_with_stream - /// /// Send file to remote using stream fn filetransfer_send_one_with_stream( &mut self, @@ -580,8 +550,6 @@ impl FileTransferActivity { Ok(()) } - /// ### filetransfer_send_one_wno_stream - /// /// Send an `File` to remote without using streams. fn filetransfer_send_one_wno_stream( &mut self, @@ -631,8 +599,6 @@ impl FileTransferActivity { Ok(()) } - /// ### filetransfer_recv - /// /// Recv fs entry from remote. /// If dst_name is Some, entry will be saved with a different name. /// If entry is a directory, this applies to directory only @@ -661,8 +627,6 @@ impl FileTransferActivity { result } - /// ### filetransfer_recv_any - /// /// Recv fs entry from remote. /// If dst_name is Some, entry will be saved with a different name. /// If entry is a directory, this applies to directory only @@ -686,8 +650,6 @@ impl FileTransferActivity { result } - /// ### filetransfer_recv_file - /// /// Receive a single file from remote. fn filetransfer_recv_file(&mut self, entry: &File, local_path: &Path) -> Result<(), String> { // Reset states @@ -705,8 +667,6 @@ impl FileTransferActivity { result.map_err(|x| x.to_string()) } - /// ### filetransfer_send_many - /// /// Send many entries to remote fn filetransfer_recv_many( &mut self, @@ -887,8 +847,6 @@ impl FileTransferActivity { result } - /// ### filetransfer_recv_one - /// /// Receive file from remote and write it to local path fn filetransfer_recv_one( &mut self, @@ -914,8 +872,6 @@ impl FileTransferActivity { } } - /// ### filetransfer_recv_one_with_stream - /// /// Receive an `Entry` from remote using stream fn filetransfer_recv_one_with_stream( &mut self, @@ -1023,8 +979,6 @@ impl FileTransferActivity { Ok(()) } - /// ### filetransfer_recv_one_with_stream - /// /// Receive an `Entry` from remote without using stream fn filetransfer_recv_one_wno_stream( &mut self, @@ -1086,8 +1040,6 @@ impl FileTransferActivity { Ok(()) } - /// ### local_changedir - /// /// Change directory for local pub(super) fn local_changedir(&mut self, path: &Path, push: bool) { // Get current directory @@ -1143,8 +1095,6 @@ impl FileTransferActivity { } } - /// ### download_file_as_temp - /// /// Download provided file as a temporary file pub(super) fn download_file_as_temp(&mut self, file: &File) -> Result { let tmpfile: PathBuf = match self.cache.as_ref() { @@ -1176,8 +1126,6 @@ impl FileTransferActivity { // -- transfer sizes - /// ### get_total_transfer_size_local - /// /// Get total size of transfer for localhost fn get_total_transfer_size_local(&mut self, entry: &Entry) -> usize { match entry { @@ -1201,8 +1149,6 @@ impl FileTransferActivity { } } - /// ### get_total_transfer_size_remote - /// /// Get total size of transfer for remote host fn get_total_transfer_size_remote(&mut self, entry: &Entry) -> usize { match entry { diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 96624a82..d301975c 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -43,8 +43,6 @@ use tuirealm::{Sub, SubClause, SubEventClause}; impl FileTransferActivity { // -- init - /// ### init - /// /// Initialize file transfer activity's view pub(super) fn init(&mut self) { // Mount local file explorer @@ -106,8 +104,6 @@ impl FileTransferActivity { // -- view - /// ### view - /// /// View gui pub(super) fn view(&mut self) { self.redraw = false; @@ -303,8 +299,6 @@ impl FileTransferActivity { // -- partials - /// ### mount_info - /// /// Mount info box pub(super) fn mount_info>(&mut self, text: S) { // Mount @@ -320,8 +314,6 @@ impl FileTransferActivity { assert!(self.app.active(&Id::ErrorPopup).is_ok()); } - /// ### mount_error - /// /// Mount error box pub(super) fn mount_error>(&mut self, text: S) { // Mount @@ -337,8 +329,6 @@ impl FileTransferActivity { assert!(self.app.active(&Id::ErrorPopup).is_ok()); } - /// ### umount_error - /// /// Umount error message pub(super) fn umount_error(&mut self) { let _ = self.app.umount(&Id::ErrorPopup); @@ -358,8 +348,6 @@ impl FileTransferActivity { assert!(self.app.active(&Id::FatalPopup).is_ok()); } - /// ### umount_fatal - /// /// Umount fatal error message pub(super) fn umount_fatal(&mut self) { let _ = self.app.umount(&Id::FatalPopup); @@ -387,8 +375,6 @@ impl FileTransferActivity { let _ = self.app.umount(&Id::WaitPopup); } - /// ### mount_quit - /// /// Mount quit popup pub(super) fn mount_quit(&mut self) { // Protocol @@ -404,15 +390,11 @@ impl FileTransferActivity { assert!(self.app.active(&Id::QuitPopup).is_ok()); } - /// ### umount_quit - /// /// Umount quit popup pub(super) fn umount_quit(&mut self) { let _ = self.app.umount(&Id::QuitPopup); } - /// ### mount_disconnect - /// /// Mount disconnect popup pub(super) fn mount_disconnect(&mut self) { // Protocol @@ -428,8 +410,6 @@ impl FileTransferActivity { assert!(self.app.active(&Id::DisconnectPopup).is_ok()); } - /// ### umount_disconnect - /// /// Umount disconnect popup pub(super) fn umount_disconnect(&mut self) { let _ = self.app.umount(&Id::DisconnectPopup); @@ -735,8 +715,6 @@ impl FileTransferActivity { assert!(self.app.active(&Id::ReplacePopup).is_ok()); } - /// ### is_radio_replace_extended - /// /// Returns whether radio replace is in "extended" mode (for many files) pub(super) fn is_radio_replace_extended(&self) -> bool { self.app.mounted(&Id::ReplacingFilesListPopup) @@ -799,8 +777,6 @@ impl FileTransferActivity { .is_ok()); } - /// ### mount_help - /// /// Mount help pub(super) fn mount_help(&mut self) { let key_color = self.theme().misc_keys; @@ -852,8 +828,6 @@ impl FileTransferActivity { .is_ok()); } - /// ### no_popup_mounted_clause - /// /// Returns a sub clause which requires that no popup is mounted in order to be satisfied fn no_popup_mounted_clause() -> SubClause { SubClause::And( diff --git a/src/ui/activities/mod.rs b/src/ui/activities/mod.rs index 488dfaaa..e060413e 100644 --- a/src/ui/activities/mod.rs +++ b/src/ui/activities/mod.rs @@ -45,28 +45,20 @@ pub enum ExitReason { // -- Activity trait pub trait Activity { - /// ### on_create - /// /// `on_create` is the function which must be called to initialize the activity. /// `on_create` must initialize all the data structures used by the activity /// Context is taken from activity manager and will be released only when activity is destroyed fn on_create(&mut self, context: Context); - /// ### on_draw - /// /// `on_draw` is the function which draws the graphical interface. /// This function must be called at each tick to refresh the interface fn on_draw(&mut self); - /// ### will_umount - /// /// `will_umount` is the method which must be able to report to the activity manager, whether /// the activity should be terminated or not. /// If not, the call will return `None`, otherwise return`Some(ExitReason)` fn will_umount(&self) -> Option<&ExitReason>; - /// ### on_destroy - /// /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity. /// This function must be called once before terminating the activity. /// This function finally releases the context diff --git a/src/ui/activities/setup/actions.rs b/src/ui/activities/setup/actions.rs index 89e3696b..ef9dd90d 100644 --- a/src/ui/activities/setup/actions.rs +++ b/src/ui/activities/setup/actions.rs @@ -35,8 +35,6 @@ use tuirealm::tui::style::Color; use tuirealm::{State, StateValue}; impl SetupActivity { - /// ### action_on_esc - /// /// On , if there are changes in the configuration, the quit dialog must be shown, otherwise /// we can exit without any problem pub(super) fn action_on_esc(&mut self) { @@ -47,8 +45,6 @@ impl SetupActivity { } } - /// ### action_save_all - /// /// Save all configurations. If current tab can load values, they will be loaded, otherwise they'll just be saved. /// Once all the configuration has been changed, set config_changed to false pub(super) fn action_save_all(&mut self) -> Result<(), String> { @@ -59,8 +55,6 @@ impl SetupActivity { Ok(()) } - /// ### action_save_config - /// /// Save configuration fn action_save_config(&mut self) -> Result<(), String> { // Collect input values if in setup form @@ -70,8 +64,6 @@ impl SetupActivity { self.save_config() } - /// ### action_save_theme - /// /// Save configuration fn action_save_theme(&mut self) -> Result<(), String> { // Collect input values if in theme form @@ -83,8 +75,6 @@ impl SetupActivity { self.save_theme() } - /// ### action_change_tab - /// /// Change view tab and load input values in order not to lose them pub(super) fn action_change_tab(&mut self, new_tab: ViewLayout) -> Result<(), String> { // load values for current tab first @@ -100,8 +90,6 @@ impl SetupActivity { Ok(()) } - /// ### action_reset_config - /// /// Reset configuration input fields pub(super) fn action_reset_config(&mut self) -> Result<(), String> { match self.reset_config_changes() { @@ -113,8 +101,6 @@ impl SetupActivity { } } - /// ### action_reset_theme - /// /// Reset configuration input fields pub(super) fn action_reset_theme(&mut self) -> Result<(), String> { match self.reset_theme_changes() { @@ -126,8 +112,6 @@ impl SetupActivity { } } - /// ### action_delete_ssh_key - /// /// delete of a ssh key pub(super) fn action_delete_ssh_key(&mut self) { // Get key @@ -160,8 +144,6 @@ impl SetupActivity { } } - /// ### action_new_ssh_key - /// /// Create a new ssh key pub(super) fn action_new_ssh_key(&mut self) { // get parameters @@ -228,8 +210,6 @@ impl SetupActivity { } } - /// ### set_color - /// /// Given a component and a color, save the color into the theme pub(super) fn action_save_color(&mut self, component: IdTheme, color: Color) { let theme: &mut Theme = self.theme_mut(); @@ -319,8 +299,6 @@ impl SetupActivity { } } - /// ### collect_styles - /// /// Collect values from input and put them into the theme. /// If a component has an invalid color, returns Err(component_id) fn collect_styles(&mut self) -> Result<(), Id> { @@ -440,8 +418,6 @@ impl SetupActivity { Ok(()) } - /// ### get_color - /// /// Get color from component fn get_color(&self, component: &Id) -> Result { match self.app.state(component) { diff --git a/src/ui/activities/setup/config.rs b/src/ui/activities/setup/config.rs index 55756799..c4cbf923 100644 --- a/src/ui/activities/setup/config.rs +++ b/src/ui/activities/setup/config.rs @@ -32,8 +32,6 @@ use super::SetupActivity; use std::env; impl SetupActivity { - /// ### save_config - /// /// Save configuration pub(super) fn save_config(&mut self) -> Result<(), String> { match self.config().write_config() { @@ -42,8 +40,6 @@ impl SetupActivity { } } - /// ### reset_config_changes - /// /// Reset configuration changes; pratically read config from file, overwriting any change made /// since last write action pub(super) fn reset_config_changes(&mut self) -> Result<(), String> { @@ -52,8 +48,6 @@ impl SetupActivity { .map_err(|e| format!("Could not reload configuration: {}", e)) } - /// ### save_theme - /// /// Save theme to file pub(super) fn save_theme(&mut self) -> Result<(), String> { self.theme_provider() @@ -61,8 +55,6 @@ impl SetupActivity { .map_err(|e| format!("Could not save theme: {}", e)) } - /// ### reset_theme_changes - /// /// Reset changes committed to theme pub(super) fn reset_theme_changes(&mut self) -> Result<(), String> { self.theme_provider() @@ -70,8 +62,6 @@ impl SetupActivity { .map_err(|e| format!("Could not restore theme: {}", e)) } - /// ### delete_ssh_key - /// /// Delete ssh key from config cli pub(super) fn delete_ssh_key(&mut self, host: &str, username: &str) -> Result<(), String> { match self.config_mut().del_ssh_key(host, username) { @@ -83,8 +73,6 @@ impl SetupActivity { } } - /// ### edit_ssh_key - /// /// Edit selected ssh key pub(super) fn edit_ssh_key(&mut self, idx: usize) -> Result<(), String> { match self.context.as_mut() { @@ -142,8 +130,6 @@ impl SetupActivity { } } - /// ### add_ssh_key - /// /// Add provided ssh key to config client pub(super) fn add_ssh_key( &mut self, diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index ba75b96a..aba8e8a7 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -256,8 +256,6 @@ pub enum ViewLayout { Theme, } -/// ## SetupActivity -/// /// Setup activity states holder pub struct SetupActivity { app: Application, @@ -282,15 +280,11 @@ impl SetupActivity { } } - /// ### context - /// /// Returns a reference to context fn context(&self) -> &Context { self.context.as_ref().unwrap() } - /// ### context_mut - /// /// Returns a mutable reference to context fn context_mut(&mut self) -> &mut Context { self.context.as_mut().unwrap() @@ -316,8 +310,6 @@ impl SetupActivity { self.context_mut().theme_provider_mut() } - /// ### config_changed - /// /// Returns whether config has changed fn config_changed(&self) -> bool { self.context() @@ -326,8 +318,6 @@ impl SetupActivity { .unwrap_or(false) } - /// ### set_config_changed - /// /// Set value for config changed key into the store fn set_config_changed(&mut self, changed: bool) { self.context_mut() @@ -337,8 +327,6 @@ impl SetupActivity { } impl Activity for SetupActivity { - /// ### on_create - /// /// `on_create` is the function which must be called to initialize the activity. /// `on_create` must initialize all the data structures used by the activity /// Context is taken from activity manager and will be released only when activity is destroyed @@ -363,8 +351,6 @@ impl Activity for SetupActivity { } } - /// ### on_draw - /// /// `on_draw` is the function which draws the graphical interface. /// This function must be called at each tick to refresh the interface fn on_draw(&mut self) { @@ -394,8 +380,6 @@ impl Activity for SetupActivity { } } - /// ### will_umount - /// /// `will_umount` is the method which must be able to report to the activity manager, whether /// the activity should be terminated or not. /// If not, the call will return `None`, otherwise return`Some(ExitReason)` @@ -403,8 +387,6 @@ impl Activity for SetupActivity { self.exit_reason.as_ref() } - /// ### on_destroy - /// /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity. /// This function must be called once before terminating the activity. /// This function finally releases the context diff --git a/src/ui/activities/setup/update.rs b/src/ui/activities/setup/update.rs index 5e91aff3..8267f1fc 100644 --- a/src/ui/activities/setup/update.rs +++ b/src/ui/activities/setup/update.rs @@ -36,8 +36,6 @@ use super::{ use tuirealm::Update; impl Update for SetupActivity { - /// ### update - /// /// Update auth activity model based on msg /// The function exits when returns None fn update(&mut self, msg: Option) -> Option { diff --git a/src/ui/activities/setup/view/mod.rs b/src/ui/activities/setup/view/mod.rs index 4da7eaff..1f36e19e 100644 --- a/src/ui/activities/setup/view/mod.rs +++ b/src/ui/activities/setup/view/mod.rs @@ -54,8 +54,6 @@ impl SetupActivity { } } - /// ### view - /// /// View gui pub(super) fn view(&mut self) { self.redraw = false; @@ -68,8 +66,6 @@ impl SetupActivity { // -- mount - /// ### mount_error - /// /// Mount error box pub(super) fn mount_error>(&mut self, text: S) { assert!(self @@ -83,15 +79,11 @@ impl SetupActivity { assert!(self.app.active(&Id::Common(IdCommon::ErrorPopup)).is_ok()); } - /// ### umount_error - /// /// Umount error message pub(super) fn umount_error(&mut self) { let _ = self.app.umount(&Id::Common(IdCommon::ErrorPopup)); } - /// ### mount_quit - /// /// Mount quit popup pub(super) fn mount_quit(&mut self) { assert!(self @@ -105,15 +97,11 @@ impl SetupActivity { assert!(self.app.active(&Id::Common(IdCommon::QuitPopup)).is_ok()); } - /// ### umount_quit - /// /// Umount quit pub(super) fn umount_quit(&mut self) { let _ = self.app.umount(&Id::Common(IdCommon::QuitPopup)); } - /// ### mount_save_popup - /// /// Mount save popup pub(super) fn mount_save_popup(&mut self) { assert!(self @@ -127,15 +115,11 @@ impl SetupActivity { assert!(self.app.active(&Id::Common(IdCommon::SavePopup)).is_ok()); } - /// ### umount_quit - /// /// Umount quit pub(super) fn umount_save_popup(&mut self) { let _ = self.app.umount(&Id::Common(IdCommon::SavePopup)); } - /// ### mount_help - /// /// Mount help pub(super) fn mount_help(&mut self) { assert!(self @@ -149,8 +133,6 @@ impl SetupActivity { assert!(self.app.active(&Id::Common(IdCommon::Keybindings)).is_ok()); } - /// ### umount_help - /// /// Umount help pub(super) fn umount_help(&mut self) { let _ = self.app.umount(&Id::Common(IdCommon::Keybindings)); @@ -180,8 +162,6 @@ impl SetupActivity { } } - /// ### new_app - /// /// Clean app up and remount common components and global listener fn new_app(&mut self, layout: ViewLayout) { self.app.umount_all(); @@ -189,8 +169,6 @@ impl SetupActivity { self.mount_commons(layout); } - /// ### mount_commons - /// /// Mount common components fn mount_commons(&mut self, layout: ViewLayout) { // Radio tab @@ -213,8 +191,6 @@ impl SetupActivity { .is_ok()); } - /// ### mount_global_listener - /// /// Mount global listener fn mount_global_listener(&mut self) { assert!(self @@ -263,8 +239,6 @@ impl SetupActivity { .is_ok()); } - /// ### no_popup_mounted_clause - /// /// Returns a sub clause which requires that no popup is mounted in order to be satisfied fn no_popup_mounted_clause() -> SubClause { SubClause::And( diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 0ecee34e..1f6946a7 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -40,8 +40,6 @@ use tuirealm::{State, StateValue}; impl SetupActivity { // -- view - /// ### init_setup - /// /// Initialize setup view pub(super) fn init_setup(&mut self) { // Init view (and mount commons) @@ -153,8 +151,6 @@ impl SetupActivity { self.context = Some(ctx); } - /// ### load_input_values - /// /// Load values from configuration into input fields pub(crate) fn load_input_values(&mut self) { // Text editor @@ -267,8 +263,6 @@ impl SetupActivity { .is_ok()); } - /// ### collect_input_values - /// /// Collect values from input and put them into the configuration pub(crate) fn collect_input_values(&mut self) { if let Ok(State::One(StateValue::String(editor))) = diff --git a/src/ui/activities/setup/view/ssh_keys.rs b/src/ui/activities/setup/view/ssh_keys.rs index 2a501937..e029ecb0 100644 --- a/src/ui/activities/setup/view/ssh_keys.rs +++ b/src/ui/activities/setup/view/ssh_keys.rs @@ -37,8 +37,6 @@ use tuirealm::tui::widgets::Clear; impl SetupActivity { // -- view - /// ### init_ssh_keys - /// /// Initialize ssh keys view pub(super) fn init_ssh_keys(&mut self) { // Init view (and mount commons) @@ -99,8 +97,6 @@ impl SetupActivity { // -- mount - /// ### mount_del_ssh_key - /// /// Mount delete ssh key component pub(crate) fn mount_del_ssh_key(&mut self) { assert!(self @@ -114,15 +110,11 @@ impl SetupActivity { assert!(self.app.active(&Id::Ssh(IdSsh::DelSshKeyPopup)).is_ok()); } - /// ### umount_del_ssh_key - /// /// Umount delete ssh key pub(crate) fn umount_del_ssh_key(&mut self) { let _ = self.app.umount(&Id::Ssh(IdSsh::DelSshKeyPopup)); } - /// ### mount_new_ssh_key - /// /// Mount new ssh key prompt pub(crate) fn mount_new_ssh_key(&mut self) { assert!(self @@ -144,16 +136,12 @@ impl SetupActivity { assert!(self.app.active(&Id::Ssh(IdSsh::SshHost)).is_ok()); } - /// ### umount_new_ssh_key - /// /// Umount new ssh key prompt pub(crate) fn umount_new_ssh_key(&mut self) { let _ = self.app.umount(&Id::Ssh(IdSsh::SshUsername)); let _ = self.app.umount(&Id::Ssh(IdSsh::SshHost)); } - /// ### reload_ssh_keys - /// /// Reload ssh keys pub(crate) fn reload_ssh_keys(&mut self) { let keys: Vec = self diff --git a/src/ui/activities/setup/view/theme.rs b/src/ui/activities/setup/view/theme.rs index 4fd0361e..c1cf2f2a 100644 --- a/src/ui/activities/setup/view/theme.rs +++ b/src/ui/activities/setup/view/theme.rs @@ -35,8 +35,6 @@ use tuirealm::tui::layout::{Constraint, Direction, Layout}; impl SetupActivity { // -- view - /// ### init_theme - /// /// Initialize thene view pub(super) fn init_theme(&mut self) { // Init view (and mount commons) @@ -298,8 +296,6 @@ impl SetupActivity { .is_ok()); } - /// ### load_styles - /// /// Load values from theme into input fields pub(crate) fn load_styles(&mut self) { let theme: Theme = self.theme().clone(); diff --git a/src/ui/context.rs b/src/ui/context.rs index 776cb38f..52fc02a6 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -33,8 +33,6 @@ use crate::system::theme_provider::ThemeProvider; use tuirealm::terminal::TerminalBridge; -/// ## Context -/// /// Context holds data structures shared by the activities pub struct Context { ft_params: Option, @@ -46,8 +44,6 @@ pub struct Context { } impl Context { - /// ### new - /// /// Instantiates a new Context pub fn new( config_client: ConfigClient, @@ -106,15 +102,11 @@ impl Context { // -- error - /// ### set_error - /// /// Set context error pub fn set_error(&mut self, err: String) { self.error = Some(err); } - /// ### error - /// /// Get error message and remove it from the context pub fn error(&mut self) -> Option { self.error.take() diff --git a/src/ui/store.rs b/src/ui/store.rs index 9d8cb174..26047518 100644 --- a/src/ui/store.rs +++ b/src/ui/store.rs @@ -31,8 +31,6 @@ use std::collections::HashMap; // -- store state -/// ## StoreState -/// /// Store state describes a value in the store #[allow(dead_code)] enum StoreState { @@ -46,8 +44,6 @@ enum StoreState { // -- store -/// ## Store -/// /// Store represent the context store /// The store is a key-value hash map. Each key must be unique /// To each key a `StoreState` is assigned @@ -57,8 +53,6 @@ pub(crate) struct Store { #[allow(dead_code)] impl Store { - /// ### init - /// /// Initialize a new Store pub fn init() -> Self { Store { @@ -68,8 +62,6 @@ impl Store { // -- getters - /// ### get_string - /// /// Get string from store pub fn get_string(&self, key: &str) -> Option<&str> { match self.store.get(key) { @@ -78,8 +70,6 @@ impl Store { } } - /// ### get_signed - /// /// Get signed from store pub fn get_signed(&self, key: &str) -> Option { match self.store.get(key) { @@ -88,8 +78,6 @@ impl Store { } } - /// ### get_unsigned - /// /// Get unsigned from store pub fn get_unsigned(&self, key: &str) -> Option { match self.store.get(key) { @@ -98,8 +86,6 @@ impl Store { } } - /// ### get_float - /// /// get float from store pub fn get_float(&self, key: &str) -> Option { match self.store.get(key) { @@ -108,8 +94,6 @@ impl Store { } } - /// ### get_boolean - /// /// get boolean from store pub fn get_boolean(&self, key: &str) -> Option { match self.store.get(key) { @@ -118,8 +102,6 @@ impl Store { } } - /// ### isset - /// /// Check if a state is set in the store pub fn isset(&self, key: &str) -> bool { self.store.get(key).is_some() @@ -127,44 +109,32 @@ impl Store { // -- setters - /// ### set_string - /// /// Set string into the store pub fn set_string(&mut self, key: &str, val: String) { self.store.insert(key.to_string(), StoreState::Str(val)); } - /// ### set_signed - /// /// Set signed number pub fn set_signed(&mut self, key: &str, val: isize) { self.store.insert(key.to_string(), StoreState::Signed(val)); } - /// ### set_signed - /// /// Set unsigned number pub fn set_unsigned(&mut self, key: &str, val: usize) { self.store .insert(key.to_string(), StoreState::Unsigned(val)); } - /// ### set_float - /// /// Set floating point number pub fn set_float(&mut self, key: &str, val: f64) { self.store.insert(key.to_string(), StoreState::Float(val)); } - /// ### set_boolean - /// /// Set boolean pub fn set_boolean(&mut self, key: &str, val: bool) { self.store.insert(key.to_string(), StoreState::Boolean(val)); } - /// ### set - /// /// Set a key as a flag; has no value pub fn set(&mut self, key: &str) { self.store.insert(key.to_string(), StoreState::Flag); @@ -172,8 +142,6 @@ impl Store { // -- Consumers - /// ### take_string - /// /// Take string from store pub fn take_string(&mut self, key: &str) -> Option { match self.store.remove(key) { @@ -182,8 +150,6 @@ impl Store { } } - /// ### take_signed - /// /// Take signed from store pub fn take_signed(&mut self, key: &str) -> Option { match self.store.remove(key) { @@ -192,8 +158,6 @@ impl Store { } } - /// ### take_unsigned - /// /// Take unsigned from store pub fn take_unsigned(&mut self, key: &str) -> Option { match self.store.remove(key) { @@ -202,8 +166,6 @@ impl Store { } } - /// ### get_float - /// /// Take float from store pub fn take_float(&mut self, key: &str) -> Option { match self.store.remove(key) { @@ -212,8 +174,6 @@ impl Store { } } - /// ### get_boolean - /// /// Take boolean from store pub fn take_boolean(&mut self, key: &str) -> Option { match self.store.remove(key) { diff --git a/src/utils/random.rs b/src/utils/random.rs index 8afa1b03..c82d857d 100644 --- a/src/utils/random.rs +++ b/src/utils/random.rs @@ -28,8 +28,6 @@ // Ext use rand::{distributions::Alphanumeric, thread_rng, Rng}; -/// ## random_alphanumeric_with_len -/// /// Generate a random alphanumeric string with provided length pub fn random_alphanumeric_with_len(len: usize) -> String { let mut rng = thread_rng(); From 5286c00a55675541e27aa06350095234b17a0672 Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 11 Dec 2021 17:40:18 +0100 Subject: [PATCH 19/45] Ux improvements (described in issue #80) --- CHANGELOG.md | 12 +++ docs/de/man.md | 20 ++--- docs/es/man.md | 20 ++--- docs/fr/man.md | 20 ++--- docs/it/man.md | 20 ++--- docs/man.md | 20 ++--- docs/zh-CN/man.md | 20 ++--- .../activities/filetransfer/components/log.rs | 12 +-- .../filetransfer/components/misc.rs | 74 +++++++++++++++ .../activities/filetransfer/components/mod.rs | 6 +- .../filetransfer/components/popups.rs | 90 ++++++++++--------- .../filetransfer/components/transfer/mod.rs | 54 +++++------ src/ui/activities/filetransfer/mod.rs | 5 +- src/ui/activities/filetransfer/update.rs | 4 +- src/ui/activities/filetransfer/view.rs | 45 ++++++++-- src/ui/activities/setup/components/commons.rs | 2 +- 16 files changed, 278 insertions(+), 146 deletions(-) create mode 100755 src/ui/activities/filetransfer/components/misc.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bca34aa..a840fd56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,18 @@ Released on FIXME: - **Ui**: - Transfer abortion is now more responsive - Selected files will now be rendered with **Reversed, underlined and italic** text modifiers instead of being prepended with `*`. + - **Midnight commander keys** + - ``: Show help + - ``: View file + - ``: Open file (with text editor) + - ``: Copy file + - ``: Rename file + - ``: Make directory + - ``: Remove file + - ``: Quit + - Added footer with most used key bindings + - ❗ `` will now switch explorer tab (will do what `` and `` currently do) + - ❗ Use `` to switch between explorer tab and log tab. ❗ Backtab is `` - **Tui-realm migration**: - migrated application to tui-realm 1.x - Improved application performance diff --git a/docs/de/man.md b/docs/de/man.md index 53f047ce..0a1d7535 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -149,8 +149,8 @@ In order to change panel you need to type `` to move the remote explorer p | Key | Command | Reminder | |---------------|-------------------------------------------------------|-------------| | `` | Disconnect from remote; return to authentication page | | -| `` | Switch between log tab and explorer | | | `` | Go to previous directory in stack | | +| `` | Switch explorer tab | | | `` | Move to remote explorer tab | | | `` | Move to local explorer tab | | | `` | Move up in selected list | | @@ -159,28 +159,28 @@ In order to change panel you need to type `` to move the remote explorer p | `` | Move down in selected list by 8 rows | | | `` | Enter directory | | | `` | Upload / download selected file | | +| `` | Switch between log tab and explorer | | | `` | Toggle hidden files | All | | `` | Sort files by | Bubblesort? | -| `` | Copy file/directory | Copy | -| `` | Make directory | Directory | -| `` | Delete file (Same as `DEL`) | Erase | +| `` | Copy file/directory | Copy | +| `` | Make directory | Directory | +| `` | Delete file | Erase | | `` | Search for files (wild match is supported) | Find | | `` | Go to supplied path | Go to | -| `` | Show help | Help | +| `` | Show help | Help | | `` | Show info about selected file or directory | Info | | `` | Reload current directory's content / Clear selection | List | | `` | Select a file | Mark | | `` | Create new file with provided name | New | -| `` | Edit file; see Text editor | Open | -| `` | Quit termscp | Quit | -| `` | Rename file | Rename | +| `` | Edit file; see Text editor | Open | +| `` | Quit termscp | Quit | +| `` | Rename file | Rename | | `` | Save file as... | Save | | `` | Go to parent directory | Upper | -| `` | Open file with default program for filetype | View | +| `` | Open file with default program for filetype | View | | `` | Open file with provided program | With | | `` | Execute a command | eXecute | | `` | Toggle synchronized browsing | sYnc | -| `` | Delete file | | | `` | Select all files | | | `` | Abort file transfer process | | diff --git a/docs/es/man.md b/docs/es/man.md index 91beaaea..cf627bf0 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -149,8 +149,8 @@ Para cambiar de panel, debe escribir `` para mover el panel del explorador | Key | Command | Reminder | |---------------|-------------------------------------------------------|-------------| | `` | Desconecte; volver a la página de autenticación | | -| `` | Cambiar entre la pestaña de registro y el explorador | | | `` | Ir al directorio anterior en la pila | | +| `` | Cambiar pestaña del explorador | | | `` | Mover a la pestaña del explorador remoto | | | `` | Mover a la pestaña del explorador local | | | `` | Subir en la lista seleccionada | | @@ -159,28 +159,28 @@ Para cambiar de panel, debe escribir `` para mover el panel del explorador | `` | Bajar 8 filas en la lista seleccionada | | | `` | Entrar directorio | | | `` | Cargar / descargar el archivo seleccionado | | +| `` | Cambiar entre la pestaña de registro y el explorador | | | `` | Alternar archivos ocultos | All | | `` | Ordenar archivos por | Bubblesort? | -| `` | Copiar archivo / directorio | Copy | -| `` | Hacer directorio | Directory | -| `` | Eliminar archivo (igual que `DEL`) | Erase | +| `` | Copiar archivo / directorio | Copy | +| `` | Hacer directorio | Directory | +| `` | Eliminar archivo | Erase | | `` | Búsqueda de archivos | Find | | `` | Ir a la ruta proporcionada | Go to | -| `` | Mostrar ayuda | Help | +| `` | Mostrar ayuda | Help | | `` | Mostrar información sobre el archivo | Info | | `` | Recargar contenido del directorio / Borrar selección | List | | `` | Seleccione un archivo | Mark | | `` | Crear un nuevo archivo con el nombre proporcionado | New | -| `` | Editar archivo | Open | -| `` | Salir de termscp | Quit | -| `` | Renombrar archivo | Rename | +| `` | Editar archivo | Open | +| `` | Salir de termscp | Quit | +| `` | Renombrar archivo | Rename | | `` | Guardar archivo como... | Save | | `` | Ir al directorio principal | Upper | -| `` | Abrir archivo con el programa predeterminado | View | +| `` | Abrir archivo con el programa predeterminado | View | | `` | Abrir archivo con el programa proporcionado | With | | `` | Ejecutar un comando | eXecute | | `` | Alternar navegación sincronizada | sYnc | -| `` | Eliminar archivo | | | `` | Seleccionar todos los archivos | | | `` | Abortar el proceso de transferencia de archivos | | diff --git a/docs/fr/man.md b/docs/fr/man.md index ee26fa6a..6252d25f 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -147,8 +147,8 @@ Pour changer de panneau, vous devez taper `` pour déplacer le panneau de | Key | Command | Reminder | |---------------|---------------------------------------------------------------------|-------------| | `` | Se Déconnecter de le serveur; retour à la page d'authentification | | -| `` | Basculer entre l'onglet journal et l'explorateur | | | `` | Aller au répertoire précédent dans la pile | | +| `` | Changer d'onglet explorateur | | | `` | Déplacer vers l'onglet explorateur distant | | | `` | Déplacer vers l'onglet explorateur local | | | `` | Remonter dans la liste sélectionnée | | @@ -157,28 +157,28 @@ Pour changer de panneau, vous devez taper `` pour déplacer le panneau de | `` | Descendre dans la liste sélectionnée de 8 lignes | | | `` | Entrer dans le directoire | | | `` | Télécharger le fichier sélectionné | | +| `` | Basculer entre l'onglet journal et l'explorateur | | | `` | Basculer les fichiers cachés | All | | `` | Trier les fichiers par | Bubblesort? | -| `` | Copier le fichier/répertoire | Copy | -| `` | Créer un dossier | Directory | -| `` | Supprimer le fichier (Identique à `DEL`) | Erase | +| `` | Copier le fichier/répertoire | Copy | +| `` | Créer un dossier | Directory | +| `` | Supprimer le fichier (Identique à `DEL`) | Erase | | `` | Rechercher des fichiers | Find | | `` | Aller au chemin fourni | Go to | -| `` | Afficher l'aide | Help | +| `` | Afficher l'aide | Help | | `` | Afficher les informations sur le fichier ou le dossier sélectionné | Info | | `` | Recharger le contenu du répertoire actuel / Effacer la sélection | List | | `` | Sélectionner un fichier | Mark | | `` | Créer un nouveau fichier avec le nom fourni | New | -| `` | Modifier le fichier | Open | -| `` | Quitter termscp | Quit | -| `` | Renommer le fichier | Rename | +| `` | Modifier le fichier | Open | +| `` | Quitter termscp | Quit | +| `` | Renommer le fichier | Rename | | `` | Enregistrer le fichier sous... | Save | | `` | Aller dans le répertoire parent | Upper | -| `` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View | +| `` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View | | `` | Ouvrir le fichier avec le programme spécifié | With | | `` | Exécuter une commande | eXecute | | `` | Basculer la navigation synchronisée | sYnc | -| `` | Supprimer le fichier | | | `` | Sélectionner tous les fichiers | | | `` | Abandonner le processus de transfert de fichiers | | diff --git a/docs/it/man.md b/docs/it/man.md index 90521da6..4789d7bb 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -143,8 +143,8 @@ Per cambiare pannello ti puoi muovere con le frecce, `` per andare sul pan | Key | Command | Reminder | |---------------|-------------------------------------------------------|-------------| | `` | Disconnettiti; chiudi popup | | -| `` | Cambia tra explorer e pannello di log | | | `` | Vai alla directory precedente | | +| `` | Cambia pannello remoto | | | `` | Vai al pannello remoto | | | `` | Vai al pannello locale | | | `` | Muovi il cursore verso l'alto | | @@ -153,28 +153,28 @@ Per cambiare pannello ti puoi muovere con le frecce, `` per andare sul pan | `` | Muovi il cursore verso il basso di 8 | | | `` | Entra nella directory | | | `` | Upload / download file selezionato/i | | +| `` | Cambia tra explorer e pannello di log | | | `` | Mostra/nascondi file nascosti | All | | `` | Ordina file per | Bubblesort? | -| `` | Copia file/directory | Copy | -| `` | Crea directory | Directory | -| `` | Elimina file (Come `DEL`) | Erase | +| `` | Copia file/directory | Copy | +| `` | Crea directory | Directory | +| `` | Elimina file | Erase | | `` | Cerca file (wild match supportato) | Find | | `` | Vai al percorso indicato | Go to | -| `` | Mostra help | Help | +| `` | Mostra help | Help | | `` | Mostra informazioni per il file selezionato | Info | | `` | Ricarica posizione corrente / pulisci selezione file | List | | `` | Seleziona file | Mark | | `` | Crea nuovo file con il nome fornito | New | -| `` | Modifica file; Vedi text editor | Open | -| `` | Termina termscp | Quit | -| `` | Rinomina file | Rename | +| `` | Modifica file; Vedi text editor | Open | +| `` | Termina termscp | Quit | +| `` | Rinomina file | Rename | | `` | Salva file con nome | Save | | `` | Vai alla directory padre | Upper | -| `` | Apri il file con il programma definito dal sistema | View | +| `` | Apri il file con il programma definito dal sistema | View | | `` | Apri il file con il programma specificato | With | | `` | Esegui comando shell | eXecute | | `` | Abilita/disabilita Sync-Browsing | sYnc | -| `` | Rimuovi file | | | `` | Seleziona tutti i file | | | `` | Annulla trasferimento file | | diff --git a/docs/man.md b/docs/man.md index 89003917..c7829372 100644 --- a/docs/man.md +++ b/docs/man.md @@ -147,8 +147,8 @@ In order to change panel you need to type `` to move the remote explorer p | Key | Command | Reminder | |---------------|-------------------------------------------------------|-------------| | `` | Disconnect from remote; return to authentication page | | -| `` | Switch between log tab and explorer | | | `` | Go to previous directory in stack | | +| `` | Switch explorer tab | | | `` | Move to remote explorer tab | | | `` | Move to local explorer tab | | | `` | Move up in selected list | | @@ -157,28 +157,28 @@ In order to change panel you need to type `` to move the remote explorer p | `` | Move down in selected list by 8 rows | | | `` | Enter directory | | | `` | Upload / download selected file | | +| `` | Switch between log tab and explorer | | | `` | Toggle hidden files | All | | `` | Sort files by | Bubblesort? | -| `` | Copy file/directory | Copy | -| `` | Make directory | Directory | -| `` | Delete file (Same as `DEL`) | Erase | +| `` | Copy file/directory | Copy | +| `` | Make directory | Directory | +| `` | Delete file | Erase | | `` | Search for files (wild match is supported) | Find | | `` | Go to supplied path | Go to | -| `` | Show help | Help | +| `` | Show help | Help | | `` | Show info about selected file or directory | Info | | `` | Reload current directory's content / Clear selection | List | | `` | Select a file | Mark | | `` | Create new file with provided name | New | -| `` | Edit file; see Text editor | Open | -| `` | Quit termscp | Quit | -| `` | Rename file | Rename | +| `` | Edit file; see Text editor | Open | +| `` | Quit termscp | Quit | +| `` | Rename file | Rename | | `` | Save file as... | Save | | `` | Go to parent directory | Upper | -| `` | Open file with default program for filetype | View | +| `` | Open file with default program for filetype | View | | `` | Open file with provided program | With | | `` | Execute a command | eXecute | | `` | Toggle synchronized browsing | sYnc | -| `` | Delete file | | | `` | Select all files | | | `` | Abort file transfer process | | diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index 13c274df..d19e64e9 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -145,8 +145,8 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到 | 按键 | 命令 | 助记词 | |---------------|-------------------------------------------------------|-------------| | `` | 断开远程连接;回到登录页 | | -| `` | 在日志面板和管理器面板之间切换 | | | `` | 返回上一次目录 | | +| `` | 切换资源管理器选项卡 | | | `` | 切换到远程管理器面板 | | | `` | 切换到本地管理器面板 | | | `` | 在当前列表中向上移动光标 | | @@ -155,28 +155,28 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到 | `` | 在当前列表中光标下移8行 | | | `` | 进入文件夹 | | | `` | 上传 / 下载选中文件 | | +| `` | 在日志面板和管理器面板之间切换 | | | `` | 是否显示隐藏文件 | All | | `` | 按..排序 | Bubblesort? | -| `` | 复制文件(夹) | Copy | -| `` | 创建文件夹 | Directory | -| `` | 删除文件(同 `DEL`) | Erase | +| `` | 复制文件(夹) | Copy | +| `` | 创建文件夹 | Directory | +| `` | 删除文件 | Erase | | `` | 文件搜索 (支持通配符) | Find | | `` | 跳转到指定路径 | Go to | -| `` | 显示帮助 | Help | +| `` | 显示帮助 | Help | | `` | 显示选中文件(夹)信息 | Info | | `` | 刷新当前目录列表 / 清除选中状态 | List | | `` | 选中文件 | Mark | | `` | 使用键入的名称新建文件 | New | -| `` | 编辑文件;参考文本编辑器文档 | Open | -| `` | 退出termscp | Quit | -| `` | 重命名文件 | Rename | +| `` | 编辑文件;参考文本编辑器文档 | Open | +| `` | 退出termscp | Quit | +| `` | 重命名文件 | Rename | | `` | 另存为... | Save | | `` | 进入上层目录 | Upper | -| `` | 使用默认方式打开文件 | View | +| `` | 使用默认方式打开文件 | View | | `` | 使用指定程序打开文件 | With | | `` | 运行命令 | eXecute | | `` | 是否开启同步浏览 | sYnc | -| `` | 删除文件 | | | `` | 选中所有文件 | | | `` | 终止文件传输 | | diff --git a/src/ui/activities/filetransfer/components/log.rs b/src/ui/activities/filetransfer/components/log.rs index 540261f2..ba1625b9 100644 --- a/src/ui/activities/filetransfer/components/log.rs +++ b/src/ui/activities/filetransfer/components/log.rs @@ -62,10 +62,10 @@ impl MockComponent for Log { .props .get_or(Attribute::Focus, AttrValue::Flag(false)) .unwrap_flag(); - let fg = self + let borders = self .props - .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset)) - .unwrap_color(); + .get_or(Attribute::Borders, AttrValue::Borders(Borders::default())) + .unwrap_borders(); let bg = self .props .get_or(Attribute::Background, AttrValue::Color(Color::Reset)) @@ -81,7 +81,7 @@ impl MockComponent for Log { .collect(); let w = TuiList::new(list_items) .block(tui_realm_stdlib::utils::get_block( - Borders::default().color(fg), + borders, Some(("Log".to_string(), Alignment::Left)), focus, None, @@ -214,7 +214,9 @@ impl Component for Log { Some(Msg::None) } // -- comp msg - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::Ui(UiMsg::LogTabbed)), + Event::Keyboard(KeyEvent { + code: Key::BackTab, .. + }) => Some(Msg::Ui(UiMsg::LogBackTabbed)), _ => None, } } diff --git a/src/ui/activities/filetransfer/components/misc.rs b/src/ui/activities/filetransfer/components/misc.rs new file mode 100755 index 00000000..0b950963 --- /dev/null +++ b/src/ui/activities/filetransfer/components/misc.rs @@ -0,0 +1,74 @@ +//! ## Components +//! +//! file transfer activity components + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::Msg; + +use tui_realm_stdlib::Span; +use tuirealm::props::{Color, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +#[derive(MockComponent)] +pub struct FooterBar { + component: Span, +} + +impl FooterBar { + pub fn new(key_color: Color) -> Self { + Self { + component: Span::default().spans(&[ + TextSpan::from("").fg(key_color), + TextSpan::from(" Help "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Change tab "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Transfer "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Enter dir "), + TextSpan::from("").fg(key_color), + TextSpan::from(" View "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Edit "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Copy "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Rename "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Make dir "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Make dir "), + TextSpan::from("").fg(key_color), + TextSpan::from(" Quit "), + ]), + } + } +} + +impl Component for FooterBar { + fn on(&mut self, _: Event) -> Option { + None + } +} diff --git a/src/ui/activities/filetransfer/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs index cd79cd72..6d54b4cd 100644 --- a/src/ui/activities/filetransfer/components/mod.rs +++ b/src/ui/activities/filetransfer/components/mod.rs @@ -35,10 +35,12 @@ use tuirealm::{ // -- export mod log; +mod misc; mod popups; mod transfer; pub use self::log::Log; +pub use misc::FooterBar; pub use popups::{ CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup, FileInfoPopup, FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup, @@ -59,11 +61,11 @@ impl Component for GlobalListener { Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) } Event::Keyboard(KeyEvent { - code: Key::Char('q'), + code: Key::Char('q') | Key::Function(10), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowQuitPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('h'), + code: Key::Char('h') | Key::Function(1), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), _ => None, diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index b21a2569..2084123f 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -676,102 +676,106 @@ impl KeybindingsPopup { .rows( TableBuilder::default() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Disconnect")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Switch between explorer and logs", - )) + .add_col(TextSpan::from(" Disconnect")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to previous directory")) + .add_col(TextSpan::from(" Go to previous directory")) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change explorer tab")) + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Change explorer tab")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Move up/down in list")) + .add_col(TextSpan::from(" Move up/down in list")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Enter directory")) + .add_col(TextSpan::from(" Enter directory")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Upload/Download file")) + .add_col(TextSpan::from(" Upload/Download file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Switch between explorer and log window", + )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Toggle hidden files")) + .add_col(TextSpan::from(" Toggle hidden files")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change file sorting mode")) + .add_col(TextSpan::from(" Change file sorting mode")) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Copy")) + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Copy")) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Make directory")) + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Make directory")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Search files")) + .add_col(TextSpan::from(" Search files")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to path")) + .add_col(TextSpan::from(" Go to path")) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Show help")) + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Show help")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Show info about selected file")) + .add_col(TextSpan::from( + " Show info about selected file", + )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Reload directory content")) + .add_col(TextSpan::from(" Reload directory content")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Select file")) + .add_col(TextSpan::from(" Select file")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Create new file")) + .add_col(TextSpan::from(" Create new file")) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( - " Open text file with preferred editor", + " Open text file with preferred editor", )) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Quit termscp")) + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Quit termscp")) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Rename file")) + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Rename file")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Save file as")) + .add_col(TextSpan::from(" Save file as")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to parent directory")) + .add_col(TextSpan::from(" Go to parent directory")) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( - " Open file with default application for file type", + " Open file with default application for file type", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( - " Open file with specified application", + " Open file with specified application", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Execute shell command")) + .add_col(TextSpan::from(" Execute shell command")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Toggle synchronized browsing")) + .add_col(TextSpan::from( + " Toggle synchronized browsing", + )) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Delete selected file")) + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Delete selected file")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Select all files")) + .add_col(TextSpan::from(" Select all files")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Interrupt file transfer")) + .add_col(TextSpan::from(" Interrupt file transfer")) .build(), ), } diff --git a/src/ui/activities/filetransfer/components/transfer/mod.rs b/src/ui/activities/filetransfer/components/transfer/mod.rs index 26a6ff01..cfbf5aba 100644 --- a/src/ui/activities/filetransfer/components/transfer/mod.rs +++ b/src/ui/activities/filetransfer/components/transfer/mod.rs @@ -105,14 +105,14 @@ impl Component for ExplorerFind { Some(Msg::None) } // -- comp msg - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ExplorerTabbed)) - } + Event::Keyboard(KeyEvent { + code: Key::BackTab, .. + }) => Some(Msg::Ui(UiMsg::ExplorerBackTabbed)), Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseFindExplorer)) } Event::Keyboard(KeyEvent { - code: Key::Left | Key::Right, + code: Key::Left | Key::Right | Key::Tab, .. }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), Event::Keyboard(KeyEvent { @@ -135,7 +135,7 @@ impl Component for ExplorerFind { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('e') | Key::Delete, + code: Key::Char('e') | Key::Delete | Key::Function(8), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), Event::Keyboard(KeyEvent { @@ -147,7 +147,7 @@ impl Component for ExplorerFind { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('v'), + code: Key::Char('v') | Key::Function(3), modifiers: KeyModifiers::NONE, }) => Some(Msg::Transfer(TransferMsg::OpenFile)), Event::Keyboard(KeyEvent { @@ -229,14 +229,15 @@ impl Component for ExplorerLocal { Some(Msg::None) } // -- comp msg - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ExplorerTabbed)) - } + Event::Keyboard(KeyEvent { + code: Key::BackTab, .. + }) => Some(Msg::Ui(UiMsg::ExplorerBackTabbed)), Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) } Event::Keyboard(KeyEvent { - code: Key::Right, .. + code: Key::Right | Key::Tab, + .. }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), Event::Keyboard(KeyEvent { code: Key::Backspace, @@ -258,15 +259,15 @@ impl Component for ExplorerLocal { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('c'), + code: Key::Char('c') | Key::Function(5), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowCopyPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('d'), + code: Key::Char('d') | Key::Function(7), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('e') | Key::Delete, + code: Key::Char('e') | Key::Delete | Key::Function(8), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), Event::Keyboard(KeyEvent { @@ -290,11 +291,11 @@ impl Component for ExplorerLocal { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)), Event::Keyboard(KeyEvent { - code: Key::Char('o'), + code: Key::Char('o') | Key::Function(4), modifiers: KeyModifiers::NONE, }) => Some(Msg::Transfer(TransferMsg::OpenTextFile)), Event::Keyboard(KeyEvent { - code: Key::Char('r'), + code: Key::Char('r') | Key::Function(6), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowRenamePopup)), Event::Keyboard(KeyEvent { @@ -314,7 +315,7 @@ impl Component for ExplorerLocal { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)), Event::Keyboard(KeyEvent { - code: Key::Char('v'), + code: Key::Char('v') | Key::Function(3), modifiers: KeyModifiers::NONE, }) => Some(Msg::Transfer(TransferMsg::OpenFile)), Event::Keyboard(KeyEvent { @@ -396,14 +397,15 @@ impl Component for ExplorerRemote { Some(Msg::None) } // -- comp msg - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ExplorerTabbed)) - } + Event::Keyboard(KeyEvent { + code: Key::BackTab, .. + }) => Some(Msg::Ui(UiMsg::ExplorerBackTabbed)), Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) } Event::Keyboard(KeyEvent { - code: Key::Left, .. + code: Key::Left | Key::Tab, + .. }) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)), Event::Keyboard(KeyEvent { code: Key::Backspace, @@ -425,15 +427,15 @@ impl Component for ExplorerRemote { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('c'), + code: Key::Char('c') | Key::Function(5), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowCopyPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('d'), + code: Key::Char('d') | Key::Function(7), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('e') | Key::Delete, + code: Key::Char('e') | Key::Delete | Key::Function(8), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowDeletePopup)), Event::Keyboard(KeyEvent { @@ -457,11 +459,11 @@ impl Component for ExplorerRemote { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)), Event::Keyboard(KeyEvent { - code: Key::Char('o'), + code: Key::Char('o') | Key::Function(4), modifiers: KeyModifiers::NONE, }) => Some(Msg::Transfer(TransferMsg::OpenTextFile)), Event::Keyboard(KeyEvent { - code: Key::Char('r'), + code: Key::Char('r') | Key::Function(6), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowRenamePopup)), Event::Keyboard(KeyEvent { @@ -481,7 +483,7 @@ impl Component for ExplorerRemote { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)), Event::Keyboard(KeyEvent { - code: Key::Char('v'), + code: Key::Char('v') | Key::Function(3), modifiers: KeyModifiers::NONE, }) => Some(Msg::Transfer(TransferMsg::OpenFile)), Event::Keyboard(KeyEvent { diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 7f526cac..543a9c47 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -74,6 +74,7 @@ enum Id { FatalPopup, FileInfoPopup, FindPopup, + FooterBar, GlobalListener, GotoPopup, KeybindingsPopup, @@ -148,8 +149,8 @@ enum UiMsg { CloseRenamePopup, CloseSaveAsPopup, Disconnect, - ExplorerTabbed, - LogTabbed, + ExplorerBackTabbed, + LogBackTabbed, Quit, ReplacePopupTabbed, ShowCopyPopup, diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 75c8c093..e60108c6 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -417,10 +417,10 @@ impl FileTransferActivity { self.disconnect(); self.umount_disconnect(); } - UiMsg::ExplorerTabbed => { + UiMsg::ExplorerBackTabbed => { assert!(self.app.active(&Id::Log).is_ok()); } - UiMsg::LogTabbed => { + UiMsg::LogBackTabbed => { assert!(self.app.active(&Id::ExplorerLocal).is_ok()); } UiMsg::Quit => { diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index d301975c..0ae02b21 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -52,8 +52,17 @@ impl FileTransferActivity { let remote_explorer_background = self.theme().transfer_remote_explorer_background; let remote_explorer_foreground = self.theme().transfer_remote_explorer_foreground; let remote_explorer_highlighted = self.theme().transfer_remote_explorer_highlighted; + let key_color = self.theme().misc_keys; let log_panel = self.theme().transfer_log_window; let log_background = self.theme().transfer_log_background; + assert!(self + .app + .mount( + Id::FooterBar, + Box::new(components::FooterBar::new(key_color)), + vec![] + ) + .is_ok()); assert!(self .app .mount( @@ -111,9 +120,19 @@ impl FileTransferActivity { let store: &mut Store = &mut context.store; let _ = context.terminal.raw_mut().draw(|f| { // Prepare chunks - let chunks = Layout::default() + let body = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Min(7), // Body + Constraint::Length(1), // Footer + ] + .as_ref(), + ) + .split(f.size()); + // main chunks + let main_chunks = Layout::default() .direction(Direction::Vertical) - .margin(1) .constraints( [ Constraint::Percentage(70), // Explorer @@ -121,17 +140,17 @@ impl FileTransferActivity { ] .as_ref(), ) - .split(f.size()); + .split(body[0]); // Create explorer chunks let tabs_chunks = Layout::default() .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .direction(Direction::Horizontal) - .split(chunks[0]); + .split(main_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]); + .split(main_chunks[1]); // Create status bar chunks let status_bar_chunks = Layout::default() .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) @@ -142,6 +161,8 @@ impl FileTransferActivity { if !store.isset(super::STORAGE_EXPLORER_WIDTH) { store.set_unsigned(super::STORAGE_EXPLORER_WIDTH, tabs_chunks[0].width as usize); } + // Draw footer + self.app.view(&Id::FooterBar, f, body[1]); // Draw explorers // @! Local explorer (Find or default) if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) { @@ -816,6 +837,20 @@ impl FileTransferActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(1), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(10), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), Sub::new( SubEventClause::Keyboard(KeyEvent { code: Key::Char('q'), diff --git a/src/ui/activities/setup/components/commons.rs b/src/ui/activities/setup/components/commons.rs index 3344f92a..bd620003 100644 --- a/src/ui/activities/setup/components/commons.rs +++ b/src/ui/activities/setup/components/commons.rs @@ -108,7 +108,7 @@ impl Header { .color(Color::Yellow) .sides(BorderSides::BOTTOM), ) - .choices(&["User interface", "SSH Keys", "Theme"]) + .choices(&["Configuration parameters", "SSH Keys", "Theme"]) .foreground(Color::Yellow) .value(match layout { ViewLayout::SetupForm => 0, From 5881a45f29ca715957e81732832fcfd374dbbfb9 Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 11 Dec 2021 17:40:27 +0100 Subject: [PATCH 20/45] mode --- src/ui/activities/filetransfer/components/misc.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 src/ui/activities/filetransfer/components/misc.rs diff --git a/src/ui/activities/filetransfer/components/misc.rs b/src/ui/activities/filetransfer/components/misc.rs old mode 100755 new mode 100644 From 7df4e847db6e164e34bd43547a287038c2ab66b4 Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Sat, 11 Dec 2021 21:43:23 +0100 Subject: [PATCH 21/45] SSH configuration path (#84) SSH configuration --- CHANGELOG.md | 6 +++ docs/de/man.md | 1 + docs/es/man.md | 1 + docs/fr/man.md | 1 + docs/it/man.md | 1 + docs/man.md | 1 + docs/zh-CN/man.md | 1 + src/config/params.rs | 22 ++++++++--- src/config/serialization.rs | 9 +++++ src/filetransfer/builder.rs | 4 ++ src/system/config_client.rs | 26 +++++++++++++ .../activities/auth/components/bookmarks.rs | 1 + src/ui/activities/setup/components/config.rs | 39 ++++++++++++++++++- src/ui/activities/setup/components/mod.rs | 2 +- src/ui/activities/setup/config.rs | 5 ++- src/ui/activities/setup/mod.rs | 3 ++ src/ui/activities/setup/update.rs | 13 ++++++- src/ui/activities/setup/view/setup.rs | 28 +++++++++++++ 18 files changed, 155 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a840fd56..aeb85212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,12 @@ Released on FIXME: - **Tui-realm migration**: - migrated application to tui-realm 1.x - Improved application performance +- **SSH Config** + - Added `ssh config` parameter in configuration + - It is now possible to specify the ssh configuration file to use + - The supported parameters are described at . + - If the field is left empty, **no file will be loaded**. + - **By default, no file will be used**. - Dependencies: - Updated `tui-realm` to `1.3.0` - Updated `tui-realm-stdlib` to `1.1.4` diff --git a/docs/de/man.md b/docs/de/man.md index 0a1d7535..1610e196 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -308,6 +308,7 @@ These parameters can be changed: - **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format) - **Enable notifications?**: If set to `Yes`, notifications will be displayed. - **Notifications: minimum transfer size**: if transfer size is greater or equal than the specified value, notifications for transfer will be displayed. The accepted values are in format `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path**: Set SSH configuration file to use when connecting to a SCP/SFTP server. If unset (empty) no file will be used. You can specify a path starting with `~` to indicate the home path (e.g. `~/.ssh/config`) ### SSH Key Storage 🔐 diff --git a/docs/es/man.md b/docs/es/man.md index cf627bf0..04362088 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -307,6 +307,7 @@ Estos parámetros se pueden cambiar: - **Local File formatter syntax**: sintaxis para mostrar información de archivo para cada archivo en el explorador local. Consulte [Formato del explorador de archivos](#formato-del-explorador-de-archivos). - **Enable notifications?**: Si se establece en "Sí", se mostrarán las notificaciones. - **Notifications: minimum transfer size**: si el tamaño de la transferencia es mayor o igual que el valor especificado, se mostrarán notificaciones de transferencia. Los valores aceptados están en formato `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path**: Configure el archivo de configuración SSH para usar al conectarse a un servidor SCP / SFTP. Si no se configura (está vacío), no se utilizará ningún archivo. Puede especificar una ruta que comience con `~` para indicar la ruta de inicio (por ejemplo, `~/.ssh/config`) ### SSH Key Storage 🔐 diff --git a/docs/fr/man.md b/docs/fr/man.md index 6252d25f..be855fc0 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -305,6 +305,7 @@ Ces paramètres peuvent être modifiés : - **Local File formatter syntax**: syntaxe pour afficher les informations de fichier pour chaque fichier dans l'explorateur local. Voir [File explorer format](#format-de-lexplorateur-de-fichiers) - **Enable notifications?**: S'il est défini sur `Yes`, les notifications seront affichées. - **Notifications: minimum transfer size**: si la taille du transfert est supérieure ou égale à la valeur spécifiée, les notifications de transfert seront affichées. Les valeurs acceptées sont au format `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path** : définissez le fichier de configuration SSH à utiliser lors de la connexion à un serveur SCP/SFTP. S'il n'est pas défini (vide), aucun fichier ne sera utilisé. Vous pouvez spécifier un chemin commençant par `~` pour indiquer le chemin d'accueil (par exemple `~/.ssh/config`) ### SSH Key Storage 🔐 diff --git a/docs/it/man.md b/docs/it/man.md index 4789d7bb..dcafab95 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -303,6 +303,7 @@ Questi parametri possono essere impostati: - **Local File formatter syntax**: La formattazione da usare per formattare i file sull'explorer locale. Vedi [File explorer format](#file-explorer-format) - **Enable notifications?**: Se impostato a `yes`, le notifiche desktop saranno abilitate. - **Notifications: minimum transfer size**: se la dimensione di un trasferimento supera o è uguale al valore impostato, al termine del trasferimento riceverai una notifica desktop (se queste sono abilitate). Il formato del valore dev'essere `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path**: Imposta il percorso del file di configurazione per SSH, per quando ci si connette ad un server SFTP/SCP. Se lasciato vuoto, nessun file verrà usato. Il percorso può anche iniziare con `~` per indicare il percorso della home dell'utente attuale (e.s. `~/.ssh/config`). ### SSH Key Storage 🔐 diff --git a/docs/man.md b/docs/man.md index c7829372..05cf1481 100644 --- a/docs/man.md +++ b/docs/man.md @@ -306,6 +306,7 @@ These parameters can be changed: - **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format) - **Enable notifications?**: If set to `Yes`, notifications will be displayed. - **Notifications: minimum transfer size**: if transfer size is greater or equal than the specified value, notifications for transfer will be displayed. The accepted values are in format `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path**: Set SSH configuration file to use when connecting to a SCP/SFTP server. If unset (empty) no file will be used. You can specify a path starting with `~` to indicate the home path (e.g. `~/.ssh/config`) ### SSH Key Storage 🔐 diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index d19e64e9..86bb9336 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -299,6 +299,7 @@ termscp和书签一样,只需要保证这些路径是可访问的: - **Local File formatter syntax**:在本地资源管理器中显示每个文件的文件信息的语法。参见[资源管理器格式](#资源管理器格式) - **Enable notifications?**: 如果设置为 `Yes`,则会显示通知。 - **Notifications: minimum transfer size**: 如果传输大小大于或等于指定值,将显示传输通知。 接受的值格式为 `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH Configuration path**:设置连接到 SCP/SFTP 服务器时使用的 SSH 配置文件。 如果未设置(空),则不会使用任何文件。 你可以指定一个以 `~` 开头的路径来表示主路径(例如 `~/.ssh/config`) ### SSH Key Storage diff --git a/src/config/params.rs b/src/config/params.rs index 3c8b5c1a..8f575bbc 100644 --- a/src/config/params.rs +++ b/src/config/params.rs @@ -52,16 +52,21 @@ pub struct UserInterfaceConfig { pub check_for_updates: Option, // @! Since 0.3.3 pub prompt_on_file_replace: Option, // @! Since 0.7.0; Default True pub group_dirs: Option, - pub file_fmt: Option, // Refers to local host (for backward compatibility) - pub remote_file_fmt: Option, // @! Since 0.5.0 - pub notifications: Option, // @! Since 0.7.0; Default true + /// file fmt. Refers to local host (for backward compatibility) + pub file_fmt: Option, + pub remote_file_fmt: Option, // @! Since 0.5.0 + pub notifications: Option, // @! Since 0.7.0; Default true pub notification_threshold: Option, // @! Since 0.7.0; Default 512MB } #[derive(Deserialize, Serialize, Debug, Default)] /// Contains configuratio related to remote hosts pub struct RemoteConfig { - pub ssh_keys: HashMap, // Association between host name and path to private key + /// Ssh configuration path. If NONE, won't be read + pub ssh_config: Option, + /// Association between host name and path to private key + /// NOTE: this parameter must stay as last: + pub ssh_keys: HashMap, } impl Default for UserInterfaceConfig { @@ -99,7 +104,10 @@ mod tests { String::from("192.168.1.31"), PathBuf::from("/tmp/private.key"), ); - let remote: RemoteConfig = RemoteConfig { ssh_keys: keys }; + let remote: RemoteConfig = RemoteConfig { + ssh_keys: keys, + ssh_config: Some(String::from("~/.ssh/config")), + }; let ui: UserInterfaceConfig = UserInterfaceConfig { default_protocol: String::from("SFTP"), text_editor: PathBuf::from("nano"), @@ -130,6 +138,10 @@ mod tests { .unwrap(), PathBuf::from("/tmp/private.key") ); + assert_eq!( + cfg.remote.ssh_config.as_deref().unwrap(), + String::from("~/.ssh/config") + ); assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP")); assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano")); assert_eq!(cfg.user_interface.show_hidden_files, true); diff --git a/src/config/serialization.rs b/src/config/serialization.rs index 2ddb8eda..84198c95 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -197,6 +197,11 @@ mod tests { assert_eq!(cfg.user_interface.notifications.unwrap(), false); assert_eq!(cfg.user_interface.notification_threshold.unwrap(), 1024); assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last"))); + // Remote + assert_eq!( + cfg.remote.ssh_config.as_deref(), + Some("/home/omar/.ssh/config") + ); assert_eq!( cfg.user_interface.file_fmt, Some(String::from("{NAME} {PEX}")) @@ -244,6 +249,7 @@ mod tests { assert!(cfg.user_interface.remote_file_fmt.is_none()); assert!(cfg.user_interface.notifications.is_none()); assert!(cfg.user_interface.notification_threshold.is_none()); + assert!(cfg.remote.ssh_config.is_none()); // Verify keys assert_eq!( *cfg.remote @@ -322,6 +328,9 @@ mod tests { notifications = false notification_threshold = 1024 + [remote] + ssh_config = "/home/omar/.ssh/config" + [remote.ssh_keys] "192.168.1.31" = "/home/omar/.ssh/raspberry.key" "192.168.1.32" = "/home/omar/.ssh/beaglebone.key" diff --git a/src/filetransfer/builder.rs b/src/filetransfer/builder.rs index 4942c24c..d389bde3 100644 --- a/src/filetransfer/builder.rs +++ b/src/filetransfer/builder.rs @@ -36,6 +36,7 @@ use remotefs::client::{ ssh::{ScpFs, SftpFs, SshOpts}, }; use remotefs::RemoteFs; +use std::path::PathBuf; /// Remotefs builder pub struct Builder; @@ -117,6 +118,9 @@ impl Builder { if let Some(password) = params.password { opts = opts.password(password); } + if let Some(config_path) = config_client.get_ssh_config() { + opts = opts.config_file(PathBuf::from(config_path)); + } opts } diff --git a/src/system/config_client.rs b/src/system/config_client.rs index e87a03e0..7b850c0b 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -239,6 +239,18 @@ impl ConfigClient { self.config.user_interface.notification_threshold = Some(value); } + // Remote params + + /// Get ssh config path + pub fn get_ssh_config(&self) -> Option<&str> { + self.config.remote.ssh_config.as_deref() + } + + /// Set ssh config path + pub fn set_ssh_config(&mut self, p: Option) { + self.config.remote.ssh_config = p; + } + // SSH Keys /// Save a SSH key into configuration. @@ -655,6 +667,20 @@ mod tests { assert_eq!(client.get_notification_threshold(), 64); } + #[test] + fn test_system_config_remote_ssh_config() { + let tmp_dir: TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path()) + .ok() + .unwrap(); + assert_eq!(client.get_ssh_config(), None); // Null ? + client.set_ssh_config(Some(String::from("/tmp/ssh_config"))); + assert_eq!(client.get_ssh_config(), Some("/tmp/ssh_config")); + client.set_ssh_config(None); + assert_eq!(client.get_ssh_config(), None); + } + #[test] fn test_system_config_ssh_keys() { let tmp_dir: TempDir = TempDir::new().ok().unwrap(); diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs index 51acfa9f..4745d223 100644 --- a/src/ui/activities/auth/components/bookmarks.rs +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -391,6 +391,7 @@ impl BookmarkName { impl Component for BookmarkName { fn on(&mut self, ev: Event) -> Option { match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseSaveBookmark), Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { diff --git a/src/ui/activities/setup/components/config.rs b/src/ui/activities/setup/components/config.rs index cc7ffa9b..1c3bbe75 100644 --- a/src/ui/activities/setup/components/config.rs +++ b/src/ui/activities/setup/components/config.rs @@ -368,6 +368,43 @@ impl Component for RemoteFileFmt { } } +#[derive(MockComponent)] +pub struct SshConfig { + component: Input, +} + +impl SshConfig { + pub fn new(value: &str) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(Color::LightBlue) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::LightBlue) + .input_type(InputType::Text) + .placeholder( + "~/.ssh/config", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("SSH configuration path", Alignment::Left) + .value(value), + } + } +} + +impl Component for SshConfig { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Config(ConfigMsg::SshConfigBlurDown), + Msg::Config(ConfigMsg::SshConfigBlurUp), + ) + } +} + #[derive(MockComponent)] pub struct TextEditor { component: Input, @@ -448,7 +485,7 @@ fn handle_input_ev( } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { component.perform(Cmd::Type(ch)); Some(Msg::Config(ConfigMsg::ConfigChanged)) diff --git a/src/ui/activities/setup/components/mod.rs b/src/ui/activities/setup/components/mod.rs index e9b58a51..841dad06 100644 --- a/src/ui/activities/setup/components/mod.rs +++ b/src/ui/activities/setup/components/mod.rs @@ -35,7 +35,7 @@ mod theme; pub(super) use commons::{ErrorPopup, Footer, Header, Keybindings, QuitPopup, SavePopup}; pub(super) use config::{ CheckUpdates, DefaultProtocol, GroupDirs, HiddenFiles, LocalFileFmt, NotificationsEnabled, - NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, TextEditor, + NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, SshConfig, TextEditor, }; pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername}; pub(super) use theme::*; diff --git a/src/ui/activities/setup/config.rs b/src/ui/activities/setup/config.rs index c4cbf923..9137d3d9 100644 --- a/src/ui/activities/setup/config.rs +++ b/src/ui/activities/setup/config.rs @@ -36,7 +36,10 @@ impl SetupActivity { pub(super) fn save_config(&mut self) -> Result<(), String> { match self.config().write_config() { Ok(_) => Ok(()), - Err(err) => Err(format!("Could not save configuration: {}", err)), + Err(err) => { + error!("Could not save configuration: {}", err); + Err(format!("Could not save configuration: {}", err)) + } } } diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index aba8e8a7..8a3029ad 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -75,6 +75,7 @@ enum IdConfig { NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, + SshConfig, TextEditor, } @@ -167,6 +168,8 @@ pub enum ConfigMsg { PromptOnFileReplaceBlurUp, RemoteFileFmtBlurDown, RemoteFileFmtBlurUp, + SshConfigBlurDown, + SshConfigBlurUp, TextEditorBlurDown, TextEditorBlurUp, } diff --git a/src/ui/activities/setup/update.rs b/src/ui/activities/setup/update.rs index 8267f1fc..7072759e 100644 --- a/src/ui/activities/setup/update.rs +++ b/src/ui/activities/setup/update.rs @@ -80,11 +80,13 @@ impl SetupActivity { CommonMsg::RevertChanges => match self.layout { ViewLayout::Theme => { if let Err(err) = self.action_reset_theme() { + error!("Failed to reset theme: {}", err); self.mount_error(err); } } ViewLayout::SshKeys | ViewLayout::SetupForm => { if let Err(err) = self.action_reset_config() { + error!("Failed to reset config: {}", err); self.mount_error(err); } } @@ -92,6 +94,7 @@ impl SetupActivity { CommonMsg::SaveAndQuit => { // Save changes if let Err(err) = self.action_save_all() { + error!("Failed to save config: {}", err); self.mount_error(err.as_str()); } // Exit @@ -99,6 +102,7 @@ impl SetupActivity { } CommonMsg::SaveConfig => { if let Err(err) = self.action_save_all() { + error!("Failed to save config: {}", err); self.mount_error(err.as_str()); } self.umount_save_popup(); @@ -173,7 +177,7 @@ impl SetupActivity { .is_ok()); } ConfigMsg::NotificationsThresholdBlurDown => { - assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); + assert!(self.app.active(&Id::Config(IdConfig::SshConfig)).is_ok()); } ConfigMsg::NotificationsThresholdBlurUp => { assert!(self @@ -203,6 +207,12 @@ impl SetupActivity { .is_ok()); } ConfigMsg::TextEditorBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::SshConfig)).is_ok()); + } + ConfigMsg::SshConfigBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); + } + ConfigMsg::SshConfigBlurUp => { assert!(self .app .active(&Id::Config(IdConfig::NotificationsThreshold)) @@ -230,6 +240,7 @@ impl SetupActivity { } SshMsg::EditSshKey(i) => { if let Err(err) = self.edit_ssh_key(i) { + error!("Failed to edit ssh key: {}", err); self.mount_error(err.as_str()); } } diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 1f6946a7..14ede283 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -119,6 +119,7 @@ impl SetupActivity { Constraint::Length(3), // Remote Format input Constraint::Length(3), // Notifications enabled Constraint::Length(3), // Notifications threshold + Constraint::Length(3), // Ssh config Constraint::Length(1), // Filler ] .as_ref(), @@ -144,6 +145,8 @@ impl SetupActivity { f, ui_cfg_chunks_col2[3], ); + self.app + .view(&Id::Config(IdConfig::SshConfig), f, ui_cfg_chunks_col2[4]); // Popups self.view_popups(f); }); @@ -261,6 +264,17 @@ impl SetupActivity { vec![] ) .is_ok()); + // Ssh config + assert!(self + .app + .remount( + Id::Config(IdConfig::SshConfig), + Box::new(components::SshConfig::new( + self.config().get_ssh_config().unwrap_or("") + )), + vec![] + ) + .is_ok()); } /// Collect values from input and put them into the configuration @@ -332,5 +346,19 @@ impl SetupActivity { { self.config_mut().set_notification_threshold(bytes); } + if let Ok(State::One(StateValue::String(mut path))) = + self.app.state(&Id::Config(IdConfig::SshConfig)) + { + if path.is_empty() { + self.config_mut().set_ssh_config(None); + } else { + // Replace '~' with home path + if path.starts_with('~') { + let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/root")); + path = path.replacen('~', &home_dir.to_string_lossy(), 1); + } + self.config_mut().set_ssh_config(Some(path)); + } + } } } From a7e17ede852770adfebe8a28733e6f8b7a774f0d Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 4 Dec 2021 10:46:22 +0100 Subject: [PATCH 22/45] ko-fi --- .github/FUNDING.yml | 2 +- README.md | 10 +++++----- docs/de/README.md | 10 +++++----- docs/es/README.md | 10 +++++----- docs/fr/README.md | 10 +++++----- docs/it/README.md | 10 +++++----- docs/zh-CN/README.md | 10 +++++----- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 7f2e0420..11623c21 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ['https://www.buymeacoffee.com/veeso'] +ko_fi: veeso diff --git a/README.md b/README.md index ef5fa789..203b8592 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,10 @@ src="https://img.shields.io/crates/v/termscp.svg" alt="Latest version" /> - Buy me a coffee + Ko-fi

@@ -218,7 +218,7 @@ If you like termscp and you're grateful for the work I've done, please consider You can make a donation with one of these platforms: -[![Buy-me-a-coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?style=for-the-badge&logo=buy-me-a-coffee)](https://www.buymeacoffee.com/veeso) +[![ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/veeso) [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/chrisintin) --- diff --git a/docs/de/README.md b/docs/de/README.md index 18243134..6f3dd2d2 100644 --- a/docs/de/README.md +++ b/docs/de/README.md @@ -86,10 +86,10 @@ src="https://img.shields.io/crates/v/termscp.svg" alt="Latest version" /> - Buy me a coffee + Ko-fi

@@ -218,7 +218,7 @@ Wenn Ihnen termscp gefällt und Sie für die Arbeit, die ich geleistet habe, dan Sie können mit einer dieser Plattformen spenden: -[![Buy-me-a-coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?style=for-the-badge&logo=buy-me-a-coffee)](https://www.buymeacoffee.com/veeso) +[![ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/veeso) [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/chrisintin) --- diff --git a/docs/es/README.md b/docs/es/README.md index 69c3b02f..a8d47eb4 100644 --- a/docs/es/README.md +++ b/docs/es/README.md @@ -86,10 +86,10 @@ src="https://img.shields.io/crates/v/termscp.svg" alt="Latest version" /> - Buy me a coffee + Ko-fi

@@ -218,7 +218,7 @@ Si te gusta termscp y te encantaría que el proyecto crezca y mejore, considera Puedes hacer una donación con una de estas plataformas: -[![Buy-me-a-coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?style=for-the-badge&logo=buy-me-a-coffee)](https://www.buymeacoffee.com/veeso) +[![ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/veeso) [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/chrisintin) --- diff --git a/docs/fr/README.md b/docs/fr/README.md index 152cbee1..aa291bf5 100644 --- a/docs/fr/README.md +++ b/docs/fr/README.md @@ -86,10 +86,10 @@ src="https://img.shields.io/crates/v/termscp.svg" alt="Latest version" /> - Buy me a coffee + Ko-fi

@@ -218,7 +218,7 @@ Si tu aime termscp et que tu aimerais voir le projet grandir et s'améliorer, vo Tu peux faire un don avec l'une de ces plateformes: -[![Buy-me-a-coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?style=for-the-badge&logo=buy-me-a-coffee)](https://www.buymeacoffee.com/veeso) +[![ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/veeso) [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/chrisintin) --- diff --git a/docs/it/README.md b/docs/it/README.md index f7bf55b2..3f4f9bac 100644 --- a/docs/it/README.md +++ b/docs/it/README.md @@ -86,10 +86,10 @@ src="https://img.shields.io/crates/v/termscp.svg" alt="Latest version" /> - Buy me a coffee + Ko-fi

@@ -218,7 +218,7 @@ Se ti piace termscp e ti piacerebbe vedere il progetto crescere e migliorare, co Puoi fare una donazione tramite una di queste piattaforme: -[![Buy-me-a-coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?style=for-the-badge&logo=buy-me-a-coffee)](https://www.buymeacoffee.com/veeso) +[![ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/veeso) [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/chrisintin) --- diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index f926e7e8..8b53fd79 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -86,10 +86,10 @@ src="https://img.shields.io/crates/v/termscp.svg" alt="Latest version" /> - Buy me a coffee + Ko-fi

@@ -217,7 +217,7 @@ choco install termscp 如果您喜欢 termscp 并且希望看到该项目不断发展和改进,请考虑在 **Buy me a coffee** 上捐款以支持我🥳 -[![Buy-me-a-coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?style=for-the-badge&logo=buy-me-a-coffee)](https://www.buymeacoffee.com/veeso) +[![ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/veeso) 或者,如果您愿意,您也可以在 PayPal 上捐款: From bb7c952e32d438d0555dbc6a0ff3cf1611a23ae2 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 12 Dec 2021 12:54:36 +0100 Subject: [PATCH 23/45] Default log level changed to `INFO`; added option to enable TRACE log level --- CHANGELOG.md | 3 +++ CONTRIBUTING.md | 2 +- docs/de/man.md | 12 ++++-------- docs/es/man.md | 12 ++++-------- docs/fr/man.md | 17 +++++++---------- docs/it/README.md | 2 +- docs/it/man.md | 16 ++++++---------- docs/man.md | 12 ++++-------- docs/zh-CN/man.md | 12 +++++------- src/main.rs | 20 +++++++++++--------- src/system/logging.rs | 9 +++++---- 11 files changed, 51 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb85212..3d3f5a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,9 @@ Released on FIXME: - The supported parameters are described at . - If the field is left empty, **no file will be loaded**. - **By default, no file will be used**. +- **Less verbose logging**: + - By default the log level is now set to `INFO` + - It is now possible to enable the `TRACE` level with the `-D` CLI option. - Dependencies: - Updated `tui-realm` to `1.3.0` - Updated `tui-realm-stdlib` to `1.1.4` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93c8c566..cebd6f04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ Don't set other labels to your issue, not even priority. When you open a bug try to be the most precise as possible in describing your issue. I'm not saying you should always be that precise, since sometimes it's very easy for maintainers to understand what you're talking about. Just try to be reasonable to understand sometimes we might not know what you're talking about or we just don't have the technical knowledge you might think. Please always provide the environment you're working on and consider that we don't provide any support for older version of termscp, at least for those not classified as LTS (if we'll ever have them). -If you can, provide the log file or the snippet involving your issue. You can find in the [user manual](docs/man.md) the location of the log file. +If you can, provide the log file or the snippet involving your issue. You can find in the [user manual](docs/man.md) the location of the log file. Please, if you can, enable the **debug mode**, before submitting the log, in order to provide us with a better overview of the problem. Last but not least: the template I've written must be used. Full stop. Maintainers will may add additional labels to your issue: diff --git a/docs/de/man.md b/docs/de/man.md index 1610e196..67c519a7 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -455,16 +455,12 @@ termscp writes a log file for each session, which is written at - `FOLDERID_RoamingAppData\termscp\termscp.log` on Windows the log won't be rotated, but will just be truncated after each launch of termscp, so if you want to report an issue and you want to attach your log file, keep in mind to save the log file in a safe place before using termscp again. -The log file always reports in *trace* level, so it is kinda verbose. -I know you might have some questions regarding log files, so I made a kind of a Q/A: - -> Is it possible to reduce verbosity? +The logging by default reports in *INFO* level, so it is not very verbose. -No. The reason is quite simple: when an issue happens, you must be able to know what's causing it and the only way to do that, is to have the log file with the maximum verbosity level set. +If you want to submit an issue, please, if you can, reproduce the issue with the level set to `TRACE`, to do so, launch termscp with +the `-D` CLI option. -> If trace level is set for logging, is the file going to reach a huge size? - -Probably not, unless you never quit termscp, but I think that's unlikely to happen. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB. +I know you might have some questions regarding log files, so I made a kind of a Q/A: > I don't want logging, can I turn it off? diff --git a/docs/es/man.md b/docs/es/man.md index 04362088..e859ef6c 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -454,16 +454,12 @@ termscp escribe un archivo de registro para cada sesión, que se escribe en - `FOLDERID_RoamingAppData\termscp\termscp.log` en Windows el registro no se rotará, sino que se truncará después de cada lanzamiento de termscp, por lo que si desea informar un problema y desea adjuntar su archivo de registro, recuerde guardar el archivo de registro en un lugar seguro antes de usar termscp de nuevo. -El archivo de registro siempre informa en el nivel de *seguimiento*, por lo que es un poco detallado. -Sé que es posible que tenga algunas preguntas sobre los archivos de registro, así que hice una especie de Q/A: - -> ¿Es posible reducir la verbosidad? +El registro por defecto informa en el nivel *INFO*, por lo que no es muy detallado. -No. La razón es bastante simple: cuando ocurre un problema, debe poder saber qué lo está causando y la única forma de hacerlo es tener el archivo de registro con el nivel de verbosidad máximo establecido. +Si desea enviar un problema, por favor, si puede, reproduzca el problema con el nivel establecido en "TRACE", para hacerlo, inicie termscp con +la opción CLI `-D`. -> Si el nivel de seguimiento está configurado para el registro, ¿el archivo alcanzará un tamaño enorme? - -Probablemente no, a menos que nunca cerra termscp, pero creo que es poco probable que eso suceda. Una sesión larga puede producir hasta 10 MB de archivos de registro (dije una sesión larga), pero creo que una sesión normal no excederá los 2 MB. +Sé que es posible que tenga algunas preguntas sobre los archivos de registro, así que hice una especie de Q/A: > No quiero el registro, ¿puedo apagarlo? diff --git a/docs/fr/man.md b/docs/fr/man.md index be855fc0..578dfa5c 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -25,7 +25,7 @@ - [Transfer page](#transfer-page) - [Misc](#misc) - [Éditeur de texte ✏](#éditeur-de-texte-) - - [Enregistrement 🩺](#enregistrement-) + - [Fichier Journal 🩺](#fichier-journal-) - [Notifications 📫](#notifications-) ## Usage ❓ @@ -443,7 +443,7 @@ Si le fichier se trouve sur l'hôte distant, le fichier sera d'abord télécharg --- -## Enregistrement 🩺 +## Fichier Journal 🩺 termscp écrit un fichier journal pour chaque session, qui est écrit à @@ -451,17 +451,14 @@ termscp écrit un fichier journal pour chaque session, qui est écrit à - `$HOME/Library/Application Support/termscp/termscp.log` sous MacOs - `FOLDERID_RoamingAppData\termscp\termscp.log` sous Windows -le journal ne sera pas tourné, mais sera simplement tronqué après chaque lancement de termscp, donc si vous souhaitez signaler un problème et que vous souhaitez joindre votre fichier journal, n'oubliez pas de sauvegarder le fichier journal dans un endroit sûr avant de l'utiliser termscp à nouveau. -Le fichier journal rapporte toujours au niveau *trace*, il est donc un peu détaillé. -Je sais que vous pourriez avoir des questions concernant les fichiers journaux, alors j'ai fait une sorte de Q/R : - -> Est-il possible de réduire la verbosité ? +le journal ne sera pas tourné, mais sera simplement tronqué après chaque lancement de termscp, donc si vous souhaitez signaler un problème et que vous souhaitez joindre votre fichier journal, n'oubliez pas de sauvegarder le fichier journal dans un endroit sûr avant de l'utiliser termescp à nouveau. -Non. La raison est assez simple : lorsqu'un problème survient, vous devez être capable de savoir ce qui en est la cause et la seule façon de le faire est d'avoir le fichier journal avec le niveau de verbosité maximum défini. +La journalisation par défaut se rapporte au niveau *INFO*, elle n'est donc pas très détaillée. -> Si le niveau de trace est défini pour la journalisation, le fichier va-t-il atteindre une taille énorme ? +Si vous souhaitez soumettre un problème, veuillez, si vous le pouvez, reproduire le problème avec le niveau défini sur `TRACE`, pour ce faire, lancez termscp avec +l'option CLI `-D`. -Probablement pas, à moins que vous ne quittiez jamais termscp, mais je pense que cela est peu probable. Une longue session peut produire jusqu'à 10 MB de fichiers journaux (j'ai dit une longue session), mais je pense qu'une session normale ne dépassera pas 2 MB. +Je sais que vous pourriez avoir des questions concernant les fichiers journaux, alors j'ai fait une sorte de Q/R : > Je ne veux pas me connecter, puis-je le désactiver ? diff --git a/docs/it/README.md b/docs/it/README.md index 3f4f9bac..d5f107d2 100644 --- a/docs/it/README.md +++ b/docs/it/README.md @@ -129,7 +129,7 @@ ## Riguardo a termscp 🖥 -Termscp è un file transfer ed explorer ricco di funzionalità, con supporto a SCP/SFTP/FTP/S3. Basicamente è un utility su terminale con una terminal user-interface per connettersi a server remoti per scambiare file ed interagire con il file system sia locale che remoto. È compatibile con **Linux**, **MacOS**, **FreeBSD** e **Windows**. +Termscp è un file transfer ed explorer ricco di funzionalità, con supporto a SCP/SFTP/FTP/S3. In pratica è un utility su terminale con una terminal user-interface per connettersi a server remoti per scambiare file ed interagire con il file system sia locale che remoto. È compatibile con **Linux**, **MacOS**, **FreeBSD** e **Windows**. ![Explorer](/assets/images/explorer.gif) diff --git a/docs/it/man.md b/docs/it/man.md index dcafab95..3c0edae0 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -451,22 +451,18 @@ termscp scrive un file di log per ogni sessione, nel percorso seguente: - `FOLDERID_RoamingAppData\termscp\termscp.log` su Windows Il log non viene ruotato, ma viene troncato ad ogni lancio di termscp, quindi se devi riportare un issue, non avviare termscp fino a che non avrai salvato il file di log. -I log sono sempre riportati a livello di *trace*, quindi sono piuttosto parlanti. -Ho scritto questo FAQ sui log, visto che potresti avere qualche dubbio: - -> Si può ridurre la verbosità? +I log sono di default riportati a livello *INFO*, quindi non sono particolarmente parlanti. -No. Il motivo è piuttosto semplice: quando c'è un problema, devi sapere cosa lo sta causando e l'unico modo per farlo e avere il log alla massima verbosità per avere la massima precisione sul controllo del flusso. +Se vuoi riportare un problema, se riesci, riproduci l'errore lanciando termscp in modalità di debug, in modo da fornire un log più dettagliato. +Per farlo, lancia termscp con l'opzione `-D`. -> Se trace è il livello di verbosità, non si raggiungono dimensioni enormi? - -Probabilmente no, a meno che tu tenga sempre termscp acceso. Una lunga sessione potrebbe raggiungere i 10MB di log, ma una sessione media all'incirca 2MB. +Ho scritto questo FAQ sui log, visto che potresti avere qualche dubbio: -> Non voglio il logging, posso disabilitarlo? +> Non voglio il log, posso disabilitarlo? Sì, puoi. Basta lanciare termscp con `-q or --quiet` come opzione. Puoi mantenerlo persistente salvandolo come alias nella tua shell. Ricorda che i log vengono usati per diagnosticare problemi e considerando che questo è un progetto open-source è anche un modo per contribuire al progetto 😉. Non voglio far sentire in colpa nessuno, ma tanto per dire. -> Il logging è sicuro? +> Il log è sicuro? Se ti chiedi se il log espone dati sensibili, il log non espone nessuna password o dato sensibile. diff --git a/docs/man.md b/docs/man.md index 05cf1481..d93e7cc9 100644 --- a/docs/man.md +++ b/docs/man.md @@ -453,16 +453,12 @@ termscp writes a log file for each session, which is written at - `FOLDERID_RoamingAppData\termscp\termscp.log` on Windows the log won't be rotated, but will just be truncated after each launch of termscp, so if you want to report an issue and you want to attach your log file, keep in mind to save the log file in a safe place before using termscp again. -The log file always reports in *trace* level, so it is kinda verbose. -I know you might have some questions regarding log files, so I made a kind of a Q/A: - -> Is it possible to reduce verbosity? +The logging by default reports in *INFO* level, so it is not very verbose. -No. The reason is quite simple: when an issue happens, you must be able to know what's causing it and the only way to do that, is to have the log file with the maximum verbosity level set. +If you want to submit an issue, please, if you can, reproduce the issue with the level set to `TRACE`, to do so, launch termscp with +the `-D` CLI option. -> If trace level is set for logging, is the file going to reach a huge size? - -Probably not, unless you never quit termscp, but I think that's unlikely to happen. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB. +I know you might have some questions regarding log files, so I made a kind of a Q/A: > I don't want logging, can I turn it off? diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index 86bb9336..1bfe2c30 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -445,15 +445,13 @@ termscp会为每个会话创建一个日志文件,该文件在 - `$HOME/Library/Application Support/termscp/termscp.log` -- MacOs - `FOLDERID_RoamingAppData\termscp\termscp.log` -- Windows -日志不会滚动,只是在每次启动 termscp 后被截断,所以如果你想报告一个问题并想附上你的日志文件,再次启动 termscp 之前请先将日志文件保存在一个安全的地方。日志文件总是以*trace*级别报告,所以它有点冗长。我知道你可能有一些关于日志文件的问题,所以我做了一个Q/A: +日志不会被轮换,但只会在每次启动 termcp 后被截断,因此如果您想报告问题并希望附加您的日志文件,请记住在使用前将日志文件保存在安全的地方 再次termscp。 +默认情况下,日志记录在 *INFO* 级别报告,因此它不是很详细。 -> 有没有可能降低日志级别? +如果你想提交一个问题,如果可以的话,请在级别设置为`TRACE`的情况下重现问题,为此,启动termscp +`-D` CLI 选项。 -不可以,原因很简单:当一个问题发生时,你必须能够知道是什么原因造成的,而唯一的办法就是在日志文件中设置记录最多的细节。 - -> 如果日志级别设置为trace,会产生很大的文件吗? - -应该不会,除非你从不退出termscp,但我认为这很可能发生。一个长的会话可能会产生高达10MB的日志文件(我说的是一个长的会话),但我认为一个正常的会话不会超过2MB。 +我知道您可能对日志文件有一些疑问,所以我做了一个问答: > 我不希望有日志记录,我可以把它关掉吗? diff --git a/src/main.rs b/src/main.rs index 64828f4e..15794742 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,7 @@ mod utils; // namespaces use activity_manager::{ActivityManager, NextActivity}; use filetransfer::FileTransferParams; -use system::logging; +use system::logging::{self, LogLevel}; enum Task { Activity(NextActivity), @@ -78,6 +78,8 @@ Please, consider supporting the author ")] struct Args { #[argh(switch, short = 'c', description = "open termscp configuration")] config: bool, + #[argh(switch, short = 'D', description = "enable TRACE log level")] + debug: bool, #[argh(option, short = 'P', description = "provide password from CLI")] password: Option, #[argh(switch, short = 'q', description = "disable logging")] @@ -110,7 +112,7 @@ struct Args { struct RunOpts { remote: Option, ticks: Duration, - log_enabled: bool, + log_level: LogLevel, task: Task, } @@ -119,7 +121,7 @@ impl Default for RunOpts { Self { remote: None, ticks: Duration::from_millis(10), - log_enabled: true, + log_level: LogLevel::Info, task: Task::Activity(NextActivity::Authentication), } } @@ -136,10 +138,8 @@ fn main() { } }; // Setup logging - if run_opts.log_enabled { - if let Err(err) = logging::init() { - eprintln!("Failed to initialize logging: {}", err); - } + if let Err(err) = logging::init(run_opts.log_level) { + eprintln!("Failed to initialize logging: {}", err); } // Read password from remote if let Err(err) = read_password(&mut run_opts) { @@ -174,8 +174,10 @@ fn parse_args(args: Args) -> Result { run_opts.task = Task::Activity(NextActivity::SetupActivity); } // Logging - if args.quiet { - run_opts.log_enabled = false; + if args.debug { + run_opts.log_level = LogLevel::Trace; + } else if args.quiet { + run_opts.log_level = LogLevel::Off; } // Match ticks run_opts.ticks = Duration::from_millis(args.ticks); diff --git a/src/system/logging.rs b/src/system/logging.rs index bea7d7dc..f22a8c36 100644 --- a/src/system/logging.rs +++ b/src/system/logging.rs @@ -29,14 +29,15 @@ use crate::system::environment::{get_log_paths, init_config_dir}; use crate::utils::file::open_file; // ext -use simplelog::{ConfigBuilder, LevelFilter, WriteLogger}; +pub use simplelog::LevelFilter as LogLevel; +use simplelog::{ConfigBuilder, WriteLogger}; use std::fs::File; use std::path::PathBuf; /// ### init /// /// Initialize logger -pub fn init() -> Result<(), String> { +pub fn init(level: LogLevel) -> Result<(), String> { // Init config dir let config_dir: PathBuf = match init_config_dir() { Ok(Some(p)) => p, @@ -56,7 +57,7 @@ pub fn init() -> Result<(), String> { .set_time_format_str("%Y-%m-%dT%H:%M:%S%z") .build(); // Make logger - WriteLogger::init(LevelFilter::Trace, config, file) + WriteLogger::init(level, config, file) .map_err(|e| format!("Failed to initialize logger: {}", e)) } @@ -67,6 +68,6 @@ mod test { #[test] fn test_system_logging_setup() { - assert!(init().is_ok()); + assert!(init(LogLevel::Trace).is_ok()); } } From 5117891f87d8e3e3402de117379f4e52501a539e Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 12 Dec 2021 13:19:12 +0100 Subject: [PATCH 24/45] Added help footers --- src/ui/activities/auth/components/mod.rs | 11 ++++- src/ui/activities/auth/components/text.rs | 25 ++++++---- src/ui/activities/auth/mod.rs | 2 +- src/ui/activities/auth/view.rs | 46 +++++++++++++++---- .../filetransfer/components/misc.rs | 22 ++++----- src/ui/activities/setup/components/commons.rs | 15 +++--- src/ui/activities/setup/components/mod.rs | 15 ++++-- src/ui/activities/setup/view/mod.rs | 21 +++++++++ src/ui/activities/setup/view/setup.rs | 9 ++-- src/ui/activities/setup/view/ssh_keys.rs | 6 +-- src/ui/activities/setup/view/theme.rs | 11 +++-- 11 files changed, 130 insertions(+), 53 deletions(-) diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index 4791f4f1..c7c43303 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -44,7 +44,7 @@ pub use popup::{ ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup, WindowSizeError, }; -pub use text::{HelpText, NewVersionDisclaimer, Subtitle, Title}; +pub use text::{HelpFooter, NewVersionDisclaimer, Subtitle, Title}; use tui_realm_stdlib::Phantom; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; @@ -60,7 +60,10 @@ pub struct GlobalListener { impl Component for GlobalListener { fn on(&mut self, ev: Event) -> Option { match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::ShowQuitPopup), + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Function(10), + .. + }) => Some(Msg::ShowQuitPopup), Event::Keyboard(KeyEvent { code: Key::Char('c'), modifiers: KeyModifiers::CONTROL, @@ -69,6 +72,10 @@ impl Component for GlobalListener { code: Key::Char('h'), modifiers: KeyModifiers::CONTROL, }) => Some(Msg::ShowKeybindingsPopup), + Event::Keyboard(KeyEvent { + code: Key::Function(1), + .. + }) => Some(Msg::ShowKeybindingsPopup), Event::Keyboard(KeyEvent { code: Key::Char('r'), modifiers: KeyModifiers::CONTROL, diff --git a/src/ui/activities/auth/components/text.rs b/src/ui/activities/auth/components/text.rs index c2f7fa74..2e32324d 100644 --- a/src/ui/activities/auth/components/text.rs +++ b/src/ui/activities/auth/components/text.rs @@ -104,28 +104,35 @@ impl Component for NewVersionDisclaimer { } } -// -- HelpText +// -- HelpFooter #[derive(MockComponent)] -pub struct HelpText { +pub struct HelpFooter { component: Span, } -impl HelpText { +impl HelpFooter { pub fn new(key_color: Color) -> Self { Self { component: Span::default().spans(&[ - TextSpan::new("Press ").bold(), - TextSpan::new("").bold().fg(key_color), - TextSpan::new(" to show keybindings; ").bold(), - TextSpan::new("").bold().fg(key_color), - TextSpan::new(" to enter setup").bold(), + TextSpan::from("").bold().fg(key_color), + TextSpan::from(" Help "), + TextSpan::from("").bold().fg(key_color), + TextSpan::from(" Enter setup "), + TextSpan::from("").bold().fg(key_color), + TextSpan::from(" Change field "), + TextSpan::from("").bold().fg(key_color), + TextSpan::from(" Switch tab "), + TextSpan::from("").bold().fg(key_color), + TextSpan::from(" Submit form "), + TextSpan::from("").bold().fg(key_color), + TextSpan::from(" Quit "), ]), } } } -impl Component for HelpText { +impl Component for HelpFooter { fn on(&mut self, _ev: Event) -> Option { None } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 2c8fd280..56e42118 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -55,7 +55,7 @@ pub enum Id { DeleteRecentPopup, ErrorPopup, GlobalListener, - HelpText, + HelpFooter, InfoPopup, InstallUpdatePopup, Keybindings, diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index e5a979af..9d2b1c6b 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -58,8 +58,8 @@ impl AuthActivity { assert!(self .app .mount( - Id::HelpText, - Box::new(components::HelpText::new(key_color)), + Id::HelpFooter, + Box::new(components::HelpFooter::new(key_color)), vec![] ) .is_ok()); @@ -111,17 +111,30 @@ impl AuthActivity { let height: u16 = f.size().height; self.check_minimum_window_size(height); // Prepare chunks - let chunks = Layout::default() + let body = Layout::default() .direction(Direction::Vertical) - .margin(1) .constraints( [ - Constraint::Length(21), // Auth Form - Constraint::Min(3), // Bookmarks + Constraint::Min(24), // Body + Constraint::Length(1), // Footer ] .as_ref(), ) .split(f.size()); + // Footer + self.app.view(&Id::HelpFooter, f, body[1]); + let auth_form_len = 7 + self.input_mask_size(); + let main_chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Length(auth_form_len), // Auth Form + Constraint::Min(3), // Bookmarks + ] + .as_ref(), + ) + .split(body[0]); // Create explorer chunks let auth_chunks = Layout::default() .constraints( @@ -131,12 +144,12 @@ impl AuthActivity { Constraint::Length(1), // Version Constraint::Length(3), // protocol Constraint::Length(self.input_mask_size()), // Input mask - Constraint::Length(3), // footer + Constraint::Length(1), // Prevents last field to overflow ] .as_ref(), ) .direction(Direction::Vertical) - .split(chunks[0]); + .split(main_chunks[0]); // Input mask chunks let input_mask = match self.input_mask() { InputMask::AwsS3 => Layout::default() @@ -167,7 +180,7 @@ impl AuthActivity { let bookmark_chunks = Layout::default() .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .direction(Direction::Horizontal) - .split(chunks[1]); + .split(main_chunks[1]); // Render // Auth chunks self.app.view(&Id::Title, f, auth_chunks[0]); @@ -188,7 +201,6 @@ impl AuthActivity { self.app.view(&Id::Password, f, input_mask[3]); } } - self.app.view(&Id::HelpText, f, auth_chunks[5]); // Bookmark chunks self.app.view(&Id::BookmarksList, f, bookmark_chunks[0]); self.app.view(&Id::RecentsList, f, bookmark_chunks[1]); @@ -788,6 +800,13 @@ impl AuthActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(10), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), Sub::new( SubEventClause::Keyboard(KeyEvent { code: Key::Char('c'), @@ -802,6 +821,13 @@ impl AuthActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(1), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), Sub::new( SubEventClause::Keyboard(KeyEvent { code: Key::Char('r'), diff --git a/src/ui/activities/filetransfer/components/misc.rs b/src/ui/activities/filetransfer/components/misc.rs index 0b950963..ef903be1 100644 --- a/src/ui/activities/filetransfer/components/misc.rs +++ b/src/ui/activities/filetransfer/components/misc.rs @@ -40,27 +40,27 @@ impl FooterBar { pub fn new(key_color: Color) -> Self { Self { component: Span::default().spans(&[ - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Help "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Change tab "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Transfer "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Enter dir "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" View "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Edit "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Copy "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Rename "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Make dir "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Make dir "), - TextSpan::from("").fg(key_color), + TextSpan::from("").bold().fg(key_color), TextSpan::from(" Quit "), ]), } diff --git a/src/ui/activities/setup/components/commons.rs b/src/ui/activities/setup/components/commons.rs index bd620003..e9f212c7 100644 --- a/src/ui/activities/setup/components/commons.rs +++ b/src/ui/activities/setup/components/commons.rs @@ -76,13 +76,16 @@ impl Default for Footer { fn default() -> Self { Self { component: Span::default().spans(&[ - TextSpan::new("Press ").bold(), - TextSpan::new("").bold().fg(Color::Cyan), - TextSpan::new(" to show keybindings; ").bold(), - TextSpan::new("").bold().fg(Color::Cyan), - TextSpan::new(" to save parameters; ").bold(), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" Help "), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" Save parameters "), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" Exit "), TextSpan::new("").bold().fg(Color::Cyan), - TextSpan::new(" to change panel").bold(), + TextSpan::new(" Change panel "), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" Change field "), ]), } } diff --git a/src/ui/activities/setup/components/mod.rs b/src/ui/activities/setup/components/mod.rs index 841dad06..7be63520 100644 --- a/src/ui/activities/setup/components/mod.rs +++ b/src/ui/activities/setup/components/mod.rs @@ -54,9 +54,10 @@ pub struct GlobalListener { impl Component for GlobalListener { fn on(&mut self, ev: Event) -> Option { match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Common(CommonMsg::ShowQuitPopup)) - } + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Function(10), + .. + }) => Some(Msg::Common(CommonMsg::ShowQuitPopup)), Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { Some(Msg::Common(CommonMsg::ChangeLayout)) } @@ -64,6 +65,10 @@ impl Component for GlobalListener { code: Key::Char('h'), modifiers: KeyModifiers::CONTROL, }) => Some(Msg::Common(CommonMsg::ShowKeybindings)), + Event::Keyboard(KeyEvent { + code: Key::Function(1), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Common(CommonMsg::ShowKeybindings)), Event::Keyboard(KeyEvent { code: Key::Char('r'), modifiers: KeyModifiers::CONTROL, @@ -72,6 +77,10 @@ impl Component for GlobalListener { code: Key::Char('s'), modifiers: KeyModifiers::CONTROL, }) => Some(Msg::Common(CommonMsg::ShowSavePopup)), + Event::Keyboard(KeyEvent { + code: Key::Function(4), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Common(CommonMsg::ShowSavePopup)), _ => None, } } diff --git a/src/ui/activities/setup/view/mod.rs b/src/ui/activities/setup/view/mod.rs index 1f36e19e..796a601b 100644 --- a/src/ui/activities/setup/view/mod.rs +++ b/src/ui/activities/setup/view/mod.rs @@ -206,6 +206,13 @@ impl SetupActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(10), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), Sub::new( SubEventClause::Keyboard(KeyEvent { code: Key::Tab, @@ -220,6 +227,13 @@ impl SetupActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(1), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), Sub::new( SubEventClause::Keyboard(KeyEvent { code: Key::Char('r'), @@ -234,6 +248,13 @@ impl SetupActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(4), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), ] ) .is_ok()); diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 14ede283..65566a9f 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -59,9 +59,9 @@ impl SetupActivity { .margin(1) .constraints( [ - Constraint::Length(3), // Current tab - Constraint::Length(18), // Main body - Constraint::Length(3), // Help footer + Constraint::Length(3), // Current tab + Constraint::Min(18), // Main body + Constraint::Length(1), // Help footer ] .as_ref(), ) @@ -85,6 +85,7 @@ impl SetupActivity { Constraint::Length(3), // Updates tab Constraint::Length(3), // Prompt file replace Constraint::Length(3), // Group dirs + Constraint::Length(1), // Prevent overflow ] .as_ref(), ) @@ -120,7 +121,7 @@ impl SetupActivity { Constraint::Length(3), // Notifications enabled Constraint::Length(3), // Notifications threshold Constraint::Length(3), // Ssh config - Constraint::Length(1), // Filler + Constraint::Length(1), // Prevent overflow ] .as_ref(), ) diff --git a/src/ui/activities/setup/view/ssh_keys.rs b/src/ui/activities/setup/view/ssh_keys.rs index e029ecb0..19f5fc6c 100644 --- a/src/ui/activities/setup/view/ssh_keys.rs +++ b/src/ui/activities/setup/view/ssh_keys.rs @@ -56,9 +56,9 @@ impl SetupActivity { .margin(1) .constraints( [ - Constraint::Length(3), // Current tab - Constraint::Percentage(90), // Main body - Constraint::Length(3), // Help footer + Constraint::Length(3), // Current tab + Constraint::Min(5), // Main body + Constraint::Length(1), // Help footer ] .as_ref(), ) diff --git a/src/ui/activities/setup/view/theme.rs b/src/ui/activities/setup/view/theme.rs index c1cf2f2a..89901366 100644 --- a/src/ui/activities/setup/view/theme.rs +++ b/src/ui/activities/setup/view/theme.rs @@ -56,9 +56,9 @@ impl SetupActivity { .margin(1) .constraints( [ - Constraint::Length(3), // Current tab - Constraint::Length(22), // Main body - Constraint::Length(3), // Help footer + Constraint::Length(3), // Current tab + Constraint::Min(22), // Main body + Constraint::Length(1), // Help footer ] .as_ref(), ) @@ -91,6 +91,7 @@ impl SetupActivity { Constraint::Length(3), // Password Constraint::Length(3), // Bookmarks Constraint::Length(3), // Recents + Constraint::Length(1), // Prevent overflow ] .as_ref(), ) @@ -126,6 +127,7 @@ impl SetupActivity { Constraint::Length(3), // Quit Constraint::Length(3), // Save Constraint::Length(3), // Warn + Constraint::Length(1), // Prevent overflow ] .as_ref(), ) @@ -158,7 +160,7 @@ impl SetupActivity { Constraint::Length(3), // remote explorer bg Constraint::Length(3), // remote explorer fg Constraint::Length(3), // remote explorer hg - Constraint::Length(3), // empty + Constraint::Length(1), // Prevent overflow ] .as_ref(), ) @@ -210,6 +212,7 @@ impl SetupActivity { Constraint::Length(3), // status sorting Constraint::Length(3), // status hidden Constraint::Length(3), // sync browsing + Constraint::Length(1), // Prevent overflow ] .as_ref(), ) From 679eb69d391416fece04cb9f3fe50aa5e311a47b Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Sun, 12 Dec 2021 20:41:10 +0100 Subject: [PATCH 25/45] Sync browsing prompts to create dir (#85) --- CHANGELOG.md | 2 + docs/de/man.md | 2 - docs/es/man.md | 2 - docs/fr/man.md | 2 - docs/it/man.md | 2 - docs/man.md | 2 - docs/zh-CN/man.md | 2 - .../filetransfer/actions/change_dir.rs | 186 +++++++++++++++--- src/ui/activities/filetransfer/actions/mod.rs | 4 +- .../filetransfer/actions/pending.rs | 63 ++++++ .../activities/filetransfer/actions/submit.rs | 20 +- .../activities/filetransfer/components/mod.rs | 5 +- .../filetransfer/components/popups.rs | 66 ++++++- src/ui/activities/filetransfer/mod.rs | 14 +- src/ui/activities/filetransfer/update.rs | 38 ++-- src/ui/activities/filetransfer/view.rs | 33 +++- 16 files changed, 361 insertions(+), 82 deletions(-) create mode 100644 src/ui/activities/filetransfer/actions/pending.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d3f5a9d..5d2cb561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,8 @@ Released on FIXME: - **Less verbose logging**: - By default the log level is now set to `INFO` - It is now possible to enable the `TRACE` level with the `-D` CLI option. +- **Synchronized browsing**: + - From now on, if synchronized browsing is *enabled* and you try to enter a directory that doesn't exist on the other host, you will be asked whether you'd like to create the directory. - Dependencies: - Updated `tui-realm` to `1.3.0` - Updated `tui-realm-stdlib` to `1.1.4` diff --git a/docs/de/man.md b/docs/de/man.md index 67c519a7..0d464905 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -201,8 +201,6 @@ All the actions are available when working with multiple files, but be aware tha When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels. This means that whenever you'll change the working directory on one panel, the same action will be reproduced on the other panel. If you want to enable synchronized browsing just press ``; press twice to disable. While enabled, the synchronized browsing state will be reported on the status bar on `ON`. -> ❗ at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update. - ### Open and Open With 🚪 Open and open with commands are powered by [open-rs](https://docs.rs/crate/open/1.7.0). diff --git a/docs/es/man.md b/docs/es/man.md index e859ef6c..b6c7ebdc 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -201,8 +201,6 @@ Todas las acciones están disponibles cuando se trabaja con varios archivos, per Cuando está habilitada, la navegación sincronizada le permitirá sincronizar la navegación entre los dos paneles. Esto significa que siempre que cambie el directorio de trabajo en un panel, la misma acción se reproducirá en el otro panel. Si desea habilitar la navegación sincronizada, simplemente presione ``; presione dos veces para deshabilitar. Mientras está habilitado, el estado de navegación sincronizada se informará en la barra de estado en "ON". -> ❗ Por el momento, cada vez que intente acceder a un directorio que no existe, no se le pedirá que lo cree. Esto podría cambiar en una actualización futura. - ### Abierta y abierta con 🚪 Al abrir archivos con el comando Ver (``), se utilizará la aplicación predeterminada del sistema para el tipo de archivo. Para hacerlo, se utilizará el servicio del sistema operativo predeterminado, así que asegúrese de tener al menos uno de estos instalado en su sistema: diff --git a/docs/fr/man.md b/docs/fr/man.md index 578dfa5c..57cf3869 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -199,8 +199,6 @@ Toutes les actions sont disponibles lorsque vous travaillez avec plusieurs fichi Lorsqu'elle est activée, la navigation synchronisée vous permettra de synchroniser la navigation entre les deux panneaux. Cela signifie que chaque fois que vous changerez de répertoire de travail sur un panneau, la même action sera reproduite sur l'autre panneau. Si vous souhaitez activer la navigation synchronisée, appuyez simplement sur `` ; appuyez deux fois pour désactiver. Lorsqu'il est activé, l'état de navigation synchronisé sera signalé dans la barre d'état sur `ON` -> ❗ pour le moment, chaque fois que vous essayez d'accéder à un répertoire inexistant, vous ne serez pas invité à le créer. Cela pourrait changer dans une future mise à jour. - ### Ouvrir et ouvrir avec 🚪 Lors de l'ouverture de fichiers avec la commande Afficher (``), l'application par défaut du système pour le type de fichier sera utilisée. Pour ce faire, le service du système d'exploitation par défaut sera utilisé, alors assurez-vous d'avoir au moins l'un de ceux-ci installé sur votre système : diff --git a/docs/it/man.md b/docs/it/man.md index 3c0edae0..0bebba2c 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -195,8 +195,6 @@ Tutte le azioni sono disponibili quando si lavora sulle selezioni, ma occhio, ch Quando abilitato, ti permetterà di sincronizzare la navigazione tra i due pannelli. Ciò comporta che quando cambierai directory in uno dei due pannelli, lo stesso verrà fatto nell'altro. Per abilitare la modalità è sufficiente premere ``; fai lo stesso per disabilitarlo. Mentre abilitato, sull'interfaccia dovrebbe essere visualizzato `Sync Browsing: ON` nella barra di stato. -> ❗ Al momento, se provi ad accedere ad una cartella non esistente su uno dei due host, mentre il sync browsing è attivo, non ti verrà chiesto di crearla, ma semplicemente fallirà. Questo sarà risolto in un aggiornamento futuro. - ### Apri e apri con 🚪 I comandi "apri" e "apri con" sono forniti da [open-rs](https://docs.rs/crate/open/2.1.0). diff --git a/docs/man.md b/docs/man.md index d93e7cc9..b13a8c33 100644 --- a/docs/man.md +++ b/docs/man.md @@ -199,8 +199,6 @@ All the actions are available when working with multiple files, but be aware tha When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels. This means that whenever you'll change the working directory on one panel, the same action will be reproduced on the other panel. If you want to enable synchronized browsing just press ``; press twice to disable. While enabled, the synchronized browsing state will be reported on the status bar on `ON`. -> ❗ at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update. - ### Open and Open With 🚪 Open and open with commands are powered by [open-rs](https://docs.rs/crate/open/1.7.0). diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index 1bfe2c30..ca47eab9 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -193,8 +193,6 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到 启用时,同步浏览将允许你在两个面板之间同步导航操作。这意味着,每当你在一个面板上改变工作目录时,同样的动作会在另一个面板上重现。如果你想启用同步浏览,只需按下``;按两次就可以禁用。当启用时,同步浏览的状态将在状态栏上显示为`ON`。 -> ❗ 目前,每当你试图访问一个不存在的目录,你不会被提示创建它。这点可能会在未来的更新中改进。 - ### 打开/打开方式 打开和打开方式的功能是由 [open-rs](https://docs.rs/crate/open/2.1.0)提供的。 diff --git a/src/ui/activities/filetransfer/actions/change_dir.rs b/src/ui/activities/filetransfer/actions/change_dir.rs index 10ffa4d0..b4e17c6c 100644 --- a/src/ui/activities/filetransfer/actions/change_dir.rs +++ b/src/ui/activities/filetransfer/actions/change_dir.rs @@ -26,84 +26,87 @@ * SOFTWARE. */ // locals -use super::FileTransferActivity; +use super::{FileExplorerTab, FileTransferActivity, LogLevel, Msg, PendingActionMsg}; use remotefs::Directory; use std::path::PathBuf; +/// Describes destination for sync browsing +enum SyncBrowsingDestination { + Path(String), + ParentDir, + PreviousDir, +} + impl FileTransferActivity { /// Enter a directory on local host from entry - /// Return true whether the directory changed - pub(crate) fn action_enter_local_dir(&mut self, dir: Directory, block_sync: bool) -> bool { + pub(crate) fn action_enter_local_dir(&mut self, dir: Directory) { self.local_changedir(dir.path.as_path(), true); - if self.browser.sync_browsing && !block_sync { - self.action_change_remote_dir(dir.name, true); + if self.browser.sync_browsing { + self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name)); } - true } /// Enter a directory on local host from entry - /// Return true whether the directory changed - pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory, block_sync: bool) -> bool { + pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory) { self.remote_changedir(dir.path.as_path(), true); - if self.browser.sync_browsing && !block_sync { - self.action_change_local_dir(dir.name, true); + if self.browser.sync_browsing { + self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name)); } - true } /// Change local directory reading value from input - pub(crate) fn action_change_local_dir(&mut self, input: String, block_sync: bool) { + pub(crate) fn action_change_local_dir(&mut self, input: String) { 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); + if self.browser.sync_browsing { + self.synchronize_browsing(SyncBrowsingDestination::Path(input)); } } /// Change remote directory reading value from input - pub(crate) fn action_change_remote_dir(&mut self, input: String, block_sync: bool) { + pub(crate) fn action_change_remote_dir(&mut self, input: String) { 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); + if self.browser.sync_browsing { + self.synchronize_browsing(SyncBrowsingDestination::Path(input)); } } /// Go to previous directory from localhost - pub(crate) fn action_go_to_previous_local_dir(&mut self, block_sync: bool) { + pub(crate) fn action_go_to_previous_local_dir(&mut self) { if let Some(d) = self.local_mut().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); + if self.browser.sync_browsing { + self.synchronize_browsing(SyncBrowsingDestination::PreviousDir); } } } /// Go to previous directory from remote host - pub(crate) fn action_go_to_previous_remote_dir(&mut self, block_sync: bool) { + pub(crate) fn action_go_to_previous_remote_dir(&mut self) { if let Some(d) = self.remote_mut().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); + if self.browser.sync_browsing { + self.synchronize_browsing(SyncBrowsingDestination::PreviousDir); } } } /// Go to upper directory on local host - pub(crate) fn action_go_to_local_upper_dir(&mut self, block_sync: bool) { + pub(crate) fn action_go_to_local_upper_dir(&mut self) { // 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); + if self.browser.sync_browsing { + self.synchronize_browsing(SyncBrowsingDestination::ParentDir); } } } @@ -111,15 +114,140 @@ impl FileTransferActivity { /// #### action_go_to_remote_upper_dir /// /// Go to upper directory on remote host - pub(crate) fn action_go_to_remote_upper_dir(&mut self, block_sync: bool) { + pub(crate) fn action_go_to_remote_upper_dir(&mut self) { // 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); + if self.browser.sync_browsing { + self.synchronize_browsing(SyncBrowsingDestination::ParentDir); + } + } + } + + // -- sync browsing + + /// Synchronize browsing on the target browser. + /// If destination doesn't exist, then prompt for directory creation. + fn synchronize_browsing(&mut self, destination: SyncBrowsingDestination) { + // Get destination path + let path = match self.resolve_sync_browsing_destination(&destination) { + Some(p) => p, + None => return, + }; + trace!("Synchronizing browsing to path {}", path.display()); + // Check whether destination exists on host + let exists = match self.browser.tab() { + FileExplorerTab::Local => match self.client.exists(path.as_path()) { + Ok(e) => e, + Err(err) => { + error!( + "Failed to check whether {} exists on remote: {}", + path.display(), + err + ); + return; + } + }, + FileExplorerTab::Remote => self.host.file_exists(path.as_path()), + _ => return, + }; + let name = path + .file_name() + .map(|x| x.to_string_lossy().to_string()) + .unwrap(); + // If file doesn't exist, ask whether to create directory + if !exists { + trace!("Directory doesn't exist; asking to user if I should create it"); + // Mount dialog + self.mount_sync_browsing_mkdir_popup(&name); + // Wait for dialog dismiss + if self.wait_for_pending_msg(&[ + Msg::PendingAction(PendingActionMsg::MakePendingDirectory), + Msg::PendingAction(PendingActionMsg::CloseSyncBrowsingMkdirPopup), + ]) == Msg::PendingAction(PendingActionMsg::MakePendingDirectory) + { + trace!("User wants to create the unexisting directory"); + // Make directory + match self.browser.tab() { + FileExplorerTab::Local => self.action_remote_mkdir(name.clone()), + FileExplorerTab::Remote => self.action_local_mkdir(name.clone()), + _ => {} + } + } else { + // Do not synchronize, disable sync browsing and return + trace!("The user doesn't want to create the directory; disabling synchronized browsing"); + self.log( + LogLevel::Warn, + format!( + "Refused to create '{}'; synchronized browsing disabled", + name + ), + ); + self.browser.toggle_sync_browsing(); + self.refresh_remote_status_bar(); + self.umount_sync_browsing_mkdir_popup(); + return; + } + // Umount dialog + self.umount_sync_browsing_mkdir_popup(); + } + trace!("Entering on the other explorer directory {}", name); + // Enter directory + match destination { + SyncBrowsingDestination::ParentDir => match self.browser.tab() { + FileExplorerTab::Local => self.remote_changedir(path.as_path(), true), + FileExplorerTab::Remote => self.local_changedir(path.as_path(), true), + _ => {} + }, + SyncBrowsingDestination::Path(_) => match self.browser.tab() { + FileExplorerTab::Local => self.remote_changedir(path.as_path(), true), + FileExplorerTab::Remote => self.local_changedir(path.as_path(), true), + _ => {} + }, + SyncBrowsingDestination::PreviousDir => match self.browser.tab() { + FileExplorerTab::Local => self.remote_changedir(path.as_path(), false), + FileExplorerTab::Remote => self.local_changedir(path.as_path(), false), + _ => {} + }, + } + } + + /// Resolve synchronized browsing destination + fn resolve_sync_browsing_destination( + &mut self, + destination: &SyncBrowsingDestination, + ) -> Option { + match (destination, self.browser.tab()) { + // NOTE: tab and methods are switched on purpose + (SyncBrowsingDestination::ParentDir, FileExplorerTab::Local) => { + self.remote().wrkdir.parent().map(|x| x.to_path_buf()) + } + (SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => { + self.local().wrkdir.parent().map(|x| x.to_path_buf()) + } + (SyncBrowsingDestination::PreviousDir, FileExplorerTab::Local) => { + if let Some(p) = self.remote_mut().popd() { + Some(p) + } else { + warn!("Cannot synchronize browsing: remote has no previous directory in stack"); + None + } + } + (SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => { + if let Some(p) = self.local_mut().popd() { + Some(p) + } else { + warn!("Cannot synchronize browsing: local has no previous directory in stack"); + None + } + } + (SyncBrowsingDestination::Path(p), _) => Some(PathBuf::from(p.as_str())), + _ => { + warn!("Cannot synchronize browsing for current explorer"); + None } } } diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index 5b51a46f..4443f989 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -26,7 +26,8 @@ * SOFTWARE. */ pub(self) use super::{ - browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, TransferOpts, TransferPayload, + browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg, + TransferOpts, TransferPayload, }; pub(self) use remotefs::Entry; use tuirealm::{State, StateValue}; @@ -41,6 +42,7 @@ pub(crate) mod find; pub(crate) mod mkdir; pub(crate) mod newfile; pub(crate) mod open; +mod pending; pub(crate) mod rename; pub(crate) mod save; pub(crate) mod submit; diff --git a/src/ui/activities/filetransfer/actions/pending.rs b/src/ui/activities/filetransfer/actions/pending.rs new file mode 100644 index 00000000..852ef80f --- /dev/null +++ b/src/ui/activities/filetransfer/actions/pending.rs @@ -0,0 +1,63 @@ +//! ## Pending actions +//! +//! this little module exposes the routine to create a pending action on the file transfer activity. +//! A pending action is an action which blocks the execution of the application in await of a certain `Msg`. + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use super::{FileTransferActivity, Msg}; + +use tuirealm::PollStrategy; + +impl FileTransferActivity { + /// Block execution of activity, preventing ANY kind of message not specified in the `wait_for` argument. + /// Once `wait_for` clause is satisfied, the function returns. + /// + /// Returns the message which satisfied the clause + /// + /// NOTE: The view is redrawn as usual + pub(super) fn wait_for_pending_msg(&mut self, wait_for: &[Msg]) -> Msg { + self.redraw = true; + loop { + // Poll + match self.app.tick(PollStrategy::Once) { + Ok(messages) => { + if !messages.is_empty() { + self.redraw = true; + } + if let Some(msg) = messages.into_iter().find(|m| wait_for.contains(m)) { + return msg; + } + } + Err(err) => { + error!("Application error: {}", err); + } + } + // Redraw + if self.redraw { + self.view(); + } + } + } +} diff --git a/src/ui/activities/filetransfer/actions/submit.rs b/src/ui/activities/filetransfer/actions/submit.rs index 692ff987..6aff312c 100644 --- a/src/ui/activities/filetransfer/actions/submit.rs +++ b/src/ui/activities/filetransfer/actions/submit.rs @@ -38,7 +38,7 @@ enum SubmitAction { impl FileTransferActivity { /// Decides which action to perform on submit for local explorer /// Return true whether the directory changed - pub(crate) fn action_submit_local(&mut self, entry: Entry) -> bool { + pub(crate) fn action_submit_local(&mut self, entry: Entry) { let (action, entry) = match &entry { Entry::Directory(_) => (SubmitAction::ChangeDir, entry), Entry::File(File { @@ -67,18 +67,14 @@ impl FileTransferActivity { } Entry::File(_) => (SubmitAction::None, entry), }; - match (action, entry) { - (SubmitAction::ChangeDir, Entry::Directory(dir)) => { - self.action_enter_local_dir(dir, false) - } - (SubmitAction::ChangeDir, _) => false, - (SubmitAction::None, _) => false, + if let (SubmitAction::ChangeDir, Entry::Directory(dir)) = (action, entry) { + self.action_enter_local_dir(dir) } } /// Decides which action to perform on submit for remote explorer /// Return true whether the directory changed - pub(crate) fn action_submit_remote(&mut self, entry: Entry) -> bool { + pub(crate) fn action_submit_remote(&mut self, entry: Entry) { let (action, entry) = match &entry { Entry::Directory(_) => (SubmitAction::ChangeDir, entry), Entry::File(File { @@ -107,12 +103,8 @@ impl FileTransferActivity { } Entry::File(_) => (SubmitAction::None, entry), }; - match (action, entry) { - (SubmitAction::ChangeDir, Entry::Directory(dir)) => { - self.action_enter_remote_dir(dir, false) - } - (SubmitAction::ChangeDir, _) => false, - (SubmitAction::None, _) => false, + if let (SubmitAction::ChangeDir, Entry::Directory(dir)) = (action, entry) { + self.action_enter_remote_dir(dir) } } } diff --git a/src/ui/activities/filetransfer/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs index 6d54b4cd..442c1611 100644 --- a/src/ui/activities/filetransfer/components/mod.rs +++ b/src/ui/activities/filetransfer/components/mod.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use super::{Msg, TransferMsg, UiMsg}; +use super::{Msg, PendingActionMsg, TransferMsg, UiMsg}; use tui_realm_stdlib::Phantom; use tuirealm::{ @@ -45,7 +45,8 @@ pub use popups::{ CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup, FileInfoPopup, FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup, - ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, WaitPopup, + ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, + SyncBrowsingMkdirPopup, WaitPopup, }; pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote}; diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index 2084123f..0f06026f 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -26,7 +26,7 @@ * SOFTWARE. */ use super::super::Browser; -use super::{Msg, TransferMsg, UiMsg}; +use super::{Msg, PendingActionMsg, TransferMsg, UiMsg}; use crate::explorer::FileSorting; use crate::utils::fmt::fmt_time; @@ -1657,6 +1657,70 @@ fn hidden_files_label(visible: bool) -> &'static str { } } +#[derive(MockComponent)] +pub struct SyncBrowsingMkdirPopup { + component: Radio, +} + +impl SyncBrowsingMkdirPopup { + pub fn new(color: Color, dir_name: &str) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Yes", "No"]) + .title( + format!( + r#"Sync browsing: directory "{}" doesn't exist. Do you want to create it?"#, + dir_name + ), + Alignment::Center, + ), + } + } +} + +impl Component for SyncBrowsingMkdirPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::PendingAction( + PendingActionMsg::CloseSyncBrowsingMkdirPopup, + )), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)) + } else { + Some(Msg::PendingAction( + PendingActionMsg::CloseSyncBrowsingMkdirPopup, + )) + } + } + _ => None, + } + } +} + #[derive(MockComponent)] pub struct WaitPopup { component: Paragraph, diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 543a9c47..07860fac 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -56,8 +56,10 @@ use tuirealm::{Application, EventListenerCfg, NoUserEvent}; // -- Storage keys -const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH"; -const STORAGE_PENDING_TRANSFER: &str = "FILETRANSFER_PENDING_TRANSFER"; +/// Stores the explorer width +const STORAGE_EXPLORER_WIDTH: &str = "FT_EW"; +/// Stores the filename of the entry to transfer, when the replace file dialog must be shown +const STORAGE_PENDING_TRANSFER: &str = "FT_PT"; // -- components @@ -92,16 +94,24 @@ enum Id { SortingPopup, StatusBarLocal, StatusBarRemote, + SyncBrowsingMkdirPopup, WaitPopup, } #[derive(Debug, PartialEq)] enum Msg { + PendingAction(PendingActionMsg), Transfer(TransferMsg), Ui(UiMsg), None, } +#[derive(Debug, PartialEq)] +enum PendingActionMsg { + CloseSyncBrowsingMkdirPopup, + MakePendingDirectory, +} + #[derive(Debug, PartialEq)] enum TransferMsg { AbortTransfer, diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index e60108c6..de43b691 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -42,6 +42,10 @@ impl Update for FileTransferActivity { fn update(&mut self, msg: Option) -> Option { match msg.unwrap_or(Msg::None) { Msg::None => None, + Msg::PendingAction(_) => { + // NOTE: Pending actions must be handled directly in the action + None + } Msg::Transfer(msg) => self.update_transfer(msg), Msg::Ui(msg) => self.update_ui(msg), } @@ -106,24 +110,22 @@ impl FileTransferActivity { } TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Local => { if let SelectedEntry::One(entry) = self.get_local_selected_entries() { - if self.action_submit_local(entry) { - // Update file list if sync - if self.browser.sync_browsing { - let _ = self.update_remote_filelist(); - } - self.update_local_filelist(); + self.action_submit_local(entry); + // Update file list if sync + if self.browser.sync_browsing { + let _ = self.update_remote_filelist(); } + self.update_local_filelist(); } } TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => { if let SelectedEntry::One(entry) = self.get_remote_selected_entries() { - if self.action_submit_remote(entry) { - // Update file list if sync - if self.browser.sync_browsing { - let _ = self.update_local_filelist(); - } - self.update_remote_filelist(); + self.action_submit_remote(entry); + // Update file list if sync + if self.browser.sync_browsing { + let _ = self.update_local_filelist(); } + self.update_remote_filelist(); } } TransferMsg::EnterDirectory => { @@ -152,8 +154,8 @@ impl FileTransferActivity { } TransferMsg::GoTo(dir) => { match self.browser.tab() { - FileExplorerTab::Local => self.action_change_local_dir(dir, false), - FileExplorerTab::Remote => self.action_change_remote_dir(dir, false), + FileExplorerTab::Local => self.action_change_local_dir(dir), + FileExplorerTab::Remote => self.action_change_remote_dir(dir), _ => panic!("Found tab doesn't support GOTO"), } // Umount @@ -168,7 +170,7 @@ impl FileTransferActivity { TransferMsg::GoToParentDirectory => { match self.browser.tab() { FileExplorerTab::Local => { - self.action_go_to_local_upper_dir(false); + self.action_go_to_local_upper_dir(); if self.browser.sync_browsing { let _ = self.update_remote_filelist(); } @@ -176,7 +178,7 @@ impl FileTransferActivity { self.update_local_filelist() } FileExplorerTab::Remote => { - self.action_go_to_remote_upper_dir(false); + self.action_go_to_remote_upper_dir(); if self.browser.sync_browsing { let _ = self.update_local_filelist(); } @@ -189,7 +191,7 @@ impl FileTransferActivity { TransferMsg::GoToPreviousDirectory => { match self.browser.tab() { FileExplorerTab::Local => { - self.action_go_to_previous_local_dir(false); + self.action_go_to_previous_local_dir(); if self.browser.sync_browsing { let _ = self.update_remote_filelist(); } @@ -197,7 +199,7 @@ impl FileTransferActivity { self.update_local_filelist() } FileExplorerTab::Remote => { - self.action_go_to_previous_remote_dir(false); + self.action_go_to_previous_remote_dir(); if self.browser.sync_browsing { let _ = self.update_local_filelist(); } diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 0ae02b21..fc09e7e3 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -307,6 +307,11 @@ impl FileTransferActivity { f.render_widget(Clear, popup); // make popup self.app.view(&Id::WaitPopup, f, popup); + } else if self.app.mounted(&Id::SyncBrowsingMkdirPopup) { + let popup = draw_area_in(f.size(), 60, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::SyncBrowsingMkdirPopup, f, popup); } else if self.app.mounted(&Id::KeybindingsPopup) { let popup = draw_area_in(f.size(), 50, 80); f.render_widget(Clear, popup); @@ -798,6 +803,23 @@ impl FileTransferActivity { .is_ok()); } + pub(super) fn mount_sync_browsing_mkdir_popup(&mut self, dir_name: &str) { + let color = self.theme().misc_info_dialog; + assert!(self + .app + .remount( + Id::SyncBrowsingMkdirPopup, + Box::new(components::SyncBrowsingMkdirPopup::new(color, dir_name,)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::SyncBrowsingMkdirPopup).is_ok()); + } + + pub(super) fn umount_sync_browsing_mkdir_popup(&mut self) { + let _ = self.app.umount(&Id::SyncBrowsingMkdirPopup); + } + /// Mount help pub(super) fn mount_help(&mut self) { let key_color = self.theme().misc_keys; @@ -949,9 +971,14 @@ impl FileTransferActivity { Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::FindPopup, )))), - Box::new(SubClause::Not(Box::new(SubClause::IsMounted( - Id::WaitPopup, - )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::SyncBrowsingMkdirPopup, + )))), + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::WaitPopup, + )))), + )), )), )), )), From e9266a5c7ededee6d597c79a507ae400ea18ae0d Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 12 Dec 2021 20:44:56 +0100 Subject: [PATCH 26/45] Disable sync browsing when found explorer is mounted --- .../filetransfer/actions/change_dir.rs | 16 ++++++++-------- src/ui/activities/filetransfer/update.rs | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ui/activities/filetransfer/actions/change_dir.rs b/src/ui/activities/filetransfer/actions/change_dir.rs index b4e17c6c..bc4e6f72 100644 --- a/src/ui/activities/filetransfer/actions/change_dir.rs +++ b/src/ui/activities/filetransfer/actions/change_dir.rs @@ -42,7 +42,7 @@ impl FileTransferActivity { /// Enter a directory on local host from entry pub(crate) fn action_enter_local_dir(&mut self, dir: Directory) { self.local_changedir(dir.path.as_path(), true); - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name)); } } @@ -50,7 +50,7 @@ impl FileTransferActivity { /// Enter a directory on local host from entry pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory) { self.remote_changedir(dir.path.as_path(), true); - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name)); } } @@ -60,7 +60,7 @@ impl FileTransferActivity { 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 { + if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::Path(input)); } } @@ -70,7 +70,7 @@ impl FileTransferActivity { 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 { + if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::Path(input)); } } @@ -80,7 +80,7 @@ impl FileTransferActivity { if let Some(d) = self.local_mut().popd() { self.local_changedir(d.as_path(), false); // Check whether to sync - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::PreviousDir); } } @@ -91,7 +91,7 @@ impl FileTransferActivity { if let Some(d) = self.remote_mut().popd() { self.remote_changedir(d.as_path(), false); // Check whether to sync - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::PreviousDir); } } @@ -105,7 +105,7 @@ impl FileTransferActivity { 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 { + if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::ParentDir); } } @@ -121,7 +121,7 @@ impl FileTransferActivity { 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 { + if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::ParentDir); } } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index de43b691..9bb93154 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -112,7 +112,7 @@ impl FileTransferActivity { if let SelectedEntry::One(entry) = self.get_local_selected_entries() { self.action_submit_local(entry); // Update file list if sync - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { let _ = self.update_remote_filelist(); } self.update_local_filelist(); @@ -122,7 +122,7 @@ impl FileTransferActivity { if let SelectedEntry::One(entry) = self.get_remote_selected_entries() { self.action_submit_remote(entry); // Update file list if sync - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { let _ = self.update_local_filelist(); } self.update_remote_filelist(); @@ -161,7 +161,7 @@ impl FileTransferActivity { // Umount self.umount_goto(); // Reload files if sync - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { self.update_browser_file_list_swapped(); } // Reload files @@ -171,7 +171,7 @@ impl FileTransferActivity { match self.browser.tab() { FileExplorerTab::Local => { self.action_go_to_local_upper_dir(); - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { let _ = self.update_remote_filelist(); } // Reload file list component @@ -179,7 +179,7 @@ impl FileTransferActivity { } FileExplorerTab::Remote => { self.action_go_to_remote_upper_dir(); - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { let _ = self.update_local_filelist(); } // Reload file list component @@ -192,7 +192,7 @@ impl FileTransferActivity { match self.browser.tab() { FileExplorerTab::Local => { self.action_go_to_previous_local_dir(); - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { let _ = self.update_remote_filelist(); } // Reload file list component @@ -200,7 +200,7 @@ impl FileTransferActivity { } FileExplorerTab::Remote => { self.action_go_to_previous_remote_dir(); - if self.browser.sync_browsing { + if self.browser.sync_browsing && self.browser.found().is_none() { let _ = self.update_local_filelist(); } // Reload file list component From 432c81f3ffc44fb4836877aa85e459d3efa0d724 Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 13 Dec 2021 12:32:16 +0100 Subject: [PATCH 27/45] Remove pending transfer with storage (use action) --- .../activities/filetransfer/actions/find.rs | 55 ++++--- .../filetransfer/actions/pending.rs | 18 ++- .../activities/filetransfer/actions/save.rs | 136 ++++++++---------- .../filetransfer/components/popups.rs | 8 +- .../activities/filetransfer/lib/transfer.rs | 24 +--- src/ui/activities/filetransfer/mod.rs | 6 +- src/ui/activities/filetransfer/update.rs | 7 - 7 files changed, 106 insertions(+), 148 deletions(-) diff --git a/src/ui/activities/filetransfer/actions/find.rs b/src/ui/activities/filetransfer/actions/find.rs index d9b844b2..c6ffba9a 100644 --- a/src/ui/activities/filetransfer/actions/find.rs +++ b/src/ui/activities/filetransfer/actions/find.rs @@ -78,15 +78,16 @@ impl FileTransferActivity { SelectedEntry::One(entry) => match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); - if opts.check_replace - && self.config().get_prompt_on_file_replace() + if self.config().get_prompt_on_file_replace() && self.remote_file_exists(file_to_check.as_path()) - { - // Save pending transfer - self.set_pending_transfer( + && !self.should_replace_file( opts.save_as.as_deref().unwrap_or_else(|| entry.name()), - ); - } else if let Err(err) = self.filetransfer_send( + ) + { + // Do not replace + return; + } + if let Err(err) = self.filetransfer_send( TransferPayload::Any(entry), wrkdir.as_path(), opts.save_as, @@ -99,15 +100,16 @@ impl FileTransferActivity { } FileExplorerTab::FindRemote | FileExplorerTab::Remote => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); - if opts.check_replace - && self.config().get_prompt_on_file_replace() + if self.config().get_prompt_on_file_replace() && self.local_file_exists(file_to_check.as_path()) - { - // Save pending transfer - self.set_pending_transfer( + && !self.should_replace_file( opts.save_as.as_deref().unwrap_or_else(|| entry.name()), - ); - } else if let Err(err) = self.filetransfer_recv( + ) + { + // Do not replace + return; + } + if let Err(err) = self.filetransfer_recv( TransferPayload::Any(entry), wrkdir.as_path(), opts.save_as, @@ -128,7 +130,7 @@ impl FileTransferActivity { // Iter files match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { - if opts.check_replace && self.config().get_prompt_on_file_replace() { + if self.config().get_prompt_on_file_replace() { // Check which file would be replaced let existing_files: Vec<&Entry> = entries .iter() @@ -138,12 +140,10 @@ impl FileTransferActivity { ) }) .collect(); - // Save pending transfer - if !existing_files.is_empty() { - self.set_pending_transfer_many( - existing_files, - &dest_path.to_string_lossy().to_owned(), - ); + // Check whether to replace files + if !existing_files.is_empty() + && !self.should_replace_files(existing_files) + { return; } } @@ -161,7 +161,7 @@ impl FileTransferActivity { } } FileExplorerTab::FindRemote | FileExplorerTab::Remote => { - if opts.check_replace && self.config().get_prompt_on_file_replace() { + if self.config().get_prompt_on_file_replace() { // Check which file would be replaced let existing_files: Vec<&Entry> = entries .iter() @@ -171,13 +171,10 @@ impl FileTransferActivity { ) }) .collect(); - // Save pending transfer - // Save pending transfer - if !existing_files.is_empty() { - self.set_pending_transfer_many( - existing_files, - &dest_path.to_string_lossy().to_owned(), - ); + // Check whether to replace files + if !existing_files.is_empty() + && !self.should_replace_files(existing_files) + { return; } } diff --git a/src/ui/activities/filetransfer/actions/pending.rs b/src/ui/activities/filetransfer/actions/pending.rs index 852ef80f..293def53 100644 --- a/src/ui/activities/filetransfer/actions/pending.rs +++ b/src/ui/activities/filetransfer/actions/pending.rs @@ -28,7 +28,7 @@ */ use super::{FileTransferActivity, Msg}; -use tuirealm::PollStrategy; +use tuirealm::{PollStrategy, Update}; impl FileTransferActivity { /// Block execution of activity, preventing ANY kind of message not specified in the `wait_for` argument. @@ -42,12 +42,22 @@ impl FileTransferActivity { loop { // Poll match self.app.tick(PollStrategy::Once) { - Ok(messages) => { + Ok(mut messages) => { if !messages.is_empty() { self.redraw = true; } - if let Some(msg) = messages.into_iter().find(|m| wait_for.contains(m)) { - return msg; + let found = messages.iter().position(|m| wait_for.contains(m)); + // Return if found + if let Some(index) = found { + return messages.remove(index); + } else { + // Update + for msg in messages.into_iter() { + let mut msg = Some(msg); + while msg.is_some() { + msg = self.update(msg); + } + } } } Err(err) => { diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index f71ef837..0b6e70db 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -27,8 +27,8 @@ */ // locals use super::{ - super::STORAGE_PENDING_TRANSFER, Entry, FileExplorerTab, FileTransferActivity, LogLevel, - SelectedEntry, TransferOpts, TransferPayload, + Entry, FileTransferActivity, LogLevel, Msg, PendingActionMsg, SelectedEntry, TransferOpts, + TransferPayload, }; use std::path::{Path, PathBuf}; @@ -49,59 +49,21 @@ impl FileTransferActivity { self.remote_recv_file(TransferOpts::default()); } - /// Finalize "pending" transfer. - /// The pending transfer is created after a transfer which required a user action to be completed first. - /// The name of the file to transfer, is contained in the storage at `STORAGE_PENDING_TRANSFER`. - /// NOTE: Panics if `STORAGE_PENDING_TRANSFER` is undefined - pub(crate) fn action_finalize_pending_transfer(&mut self) { - // Retrieve pending transfer - let file_name = self - .context_mut() - .store_mut() - .take_string(STORAGE_PENDING_TRANSFER); - // Send file - match self.browser.tab() { - FileExplorerTab::Local => self.local_send_file( - TransferOpts::default() - .save_as(file_name) - .check_replace(false), - ), - FileExplorerTab::Remote => self.remote_recv_file( - TransferOpts::default() - .save_as(file_name) - .check_replace(false), - ), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => self.action_find_transfer( - TransferOpts::default() - .save_as(file_name) - .check_replace(false), - ), - } - // Reload browsers - match self.browser.tab() { - FileExplorerTab::Local | FileExplorerTab::FindLocal => { - self.update_remote_filelist(); - } - FileExplorerTab::Remote | FileExplorerTab::FindRemote => { - self.update_local_filelist(); - } - } - } - fn local_send_file(&mut self, opts: TransferOpts) { let wrkdir: PathBuf = self.remote().wrkdir.clone(); match self.get_local_selected_entries() { SelectedEntry::One(entry) => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); - if opts.check_replace - && self.config().get_prompt_on_file_replace() + if self.config().get_prompt_on_file_replace() && self.remote_file_exists(file_to_check.as_path()) - { - // Save pending transfer - self.set_pending_transfer( + && !self.should_replace_file( opts.save_as.as_deref().unwrap_or_else(|| entry.name()), - ); - } else if let Err(err) = self.filetransfer_send( + ) + { + // Do not replace + return; + } + if let Err(err) = self.filetransfer_send( TransferPayload::Any(entry.clone()), wrkdir.as_path(), opts.save_as, @@ -121,7 +83,7 @@ impl FileTransferActivity { dest_path.push(save_as); } // Iter files - if opts.check_replace && self.config().get_prompt_on_file_replace() { + if self.config().get_prompt_on_file_replace() { // Check which file would be replaced let existing_files: Vec<&Entry> = entries .iter() @@ -131,12 +93,8 @@ impl FileTransferActivity { ) }) .collect(); - // Save pending transfer - if !existing_files.is_empty() { - self.set_pending_transfer_many( - existing_files, - &dest_path.to_string_lossy().to_owned(), - ); + // Check whether to replace files + if !existing_files.is_empty() && !self.should_replace_files(existing_files) { return; } } @@ -162,15 +120,15 @@ impl FileTransferActivity { match self.get_remote_selected_entries() { SelectedEntry::One(entry) => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); - if opts.check_replace - && self.config().get_prompt_on_file_replace() + if self.config().get_prompt_on_file_replace() && self.local_file_exists(file_to_check.as_path()) - { - // Save pending transfer - self.set_pending_transfer( + && !self.should_replace_file( opts.save_as.as_deref().unwrap_or_else(|| entry.name()), - ); - } else if let Err(err) = self.filetransfer_recv( + ) + { + return; + } + if let Err(err) = self.filetransfer_recv( TransferPayload::Any(entry.clone()), wrkdir.as_path(), opts.save_as, @@ -190,7 +148,7 @@ impl FileTransferActivity { dest_path.push(save_as); } // Iter files - if opts.check_replace && self.config().get_prompt_on_file_replace() { + if self.config().get_prompt_on_file_replace() { // Check which file would be replaced let existing_files: Vec<&Entry> = entries .iter() @@ -200,12 +158,8 @@ impl FileTransferActivity { ) }) .collect(); - // Save pending transfer - if !existing_files.is_empty() { - self.set_pending_transfer_many( - existing_files, - &dest_path.to_string_lossy().to_owned(), - ); + // Check whether to replace files + if !existing_files.is_empty() && !self.should_replace_files(existing_files) { return; } } @@ -227,21 +181,47 @@ impl FileTransferActivity { } /// Set pending transfer into storage - pub(crate) fn set_pending_transfer(&mut self, file_name: &str) { + pub(crate) fn should_replace_file(&mut self, file_name: &str) -> bool { self.mount_radio_replace(file_name); - // Put pending transfer in store - self.context_mut() - .store_mut() - .set_string(STORAGE_PENDING_TRANSFER, file_name.to_string()); + // Wait for answer + trace!("Asking user whether he wants to replace file {}", file_name); + if self.wait_for_pending_msg(&[ + Msg::PendingAction(PendingActionMsg::CloseReplacePopups), + Msg::PendingAction(PendingActionMsg::TransferPendingFile), + ]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile) + { + trace!("User wants to replace file"); + self.umount_radio_replace(); + true + } else { + trace!("The user doesn't want replace file"); + self.umount_radio_replace(); + false + } } /// Set pending transfer for many files into storage and mount radio - pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&Entry>, dest_path: &str) { + pub(crate) fn should_replace_files(&mut self, files: Vec<&Entry>) -> bool { let file_names: Vec<&str> = files.iter().map(|x| x.name()).collect(); self.mount_radio_replace_many(file_names.as_slice()); - self.context_mut() - .store_mut() - .set_string(STORAGE_PENDING_TRANSFER, dest_path.to_string()); + // Wait for answer + trace!( + "Asking user whether he wants to replace files {:?}", + file_names + ); + if self.wait_for_pending_msg(&[ + Msg::PendingAction(PendingActionMsg::CloseReplacePopups), + Msg::PendingAction(PendingActionMsg::TransferPendingFile), + ]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile) + { + trace!("User wants to replace files"); + self.umount_radio_replace(); + true + } else { + trace!("The user doesn't want replace file"); + self.umount_radio_replace(); + false + } } /// Get file to check for path diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index 0f06026f..c0ca5c9f 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -1343,7 +1343,7 @@ impl Component for ReplacePopup { Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseReplacePopups)) + Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups)) } Event::Keyboard(KeyEvent { code: Key::Enter, .. @@ -1352,9 +1352,9 @@ impl Component for ReplacePopup { self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { - Some(Msg::Transfer(TransferMsg::TransferPendingFile)) + Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile)) } else { - Some(Msg::Ui(UiMsg::CloseReplacePopups)) + Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups)) } } _ => None, @@ -1393,7 +1393,7 @@ impl Component for ReplacingFilesListPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseReplacePopups)) + Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups)) } Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { Some(Msg::Ui(UiMsg::ReplacePopupTabbed)) diff --git a/src/ui/activities/filetransfer/lib/transfer.rs b/src/ui/activities/filetransfer/lib/transfer.rs index 6b1659f5..2b609699 100644 --- a/src/ui/activities/filetransfer/lib/transfer.rs +++ b/src/ui/activities/filetransfer/lib/transfer.rs @@ -183,20 +183,10 @@ impl ProgressStates { // -- Options /// Defines the transfer options for transfer actions +#[derive(Default)] pub struct TransferOpts { /// Save file as pub save_as: Option, - /// Whether to check if file is being replaced - pub check_replace: bool, -} - -impl Default for TransferOpts { - fn default() -> Self { - Self { - save_as: None, - check_replace: true, - } - } } impl TransferOpts { @@ -205,12 +195,6 @@ impl TransferOpts { self.save_as = n.map(|x| x.as_ref().to_string()); self } - - /// Set whether to check if the file being transferred will "replace" an existing one - pub fn check_replace(mut self, opt: bool) -> Self { - self.check_replace = opt; - self - } } #[cfg(test)] @@ -289,12 +273,8 @@ mod test { #[test] fn transfer_opts() { let opts = TransferOpts::default(); - assert_eq!(opts.check_replace, true); assert!(opts.save_as.is_none()); - let opts = TransferOpts::default() - .check_replace(false) - .save_as(Some("omar.txt")); + let opts = TransferOpts::default().save_as(Some("omar.txt")); assert_eq!(opts.save_as.as_deref().unwrap(), "omar.txt"); - assert_eq!(opts.check_replace, false); } } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 07860fac..c64783cd 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -58,8 +58,6 @@ use tuirealm::{Application, EventListenerCfg, NoUserEvent}; /// Stores the explorer width const STORAGE_EXPLORER_WIDTH: &str = "FT_EW"; -/// Stores the filename of the entry to transfer, when the replace file dialog must be shown -const STORAGE_PENDING_TRANSFER: &str = "FT_PT"; // -- components @@ -108,8 +106,10 @@ enum Msg { #[derive(Debug, PartialEq)] enum PendingActionMsg { + CloseReplacePopups, CloseSyncBrowsingMkdirPopup, MakePendingDirectory, + TransferPendingFile, } #[derive(Debug, PartialEq)] @@ -132,7 +132,6 @@ enum TransferMsg { SaveFileAs(String), SearchFile(String), TransferFile, - TransferPendingFile, } #[derive(Debug, PartialEq)] @@ -155,7 +154,6 @@ enum UiMsg { CloseNewFilePopup, CloseOpenWithPopup, CloseQuitPopup, - CloseReplacePopups, CloseRenamePopup, CloseSaveAsPopup, Disconnect, diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 9bb93154..fefb5132 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -340,10 +340,6 @@ impl FileTransferActivity { } self.update_browser_file_list_swapped(); } - TransferMsg::TransferPendingFile => { - self.umount_radio_replace(); - self.action_finalize_pending_transfer(); - } } // Force redraw self.redraw = true; @@ -411,9 +407,6 @@ impl FileTransferActivity { UiMsg::CloseOpenWithPopup => self.umount_openwith(), UiMsg::CloseQuitPopup => self.umount_quit(), UiMsg::CloseRenamePopup => self.umount_rename(), - UiMsg::CloseReplacePopups => { - self.umount_radio_replace(); - } UiMsg::CloseSaveAsPopup => self.umount_saveas(), UiMsg::Disconnect => { self.disconnect(); From fc91da5105e2a2ffe1e5a76e75b3db7b5a6e13c7 Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 13 Dec 2021 17:12:38 +0100 Subject: [PATCH 28/45] Removed storage key for filetransfer terminal width --- src/ui/activities/filetransfer/misc.rs | 24 ++++++++++++++---------- src/ui/activities/filetransfer/mod.rs | 5 ----- src/ui/activities/filetransfer/view.rs | 6 ------ 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index eebea5cb..37664c76 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -214,11 +214,13 @@ impl FileTransferActivity { /// Update local file list pub(super) fn update_local_filelist(&mut self) { // Get width - let width: usize = self - .context() - .store() - .get_unsigned(super::STORAGE_EXPLORER_WIDTH) - .unwrap_or(256); + let width = self + .context_mut() + .terminal() + .raw() + .size() + .map(|x| (x.width / 2) - 2) + .unwrap_or(0) as usize; let hostname: String = match hostname::get() { Ok(h) => { let hostname: String = h.as_os_str().to_string_lossy().to_string(); @@ -258,11 +260,13 @@ impl FileTransferActivity { /// Update remote file list pub(super) fn update_remote_filelist(&mut self) { - let width: usize = self - .context() - .store() - .get_unsigned(super::STORAGE_EXPLORER_WIDTH) - .unwrap_or(256); + let width = self + .context_mut() + .terminal() + .raw() + .size() + .map(|x| (x.width / 2) - 2) + .unwrap_or(0) as usize; let hostname = self.get_remote_hostname(); let hostname: String = format!( "{}:{} ", diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index c64783cd..b12c217e 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -54,11 +54,6 @@ use std::time::Duration; use tempfile::TempDir; use tuirealm::{Application, EventListenerCfg, NoUserEvent}; -// -- Storage keys - -/// Stores the explorer width -const STORAGE_EXPLORER_WIDTH: &str = "FT_EW"; - // -- components #[derive(Debug, Eq, PartialEq, Clone, Hash)] diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index fc09e7e3..f8874d7e 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -31,7 +31,6 @@ use super::{ components, Context, FileTransferActivity, Id, }; use crate::explorer::FileSorting; -use crate::ui::store::Store; use crate::utils::ui::draw_area_in; // Ext use remotefs::fs::Entry; @@ -117,7 +116,6 @@ impl FileTransferActivity { pub(super) fn view(&mut self) { self.redraw = false; let mut context: Context = self.context.take().unwrap(); - let store: &mut Store = &mut context.store; let _ = context.terminal.raw_mut().draw(|f| { // Prepare chunks let body = Layout::default() @@ -157,10 +155,6 @@ impl FileTransferActivity { .direction(Direction::Horizontal) .horizontal_margin(1) .split(bottom_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); - } // Draw footer self.app.view(&Id::FooterBar, f, body[1]); // Draw explorers From 530b8dc865ef6d895964424b7e215d7e501f75ae Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 13 Dec 2021 17:20:58 +0100 Subject: [PATCH 29/45] Use tui-realm color parser --- src/utils/parser.rs | 443 +------------------------------------------- 1 file changed, 4 insertions(+), 439 deletions(-) diff --git a/src/utils/parser.rs b/src/utils/parser.rs index 3b550227..aaa1de16 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -41,6 +41,7 @@ use regex::Regex; use std::path::PathBuf; use std::str::FromStr; use tuirealm::tui::style::Color; +use tuirealm::utils::parser as tuirealm_parser; // Regex lazy_static! { @@ -71,7 +72,6 @@ lazy_static! { */ static ref REMOTE_S3_OPT_REGEX: Regex = Regex::new(r"(?:([^@]+)@)(?:([^:]+))(?::([a-zA-Z0-9][^:]+))?(?::([^:]+))?").unwrap(); - /** * Regex matches: * - group 1: Version @@ -79,20 +79,7 @@ lazy_static! { * v0.4.0 => 0.4.0 */ static ref SEMVER_REGEX: Regex = Regex::new(r".*(:?[0-9]\.[0-9]\.[0-9])").unwrap(); - /** - * Regex matches: - * - group 1: Red - * - group 2: Green - * - group 3: Blue - */ - static ref COLOR_HEX_REGEX: Regex = Regex::new(r"#(:?[0-9a-fA-F]{2})(:?[0-9a-fA-F]{2})(:?[0-9a-fA-F]{2})").unwrap(); - /** - * Regex matches: - * - group 2: Red - * - group 4: Green - * - group 6: blue - */ - static ref COLOR_RGB_REGEX: Regex = Regex::new(r"^(rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(([01]?\d\d?|2[0-4]\d|25[0-5])\)?)").unwrap(); + /** * Regex matches: * - group 1: amount (number) @@ -306,203 +293,7 @@ pub fn parse_semver(haystack: &str) -> Option { /// - rgb(255,64,32) /// - 255, 64, 32 pub fn parse_color(color: &str) -> Option { - match color.to_lowercase().as_str() { - // -- lib colors - "black" => Some(Color::Black), - "blue" => Some(Color::Blue), - "cyan" => Some(Color::Cyan), - "darkgray" | "darkgrey" => Some(Color::DarkGray), - "default" => Some(Color::Reset), - "gray" => Some(Color::Gray), - "green" => Some(Color::Green), - "lightblue" => Some(Color::LightBlue), - "lightcyan" => Some(Color::LightCyan), - "lightgreen" => Some(Color::LightGreen), - "lightmagenta" => Some(Color::LightMagenta), - "lightred" => Some(Color::LightRed), - "lightyellow" => Some(Color::LightYellow), - "magenta" => Some(Color::Magenta), - "red" => Some(Color::Red), - "white" => Some(Color::White), - "yellow" => Some(Color::Yellow), - // -- css colors - "aliceblue" => Some(Color::Rgb(240, 248, 255)), - "antiquewhite" => Some(Color::Rgb(250, 235, 215)), - "aqua" => Some(Color::Rgb(0, 255, 255)), - "aquamarine" => Some(Color::Rgb(127, 255, 212)), - "azure" => Some(Color::Rgb(240, 255, 255)), - "beige" => Some(Color::Rgb(245, 245, 220)), - "bisque" => Some(Color::Rgb(255, 228, 196)), - "blanchedalmond" => Some(Color::Rgb(255, 235, 205)), - "blueviolet" => Some(Color::Rgb(138, 43, 226)), - "brown" => Some(Color::Rgb(165, 42, 42)), - "burlywood" => Some(Color::Rgb(222, 184, 135)), - "cadetblue" => Some(Color::Rgb(95, 158, 160)), - "chartreuse" => Some(Color::Rgb(127, 255, 0)), - "chocolate" => Some(Color::Rgb(210, 105, 30)), - "coral" => Some(Color::Rgb(255, 127, 80)), - "cornflowerblue" => Some(Color::Rgb(100, 149, 237)), - "cornsilk" => Some(Color::Rgb(255, 248, 220)), - "crimson" => Some(Color::Rgb(220, 20, 60)), - "darkblue" => Some(Color::Rgb(0, 0, 139)), - "darkcyan" => Some(Color::Rgb(0, 139, 139)), - "darkgoldenrod" => Some(Color::Rgb(184, 134, 11)), - "darkgreen" => Some(Color::Rgb(0, 100, 0)), - "darkkhaki" => Some(Color::Rgb(189, 183, 107)), - "darkmagenta" => Some(Color::Rgb(139, 0, 139)), - "darkolivegreen" => Some(Color::Rgb(85, 107, 47)), - "darkorange" => Some(Color::Rgb(255, 140, 0)), - "darkorchid" => Some(Color::Rgb(153, 50, 204)), - "darkred" => Some(Color::Rgb(139, 0, 0)), - "darksalmon" => Some(Color::Rgb(233, 150, 122)), - "darkseagreen" => Some(Color::Rgb(143, 188, 143)), - "darkslateblue" => Some(Color::Rgb(72, 61, 139)), - "darkslategray" | "darkslategrey" => Some(Color::Rgb(47, 79, 79)), - "darkturquoise" => Some(Color::Rgb(0, 206, 209)), - "darkviolet" => Some(Color::Rgb(148, 0, 211)), - "deeppink" => Some(Color::Rgb(255, 20, 147)), - "deepskyblue" => Some(Color::Rgb(0, 191, 255)), - "dimgray" | "dimgrey" => Some(Color::Rgb(105, 105, 105)), - "dodgerblue" => Some(Color::Rgb(30, 144, 255)), - "firebrick" => Some(Color::Rgb(178, 34, 34)), - "floralwhite" => Some(Color::Rgb(255, 250, 240)), - "forestgreen" => Some(Color::Rgb(34, 139, 34)), - "fuchsia" => Some(Color::Rgb(255, 0, 255)), - "gainsboro" => Some(Color::Rgb(220, 220, 220)), - "ghostwhite" => Some(Color::Rgb(248, 248, 255)), - "gold" => Some(Color::Rgb(255, 215, 0)), - "goldenrod" => Some(Color::Rgb(218, 165, 32)), - "greenyellow" => Some(Color::Rgb(173, 255, 47)), - "grey" => Some(Color::Rgb(128, 128, 128)), - "honeydew" => Some(Color::Rgb(240, 255, 240)), - "hotpink" => Some(Color::Rgb(255, 105, 180)), - "indianred" => Some(Color::Rgb(205, 92, 92)), - "indigo" => Some(Color::Rgb(75, 0, 130)), - "ivory" => Some(Color::Rgb(255, 255, 240)), - "khaki" => Some(Color::Rgb(240, 230, 140)), - "lavender" => Some(Color::Rgb(230, 230, 250)), - "lavenderblush" => Some(Color::Rgb(255, 240, 245)), - "lawngreen" => Some(Color::Rgb(124, 252, 0)), - "lemonchiffon" => Some(Color::Rgb(255, 250, 205)), - "lightcoral" => Some(Color::Rgb(240, 128, 128)), - "lightgoldenrodyellow" => Some(Color::Rgb(250, 250, 210)), - "lightgray" | "lightgrey" => Some(Color::Rgb(211, 211, 211)), - "lightpink" => Some(Color::Rgb(255, 182, 193)), - "lightsalmon" => Some(Color::Rgb(255, 160, 122)), - "lightseagreen" => Some(Color::Rgb(32, 178, 170)), - "lightskyblue" => Some(Color::Rgb(135, 206, 250)), - "lightslategray" | "lightslategrey" => Some(Color::Rgb(119, 136, 153)), - "lightsteelblue" => Some(Color::Rgb(176, 196, 222)), - "lime" => Some(Color::Rgb(0, 255, 0)), - "limegreen" => Some(Color::Rgb(50, 205, 50)), - "linen" => Some(Color::Rgb(250, 240, 230)), - "maroon" => Some(Color::Rgb(128, 0, 0)), - "mediumaquamarine" => Some(Color::Rgb(102, 205, 170)), - "mediumblue" => Some(Color::Rgb(0, 0, 205)), - "mediumorchid" => Some(Color::Rgb(186, 85, 211)), - "mediumpurple" => Some(Color::Rgb(147, 112, 219)), - "mediumseagreen" => Some(Color::Rgb(60, 179, 113)), - "mediumslateblue" => Some(Color::Rgb(123, 104, 238)), - "mediumspringgreen" => Some(Color::Rgb(0, 250, 154)), - "mediumturquoise" => Some(Color::Rgb(72, 209, 204)), - "mediumvioletred" => Some(Color::Rgb(199, 21, 133)), - "midnightblue" => Some(Color::Rgb(25, 25, 112)), - "mintcream" => Some(Color::Rgb(245, 255, 250)), - "mistyrose" => Some(Color::Rgb(255, 228, 225)), - "moccasin" => Some(Color::Rgb(255, 228, 181)), - "navajowhite" => Some(Color::Rgb(255, 222, 173)), - "navy" => Some(Color::Rgb(0, 0, 128)), - "oldlace" => Some(Color::Rgb(253, 245, 230)), - "olive" => Some(Color::Rgb(128, 128, 0)), - "olivedrab" => Some(Color::Rgb(107, 142, 35)), - "orange" => Some(Color::Rgb(255, 165, 0)), - "orangered" => Some(Color::Rgb(255, 69, 0)), - "orchid" => Some(Color::Rgb(218, 112, 214)), - "palegoldenrod" => Some(Color::Rgb(238, 232, 170)), - "palegreen" => Some(Color::Rgb(152, 251, 152)), - "paleturquoise" => Some(Color::Rgb(175, 238, 238)), - "palevioletred" => Some(Color::Rgb(219, 112, 147)), - "papayawhip" => Some(Color::Rgb(255, 239, 213)), - "peachpuff" => Some(Color::Rgb(255, 218, 185)), - "peru" => Some(Color::Rgb(205, 133, 63)), - "pink" => Some(Color::Rgb(255, 192, 203)), - "plum" => Some(Color::Rgb(221, 160, 221)), - "powderblue" => Some(Color::Rgb(176, 224, 230)), - "purple" => Some(Color::Rgb(128, 0, 128)), - "rebeccapurple" => Some(Color::Rgb(102, 51, 153)), - "rosybrown" => Some(Color::Rgb(188, 143, 143)), - "royalblue" => Some(Color::Rgb(65, 105, 225)), - "saddlebrown" => Some(Color::Rgb(139, 69, 19)), - "salmon" => Some(Color::Rgb(250, 128, 114)), - "sandybrown" => Some(Color::Rgb(244, 164, 96)), - "seagreen" => Some(Color::Rgb(46, 139, 87)), - "seashell" => Some(Color::Rgb(255, 245, 238)), - "sienna" => Some(Color::Rgb(160, 82, 45)), - "silver" => Some(Color::Rgb(192, 192, 192)), - "skyblue" => Some(Color::Rgb(135, 206, 235)), - "slateblue" => Some(Color::Rgb(106, 90, 205)), - "slategray" | "slategrey" => Some(Color::Rgb(112, 128, 144)), - "snow" => Some(Color::Rgb(255, 250, 250)), - "springgreen" => Some(Color::Rgb(0, 255, 127)), - "steelblue" => Some(Color::Rgb(70, 130, 180)), - "tan" => Some(Color::Rgb(210, 180, 140)), - "teal" => Some(Color::Rgb(0, 128, 128)), - "thistle" => Some(Color::Rgb(216, 191, 216)), - "tomato" => Some(Color::Rgb(255, 99, 71)), - "turquoise" => Some(Color::Rgb(64, 224, 208)), - "violet" => Some(Color::Rgb(238, 130, 238)), - "wheat" => Some(Color::Rgb(245, 222, 179)), - "whitesmoke" => Some(Color::Rgb(245, 245, 245)), - "yellowgreen" => Some(Color::Rgb(154, 205, 50)), - // -- hex and rgb - other => { - // Try as hex - if let Some(color) = parse_hex_color(other) { - Some(color) - } else { - parse_rgb_color(other) - } - } - } -} - -/// ### parse_hex_color -/// -/// Try to parse a color in hex format, such as: -/// -/// - #f0ab05 -/// - #AA33BC -fn parse_hex_color(color: &str) -> Option { - COLOR_HEX_REGEX.captures(color).map(|groups| { - Color::Rgb( - u8::from_str_radix(groups.get(1).unwrap().as_str(), 16) - .ok() - .unwrap(), - u8::from_str_radix(groups.get(2).unwrap().as_str(), 16) - .ok() - .unwrap(), - u8::from_str_radix(groups.get(3).unwrap().as_str(), 16) - .ok() - .unwrap(), - ) - }) -} - -/// ### parse_rgb_color -/// -/// Try to parse a color in rgb format, such as: -/// -/// - rgb(255, 64, 32) -/// - rgb(255,64,32) -/// - 255, 64, 32 -fn parse_rgb_color(color: &str) -> Option { - COLOR_RGB_REGEX.captures(color).map(|groups| { - Color::Rgb( - u8::from_str(groups.get(2).unwrap().as_str()).ok().unwrap(), - u8::from_str(groups.get(4).unwrap().as_str()).ok().unwrap(), - u8::from_str(groups.get(6).unwrap().as_str()).ok().unwrap(), - ) - }) + tuirealm_parser::parse_color(color) } #[derive(Debug, PartialEq)] @@ -759,244 +550,18 @@ mod tests { assert!(parse_semver("v1.1").is_none()); } - #[test] - fn test_utils_parse_color_hex() { - assert_eq!( - parse_hex_color("#f0f0f0").unwrap(), - Color::Rgb(240, 240, 240) - ); - assert_eq!( - parse_hex_color("#60AAcc").unwrap(), - Color::Rgb(96, 170, 204) - ); - assert!(parse_hex_color("#fatboy").is_none()); - } - - #[test] - fn test_utils_parse_color_rgb() { - assert_eq!( - parse_rgb_color("rgb(255, 64, 32)").unwrap(), - Color::Rgb(255, 64, 32) - ); - assert_eq!( - parse_rgb_color("rgb(255,64,32)").unwrap(), - Color::Rgb(255, 64, 32) - ); - assert_eq!( - parse_rgb_color("(255,64,32)").unwrap(), - Color::Rgb(255, 64, 32) - ); - assert_eq!( - parse_rgb_color("255,64,32").unwrap(), - Color::Rgb(255, 64, 32) - ); - assert!(parse_rgb_color("(300, 128, 512)").is_none()); - } - #[test] fn test_utils_parse_color() { assert_eq!(parse_color("Black").unwrap(), Color::Black); - assert_eq!(parse_color("BLUE").unwrap(), Color::Blue); - assert_eq!(parse_color("Cyan").unwrap(), Color::Cyan); - assert_eq!(parse_color("DarkGray").unwrap(), Color::DarkGray); - assert_eq!(parse_color("Gray").unwrap(), Color::Gray); - assert_eq!(parse_color("Green").unwrap(), Color::Green); - assert_eq!(parse_color("LightBlue").unwrap(), Color::LightBlue); - assert_eq!(parse_color("LightCyan").unwrap(), Color::LightCyan); - assert_eq!(parse_color("LightGreen").unwrap(), Color::LightGreen); - assert_eq!(parse_color("LightMagenta").unwrap(), Color::LightMagenta); - assert_eq!(parse_color("LightRed").unwrap(), Color::LightRed); - assert_eq!(parse_color("LightYellow").unwrap(), Color::LightYellow); - assert_eq!(parse_color("Magenta").unwrap(), Color::Magenta); - assert_eq!(parse_color("Red").unwrap(), Color::Red); - assert_eq!(parse_color("Default").unwrap(), Color::Reset); - assert_eq!(parse_color("White").unwrap(), Color::White); - assert_eq!(parse_color("Yellow").unwrap(), Color::Yellow); assert_eq!(parse_color("#f0f0f0").unwrap(), Color::Rgb(240, 240, 240)); // -- css colors assert_eq!(parse_color("aliceblue"), Some(Color::Rgb(240, 248, 255))); - assert_eq!(parse_color("antiquewhite"), Some(Color::Rgb(250, 235, 215))); - assert_eq!(parse_color("aqua"), Some(Color::Rgb(0, 255, 255))); - assert_eq!(parse_color("aquamarine"), Some(Color::Rgb(127, 255, 212))); - assert_eq!(parse_color("azure"), Some(Color::Rgb(240, 255, 255))); - assert_eq!(parse_color("beige"), Some(Color::Rgb(245, 245, 220))); - assert_eq!(parse_color("bisque"), Some(Color::Rgb(255, 228, 196))); - assert_eq!( - parse_color("blanchedalmond"), - Some(Color::Rgb(255, 235, 205)) - ); - assert_eq!(parse_color("blueviolet"), Some(Color::Rgb(138, 43, 226))); - assert_eq!(parse_color("brown"), Some(Color::Rgb(165, 42, 42))); - assert_eq!(parse_color("burlywood"), Some(Color::Rgb(222, 184, 135))); - assert_eq!(parse_color("cadetblue"), Some(Color::Rgb(95, 158, 160))); - assert_eq!(parse_color("chartreuse"), Some(Color::Rgb(127, 255, 0))); - assert_eq!(parse_color("chocolate"), Some(Color::Rgb(210, 105, 30))); - assert_eq!(parse_color("coral"), Some(Color::Rgb(255, 127, 80))); - assert_eq!( - parse_color("cornflowerblue"), - Some(Color::Rgb(100, 149, 237)) - ); - assert_eq!(parse_color("cornsilk"), Some(Color::Rgb(255, 248, 220))); - assert_eq!(parse_color("crimson"), Some(Color::Rgb(220, 20, 60))); - assert_eq!(parse_color("darkblue"), Some(Color::Rgb(0, 0, 139))); - assert_eq!(parse_color("darkcyan"), Some(Color::Rgb(0, 139, 139))); - assert_eq!(parse_color("darkgoldenrod"), Some(Color::Rgb(184, 134, 11))); - assert_eq!(parse_color("darkgreen"), Some(Color::Rgb(0, 100, 0))); - assert_eq!(parse_color("darkkhaki"), Some(Color::Rgb(189, 183, 107))); - assert_eq!(parse_color("darkmagenta"), Some(Color::Rgb(139, 0, 139))); - assert_eq!(parse_color("darkolivegreen"), Some(Color::Rgb(85, 107, 47))); - assert_eq!(parse_color("darkorange"), Some(Color::Rgb(255, 140, 0))); - assert_eq!(parse_color("darkorchid"), Some(Color::Rgb(153, 50, 204))); - assert_eq!(parse_color("darkred"), Some(Color::Rgb(139, 0, 0))); - assert_eq!(parse_color("darksalmon"), Some(Color::Rgb(233, 150, 122))); - assert_eq!(parse_color("darkseagreen"), Some(Color::Rgb(143, 188, 143))); - assert_eq!(parse_color("darkslateblue"), Some(Color::Rgb(72, 61, 139))); - assert_eq!(parse_color("darkslategray"), Some(Color::Rgb(47, 79, 79))); - assert_eq!(parse_color("darkslategrey"), Some(Color::Rgb(47, 79, 79))); - assert_eq!(parse_color("darkturquoise"), Some(Color::Rgb(0, 206, 209))); - assert_eq!(parse_color("darkviolet"), Some(Color::Rgb(148, 0, 211))); - assert_eq!(parse_color("deeppink"), Some(Color::Rgb(255, 20, 147))); - assert_eq!(parse_color("deepskyblue"), Some(Color::Rgb(0, 191, 255))); - assert_eq!(parse_color("dimgray"), Some(Color::Rgb(105, 105, 105))); - assert_eq!(parse_color("dimgrey"), Some(Color::Rgb(105, 105, 105))); - assert_eq!(parse_color("dodgerblue"), Some(Color::Rgb(30, 144, 255))); - assert_eq!(parse_color("firebrick"), Some(Color::Rgb(178, 34, 34))); - assert_eq!(parse_color("floralwhite"), Some(Color::Rgb(255, 250, 240))); - assert_eq!(parse_color("forestgreen"), Some(Color::Rgb(34, 139, 34))); - assert_eq!(parse_color("fuchsia"), Some(Color::Rgb(255, 0, 255))); - assert_eq!(parse_color("gainsboro"), Some(Color::Rgb(220, 220, 220))); - assert_eq!(parse_color("ghostwhite"), Some(Color::Rgb(248, 248, 255))); - assert_eq!(parse_color("gold"), Some(Color::Rgb(255, 215, 0))); - assert_eq!(parse_color("goldenrod"), Some(Color::Rgb(218, 165, 32))); - assert_eq!(parse_color("greenyellow"), Some(Color::Rgb(173, 255, 47))); - assert_eq!(parse_color("honeydew"), Some(Color::Rgb(240, 255, 240))); - assert_eq!(parse_color("hotpink"), Some(Color::Rgb(255, 105, 180))); - assert_eq!(parse_color("indianred"), Some(Color::Rgb(205, 92, 92))); - assert_eq!(parse_color("indigo"), Some(Color::Rgb(75, 0, 130))); - assert_eq!(parse_color("ivory"), Some(Color::Rgb(255, 255, 240))); - assert_eq!(parse_color("khaki"), Some(Color::Rgb(240, 230, 140))); - assert_eq!(parse_color("lavender"), Some(Color::Rgb(230, 230, 250))); - assert_eq!( - parse_color("lavenderblush"), - Some(Color::Rgb(255, 240, 245)) - ); - assert_eq!(parse_color("lawngreen"), Some(Color::Rgb(124, 252, 0))); - assert_eq!(parse_color("lemonchiffon"), Some(Color::Rgb(255, 250, 205))); - assert_eq!(parse_color("lightcoral"), Some(Color::Rgb(240, 128, 128))); - assert_eq!( - parse_color("lightgoldenrodyellow"), - Some(Color::Rgb(250, 250, 210)) - ); - assert_eq!(parse_color("lightpink"), Some(Color::Rgb(255, 182, 193))); - assert_eq!(parse_color("lightsalmon"), Some(Color::Rgb(255, 160, 122))); - assert_eq!(parse_color("lightseagreen"), Some(Color::Rgb(32, 178, 170))); - assert_eq!(parse_color("lightskyblue"), Some(Color::Rgb(135, 206, 250))); - assert_eq!( - parse_color("lightslategray"), - Some(Color::Rgb(119, 136, 153)) - ); - assert_eq!( - parse_color("lightslategrey"), - Some(Color::Rgb(119, 136, 153)) - ); - assert_eq!( - parse_color("lightsteelblue"), - Some(Color::Rgb(176, 196, 222)) - ); - assert_eq!(parse_color("lime"), Some(Color::Rgb(0, 255, 0))); - assert_eq!(parse_color("limegreen"), Some(Color::Rgb(50, 205, 50))); - assert_eq!(parse_color("linen"), Some(Color::Rgb(250, 240, 230))); - assert_eq!(parse_color("maroon"), Some(Color::Rgb(128, 0, 0))); - assert_eq!( - parse_color("mediumaquamarine"), - Some(Color::Rgb(102, 205, 170)) - ); - assert_eq!(parse_color("mediumblue"), Some(Color::Rgb(0, 0, 205))); - assert_eq!(parse_color("mediumorchid"), Some(Color::Rgb(186, 85, 211))); - assert_eq!(parse_color("mediumpurple"), Some(Color::Rgb(147, 112, 219))); - assert_eq!( - parse_color("mediumseagreen"), - Some(Color::Rgb(60, 179, 113)) - ); - assert_eq!( - parse_color("mediumslateblue"), - Some(Color::Rgb(123, 104, 238)) - ); - assert_eq!( - parse_color("mediumspringgreen"), - Some(Color::Rgb(0, 250, 154)) - ); - assert_eq!( - parse_color("mediumturquoise"), - Some(Color::Rgb(72, 209, 204)) - ); - assert_eq!( - parse_color("mediumvioletred"), - Some(Color::Rgb(199, 21, 133)) - ); - assert_eq!(parse_color("midnightblue"), Some(Color::Rgb(25, 25, 112))); - assert_eq!(parse_color("mintcream"), Some(Color::Rgb(245, 255, 250))); - assert_eq!(parse_color("mistyrose"), Some(Color::Rgb(255, 228, 225))); - assert_eq!(parse_color("moccasin"), Some(Color::Rgb(255, 228, 181))); - assert_eq!(parse_color("navajowhite"), Some(Color::Rgb(255, 222, 173))); - assert_eq!(parse_color("navy"), Some(Color::Rgb(0, 0, 128))); - assert_eq!(parse_color("oldlace"), Some(Color::Rgb(253, 245, 230))); - assert_eq!(parse_color("olive"), Some(Color::Rgb(128, 128, 0))); - assert_eq!(parse_color("olivedrab"), Some(Color::Rgb(107, 142, 35))); - assert_eq!(parse_color("orange"), Some(Color::Rgb(255, 165, 0))); - assert_eq!(parse_color("orangered"), Some(Color::Rgb(255, 69, 0))); - assert_eq!(parse_color("orchid"), Some(Color::Rgb(218, 112, 214))); - assert_eq!( - parse_color("palegoldenrod"), - Some(Color::Rgb(238, 232, 170)) - ); - assert_eq!(parse_color("palegreen"), Some(Color::Rgb(152, 251, 152))); - assert_eq!( - parse_color("paleturquoise"), - Some(Color::Rgb(175, 238, 238)) - ); - assert_eq!( - parse_color("palevioletred"), - Some(Color::Rgb(219, 112, 147)) - ); - assert_eq!(parse_color("papayawhip"), Some(Color::Rgb(255, 239, 213))); - assert_eq!(parse_color("peachpuff"), Some(Color::Rgb(255, 218, 185))); - assert_eq!(parse_color("peru"), Some(Color::Rgb(205, 133, 63))); - assert_eq!(parse_color("pink"), Some(Color::Rgb(255, 192, 203))); - assert_eq!(parse_color("plum"), Some(Color::Rgb(221, 160, 221))); - assert_eq!(parse_color("powderblue"), Some(Color::Rgb(176, 224, 230))); - assert_eq!(parse_color("purple"), Some(Color::Rgb(128, 0, 128))); - assert_eq!(parse_color("rebeccapurple"), Some(Color::Rgb(102, 51, 153))); - assert_eq!(parse_color("rosybrown"), Some(Color::Rgb(188, 143, 143))); - assert_eq!(parse_color("royalblue"), Some(Color::Rgb(65, 105, 225))); - assert_eq!(parse_color("saddlebrown"), Some(Color::Rgb(139, 69, 19))); - assert_eq!(parse_color("salmon"), Some(Color::Rgb(250, 128, 114))); - assert_eq!(parse_color("sandybrown"), Some(Color::Rgb(244, 164, 96))); - assert_eq!(parse_color("seagreen"), Some(Color::Rgb(46, 139, 87))); - assert_eq!(parse_color("seashell"), Some(Color::Rgb(255, 245, 238))); - assert_eq!(parse_color("sienna"), Some(Color::Rgb(160, 82, 45))); - assert_eq!(parse_color("silver"), Some(Color::Rgb(192, 192, 192))); - assert_eq!(parse_color("skyblue"), Some(Color::Rgb(135, 206, 235))); - assert_eq!(parse_color("slateblue"), Some(Color::Rgb(106, 90, 205))); - assert_eq!(parse_color("slategray"), Some(Color::Rgb(112, 128, 144))); - assert_eq!(parse_color("slategrey"), Some(Color::Rgb(112, 128, 144))); - assert_eq!(parse_color("snow"), Some(Color::Rgb(255, 250, 250))); - assert_eq!(parse_color("springgreen"), Some(Color::Rgb(0, 255, 127))); - assert_eq!(parse_color("steelblue"), Some(Color::Rgb(70, 130, 180))); - assert_eq!(parse_color("tan"), Some(Color::Rgb(210, 180, 140))); - assert_eq!(parse_color("teal"), Some(Color::Rgb(0, 128, 128))); - assert_eq!(parse_color("thistle"), Some(Color::Rgb(216, 191, 216))); - assert_eq!(parse_color("tomato"), Some(Color::Rgb(255, 99, 71))); - assert_eq!(parse_color("turquoise"), Some(Color::Rgb(64, 224, 208))); - assert_eq!(parse_color("violet"), Some(Color::Rgb(238, 130, 238))); - assert_eq!(parse_color("wheat"), Some(Color::Rgb(245, 222, 179))); - assert_eq!(parse_color("whitesmoke"), Some(Color::Rgb(245, 245, 245))); - assert_eq!(parse_color("yellowgreen"), Some(Color::Rgb(154, 205, 50))); // -- hex and rgb assert_eq!( parse_color("rgb(255, 64, 32)").unwrap(), Color::Rgb(255, 64, 32) ); + // bad assert!(parse_color("redd").is_none()); } From 6973123ea4523e2a6955dd5ab451ea9c8b1f7aae Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 13 Dec 2021 17:42:11 +0100 Subject: [PATCH 30/45] Categorize Auth activity msg --- .../activities/auth/components/bookmarks.rs | 60 ++++-- src/ui/activities/auth/components/form.rs | 92 ++++---- src/ui/activities/auth/components/mod.rs | 14 +- src/ui/activities/auth/components/popup.rs | 26 ++- src/ui/activities/auth/mod.rs | 32 ++- src/ui/activities/auth/update.rs | 204 ++++++++++-------- 6 files changed, 248 insertions(+), 180 deletions(-) diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs index 4745d223..8ad9661f 100644 --- a/src/ui/activities/auth/components/bookmarks.rs +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use super::Msg; +use super::{FormMsg, Msg, UiMsg}; use tui_realm_stdlib::{Input, List, Radio}; use tuirealm::command::{Cmd, CmdResult, Direction, Position}; @@ -99,16 +99,20 @@ impl Component for BookmarksList { Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { - State::One(StateValue::Usize(choice)) => Some(Msg::LoadBookmark(choice)), + State::One(StateValue::Usize(choice)) => { + Some(Msg::Form(FormMsg::LoadBookmark(choice))) + } _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Right, .. - }) => Some(Msg::BookmarksListBlur), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::BookmarksTabBlur), + }) => Some(Msg::Ui(UiMsg::BookmarksListBlur)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::BookmarksTabBlur)) + } Event::Keyboard(KeyEvent { code: Key::Delete, .. - }) => Some(Msg::ShowDeleteBookmarkPopup), + }) => Some(Msg::Ui(UiMsg::ShowDeleteBookmarkPopup)), _ => None, } } @@ -180,16 +184,20 @@ impl Component for RecentsList { Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { - State::One(StateValue::Usize(choice)) => Some(Msg::LoadRecent(choice)), + State::One(StateValue::Usize(choice)) => { + Some(Msg::Form(FormMsg::LoadRecent(choice))) + } _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Left, .. - }) => Some(Msg::RececentsListBlur), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::BookmarksTabBlur), + }) => Some(Msg::Ui(UiMsg::RececentsListBlur)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::BookmarksTabBlur)) + } Event::Keyboard(KeyEvent { code: Key::Delete, .. - }) => Some(Msg::ShowDeleteRecentPopup), + }) => Some(Msg::Ui(UiMsg::ShowDeleteRecentPopup)), _ => None, } } @@ -223,7 +231,9 @@ impl DeleteBookmarkPopup { impl Component for DeleteBookmarkPopup { fn on(&mut self, ev: Event) -> Option { match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseDeleteBookmark), + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseDeleteBookmark)) + } Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { @@ -243,9 +253,9 @@ impl Component for DeleteBookmarkPopup { self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { - Some(Msg::DeleteBookmark) + Some(Msg::Form(FormMsg::DeleteBookmark)) } else { - Some(Msg::CloseDeleteBookmark) + Some(Msg::Ui(UiMsg::CloseDeleteBookmark)) } } _ => None, @@ -281,7 +291,9 @@ impl DeleteRecentPopup { impl Component for DeleteRecentPopup { fn on(&mut self, ev: Event) -> Option { match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseDeleteRecent), + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseDeleteRecent)) + } Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { @@ -301,9 +313,9 @@ impl Component for DeleteRecentPopup { self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { - Some(Msg::DeleteRecent) + Some(Msg::Form(FormMsg::DeleteRecent)) } else { - Some(Msg::CloseDeleteRecent) + Some(Msg::Ui(UiMsg::CloseDeleteRecent)) } } _ => None, @@ -342,7 +354,9 @@ impl BookmarkSavePassword { impl Component for BookmarkSavePassword { fn on(&mut self, ev: Event) -> Option { match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseSaveBookmark), + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseSaveBookmark)) + } Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { @@ -357,8 +371,10 @@ impl Component for BookmarkSavePassword { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::SaveBookmark), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::SaveBookmarkPasswordBlur), + }) => Some(Msg::Form(FormMsg::SaveBookmark)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur)) + } _ => None, } } @@ -391,7 +407,9 @@ impl BookmarkName { impl Component for BookmarkName { fn on(&mut self, ev: Event) -> Option { match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseSaveBookmark), + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseSaveBookmark)) + } Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { @@ -436,10 +454,10 @@ impl Component for BookmarkName { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::SaveBookmark), + }) => Some(Msg::Form(FormMsg::SaveBookmark)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::BookmarkNameBlur), + }) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)), _ => None, } } diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs index c6950621..d45150ed 100644 --- a/src/ui/activities/auth/components/form.rs +++ b/src/ui/activities/auth/components/form.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use super::{FileTransferProtocol, Msg}; +use super::{FileTransferProtocol, FormMsg, Msg, UiMsg}; use tui_realm_stdlib::{Input, Radio}; use tuirealm::command::{Cmd, CmdResult, Direction, Position}; @@ -91,18 +91,22 @@ impl Component for ProtocolRadio { }) => self.perform(Cmd::Move(Direction::Right)), Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => return Some(Msg::Connect), + }) => return Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => return Some(Msg::ProtocolBlurDown), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => return Some(Msg::ProtocolBlurUp), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => return Some(Msg::ParamsFormBlur), + }) => return Some(Msg::Ui(UiMsg::ProtocolBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + return Some(Msg::Ui(UiMsg::ProtocolBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + return Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } _ => return None, }; match result { - CmdResult::Changed(State::One(StateValue::Usize(choice))) => { - Some(Msg::ProtocolChanged(Self::protocol_opt_to_enum(choice))) - } + CmdResult::Changed(State::One(StateValue::Usize(choice))) => Some(Msg::Form( + FormMsg::ProtocolChanged(Self::protocol_opt_to_enum(choice)), + )), _ => Some(Msg::None), } } @@ -180,12 +184,14 @@ impl Component for InputAddress { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Connect), + }) => Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::AddressBlurDown), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::AddressBlurUp), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + }) => Some(Msg::Ui(UiMsg::AddressBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::AddressBlurUp)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } _ => None, } } @@ -264,12 +270,14 @@ impl Component for InputPort { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Connect), + }) => Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::PortBlurDown), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PortBlurUp), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + }) => Some(Msg::Ui(UiMsg::PortBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::PortBlurUp)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } _ => None, } } @@ -347,12 +355,14 @@ impl Component for InputUsername { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Connect), + }) => Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::UsernameBlurDown), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::UsernameBlurUp), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + }) => Some(Msg::Ui(UiMsg::UsernameBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::UsernameBlurUp)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } _ => None, } } @@ -429,12 +439,14 @@ impl Component for InputPassword { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Connect), + }) => Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::PasswordBlurDown), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PasswordBlurUp), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + }) => Some(Msg::Ui(UiMsg::PasswordBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::PasswordBlurUp)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } _ => None, } } @@ -512,12 +524,14 @@ impl Component for InputS3Bucket { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Connect), + }) => Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::S3BucketBlurDown), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3BucketBlurUp), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + }) => Some(Msg::Ui(UiMsg::S3BucketBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::S3BucketBlurUp)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } _ => None, } } @@ -595,12 +609,14 @@ impl Component for InputS3Region { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Connect), + }) => Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::S3RegionBlurDown), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3RegionBlurUp), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + }) => Some(Msg::Ui(UiMsg::S3RegionBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::S3RegionBlurUp)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } _ => None, } } @@ -678,12 +694,16 @@ impl Component for InputS3Profile { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Connect), + }) => Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::S3ProfileBlurDown), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3ProfileBlurUp), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur), + }) => Some(Msg::Ui(UiMsg::S3ProfileBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3ProfileBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } _ => None, } } diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index c7c43303..73b4e219 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use super::{FileTransferProtocol, Msg}; +use super::{FileTransferProtocol, FormMsg, Msg, UiMsg}; mod bookmarks; mod form; @@ -63,27 +63,27 @@ impl Component for GlobalListener { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Function(10), .. - }) => Some(Msg::ShowQuitPopup), + }) => Some(Msg::Ui(UiMsg::ShowQuitPopup)), Event::Keyboard(KeyEvent { code: Key::Char('c'), modifiers: KeyModifiers::CONTROL, - }) => Some(Msg::EnterSetup), + }) => Some(Msg::Form(FormMsg::EnterSetup)), Event::Keyboard(KeyEvent { code: Key::Char('h'), modifiers: KeyModifiers::CONTROL, - }) => Some(Msg::ShowKeybindingsPopup), + }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), Event::Keyboard(KeyEvent { code: Key::Function(1), .. - }) => Some(Msg::ShowKeybindingsPopup), + }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), Event::Keyboard(KeyEvent { code: Key::Char('r'), modifiers: KeyModifiers::CONTROL, - }) => Some(Msg::ShowReleaseNotes), + }) => Some(Msg::Ui(UiMsg::ShowReleaseNotes)), Event::Keyboard(KeyEvent { code: Key::Char('s'), modifiers: KeyModifiers::CONTROL, - }) => Some(Msg::ShowSaveBookmarkPopup), + }) => Some(Msg::Ui(UiMsg::ShowSaveBookmarkPopup)), _ => None, } } diff --git a/src/ui/activities/auth/components/popup.rs b/src/ui/activities/auth/components/popup.rs index 3613ee87..55256fb8 100644 --- a/src/ui/activities/auth/components/popup.rs +++ b/src/ui/activities/auth/components/popup.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use super::Msg; +use super::{FormMsg, Msg, UiMsg}; use tui_realm_stdlib::{List, Paragraph, Radio, Textarea}; use tuirealm::command::{Cmd, CmdResult, Direction, Position}; @@ -63,7 +63,7 @@ impl Component for ErrorPopup { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. - }) => Some(Msg::CloseErrorPopup), + }) => Some(Msg::Ui(UiMsg::CloseErrorPopup)), _ => None, } } @@ -99,7 +99,7 @@ impl Component for InfoPopup { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. - }) => Some(Msg::CloseInfoPopup), + }) => Some(Msg::Ui(UiMsg::CloseInfoPopup)), _ => None, } } @@ -194,7 +194,9 @@ impl QuitPopup { impl Component for QuitPopup { fn on(&mut self, ev: Event) -> Option { match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseQuitPopup), + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseQuitPopup)) + } Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { @@ -214,9 +216,9 @@ impl Component for QuitPopup { self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { - Some(Msg::Quit) + Some(Msg::Form(FormMsg::Quit)) } else { - Some(Msg::CloseQuitPopup) + Some(Msg::Ui(UiMsg::CloseQuitPopup)) } } _ => None, @@ -251,7 +253,9 @@ impl InstallUpdatePopup { impl Component for InstallUpdatePopup { fn on(&mut self, ev: Event) -> Option { match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseInstallUpdatePopup), + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseInstallUpdatePopup)) + } Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { @@ -271,9 +275,9 @@ impl Component for InstallUpdatePopup { self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { - Some(Msg::InstallUpdate) + Some(Msg::Form(FormMsg::InstallUpdate)) } else { - Some(Msg::CloseInstallUpdatePopup) + Some(Msg::Ui(UiMsg::CloseInstallUpdatePopup)) } } _ => None, @@ -316,7 +320,7 @@ impl Component for ReleaseNotes { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. - }) => Some(Msg::CloseInstallUpdatePopup), + }) => Some(Msg::Ui(UiMsg::CloseInstallUpdatePopup)), Event::Keyboard(KeyEvent { code: Key::Down, .. }) => { @@ -412,7 +416,7 @@ impl Component for Keybindings { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. - }) => Some(Msg::CloseKeybindingsPopup), + }) => Some(Msg::Ui(UiMsg::CloseKeybindingsPopup)), Event::Keyboard(KeyEvent { code: Key::Down, .. }) => { diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 56e42118..bff020b0 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -78,6 +78,27 @@ pub enum Id { #[derive(Debug, PartialEq)] pub enum Msg { + Form(FormMsg), + Ui(UiMsg), + None, +} + +#[derive(Debug, PartialEq)] +pub enum FormMsg { + Connect, + DeleteBookmark, + DeleteRecent, + EnterSetup, + InstallUpdate, + LoadBookmark(usize), + LoadRecent(usize), + ProtocolChanged(FileTransferProtocol), + Quit, + SaveBookmark, +} + +#[derive(Debug, PartialEq)] +pub enum UiMsg { AddressBlurDown, AddressBlurUp, BookmarksListBlur, @@ -90,13 +111,6 @@ pub enum Msg { CloseKeybindingsPopup, CloseQuitPopup, CloseSaveBookmark, - Connect, - DeleteBookmark, - DeleteRecent, - EnterSetup, - InstallUpdate, - LoadBookmark(usize), - LoadRecent(usize), ParamsFormBlur, PasswordBlurDown, PasswordBlurUp, @@ -104,8 +118,6 @@ pub enum Msg { PortBlurUp, ProtocolBlurDown, ProtocolBlurUp, - ProtocolChanged(FileTransferProtocol), - Quit, RececentsListBlur, S3BucketBlurDown, S3BucketBlurUp, @@ -113,7 +125,6 @@ pub enum Msg { S3ProfileBlurUp, S3RegionBlurDown, S3RegionBlurUp, - SaveBookmark, BookmarkNameBlur, SaveBookmarkPasswordBlur, ShowDeleteBookmarkPopup, @@ -124,7 +135,6 @@ pub enum Msg { ShowSaveBookmarkPopup, UsernameBlurDown, UsernameBlurUp, - None, } /// Auth form input mask diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index 2457bee6..f55cb4ab 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use super::{AuthActivity, ExitReason, Id, InputMask, Msg, Update}; +use super::{AuthActivity, ExitReason, FormMsg, Id, InputMask, Msg, UiMsg, Update}; use tuirealm::{State, StateValue}; @@ -33,46 +33,17 @@ impl Update for AuthActivity { fn update(&mut self, msg: Option) -> Option { self.redraw = true; match msg.unwrap_or(Msg::None) { - Msg::AddressBlurDown => { - assert!(self.app.active(&Id::Port).is_ok()); - } - Msg::AddressBlurUp => { - assert!(self.app.active(&Id::Protocol).is_ok()); - } - Msg::BookmarksListBlur => { - assert!(self.app.active(&Id::RecentsList).is_ok()); - } - Msg::BookmarkNameBlur => { - assert!(self.app.active(&Id::BookmarkSavePassword).is_ok()); - } - Msg::BookmarksTabBlur => { - assert!(self.app.active(&Id::Protocol).is_ok()); - } - Msg::CloseDeleteBookmark => { - assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok()); - } - Msg::CloseDeleteRecent => { - assert!(self.app.umount(&Id::DeleteRecentPopup).is_ok()); - } - Msg::CloseErrorPopup => { - self.umount_error(); - } - Msg::CloseInfoPopup => { - self.umount_info(); - } - Msg::CloseInstallUpdatePopup => { - assert!(self.app.umount(&Id::NewVersionChangelog).is_ok()); - assert!(self.app.umount(&Id::InstallUpdatePopup).is_ok()); - } - Msg::CloseKeybindingsPopup => { - self.umount_help(); - } - Msg::CloseQuitPopup => self.umount_quit(), - Msg::CloseSaveBookmark => { - assert!(self.app.umount(&Id::BookmarkName).is_ok()); - assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok()); - } - Msg::Connect => { + Msg::Form(msg) => self.update_form(msg), + Msg::Ui(msg) => self.update_ui(msg), + Msg::None => None, + } + } +} + +impl AuthActivity { + fn update_form(&mut self, msg: FormMsg) -> Option { + match msg { + FormMsg::Connect => { match self.collect_host_params() { Err(err) => { // mount error @@ -87,7 +58,7 @@ impl Update for AuthActivity { } } } - Msg::DeleteBookmark => { + FormMsg::DeleteBookmark => { if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) { // Umount dialog self.umount_bookmark_del_dialog(); @@ -97,7 +68,7 @@ impl Update for AuthActivity { self.view_bookmarks() } } - Msg::DeleteRecent => { + FormMsg::DeleteRecent => { if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::RecentsList) { // Umount dialog self.umount_recent_del_dialog(); @@ -107,13 +78,13 @@ impl Update for AuthActivity { self.view_recent_connections(); } } - Msg::EnterSetup => { + FormMsg::EnterSetup => { self.exit_reason = Some(ExitReason::EnterSetup); } - Msg::InstallUpdate => { + FormMsg::InstallUpdate => { self.install_update(); } - Msg::LoadBookmark(i) => { + FormMsg::LoadBookmark(i) => { self.load_bookmark(i); // Give focus to input password (or to protocol if not generic) assert!(self @@ -124,7 +95,7 @@ impl Update for AuthActivity { }) .is_ok()); } - Msg::LoadRecent(i) => { + FormMsg::LoadRecent(i) => { self.load_recent(i); // Give focus to input password (or to protocol if not generic) assert!(self @@ -135,22 +106,90 @@ impl Update for AuthActivity { }) .is_ok()); } - Msg::ParamsFormBlur => { + FormMsg::ProtocolChanged(protocol) => { + self.protocol = protocol; + // Update port + let port: u16 = self.get_input_port(); + if Self::is_port_standard(port) { + self.mount_port(Self::get_default_port_for_protocol(protocol)); + } + } + FormMsg::Quit => { + self.exit_reason = Some(ExitReason::Quit); + } + FormMsg::SaveBookmark => { + // get bookmark name + let (name, save_password) = self.get_new_bookmark(); + // Save bookmark + if !name.is_empty() { + self.save_bookmark(name, save_password); + } + // Umount popup + self.umount_bookmark_save_dialog(); + // Reload bookmarks + self.view_bookmarks() + } + } + None + } + + fn update_ui(&mut self, msg: UiMsg) -> Option { + match msg { + UiMsg::AddressBlurDown => { + assert!(self.app.active(&Id::Port).is_ok()); + } + UiMsg::AddressBlurUp => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + UiMsg::BookmarksListBlur => { + assert!(self.app.active(&Id::RecentsList).is_ok()); + } + UiMsg::BookmarkNameBlur => { + assert!(self.app.active(&Id::BookmarkSavePassword).is_ok()); + } + UiMsg::BookmarksTabBlur => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + UiMsg::CloseDeleteBookmark => { + assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok()); + } + UiMsg::CloseDeleteRecent => { + assert!(self.app.umount(&Id::DeleteRecentPopup).is_ok()); + } + UiMsg::CloseErrorPopup => { + self.umount_error(); + } + UiMsg::CloseInfoPopup => { + self.umount_info(); + } + UiMsg::CloseInstallUpdatePopup => { + assert!(self.app.umount(&Id::NewVersionChangelog).is_ok()); + assert!(self.app.umount(&Id::InstallUpdatePopup).is_ok()); + } + UiMsg::CloseKeybindingsPopup => { + self.umount_help(); + } + UiMsg::CloseQuitPopup => self.umount_quit(), + UiMsg::CloseSaveBookmark => { + assert!(self.app.umount(&Id::BookmarkName).is_ok()); + assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok()); + } + UiMsg::ParamsFormBlur => { assert!(self.app.active(&Id::BookmarksList).is_ok()); } - Msg::PasswordBlurDown => { + UiMsg::PasswordBlurDown => { assert!(self.app.active(&Id::Protocol).is_ok()); } - Msg::PasswordBlurUp => { + UiMsg::PasswordBlurUp => { assert!(self.app.active(&Id::Username).is_ok()); } - Msg::PortBlurDown => { + UiMsg::PortBlurDown => { assert!(self.app.active(&Id::Username).is_ok()); } - Msg::PortBlurUp => { + UiMsg::PortBlurUp => { assert!(self.app.active(&Id::Address).is_ok()); } - Msg::ProtocolBlurDown => { + UiMsg::ProtocolBlurDown => { assert!(self .app .active(match self.input_mask() { @@ -159,7 +198,7 @@ impl Update for AuthActivity { }) .is_ok()); } - Msg::ProtocolBlurUp => { + UiMsg::ProtocolBlurUp => { assert!(self .app .active(match self.input_mask() { @@ -168,79 +207,56 @@ impl Update for AuthActivity { }) .is_ok()); } - Msg::ProtocolChanged(protocol) => { - self.protocol = protocol; - // Update port - let port: u16 = self.get_input_port(); - if Self::is_port_standard(port) { - self.mount_port(Self::get_default_port_for_protocol(protocol)); - } - } - Msg::Quit => { - self.exit_reason = Some(ExitReason::Quit); - } - Msg::RececentsListBlur => { + UiMsg::RececentsListBlur => { assert!(self.app.active(&Id::BookmarksList).is_ok()); } - Msg::S3BucketBlurDown => { + UiMsg::S3BucketBlurDown => { assert!(self.app.active(&Id::S3Region).is_ok()); } - Msg::S3BucketBlurUp => { + UiMsg::S3BucketBlurUp => { assert!(self.app.active(&Id::Protocol).is_ok()); } - Msg::S3RegionBlurDown => { + UiMsg::S3RegionBlurDown => { assert!(self.app.active(&Id::S3Profile).is_ok()); } - Msg::S3RegionBlurUp => { + UiMsg::S3RegionBlurUp => { assert!(self.app.active(&Id::S3Bucket).is_ok()); } - Msg::S3ProfileBlurDown => { + UiMsg::S3ProfileBlurDown => { assert!(self.app.active(&Id::Protocol).is_ok()); } - Msg::S3ProfileBlurUp => { + UiMsg::S3ProfileBlurUp => { assert!(self.app.active(&Id::S3Region).is_ok()); } - Msg::SaveBookmark => { - // get bookmark name - let (name, save_password) = self.get_new_bookmark(); - // Save bookmark - if !name.is_empty() { - self.save_bookmark(name, save_password); - } - // Umount popup - self.umount_bookmark_save_dialog(); - // Reload bookmarks - self.view_bookmarks() - } - Msg::SaveBookmarkPasswordBlur => { + UiMsg::SaveBookmarkPasswordBlur => { assert!(self.app.active(&Id::BookmarkName).is_ok()); } - Msg::ShowDeleteBookmarkPopup => { + UiMsg::ShowDeleteBookmarkPopup => { self.mount_bookmark_del_dialog(); } - Msg::ShowDeleteRecentPopup => { + UiMsg::ShowDeleteRecentPopup => { self.mount_recent_del_dialog(); } - Msg::ShowKeybindingsPopup => { + UiMsg::ShowKeybindingsPopup => { self.mount_keybindings(); } - Msg::ShowQuitPopup => { + UiMsg::ShowQuitPopup => { self.mount_quit(); } - Msg::ShowReleaseNotes => { + UiMsg::ShowReleaseNotes => { self.mount_release_notes(); } - Msg::ShowSaveBookmarkPopup => { + UiMsg::ShowSaveBookmarkPopup => { self.mount_bookmark_save_dialog(); } - Msg::UsernameBlurDown => { + UiMsg::UsernameBlurDown => { assert!(self.app.active(&Id::Password).is_ok()); } - Msg::UsernameBlurUp => { + UiMsg::UsernameBlurUp => { assert!(self.app.active(&Id::Port).is_ok()); } - Msg::None => {} } + None } } From 1641c05346e6b855f2ca2878bd0fce8433164d53 Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 13 Dec 2021 17:50:41 +0100 Subject: [PATCH 31/45] F2 to save file as --- CHANGELOG.md | 1 + docs/de/man.md | 2 +- docs/es/man.md | 2 +- docs/fr/man.md | 2 +- docs/it/man.md | 2 +- docs/man.md | 2 +- docs/zh-CN/man.md | 2 +- src/ui/activities/filetransfer/components/misc.rs | 2 ++ src/ui/activities/filetransfer/components/popups.rs | 4 ++-- src/ui/activities/filetransfer/components/transfer/mod.rs | 6 +++--- 10 files changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2cb561..8071f3cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Released on FIXME: - Selected files will now be rendered with **Reversed, underlined and italic** text modifiers instead of being prepended with `*`. - **Midnight commander keys** - ``: Show help + - ``: Save file as (actually I invented this) - ``: View file - ``: Open file (with text editor) - ``: Copy file diff --git a/docs/de/man.md b/docs/de/man.md index 0d464905..38dff054 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -175,7 +175,7 @@ In order to change panel you need to type `` to move the remote explorer p | `` | Edit file; see Text editor | Open | | `` | Quit termscp | Quit | | `` | Rename file | Rename | -| `` | Save file as... | Save | +| `` | Save file as... | Save | | `` | Go to parent directory | Upper | | `` | Open file with default program for filetype | View | | `` | Open file with provided program | With | diff --git a/docs/es/man.md b/docs/es/man.md index b6c7ebdc..1465b516 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -175,7 +175,7 @@ Para cambiar de panel, debe escribir `` para mover el panel del explorador | `` | Editar archivo | Open | | `` | Salir de termscp | Quit | | `` | Renombrar archivo | Rename | -| `` | Guardar archivo como... | Save | +| `` | Guardar archivo como... | Save | | `` | Ir al directorio principal | Upper | | `` | Abrir archivo con el programa predeterminado | View | | `` | Abrir archivo con el programa proporcionado | With | diff --git a/docs/fr/man.md b/docs/fr/man.md index 57cf3869..78b495c2 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -173,7 +173,7 @@ Pour changer de panneau, vous devez taper `` pour déplacer le panneau de | `` | Modifier le fichier | Open | | `` | Quitter termscp | Quit | | `` | Renommer le fichier | Rename | -| `` | Enregistrer le fichier sous... | Save | +| `` | Enregistrer le fichier sous... | Save | | `` | Aller dans le répertoire parent | Upper | | `` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View | | `` | Ouvrir le fichier avec le programme spécifié | With | diff --git a/docs/it/man.md b/docs/it/man.md index 0bebba2c..ea26039d 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -169,7 +169,7 @@ Per cambiare pannello ti puoi muovere con le frecce, `` per andare sul pan | `` | Modifica file; Vedi text editor | Open | | `` | Termina termscp | Quit | | `` | Rinomina file | Rename | -| `` | Salva file con nome | Save | +| `` | Salva file con nome | Save | | `` | Vai alla directory padre | Upper | | `` | Apri il file con il programma definito dal sistema | View | | `` | Apri il file con il programma specificato | With | diff --git a/docs/man.md b/docs/man.md index b13a8c33..0ccad6be 100644 --- a/docs/man.md +++ b/docs/man.md @@ -173,7 +173,7 @@ In order to change panel you need to type `` to move the remote explorer p | `` | Edit file; see Text editor | Open | | `` | Quit termscp | Quit | | `` | Rename file | Rename | -| `` | Save file as... | Save | +| `` | Save file as... | Save | | `` | Go to parent directory | Upper | | `` | Open file with default program for filetype | View | | `` | Open file with provided program | With | diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index ca47eab9..f9788df7 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -171,7 +171,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到 | `` | 编辑文件;参考文本编辑器文档 | Open | | `` | 退出termscp | Quit | | `` | 重命名文件 | Rename | -| `` | 另存为... | Save | +| `` | 另存为... | Save | | `` | 进入上层目录 | Upper | | `` | 使用默认方式打开文件 | View | | `` | 使用指定程序打开文件 | With | diff --git a/src/ui/activities/filetransfer/components/misc.rs b/src/ui/activities/filetransfer/components/misc.rs index ef903be1..f74013a8 100644 --- a/src/ui/activities/filetransfer/components/misc.rs +++ b/src/ui/activities/filetransfer/components/misc.rs @@ -48,6 +48,8 @@ impl FooterBar { TextSpan::from(" Transfer "), TextSpan::from("").bold().fg(key_color), TextSpan::from(" Enter dir "), + TextSpan::from("").bold().fg(key_color), + TextSpan::from(" Save as "), TextSpan::from("").bold().fg(key_color), TextSpan::from(" View "), TextSpan::from("").bold().fg(key_color), diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index c0ca5c9f..de56ce2d 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -744,8 +744,8 @@ impl KeybindingsPopup { .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Rename file")) .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Save file as")) + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Save file as")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Go to parent directory")) diff --git a/src/ui/activities/filetransfer/components/transfer/mod.rs b/src/ui/activities/filetransfer/components/transfer/mod.rs index cfbf5aba..71be8b5e 100644 --- a/src/ui/activities/filetransfer/components/transfer/mod.rs +++ b/src/ui/activities/filetransfer/components/transfer/mod.rs @@ -143,7 +143,7 @@ impl Component for ExplorerFind { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)), Event::Keyboard(KeyEvent { - code: Key::Char('s'), + code: Key::Char('s') | Key::Function(2), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), Event::Keyboard(KeyEvent { @@ -299,7 +299,7 @@ impl Component for ExplorerLocal { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowRenamePopup)), Event::Keyboard(KeyEvent { - code: Key::Char('s'), + code: Key::Char('s') | Key::Function(2), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), Event::Keyboard(KeyEvent { @@ -467,7 +467,7 @@ impl Component for ExplorerRemote { modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowRenamePopup)), Event::Keyboard(KeyEvent { - code: Key::Char('s'), + code: Key::Char('s') | Key::Function(2), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)), Event::Keyboard(KeyEvent { From c330f3f7ad33ef61840ff69b8fec93d9fe5d7982 Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 13 Dec 2021 17:57:52 +0100 Subject: [PATCH 32/45] remove temp archive once installation has finished --- install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.sh b/install.sh index 0a143772..46948b2c 100755 --- a/install.sh +++ b/install.sh @@ -230,6 +230,7 @@ install_on_linux() { fi info "$msg" $sudo dpkg -i "${archive}" + rm -f ${archive} fi elif has rpm; then if [ "${ARCH}" != "x86_64" ]; then # It's okay on AUR; not on other distros @@ -251,6 +252,7 @@ install_on_linux() { fi info "$msg" $sudo rpm -U "${archive}" + rm -f ${archive} fi else try_with_cargo "No suitable installation method found for your Linux distribution; if you're running on Arch linux, please install an AUR package manager (such as yay). Currently only Arch, Debian based and Red Hat based distros are supported" @@ -330,6 +332,7 @@ install_cargo() { download "${rustup}" "https://sh.rustup.rs" chmod +x $rustup $rustup -y + rm -f ${archive} info "Rust installed with success" . $cargo_env fi From 6b7d5c8905afc90d3eaee16ecdc9b8aa578eb133 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 14 Dec 2021 15:52:56 +0100 Subject: [PATCH 33/45] symlink command --- CHANGELOG.md | 6 +- docs/de/man.md | 3 +- docs/es/man.md | 75 +++++++------- docs/fr/man.md | 75 +++++++------- docs/it/man.md | 5 +- docs/man.md | 75 +++++++------- docs/zh-CN/man.md | 3 +- src/activity_manager.rs | 2 +- src/host/mod.rs | 33 +++++++ .../activities/filetransfer/actions/copy.rs | 8 -- .../activities/filetransfer/actions/delete.rs | 8 -- .../activities/filetransfer/actions/edit.rs | 4 - .../activities/filetransfer/actions/exec.rs | 3 - .../activities/filetransfer/actions/mkdir.rs | 3 - src/ui/activities/filetransfer/actions/mod.rs | 11 +++ .../filetransfer/actions/newfile.rs | 4 - .../activities/filetransfer/actions/rename.rs | 8 -- .../filetransfer/actions/symlink.rs | 97 +++++++++++++++++++ .../activities/filetransfer/components/mod.rs | 2 +- .../filetransfer/components/popups.rs | 94 ++++++++++++++++++ .../filetransfer/components/transfer/mod.rs | 8 ++ src/ui/activities/filetransfer/misc.rs | 2 + src/ui/activities/filetransfer/mod.rs | 4 + src/ui/activities/filetransfer/session.rs | 2 - src/ui/activities/filetransfer/update.rs | 27 ++++++ src/ui/activities/filetransfer/view.rs | 33 ++++++- 26 files changed, 433 insertions(+), 162 deletions(-) create mode 100644 src/ui/activities/filetransfer/actions/symlink.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8071f3cf..b6b57584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ Released on FIXME: > ❄️ Winter update 2022 ⛄ - **Enhancements**: + - **Synchronized browsing**: + - From now on, if synchronized browsing is *enabled* and you try to enter a directory that doesn't exist on the other host, you will be asked whether you'd like to create the directory. - **Find** feature: - A "wait popup" will now be displayed while searching files - If find command doesn't return any result show an info dialog and not an empty explorer @@ -60,11 +62,11 @@ Released on FIXME: - The supported parameters are described at . - If the field is left empty, **no file will be loaded**. - **By default, no file will be used**. +- **Symlink command**: + - You can now create symlinks, pressing `` key on the file explorer. - **Less verbose logging**: - By default the log level is now set to `INFO` - It is now possible to enable the `TRACE` level with the `-D` CLI option. -- **Synchronized browsing**: - - From now on, if synchronized browsing is *enabled* and you try to enter a directory that doesn't exist on the other host, you will be asked whether you'd like to create the directory. - Dependencies: - Updated `tui-realm` to `1.3.0` - Updated `tui-realm-stdlib` to `1.1.4` diff --git a/docs/de/man.md b/docs/de/man.md index 38dff054..0dafeb2f 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -168,6 +168,7 @@ In order to change panel you need to type `` to move the remote explorer p | `` | Search for files (wild match is supported) | Find | | `` | Go to supplied path | Go to | | `` | Show help | Help | +| `` | Create symlink pointing to the currently selected entry | symlinK | | `` | Show info about selected file or directory | Info | | `` | Reload current directory's content / Clear selection | List | | `` | Select a file | Mark | @@ -175,7 +176,7 @@ In order to change panel you need to type `` to move the remote explorer p | `` | Edit file; see Text editor | Open | | `` | Quit termscp | Quit | | `` | Rename file | Rename | -| `` | Save file as... | Save | +| `` | Save file as... | Save | | `` | Go to parent directory | Upper | | `` | Open file with default program for filetype | View | | `` | Open file with provided program | With | diff --git a/docs/es/man.md b/docs/es/man.md index 1465b516..67c97a33 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -146,43 +146,44 @@ Para cambiar de panel, debe escribir `` para mover el panel del explorador ### Keybindings ⌨ -| Key | Command | Reminder | -|---------------|-------------------------------------------------------|-------------| -| `` | Desconecte; volver a la página de autenticación | | -| `` | Ir al directorio anterior en la pila | | -| `` | Cambiar pestaña del explorador | | -| `` | Mover a la pestaña del explorador remoto | | -| `` | Mover a la pestaña del explorador local | | -| `` | Subir en la lista seleccionada | | -| `` | Bajar en la lista seleccionada | | -| `` | Subir 8 filas en la lista seleccionada | | -| `` | Bajar 8 filas en la lista seleccionada | | -| `` | Entrar directorio | | -| `` | Cargar / descargar el archivo seleccionado | | -| `` | Cambiar entre la pestaña de registro y el explorador | | -| `` | Alternar archivos ocultos | All | -| `` | Ordenar archivos por | Bubblesort? | -| `` | Copiar archivo / directorio | Copy | -| `` | Hacer directorio | Directory | -| `` | Eliminar archivo | Erase | -| `` | Búsqueda de archivos | Find | -| `` | Ir a la ruta proporcionada | Go to | -| `` | Mostrar ayuda | Help | -| `` | Mostrar información sobre el archivo | Info | -| `` | Recargar contenido del directorio / Borrar selección | List | -| `` | Seleccione un archivo | Mark | -| `` | Crear un nuevo archivo con el nombre proporcionado | New | -| `` | Editar archivo | Open | -| `` | Salir de termscp | Quit | -| `` | Renombrar archivo | Rename | -| `` | Guardar archivo como... | Save | -| `` | Ir al directorio principal | Upper | -| `` | Abrir archivo con el programa predeterminado | View | -| `` | Abrir archivo con el programa proporcionado | With | -| `` | Ejecutar un comando | eXecute | -| `` | Alternar navegación sincronizada | sYnc | -| `` | Seleccionar todos los archivos | | -| `` | Abortar el proceso de transferencia de archivos | | +| Key | Command | Reminder | +|---------------|----------------------------------------------------------------------------|-------------| +| `` | Desconecte; volver a la página de autenticación | | +| `` | Ir al directorio anterior en la pila | | +| `` | Cambiar pestaña del explorador | | +| `` | Mover a la pestaña del explorador remoto | | +| `` | Mover a la pestaña del explorador local | | +| `` | Subir en la lista seleccionada | | +| `` | Bajar en la lista seleccionada | | +| `` | Subir 8 filas en la lista seleccionada | | +| `` | Bajar 8 filas en la lista seleccionada | | +| `` | Entrar directorio | | +| `` | Cargar / descargar el archivo seleccionado | | +| `` | Cambiar entre la pestaña de registro y el explorador | | +| `` | Alternar archivos ocultos | All | +| `` | Ordenar archivos por | Bubblesort? | +| `` | Copiar archivo / directorio | Copy | +| `` | Hacer directorio | Directory | +| `` | Eliminar archivo | Erase | +| `` | Búsqueda de archivos | Find | +| `` | Ir a la ruta proporcionada | Go to | +| `` | Mostrar ayuda | Help | +| `` | Mostrar información sobre el archivo | Info | +| `` | Crear un enlace simbólico que apunte a la entrada seleccionada actualmente | symlinK | +| `` | Recargar contenido del directorio / Borrar selección | List | +| `` | Seleccione un archivo | Mark | +| `` | Crear un nuevo archivo con el nombre proporcionado | New | +| `` | Editar archivo | Open | +| `` | Salir de termscp | Quit | +| `` | Renombrar archivo | Rename | +| `` | Guardar archivo como... | Save | +| `` | Ir al directorio principal | Upper | +| `` | Abrir archivo con el programa predeterminado | View | +| `` | Abrir archivo con el programa proporcionado | With | +| `` | Ejecutar un comando | eXecute | +| `` | Alternar navegación sincronizada | sYnc | +| `` | Seleccionar todos los archivos | | +| `` | Abortar el proceso de transferencia de archivos | | ### Trabaja en varios archivos 🥷 diff --git a/docs/fr/man.md b/docs/fr/man.md index 78b495c2..b84bd9a5 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -144,43 +144,44 @@ Pour changer de panneau, vous devez taper `` pour déplacer le panneau de ### Raccourcis clavier ⌨ -| Key | Command | Reminder | -|---------------|---------------------------------------------------------------------|-------------| -| `` | Se Déconnecter de le serveur; retour à la page d'authentification | | -| `` | Aller au répertoire précédent dans la pile | | -| `` | Changer d'onglet explorateur | | -| `` | Déplacer vers l'onglet explorateur distant | | -| `` | Déplacer vers l'onglet explorateur local | | -| `` | Remonter dans la liste sélectionnée | | -| `` | Descendre dans la liste sélectionnée | | -| `` | Remonter dans la liste sélectionnée de 8 lignes | | -| `` | Descendre dans la liste sélectionnée de 8 lignes | | -| `` | Entrer dans le directoire | | -| `` | Télécharger le fichier sélectionné | | -| `` | Basculer entre l'onglet journal et l'explorateur | | -| `` | Basculer les fichiers cachés | All | -| `` | Trier les fichiers par | Bubblesort? | -| `` | Copier le fichier/répertoire | Copy | -| `` | Créer un dossier | Directory | -| `` | Supprimer le fichier (Identique à `DEL`) | Erase | -| `` | Rechercher des fichiers | Find | -| `` | Aller au chemin fourni | Go to | -| `` | Afficher l'aide | Help | -| `` | Afficher les informations sur le fichier ou le dossier sélectionné | Info | -| `` | Recharger le contenu du répertoire actuel / Effacer la sélection | List | -| `` | Sélectionner un fichier | Mark | -| `` | Créer un nouveau fichier avec le nom fourni | New | -| `` | Modifier le fichier | Open | -| `` | Quitter termscp | Quit | -| `` | Renommer le fichier | Rename | -| `` | Enregistrer le fichier sous... | Save | -| `` | Aller dans le répertoire parent | Upper | -| `` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View | -| `` | Ouvrir le fichier avec le programme spécifié | With | -| `` | Exécuter une commande | eXecute | -| `` | Basculer la navigation synchronisée | sYnc | -| `` | Sélectionner tous les fichiers | | -| `` | Abandonner le processus de transfert de fichiers | | +| Key | Command | Reminder | +|---------------|---------------------------------------------------------------------------|-------------| +| `` | Se Déconnecter de le serveur; retour à la page d'authentification | | +| `` | Aller au répertoire précédent dans la pile | | +| `` | Changer d'onglet explorateur | | +| `` | Déplacer vers l'onglet explorateur distant | | +| `` | Déplacer vers l'onglet explorateur local | | +| `` | Remonter dans la liste sélectionnée | | +| `` | Descendre dans la liste sélectionnée | | +| `` | Remonter dans la liste sélectionnée de 8 lignes | | +| `` | Descendre dans la liste sélectionnée de 8 lignes | | +| `` | Entrer dans le directoire | | +| `` | Télécharger le fichier sélectionné | | +| `` | Basculer entre l'onglet journal et l'explorateur | | +| `` | Basculer les fichiers cachés | All | +| `` | Trier les fichiers par | Bubblesort? | +| `` | Copier le fichier/répertoire | Copy | +| `` | Créer un dossier | Directory | +| `` | Supprimer le fichier (Identique à `DEL`) | Erase | +| `` | Rechercher des fichiers | Find | +| `` | Aller au chemin fourni | Go to | +| `` | Afficher l'aide | Help | +| `` | Afficher les informations sur le fichier ou le dossier sélectionné | Info | +| `` | Créer un lien symbolique pointant vers l'entrée actuellement sélectionnée | symlinK | +| `` | Recharger le contenu du répertoire actuel / Effacer la sélection | List | +| `` | Sélectionner un fichier | Mark | +| `` | Créer un nouveau fichier avec le nom fourni | New | +| `` | Modifier le fichier | Open | +| `` | Quitter termscp | Quit | +| `` | Renommer le fichier | Rename | +| `` | Enregistrer le fichier sous... | Save | +| `` | Aller dans le répertoire parent | Upper | +| `` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View | +| `` | Ouvrir le fichier avec le programme spécifié | With | +| `` | Exécuter une commande | eXecute | +| `` | Basculer la navigation synchronisée | sYnc | +| `` | Sélectionner tous les fichiers | | +| `` | Abandonner le processus de transfert de fichiers | | ### Travailler sur plusieurs fichiers 🥷 diff --git a/docs/it/man.md b/docs/it/man.md index ea26039d..b90264c0 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -163,13 +163,14 @@ Per cambiare pannello ti puoi muovere con le frecce, `` per andare sul pan | `` | Vai al percorso indicato | Go to | | `` | Mostra help | Help | | `` | Mostra informazioni per il file selezionato | Info | +| `` | Crea un link simbolico che punta al file selezionato | symlinK | | `` | Ricarica posizione corrente / pulisci selezione file | List | -| `` | Seleziona file | Mark | +| `` | Seleziona file | Mark | | `` | Crea nuovo file con il nome fornito | New | | `` | Modifica file; Vedi text editor | Open | | `` | Termina termscp | Quit | | `` | Rinomina file | Rename | -| `` | Salva file con nome | Save | +| `` | Salva file con nome | Save | | `` | Vai alla directory padre | Upper | | `` | Apri il file con il programma definito dal sistema | View | | `` | Apri il file con il programma specificato | With | diff --git a/docs/man.md b/docs/man.md index 0ccad6be..4eef1e06 100644 --- a/docs/man.md +++ b/docs/man.md @@ -144,43 +144,44 @@ In order to change panel you need to type `` to move the remote explorer p ### Keybindings ⌨ -| Key | Command | Reminder | -|---------------|-------------------------------------------------------|-------------| -| `` | Disconnect from remote; return to authentication page | | -| `` | Go to previous directory in stack | | -| `` | Switch explorer tab | | -| `` | Move to remote explorer tab | | -| `` | Move to local explorer tab | | -| `` | Move up in selected list | | -| `` | Move down in selected list | | -| `` | Move up in selected list by 8 rows | | -| `` | Move down in selected list by 8 rows | | -| `` | Enter directory | | -| `` | Upload / download selected file | | -| `` | Switch between log tab and explorer | | -| `` | Toggle hidden files | All | -| `` | Sort files by | Bubblesort? | -| `` | Copy file/directory | Copy | -| `` | Make directory | Directory | -| `` | Delete file | Erase | -| `` | Search for files (wild match is supported) | Find | -| `` | Go to supplied path | Go to | -| `` | Show help | Help | -| `` | Show info about selected file or directory | Info | -| `` | Reload current directory's content / Clear selection | List | -| `` | Select a file | Mark | -| `` | Create new file with provided name | New | -| `` | Edit file; see Text editor | Open | -| `` | Quit termscp | Quit | -| `` | Rename file | Rename | -| `` | Save file as... | Save | -| `` | Go to parent directory | Upper | -| `` | Open file with default program for filetype | View | -| `` | Open file with provided program | With | -| `` | Execute a command | eXecute | -| `` | Toggle synchronized browsing | sYnc | -| `` | Select all files | | -| `` | Abort file transfer process | | +| Key | Command | Reminder | +|---------------|---------------------------------------------------------|-------------| +| `` | Disconnect from remote; return to authentication page | | +| `` | Go to previous directory in stack | | +| `` | Switch explorer tab | | +| `` | Move to remote explorer tab | | +| `` | Move to local explorer tab | | +| `` | Move up in selected list | | +| `` | Move down in selected list | | +| `` | Move up in selected list by 8 rows | | +| `` | Move down in selected list by 8 rows | | +| `` | Enter directory | | +| `` | Upload / download selected file | | +| `` | Switch between log tab and explorer | | +| `` | Toggle hidden files | All | +| `` | Sort files by | Bubblesort? | +| `` | Copy file/directory | Copy | +| `` | Make directory | Directory | +| `` | Delete file | Erase | +| `` | Search for files (wild match is supported) | Find | +| `` | Go to supplied path | Go to | +| `` | Show help | Help | +| `` | Show info about selected file or directory | Info | +| `` | Create symlink pointing to the currently selected entry | symlinK | +| `` | Reload current directory's content / Clear selection | List | +| `` | Select a file | Mark | +| `` | Create new file with provided name | New | +| `` | Edit file; see Text editor | Open | +| `` | Quit termscp | Quit | +| `` | Rename file | Rename | +| `` | Save file as... | Save | +| `` | Go to parent directory | Up | +| `` | Open file with default program for filetype | View | +| `` | Open file with provided program | With | +| `` | Execute a command | eXecute | +| `` | Toggle synchronized browsing | sYnc | +| `` | Select all files | | +| `` | Abort file transfer process | | ### Work on multiple files 🥷 diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index f9788df7..470f9b91 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -165,13 +165,14 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到 | `` | 跳转到指定路径 | Go to | | `` | 显示帮助 | Help | | `` | 显示选中文件(夹)信息 | Info | +| `` | 创建指向当前选定条目的符号链接 | symlinK | | `` | 刷新当前目录列表 / 清除选中状态 | List | | `` | 选中文件 | Mark | | `` | 使用键入的名称新建文件 | New | | `` | 编辑文件;参考文本编辑器文档 | Open | | `` | 退出termscp | Quit | | `` | 重命名文件 | Rename | -| `` | 另存为... | Save | +| `` | 另存为... | Save | | `` | 进入上层目录 | Upper | | `` | 使用默认方式打开文件 | View | | `` | 使用指定程序打开文件 | With | diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 224fd537..301d8726 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -43,7 +43,7 @@ use std::time::Duration; /// ### NextActivity /// -/// NextActivity identified the next identity to run once the current has ended +/// NextActivity identifies the next identity to run once the current has ended pub enum NextActivity { Authentication, FileTransfer, diff --git a/src/host/mod.rs b/src/host/mod.rs index b19a585e..78cec83b 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -672,6 +672,21 @@ impl Localhost { self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search)) } + /// Create a symlink at path pointing at target + #[cfg(target_family = "unix")] + pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> { + let path = self.to_path(path); + std::os::unix::fs::symlink(target, path.as_path()).map_err(|e| { + error!( + "Failed to create symlink at {} pointing at {}: {}", + path.display(), + target.display(), + e + ); + HostError::new(HostErrorType::CouldNotCreateFile, Some(e), path.as_path()) + }) + } + // -- privates /// Recursive call for `find` method. @@ -1179,6 +1194,24 @@ mod tests { assert_eq!(result[1].name(), "examples.csv"); } + #[test] + fn should_create_symlink() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + let dir_path: &Path = tmpdir.path(); + // Make file + assert!(make_file_at(dir_path, "pippo.txt").is_ok()); + let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap(); + let mut p = dir_path.to_path_buf(); + p.push("pippo.txt"); + // Make symlink + assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_ok()); + // Fail symlink + assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_err()); + assert!(host + .symlink(Path::new("/tmp/oooo/aaaa"), p.as_path()) + .is_err()); + } + #[test] fn test_host_fmt_error() { let err: HostError = HostError::new( diff --git a/src/ui/activities/filetransfer/actions/copy.rs b/src/ui/activities/filetransfer/actions/copy.rs index 15b7dead..7674b9a7 100644 --- a/src/ui/activities/filetransfer/actions/copy.rs +++ b/src/ui/activities/filetransfer/actions/copy.rs @@ -38,8 +38,6 @@ impl FileTransferActivity { SelectedEntry::One(entry) => { let dest_path: PathBuf = PathBuf::from(input); self.local_copy_file(&entry, dest_path.as_path()); - // Reload entries - self.reload_local_dir(); } SelectedEntry::Many(entries) => { // Try to copy each file to Input/{FILE_NAME} @@ -50,8 +48,6 @@ impl FileTransferActivity { dest_path.push(entry.name()); self.local_copy_file(entry, dest_path.as_path()); } - // Reload entries - self.reload_local_dir(); } SelectedEntry::None => {} } @@ -63,8 +59,6 @@ impl FileTransferActivity { SelectedEntry::One(entry) => { let dest_path: PathBuf = PathBuf::from(input); self.remote_copy_file(entry, dest_path.as_path()); - // Reload entries - self.reload_remote_dir(); } SelectedEntry::Many(entries) => { // Try to copy each file to Input/{FILE_NAME} @@ -75,8 +69,6 @@ impl FileTransferActivity { dest_path.push(entry.name()); self.remote_copy_file(entry, dest_path.as_path()); } - // Reload entries - self.reload_remote_dir(); } SelectedEntry::None => {} } diff --git a/src/ui/activities/filetransfer/actions/delete.rs b/src/ui/activities/filetransfer/actions/delete.rs index 36eb06d5..1890791f 100644 --- a/src/ui/activities/filetransfer/actions/delete.rs +++ b/src/ui/activities/filetransfer/actions/delete.rs @@ -36,8 +36,6 @@ impl FileTransferActivity { SelectedEntry::One(entry) => { // Delete file self.local_remove_file(&entry); - // Reload - self.reload_local_dir(); } SelectedEntry::Many(entries) => { // Iter files @@ -45,8 +43,6 @@ impl FileTransferActivity { // Delete file self.local_remove_file(entry); } - // Reload entries - self.reload_local_dir(); } SelectedEntry::None => {} } @@ -57,8 +53,6 @@ impl FileTransferActivity { SelectedEntry::One(entry) => { // Delete file self.remote_remove_file(&entry); - // Reload - self.reload_remote_dir(); } SelectedEntry::Many(entries) => { // Iter files @@ -66,8 +60,6 @@ impl FileTransferActivity { // Delete file self.remote_remove_file(entry); } - // Reload entries - self.reload_remote_dir(); } SelectedEntry::None => {} } diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index 355f8ff1..e9e377f1 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -56,8 +56,6 @@ impl FileTransferActivity { } } } - // Reload entries - self.reload_local_dir(); } pub(crate) fn action_edit_remote_file(&mut self) { @@ -80,8 +78,6 @@ impl FileTransferActivity { } } } - // Reload entries - self.reload_remote_dir(); } /// Edit a file on localhost diff --git a/src/ui/activities/filetransfer/actions/exec.rs b/src/ui/activities/filetransfer/actions/exec.rs index 089943c9..b6a5cd52 100644 --- a/src/ui/activities/filetransfer/actions/exec.rs +++ b/src/ui/activities/filetransfer/actions/exec.rs @@ -34,8 +34,6 @@ impl FileTransferActivity { Ok(output) => { // Reload files self.log(LogLevel::Info, format!("\"{}\": {}", input, output)); - // Reload entries - self.reload_local_dir(); } Err(err) => { // Report err @@ -55,7 +53,6 @@ impl FileTransferActivity { LogLevel::Info, format!("\"{}\" (exitcode: {}): {}", input, rc, output), ); - self.reload_remote_dir(); } Err(err) => { // Report err diff --git a/src/ui/activities/filetransfer/actions/mkdir.rs b/src/ui/activities/filetransfer/actions/mkdir.rs index 5a9b1c8a..eaacb2bb 100644 --- a/src/ui/activities/filetransfer/actions/mkdir.rs +++ b/src/ui/activities/filetransfer/actions/mkdir.rs @@ -36,8 +36,6 @@ impl FileTransferActivity { Ok(_) => { // Reload files self.log(LogLevel::Info, format!("Created directory \"{}\"", input)); - // Reload entries - self.reload_local_dir(); } Err(err) => { // Report err @@ -56,7 +54,6 @@ impl FileTransferActivity { Ok(_) => { // Reload files self.log(LogLevel::Info, format!("Created directory \"{}\"", input)); - self.reload_remote_dir(); } Err(err) => { // Report err diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index 4443f989..e3b73505 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -46,6 +46,7 @@ mod pending; pub(crate) mod rename; pub(crate) mod save; pub(crate) mod submit; +pub(crate) mod symlink; #[derive(Debug)] pub(crate) enum SelectedEntry { @@ -109,6 +110,16 @@ impl FileTransferActivity { } } + /// Returns whether only one entry is selected on local host + pub(crate) fn is_local_selected_one(&self) -> bool { + matches!(self.get_local_selected_entries(), SelectedEntry::One(_)) + } + + /// Returns whether only one entry is selected on remote host + pub(crate) fn is_remote_selected_one(&self) -> bool { + matches!(self.get_remote_selected_entries(), SelectedEntry::One(_)) + } + /// Get remote file entry pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry { match self.get_selected_index(&Id::ExplorerFind) { diff --git a/src/ui/activities/filetransfer/actions/newfile.rs b/src/ui/activities/filetransfer/actions/newfile.rs index 18bfee02..c8119e4f 100644 --- a/src/ui/activities/filetransfer/actions/newfile.rs +++ b/src/ui/activities/filetransfer/actions/newfile.rs @@ -59,8 +59,6 @@ impl FileTransferActivity { format!("Created file \"{}\"", file_path.display()), ); } - // Reload files - self.reload_local_dir(); } pub(crate) fn action_remote_newfile(&mut self, input: String) { @@ -123,8 +121,6 @@ impl FileTransferActivity { LogLevel::Info, format!("Created file \"{}\"", file_path.display()), ); - // Reload files - self.reload_remote_dir(); } } } diff --git a/src/ui/activities/filetransfer/actions/rename.rs b/src/ui/activities/filetransfer/actions/rename.rs index 2046a4f2..9bb1cf70 100644 --- a/src/ui/activities/filetransfer/actions/rename.rs +++ b/src/ui/activities/filetransfer/actions/rename.rs @@ -37,8 +37,6 @@ impl FileTransferActivity { SelectedEntry::One(entry) => { let dest_path: PathBuf = PathBuf::from(input); self.local_rename_file(&entry, dest_path.as_path()); - // Reload entries - self.reload_local_dir(); } SelectedEntry::Many(entries) => { // Try to copy each file to Input/{FILE_NAME} @@ -49,8 +47,6 @@ impl FileTransferActivity { dest_path.push(entry.name()); self.local_rename_file(entry, dest_path.as_path()); } - // Reload entries - self.reload_local_dir(); } SelectedEntry::None => {} } @@ -61,8 +57,6 @@ impl FileTransferActivity { SelectedEntry::One(entry) => { let dest_path: PathBuf = PathBuf::from(input); self.remote_rename_file(&entry, dest_path.as_path()); - // Reload entries - self.reload_remote_dir(); } SelectedEntry::Many(entries) => { // Try to copy each file to Input/{FILE_NAME} @@ -73,8 +67,6 @@ impl FileTransferActivity { dest_path.push(entry.name()); self.remote_rename_file(entry, dest_path.as_path()); } - // Reload entries - self.reload_remote_dir(); } SelectedEntry::None => {} } diff --git a/src/ui/activities/filetransfer/actions/symlink.rs b/src/ui/activities/filetransfer/actions/symlink.rs new file mode 100644 index 00000000..3006944a --- /dev/null +++ b/src/ui/activities/filetransfer/actions/symlink.rs @@ -0,0 +1,97 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +// locals +use super::{FileTransferActivity, LogLevel, SelectedEntry}; + +use std::path::PathBuf; + +impl FileTransferActivity { + /// Create symlink on localhost + #[cfg(target_family = "unix")] + pub(crate) fn action_local_symlink(&mut self, name: String) { + if let SelectedEntry::One(entry) = self.get_local_selected_entries() { + match self + .host + .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) + { + Ok(_) => { + self.log( + LogLevel::Info, + format!( + "Created symlink at {}, pointing to {}", + name, + entry.path().display() + ), + ); + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not create symlink: {}", err), + ); + } + } + } + } + + #[cfg(target_family = "windows")] + pub(crate) fn action_local_symlink(&mut self, _name: String) { + self.mount_error("Symlinks are not supported on Windows hosts"); + } + + /// Copy file on remote + pub(crate) fn action_remote_symlink(&mut self, name: String) { + if let SelectedEntry::One(entry) = self.get_remote_selected_entries() { + match self + .client + .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) + { + Ok(_) => { + self.log( + LogLevel::Info, + format!( + "Created symlink at {}, pointing to {}", + name, + entry.path().display() + ), + ); + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not create symlink pointing to {}: {}", + entry.path().display(), + err + ), + ); + } + } + } + } +} diff --git a/src/ui/activities/filetransfer/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs index 442c1611..80f5d8c9 100644 --- a/src/ui/activities/filetransfer/components/mod.rs +++ b/src/ui/activities/filetransfer/components/mod.rs @@ -46,7 +46,7 @@ pub use popups::{ FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup, ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, - SyncBrowsingMkdirPopup, WaitPopup, + SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, }; pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote}; diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index de56ce2d..17bf2421 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -724,6 +724,11 @@ impl KeybindingsPopup { " Show info about selected file", )) .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Create symlink pointing to the current selected entry", + )) + .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Reload directory content")) .add_row() @@ -1657,6 +1662,95 @@ fn hidden_files_label(visible: bool) -> &'static str { } } +#[derive(MockComponent)] +pub struct SymlinkPopup { + component: Input, +} + +impl SymlinkPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "Symlink name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title( + "Create a symlink pointing to the selected entry", + Alignment::Center, + ), + } + } +} + +impl Component for SymlinkPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::CreateSymlink(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseSymlinkPopup)) + } + _ => None, + } + } +} + #[derive(MockComponent)] pub struct SyncBrowsingMkdirPopup { component: Radio, diff --git a/src/ui/activities/filetransfer/components/transfer/mod.rs b/src/ui/activities/filetransfer/components/transfer/mod.rs index 71be8b5e..0cfe4ccf 100644 --- a/src/ui/activities/filetransfer/components/transfer/mod.rs +++ b/src/ui/activities/filetransfer/components/transfer/mod.rs @@ -282,6 +282,10 @@ impl Component for ExplorerLocal { code: Key::Char('i'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('k'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowSymlinkPopup)), Event::Keyboard(KeyEvent { code: Key::Char('l'), modifiers: KeyModifiers::NONE, @@ -450,6 +454,10 @@ impl Component for ExplorerRemote { code: Key::Char('i'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)), + Event::Keyboard(KeyEvent { + code: Key::Char('k'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::ShowSymlinkPopup)), Event::Keyboard(KeyEvent { code: Key::Char('l'), modifiers: KeyModifiers::NONE, diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 37664c76..836a8e2b 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -213,6 +213,7 @@ impl FileTransferActivity { /// Update local file list pub(super) fn update_local_filelist(&mut self) { + self.reload_local_dir(); // Get width let width = self .context_mut() @@ -260,6 +261,7 @@ impl FileTransferActivity { /// Update remote file list pub(super) fn update_remote_filelist(&mut self) { + self.reload_remote_dir(); let width = self .context_mut() .terminal() diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index b12c217e..2157f35f 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -87,6 +87,7 @@ enum Id { SortingPopup, StatusBarLocal, StatusBarRemote, + SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, } @@ -111,6 +112,7 @@ enum PendingActionMsg { enum TransferMsg { AbortTransfer, CopyFileTo(String), + CreateSymlink(String), DeleteFile, EnterDirectory, ExecuteCmd(String), @@ -151,6 +153,7 @@ enum UiMsg { CloseQuitPopup, CloseRenamePopup, CloseSaveAsPopup, + CloseSymlinkPopup, Disconnect, ExplorerBackTabbed, LogBackTabbed, @@ -171,6 +174,7 @@ enum UiMsg { ShowQuitPopup, ShowRenamePopup, ShowSaveAsPopup, + ShowSymlinkPopup, ToggleHiddenFiles, ToggleSyncBrowsing, } diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 5eba2349..651a1580 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -1051,8 +1051,6 @@ impl FileTransferActivity { LogLevel::Info, format!("Changed directory on local: {}", path.display()), ); - // Reload files - self.reload_local_dir(); // Push prev_dir to stack if push { self.local_mut().pushd(prev_dir.as_path()) diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index fefb5132..fcb6cd59 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -70,6 +70,18 @@ impl FileTransferActivity { // Reload files self.update_browser_file_list() } + TransferMsg::CreateSymlink(name) => { + self.umount_symlink(); + self.mount_blocking_wait("Creating symlink…"); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_symlink(name), + FileExplorerTab::Remote => self.action_remote_symlink(name), + _ => panic!("Found tab doesn't support SYMLINK"), + } + self.umount_wait(); + // Reload files + self.update_browser_file_list() + } TransferMsg::DeleteFile => { self.umount_radio_delete(); self.mount_blocking_wait("Removing file(s)…"); @@ -408,6 +420,7 @@ impl FileTransferActivity { UiMsg::CloseQuitPopup => self.umount_quit(), UiMsg::CloseRenamePopup => self.umount_rename(), UiMsg::CloseSaveAsPopup => self.umount_saveas(), + UiMsg::CloseSymlinkPopup => self.umount_symlink(), UiMsg::Disconnect => { self.disconnect(); self.umount_disconnect(); @@ -460,6 +473,20 @@ impl FileTransferActivity { UiMsg::ShowQuitPopup => self.mount_quit(), UiMsg::ShowRenamePopup => self.mount_rename(), UiMsg::ShowSaveAsPopup => self.mount_saveas(), + UiMsg::ShowSymlinkPopup => { + if match self.browser.tab() { + FileExplorerTab::Local => self.is_local_selected_one(), + FileExplorerTab::Remote => self.is_remote_selected_one(), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => false, + } { + // Only if only one entry is selected + self.mount_symlink(); + } else { + self.mount_error( + "Symlink cannot be performed if more than one file is selected", + ); + } + } UiMsg::ToggleHiddenFiles => match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { self.browser.local_mut().toggle_hidden_files(); diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index f8874d7e..78bdf537 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -216,6 +216,11 @@ impl FileTransferActivity { f.render_widget(Clear, popup); // make popup self.app.view(&Id::SaveAsPopup, f, popup); + } else if self.app.mounted(&Id::SymlinkPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::SymlinkPopup, f, popup); } else if self.app.mounted(&Id::ExecPopup) { let popup = draw_area_in(f.size(), 40, 10); f.render_widget(Clear, popup); @@ -797,6 +802,23 @@ impl FileTransferActivity { .is_ok()); } + pub(super) fn mount_symlink(&mut self) { + let input_color = self.theme().misc_input_dialog; + assert!(self + .app + .remount( + Id::SymlinkPopup, + Box::new(components::SymlinkPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::SymlinkPopup).is_ok()); + } + + pub(super) fn umount_symlink(&mut self) { + let _ = self.app.umount(&Id::SymlinkPopup); + } + pub(super) fn mount_sync_browsing_mkdir_popup(&mut self, dir_name: &str) { let color = self.theme().misc_info_dialog; assert!(self @@ -969,9 +991,14 @@ impl FileTransferActivity { Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::SyncBrowsingMkdirPopup, )))), - Box::new(SubClause::Not(Box::new(SubClause::IsMounted( - Id::WaitPopup, - )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::SymlinkPopup, + )))), + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::WaitPopup, + )))), + )), )), )), )), From 1650788f82698c25333936e244266e042e40c149 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 14 Dec 2021 18:06:02 +0100 Subject: [PATCH 34/45] symlink command --- src/host/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/host/mod.rs b/src/host/mod.rs index 78cec83b..a6c07962 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1194,6 +1194,7 @@ mod tests { assert_eq!(result[1].name(), "examples.csv"); } + #[cfg(target_family = "unix")] #[test] fn should_create_symlink() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); From cd309c8d704dcaaef137369bea9f1f8436f0d119 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 28 Dec 2021 11:00:00 +0100 Subject: [PATCH 35/45] Redraw UI on window resize --- src/ui/activities/auth/components/mod.rs | 1 + src/ui/activities/auth/mod.rs | 1 + src/ui/activities/auth/update.rs | 3 +++ src/ui/activities/auth/view.rs | 1 + src/ui/activities/filetransfer/components/mod.rs | 1 + src/ui/activities/filetransfer/mod.rs | 1 + src/ui/activities/filetransfer/update.rs | 3 +++ src/ui/activities/filetransfer/view.rs | 1 + src/ui/activities/setup/components/mod.rs | 1 + src/ui/activities/setup/mod.rs | 1 + src/ui/activities/setup/update.rs | 3 +++ src/ui/activities/setup/view/mod.rs | 1 + 12 files changed, 18 insertions(+) diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index 73b4e219..bb014ae2 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -84,6 +84,7 @@ impl Component for GlobalListener { code: Key::Char('s'), modifiers: KeyModifiers::CONTROL, }) => Some(Msg::Ui(UiMsg::ShowSaveBookmarkPopup)), + Event::WindowResize(_, _) => Some(Msg::Ui(UiMsg::WindowResized)), _ => None, } } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index bff020b0..08ee9c35 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -135,6 +135,7 @@ pub enum UiMsg { ShowSaveBookmarkPopup, UsernameBlurDown, UsernameBlurUp, + WindowResized, } /// Auth form input mask diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index f55cb4ab..bbe62d86 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -255,6 +255,9 @@ impl AuthActivity { UiMsg::UsernameBlurUp => { assert!(self.app.active(&Id::Port).is_ok()); } + UiMsg::WindowResized => { + self.redraw = true; + } } None diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 9d2b1c6b..29ce8588 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -842,6 +842,7 @@ impl AuthActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new(SubEventClause::WindowResize, SubClause::Always) ] ) .is_ok()); diff --git a/src/ui/activities/filetransfer/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs index 80f5d8c9..6176a23a 100644 --- a/src/ui/activities/filetransfer/components/mod.rs +++ b/src/ui/activities/filetransfer/components/mod.rs @@ -69,6 +69,7 @@ impl Component for GlobalListener { code: Key::Char('h') | Key::Function(1), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), + Event::WindowResize(_, _) => Some(Msg::Ui(UiMsg::WindowResized)), _ => None, } } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 2157f35f..1bd08551 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -177,6 +177,7 @@ enum UiMsg { ShowSymlinkPopup, ToggleHiddenFiles, ToggleSyncBrowsing, + WindowResized, } /// Log level type diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index fcb6cd59..0924447f 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -503,6 +503,9 @@ impl FileTransferActivity { self.browser.toggle_sync_browsing(); self.refresh_remote_status_bar(); } + UiMsg::WindowResized => { + self.redraw = true; + } } None } diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 78bdf537..a3ae2915 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -896,6 +896,7 @@ impl FileTransferActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new(SubEventClause::WindowResize, SubClause::Always) ] ) .is_ok()); diff --git a/src/ui/activities/setup/components/mod.rs b/src/ui/activities/setup/components/mod.rs index 7be63520..25b3d739 100644 --- a/src/ui/activities/setup/components/mod.rs +++ b/src/ui/activities/setup/components/mod.rs @@ -81,6 +81,7 @@ impl Component for GlobalListener { code: Key::Function(4), modifiers: KeyModifiers::NONE, }) => Some(Msg::Common(CommonMsg::ShowSavePopup)), + Event::WindowResize(_, _) => Some(Msg::Common(CommonMsg::WindowResized)), _ => None, } } diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index 8a3029ad..f9293983 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -145,6 +145,7 @@ pub enum CommonMsg { ShowKeybindings, ShowQuitPopup, ShowSavePopup, + WindowResized, } #[derive(Debug, Clone, PartialEq)] diff --git a/src/ui/activities/setup/update.rs b/src/ui/activities/setup/update.rs index 7072759e..7d74fcd9 100644 --- a/src/ui/activities/setup/update.rs +++ b/src/ui/activities/setup/update.rs @@ -116,6 +116,9 @@ impl SetupActivity { CommonMsg::ShowSavePopup => { self.mount_save_popup(); } + CommonMsg::WindowResized => { + self.redraw = true; + } } None } diff --git a/src/ui/activities/setup/view/mod.rs b/src/ui/activities/setup/view/mod.rs index 796a601b..d86069a5 100644 --- a/src/ui/activities/setup/view/mod.rs +++ b/src/ui/activities/setup/view/mod.rs @@ -255,6 +255,7 @@ impl SetupActivity { }), Self::no_popup_mounted_clause(), ), + Sub::new(SubEventClause::WindowResize, SubClause::Always) ] ) .is_ok()); From e99ad5b906132e74a9cc0b48c4b148a54df7c2fb Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 4 Jan 2022 12:48:36 +0100 Subject: [PATCH 36/45] remotefs 0.2.0 --- CHANGELOG.md | 2 +- Cargo.lock | 57 +- Cargo.toml | 11 +- docs/developer.md | 2 +- src/explorer/formatter.rs | 222 ++++---- src/explorer/mod.rs | 149 +++--- src/filetransfer/builder.rs | 8 +- src/host/mod.rs | 393 +++++--------- src/system/sshkey_storage.rs | 2 +- .../filetransfer/actions/change_dir.rs | 14 +- .../activities/filetransfer/actions/copy.rs | 204 ++++--- .../activities/filetransfer/actions/delete.rs | 20 +- .../activities/filetransfer/actions/edit.rs | 43 +- .../activities/filetransfer/actions/find.rs | 53 +- src/ui/activities/filetransfer/actions/mod.rs | 80 +-- .../filetransfer/actions/newfile.rs | 10 +- .../activities/filetransfer/actions/open.rs | 53 +- .../activities/filetransfer/actions/rename.rs | 20 +- .../activities/filetransfer/actions/save.rs | 44 +- .../activities/filetransfer/actions/submit.rs | 110 ++-- .../filetransfer/actions/symlink.rs | 6 +- .../filetransfer/components/popups.rs | 22 +- src/ui/activities/filetransfer/lib/browser.rs | 4 +- src/ui/activities/filetransfer/misc.rs | 3 +- src/ui/activities/filetransfer/session.rs | 506 +++++++++--------- src/ui/activities/filetransfer/update.rs | 16 +- src/ui/activities/filetransfer/view.rs | 6 +- src/utils/test_helpers.rs | 36 +- 28 files changed, 1007 insertions(+), 1089 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b57584..955f7d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ Released on FIXME: - Dependencies: - Updated `tui-realm` to `1.3.0` - Updated `tui-realm-stdlib` to `1.1.4` - - Removed `rust-s3`, `ssh2`, `suppaftp`; replaced by `remotefs 0.1.1` + - Removed `rust-s3`, `ssh2`, `suppaftp`; replaced by `remotefs 0.2.0`, `remotefs-aws-s3 0.1.0`, `remotefs-ftp 0.1.0` and `remotefs-ssh 0.1.0` - Removed `crossterm` (since bridged by tui-realm) ## 0.7.0 diff --git a/Cargo.lock b/Cargo.lock index 6d1e714d..1aa09008 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1676,22 +1676,59 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remotefs" -version = "0.1.1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8df5d94ca7480505315216544f145f358b7609781beef33a9149ecd9be44a5b" +dependencies = [ + "chrono", + "log", + "thiserror", + "wildmatch", +] + +[[package]] +name = "remotefs-aws-s3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36b130b79d6ade3821554c46a87fd54dea024255daa5ecac5e9f68e2a4d3230a" +dependencies = [ + "chrono", + "log", + "path-slash", + "remotefs", + "rust-s3", + "thiserror", + "users", +] + +[[package]] +name = "remotefs-ftp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359fd989a6ad50fa6defae771e453695412cbfdb5476f49e42d1bb1d51b5c096" +dependencies = [ + "log", + "path-slash", + "remotefs", + "suppaftp", + "users", +] + +[[package]] +name = "remotefs-ssh" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c72915b01014a11d7e21b3a28141ff32b881bd8103c0f65269cc7932a03ae61c" +checksum = "7f2c6959740eeee7e773166e79dd52c7dea41f791456aa5addfb949c61c15b27" dependencies = [ "chrono", "lazy_static", "log", "path-slash", "regex", - "rust-s3", + "remotefs", "ssh2", "ssh2-config", - "suppaftp", - "thiserror", "users", - "wildmatch", ] [[package]] @@ -2095,10 +2132,11 @@ dependencies = [ [[package]] name = "ssh2-config" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e64d0ea4897c9415c34011a4cdf21a0e0168c200595f1f543be1ca807942d8" +checksum = "0e55cdf8a42b24c57788bc8b85e8d6e7f6e0614b5316322b2e7f455f9d93230e" dependencies = [ + "dirs 4.0.0", "thiserror", "wildmatch", ] @@ -2236,6 +2274,9 @@ dependencies = [ "rand 0.8.4", "regex", "remotefs", + "remotefs-aws-s3", + "remotefs-ftp", + "remotefs-ssh", "rpassword", "self_update", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3a2cf96e..9acac0ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,10 @@ notify-rust = { version = "4.5.3", default-features = false, features = [ "d" ] open = "2.0.1" rand = "0.8.4" regex = "1.5.4" -remotefs = { version = "0.1.1", features = [ "aws-s3", "ftp", "ssh" ] } +remotefs = "^0.2.0" +remotefs-aws-s3 = "^0.1.0" +remotefs-ftp = { version = "^0.1.0", features = [ "secure" ] } +remotefs-ssh = "^0.1.0" rpassword = "5.0.1" self_update = { version = "0.27.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] } serde = { version = "^1.0.0", features = [ "derive" ] } @@ -74,3 +77,9 @@ with-keyring = [ "keyring" ] [target."cfg(target_family = \"unix\")"] [target."cfg(target_family = \"unix\")".dependencies] users = "0.11.0" + +[profile.release] +incremental = true + +[profile.dev] +incremental = true diff --git a/docs/developer.md b/docs/developer.md index a9d606f4..11023b62 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -24,7 +24,7 @@ termscp is basically made up of 4 components: In addition to the 4 main components, other have been added through the time: - **config**: this module provides the configuration schema and serialization methods for it. -- **fs**: this modules exposes the FsEntry entity and the explorers. The explorers are structs which hold the content of the current directory; they also they take of filtering files up to your preferences and format file entries based on your configuration. +- **fs**: this modules exposes the FsFile entity and the explorers. The explorers are structs which hold the content of the current directory; they also they take of filtering files up to your preferences and format file entries based on your configuration. - **system**: the system module provides a way to actually interact with the configuration, the ssh key storage and with the bookmarks. - **utils**: contains the utilities used by pretty much all the project. diff --git a/src/explorer/formatter.rs b/src/explorer/formatter.rs index e2f61d33..24f136b9 100644 --- a/src/explorer/formatter.rs +++ b/src/explorer/formatter.rs @@ -31,13 +31,14 @@ use crate::utils::path::diff_paths; // Ext use bytesize::ByteSize; use regex::Regex; -use remotefs::Entry; +use remotefs::File; use std::path::PathBuf; +use std::time::UNIX_EPOCH; #[cfg(target_family = "unix")] use users::{get_group_by_gid, get_user_by_uid}; // Types -// FmtCallback: Formatter, fsentry: &Entry, cur_str, prefix, length, extra -type FmtCallback = fn(&Formatter, &Entry, &str, &str, Option<&usize>, Option<&String>) -> String; +// FmtCallback: Formatter, fsentry: &File, cur_str, prefix, length, extra +type FmtCallback = fn(&Formatter, &File, &str, &str, Option<&usize>, Option<&String>) -> String; // Keys const FMT_KEY_ATIME: &str = "ATIME"; @@ -64,7 +65,7 @@ lazy_static! { static ref FMT_ATTR_REGEX: Regex = Regex::new(r"(?:([A-Z]+))(:?([0-9]+))?(:?(.+))?").ok().unwrap(); } -/// Call Chain block is a block in a chain of functions which are called in order to format the Entry. +/// Call Chain block is a block in a chain of functions which are called in order to format the File. /// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found /// a chain of function is made using the Formatters method. /// This method provides an extremely fast way to format fs entries @@ -99,7 +100,7 @@ impl CallChainBlock { } /// Call next callback in the CallChain - pub fn next(&self, fmt: &Formatter, fsentry: &Entry, cur_str: &str) -> String { + pub fn next(&self, fmt: &Formatter, fsentry: &File, cur_str: &str) -> String { // Call func let new_str: String = (self.func)( fmt, @@ -161,7 +162,7 @@ impl Formatter { } /// Format fsentry - pub fn fmt(&self, fsentry: &Entry) -> String { + pub fn fmt(&self, fsentry: &File) -> String { // Execute callchain blocks self.call_chain.next(self, fsentry, "") } @@ -171,7 +172,7 @@ impl Formatter { /// Format last access time fn fmt_atime( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -179,7 +180,7 @@ impl Formatter { ) -> String { // Get date (use extra args as format or default "%b %d %Y %H:%M") let datetime: String = fmt_time( - fsentry.metadata().atime, + fsentry.metadata().accessed.unwrap_or(UNIX_EPOCH), match fmt_extra { Some(fmt) => fmt.as_ref(), None => "%b %d %Y %H:%M", @@ -198,7 +199,7 @@ impl Formatter { /// Format creation time fn fmt_ctime( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -206,7 +207,7 @@ impl Formatter { ) -> String { // Get date let datetime: String = fmt_time( - fsentry.metadata().ctime, + fsentry.metadata().created.unwrap_or(UNIX_EPOCH), match fmt_extra { Some(fmt) => fmt.as_ref(), None => "%b %d %Y %H:%M", @@ -225,7 +226,7 @@ impl Formatter { /// Format owner group fn fmt_group( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -258,7 +259,7 @@ impl Formatter { /// Format last change time fn fmt_mtime( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -266,7 +267,7 @@ impl Formatter { ) -> String { // Get date let datetime: String = fmt_time( - fsentry.metadata().mtime, + fsentry.metadata().modified.unwrap_or(UNIX_EPOCH), match fmt_extra { Some(fmt) => fmt.as_ref(), None => "%b %d %Y %H:%M", @@ -285,7 +286,7 @@ impl Formatter { /// Format file name fn fmt_name( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -296,14 +297,14 @@ impl Formatter { Some(l) => *l, None => 24, }; - let name: &str = fsentry.name(); + let name = fsentry.name(); let last_idx: usize = match fsentry.is_dir() { // NOTE: For directories is l - 2, since we push '/' to name true => file_len - 2, false => file_len - 1, }; let mut name: String = match name.len() >= file_len { - false => name.to_string(), + false => name, true => format!("{}…", &name[0..last_idx]), }; if fsentry.is_dir() { @@ -316,7 +317,7 @@ impl Formatter { /// Format path fn fmt_path( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -341,7 +342,7 @@ impl Formatter { /// Format file permissions fn fmt_pex( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, @@ -376,7 +377,7 @@ impl Formatter { /// Format file size fn fmt_size( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, @@ -387,6 +388,17 @@ impl Formatter { let size: ByteSize = ByteSize(fsentry.metadata().size); // Add to cur str, prefix and the key value format!("{}{}{:10}", cur_str, prefix, size.to_string()) + } else if fsentry.metadata().symlink.is_some() { + let size = ByteSize( + fsentry + .metadata() + .symlink + .as_ref() + .unwrap() + .to_string_lossy() + .len() as u64, + ); + format!("{}{}{:10}", cur_str, prefix, size.to_string()) } else { // Add to cur str, prefix and the key value format!("{}{} ", cur_str, prefix) @@ -396,7 +408,7 @@ impl Formatter { /// Format file symlink (if any) fn fmt_symlink( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, fmt_len: Option<&usize>, @@ -423,7 +435,7 @@ impl Formatter { /// Format owner user fn fmt_user( &self, - fsentry: &Entry, + fsentry: &File, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, @@ -451,7 +463,7 @@ impl Formatter { /// It does nothing, just returns cur_str fn fmt_fallback( &self, - _fsentry: &Entry, + _fsentry: &File, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, @@ -536,7 +548,7 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - use remotefs::fs::{Directory, File, Metadata, UnixPex}; + use remotefs::fs::{File, FileType, Metadata, UnixPex}; use std::path::PathBuf; use std::time::SystemTime; @@ -546,21 +558,20 @@ mod tests { let dummy_formatter: Formatter = Formatter::new(""); // Make a dummy entry let t: SystemTime = SystemTime::now(); - let dummy_entry: Entry = Entry::File(File { - name: String::from("bar.txt"), + let dummy_entry = File { path: PathBuf::from("/bar.txt"), - extension: Some(String::from("txt")), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::File, size: 8192, symlink: None, uid: Some(0), gid: Some(0), mode: Some(UnixPex::from(0o644)), }, - }); + }; let prefix: String = String::from("h"); let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None); assert!(callchain.next_block.is_none()); @@ -588,21 +599,20 @@ mod tests { let formatter: Formatter = Formatter::default(); // Experiments :D let t: SystemTime = SystemTime::now(); - let entry: Entry = Entry::File(File { - name: String::from("bar.txt"), + let entry = File { path: PathBuf::from("/bar.txt"), - extension: Some(String::from("txt")), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::File, size: 8192, symlink: None, uid: Some(0), gid: Some(0), mode: Some(UnixPex::from(0o644)), }, - }); + }; #[cfg(target_family = "unix")] assert_eq!( formatter.fmt(&entry), @@ -620,21 +630,20 @@ mod tests { ) ); // Elide name - let entry: Entry = Entry::File(File { - name: String::from("piroparoporoperoperupupu.txt"), - path: PathBuf::from("/bar.txt"), - extension: Some(String::from("txt")), + let entry = File { + path: PathBuf::from("/piroparoporoperoperupupu.txt"), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::File, size: 8192, symlink: None, uid: Some(0), gid: Some(0), mode: Some(UnixPex::from(0o644)), }, - }); + }; #[cfg(target_family = "unix")] assert_eq!( formatter.fmt(&entry), @@ -652,21 +661,20 @@ mod tests { ) ); // No pex - let entry: Entry = Entry::File(File { - name: String::from("bar.txt"), + let entry = File { path: PathBuf::from("/bar.txt"), - extension: Some(String::from("txt")), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::File, size: 8192, symlink: None, uid: Some(0), gid: Some(0), mode: None, }, - }); + }; #[cfg(target_family = "unix")] assert_eq!( formatter.fmt(&entry), @@ -684,21 +692,20 @@ mod tests { ) ); // No user - let entry: Entry = Entry::File(File { - name: String::from("bar.txt"), + let entry = File { path: PathBuf::from("/bar.txt"), - extension: Some(String::from("txt")), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::File, size: 8192, symlink: None, uid: None, gid: Some(0), mode: None, }, - }); + }; #[cfg(target_family = "unix")] assert_eq!( formatter.fmt(&entry), @@ -723,20 +730,20 @@ mod tests { let formatter: Formatter = Formatter::default(); // Experiments :D let t: SystemTime = SystemTime::now(); - let entry: Entry = Entry::Directory(Directory { - name: String::from("projects"), + let entry = File { path: PathBuf::from("/home/cvisintin/projects"), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::Directory, size: 4096, symlink: None, uid: Some(0), gid: Some(0), mode: Some(UnixPex::from(0o755)), }, - }); + }; #[cfg(target_family = "unix")] assert_eq!( formatter.fmt(&entry), @@ -754,20 +761,20 @@ mod tests { ) ); // No pex, no user - let entry: Entry = Entry::Directory(Directory { - name: String::from("projects"), + let entry = File { path: PathBuf::from("/home/cvisintin/projects"), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::Directory, size: 4096, symlink: None, uid: None, gid: Some(0), mode: None, }, - }); + }; #[cfg(target_family = "unix")] assert_eq!( formatter.fmt(&entry), @@ -792,41 +799,41 @@ mod tests { Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}"); // Directory (with symlink) let t: SystemTime = SystemTime::now(); - let entry: Entry = Entry::Directory(Directory { - name: String::from("projects"), - path: PathBuf::from("/home/cvisintin/project"), + let entry = File { + path: PathBuf::from("/home/cvisintin/projects"), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::Symlink, size: 4096, symlink: Some(PathBuf::from("project.info")), uid: None, gid: None, mode: Some(UnixPex::from(0o755)), }, - }); + }; assert_eq!(formatter.fmt(&entry), format!( - "projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}", + "projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}", fmt_time(t, "%a %b %d %Y %H:%M"), fmt_time(t, "%a %b %d %Y %H:%M"), fmt_time(t, "%a %b %d %Y %H:%M"), )); // Directory without symlink - let entry: Entry = Entry::Directory(Directory { - name: String::from("projects"), - path: PathBuf::from("/home/cvisintin/project"), + let entry = File { + path: PathBuf::from("/home/cvisintin/projects"), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::Directory, size: 4096, symlink: None, uid: None, gid: None, mode: Some(UnixPex::from(0o755)), }, - }); + }; assert_eq!(formatter.fmt(&entry), format!( "projects/ 0 0 drwxr-xr-x {} {} {}", fmt_time(t, "%a %b %d %Y %H:%M"), @@ -834,43 +841,41 @@ mod tests { fmt_time(t, "%a %b %d %Y %H:%M"), )); // File with symlink - let entry: Entry = Entry::File(File { - name: String::from("bar.txt"), + let entry = File { path: PathBuf::from("/bar.txt"), - extension: Some(String::from("txt")), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::Symlink, size: 8192, symlink: Some(PathBuf::from("project.info")), uid: None, gid: None, mode: Some(UnixPex::from(0o644)), }, - }); + }; assert_eq!(formatter.fmt(&entry), format!( - "bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}", + "bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}", fmt_time(t, "%a %b %d %Y %H:%M"), fmt_time(t, "%a %b %d %Y %H:%M"), fmt_time(t, "%a %b %d %Y %H:%M"), )); // File without symlink - let entry: Entry = Entry::File(File { - name: String::from("bar.txt"), + let entry = File { path: PathBuf::from("/bar.txt"), - extension: Some(String::from("txt")), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::File, size: 8192, symlink: None, uid: None, gid: None, mode: Some(UnixPex::from(0o644)), }, - }); + }; assert_eq!(formatter.fmt(&entry), format!( "bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}", fmt_time(t, "%a %b %d %Y %H:%M"), @@ -883,21 +888,20 @@ mod tests { #[cfg(target_family = "unix")] fn should_fmt_path() { let t: SystemTime = SystemTime::now(); - let entry: Entry = Entry::File(File { - name: String::from("bar.txt"), + let entry = File { path: PathBuf::from("/tmp/a/b/c/bar.txt"), - extension: Some(String::from("txt")), metadata: Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::Symlink, size: 8192, symlink: Some(PathBuf::from("project.info")), uid: None, gid: None, mode: Some(UnixPex::from(0o644)), }, - }); + }; let formatter: Formatter = Formatter::new("File path: {PATH}"); assert_eq!( formatter.fmt(&entry).as_str(), @@ -915,7 +919,7 @@ mod tests { /// Dummy formatter, just yelds an 'A' at the end of the current string fn dummy_fmt( _fmt: &Formatter, - _entry: &Entry, + _entry: &File, cur_str: &str, prefix: &str, _fmt_len: Option<&usize>, diff --git a/src/explorer/mod.rs b/src/explorer/mod.rs index ff183fda..0639af28 100644 --- a/src/explorer/mod.rs +++ b/src/explorer/mod.rs @@ -31,7 +31,7 @@ mod formatter; // Locals use formatter::Formatter; // Ext -use remotefs::fs::Entry; +use remotefs::fs::File; use std::cmp::Reverse; use std::collections::VecDeque; use std::path::{Path, PathBuf}; @@ -71,8 +71,8 @@ pub struct FileExplorer { pub(crate) file_sorting: FileSorting, // File sorting criteria pub(crate) group_dirs: Option, // If Some, defines how to group directories pub(crate) opts: ExplorerOpts, // Explorer options - pub(crate) fmt: Formatter, // Entry formatter - files: Vec, // Files in directory + pub(crate) fmt: Formatter, // File formatter + files: Vec, // Files in directory } impl Default for FileExplorer { @@ -109,7 +109,7 @@ impl FileExplorer { /// Set Explorer files /// This method will also sort entries based on current options /// Once all sorting have been performed, index is moved to first valid entry. - pub fn set_files(&mut self, files: Vec) { + pub fn set_files(&mut self, files: Vec) { self.files = files; // Sort self.sort(); @@ -131,7 +131,7 @@ impl FileExplorer { /// Iterate over files /// Filters are applied based on current options (e.g. hidden files not returned) - pub fn iter_files(&self) -> impl Iterator + '_ { + pub fn iter_files(&self) -> impl Iterator + '_ { // Filter let opts: ExplorerOpts = self.opts; Box::new(self.files.iter().filter(move |x| { @@ -146,12 +146,12 @@ impl FileExplorer { } /// Iterate all files; doesn't care about options - pub fn iter_files_all(&self) -> impl Iterator + '_ { + pub fn iter_files_all(&self) -> impl Iterator + '_ { Box::new(self.files.iter()) } /// Get file at relative index - pub fn get(&self, idx: usize) -> Option<&Entry> { + pub fn get(&self, idx: usize) -> Option<&File> { let opts: ExplorerOpts = self.opts; let filtered = self .files @@ -172,7 +172,7 @@ impl FileExplorer { // Formatting /// Format a file entry - pub fn fmt_file(&self, entry: &Entry) -> String { + pub fn fmt_file(&self, entry: &File) -> String { self.fmt.fmt(entry) } @@ -222,35 +222,35 @@ impl FileExplorer { /// Sort explorer files by their name. All names are converted to lowercase fn sort_files_by_name(&mut self) { - self.files.sort_by_key(|x: &Entry| x.name().to_lowercase()); + self.files.sort_by_key(|x: &File| x.name().to_lowercase()); } /// Sort files by mtime; the newest comes first fn sort_files_by_mtime(&mut self) { self.files - .sort_by(|a: &Entry, b: &Entry| b.metadata().mtime.cmp(&a.metadata().mtime)); + .sort_by_key(|b: &File| Reverse(b.metadata().modified)); } /// Sort files by creation time; the newest comes first fn sort_files_by_creation_time(&mut self) { self.files - .sort_by_key(|b: &Entry| Reverse(b.metadata().ctime)); + .sort_by_key(|b: &File| Reverse(b.metadata().created)); } /// Sort files by size fn sort_files_by_size(&mut self) { self.files - .sort_by_key(|b: &Entry| Reverse(b.metadata().size)); + .sort_by_key(|b: &File| Reverse(b.metadata().size)); } /// Sort files; directories come first fn sort_files_directories_first(&mut self) { - self.files.sort_by_key(|x: &Entry| x.is_file()); + self.files.sort_by_key(|x: &File| !x.is_dir()); } /// Sort files; directories come last fn sort_files_directories_last(&mut self) { - self.files.sort_by_key(|x: &Entry| x.is_dir()); + self.files.sort_by_key(|x: &File| x.is_dir()); } /// Enable/disable hidden files @@ -317,7 +317,7 @@ mod tests { use crate::utils::fmt::fmt_time; use pretty_assertions::assert_eq; - use remotefs::fs::{Directory, File, Metadata, UnixPex}; + use remotefs::fs::{File, FileType, Metadata, UnixPex}; use std::thread::sleep; use std::time::{Duration, SystemTime}; @@ -371,8 +371,8 @@ mod tests { // Create files explorer.set_files(vec![ make_fs_entry("README.md", false), - make_fs_entry("src/", true), - make_fs_entry(".git/", true), + make_fs_entry("src", true), + make_fs_entry(".git", true), make_fs_entry("CONTRIBUTING.md", false), make_fs_entry("codecov.yml", false), make_fs_entry(".gitignore", false), @@ -381,7 +381,7 @@ mod tests { assert!(explorer.get(100).is_none()); //assert_eq!(explorer.count(), 6); // Verify (files are sorted by name) - assert_eq!(explorer.files.get(0).unwrap().name(), ".git/"); + assert_eq!(explorer.files.get(0).unwrap().name(), ".git"); // Iter files (all) assert_eq!(explorer.iter_files_all().count(), 6); // Iter files (hidden excluded) (.git, .gitignore are hidden) @@ -398,7 +398,7 @@ mod tests { // Create files (files are then sorted by name) explorer.set_files(vec![ make_fs_entry("README.md", false), - make_fs_entry("src/", true), + make_fs_entry("src", true), make_fs_entry("CONTRIBUTING.md", false), make_fs_entry("CODE_OF_CONDUCT.md", false), make_fs_entry("CHANGELOG.md", false), @@ -410,39 +410,39 @@ mod tests { explorer.sort_by(FileSorting::Name); // First entry should be "Cargo.lock" assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock"); - // Last should be "src/" - assert_eq!(explorer.files.get(8).unwrap().name(), "src/"); + // Last should be "src" + assert_eq!(explorer.files.get(8).unwrap().name(), "src"); } #[test] fn test_fs_explorer_sort_by_mtime() { let mut explorer: FileExplorer = FileExplorer::default(); - let entry1: Entry = make_fs_entry("README.md", false); + let entry1: File = make_fs_entry("README.md", false); // Wait 1 sec sleep(Duration::from_secs(1)); - let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false); + let entry2: File = make_fs_entry("CODE_OF_CONDUCT.md", false); // Create files (files are then sorted by name) explorer.set_files(vec![entry1, entry2]); explorer.sort_by(FileSorting::ModifyTime); // First entry should be "CODE_OF_CONDUCT.md" assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md"); - // Last should be "src/" + // Last should be "src" assert_eq!(explorer.files.get(1).unwrap().name(), "README.md"); } #[test] fn test_fs_explorer_sort_by_creation_time() { let mut explorer: FileExplorer = FileExplorer::default(); - let entry1: Entry = make_fs_entry("README.md", false); + let entry1: File = make_fs_entry("README.md", false); // Wait 1 sec sleep(Duration::from_secs(1)); - let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false); + let entry2: File = make_fs_entry("CODE_OF_CONDUCT.md", false); // Create files (files are then sorted by name) explorer.set_files(vec![entry1, entry2]); explorer.sort_by(FileSorting::CreationTime); // First entry should be "CODE_OF_CONDUCT.md" assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md"); - // Last should be "src/" + // Last should be "src" assert_eq!(explorer.files.get(1).unwrap().name(), "README.md"); } @@ -452,12 +452,12 @@ mod tests { // Create files (files are then sorted by name) explorer.set_files(vec![ make_fs_entry_with_size("README.md", false, 1024), - make_fs_entry_with_size("src/", true, 4096), + make_fs_entry_with_size("src", true, 4096), make_fs_entry_with_size("CONTRIBUTING.md", false, 256), ]); explorer.sort_by(FileSorting::Size); // Directory has size 4096 - assert_eq!(explorer.files.get(0).unwrap().name(), "src/"); + assert_eq!(explorer.files.get(0).unwrap().name(), "src"); assert_eq!(explorer.files.get(1).unwrap().name(), "README.md"); assert_eq!(explorer.files.get(2).unwrap().name(), "CONTRIBUTING.md"); } @@ -468,8 +468,8 @@ mod tests { // Create files (files are then sorted by name) explorer.set_files(vec![ make_fs_entry("README.md", false), - make_fs_entry("src/", true), - make_fs_entry("docs/", true), + make_fs_entry("src", true), + make_fs_entry("docs", true), make_fs_entry("CONTRIBUTING.md", false), make_fs_entry("CODE_OF_CONDUCT.md", false), make_fs_entry("CHANGELOG.md", false), @@ -481,8 +481,8 @@ mod tests { explorer.sort_by(FileSorting::Name); explorer.group_dirs_by(Some(GroupDirs::First)); // First entry should be "docs" - assert_eq!(explorer.files.get(0).unwrap().name(), "docs/"); - assert_eq!(explorer.files.get(1).unwrap().name(), "src/"); + assert_eq!(explorer.files.get(0).unwrap().name(), "docs"); + assert_eq!(explorer.files.get(1).unwrap().name(), "src"); // 3rd is file first for alphabetical order assert_eq!(explorer.files.get(2).unwrap().name(), "Cargo.lock"); // Last should be "README.md" (last file for alphabetical ordening) @@ -495,8 +495,8 @@ mod tests { // Create files (files are then sorted by name) explorer.set_files(vec![ make_fs_entry("README.md", false), - make_fs_entry("src/", true), - make_fs_entry("docs/", true), + make_fs_entry("src", true), + make_fs_entry("docs", true), make_fs_entry("CONTRIBUTING.md", false), make_fs_entry("CODE_OF_CONDUCT.md", false), make_fs_entry("CHANGELOG.md", false), @@ -508,8 +508,8 @@ mod tests { explorer.sort_by(FileSorting::Name); explorer.group_dirs_by(Some(GroupDirs::Last)); // Last entry should be "src" - assert_eq!(explorer.files.get(8).unwrap().name(), "docs/"); - assert_eq!(explorer.files.get(9).unwrap().name(), "src/"); + assert_eq!(explorer.files.get(8).unwrap().name(), "docs"); + assert_eq!(explorer.files.get(9).unwrap().name(), "src"); // first is file for alphabetical order assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock"); // Last in files should be "README.md" (last file for alphabetical ordening) @@ -521,21 +521,20 @@ mod tests { let explorer: FileExplorer = FileExplorer::default(); // Create fs entry let t: SystemTime = SystemTime::now(); - let entry: Entry = Entry::File(File { - name: String::from("bar.txt"), + let entry = File { path: PathBuf::from("/bar.txt"), - extension: Some(String::from("txt")), metadata: Metadata { - atime: t, - ctime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::File, size: 8192, - mtime: t, symlink: None, uid: Some(0), gid: Some(0), mode: Some(UnixPex::from(0o644)), }, - }); + }; #[cfg(target_family = "unix")] assert_eq!( explorer.fmt_file(&entry), @@ -592,68 +591,60 @@ mod tests { // Create files (files are then sorted by name) explorer.set_files(vec![ make_fs_entry("CONTRIBUTING.md", false), - make_fs_entry("docs/", true), - make_fs_entry("src/", true), + make_fs_entry("docs", true), + make_fs_entry("src", true), make_fs_entry("README.md", false), ]); explorer.del_entry(0); assert_eq!(explorer.files.len(), 3); - assert_eq!(explorer.files[0].name(), "docs/"); + assert_eq!(explorer.files[0].name(), "docs"); explorer.del_entry(5); assert_eq!(explorer.files.len(), 3); } - fn make_fs_entry(name: &str, is_dir: bool) -> Entry { + fn make_fs_entry(name: &str, is_dir: bool) -> File { let t: SystemTime = SystemTime::now(); let metadata = Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: if is_dir { + FileType::Directory + } else { + FileType::File + }, symlink: None, gid: Some(0), uid: Some(0), mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })), size: 64, }; - match is_dir { - false => Entry::File(File { - name: name.to_string(), - path: PathBuf::from(name), - extension: None, - metadata, - }), - true => Entry::Directory(Directory { - name: name.to_string(), - path: PathBuf::from(name), - metadata, - }), + File { + path: PathBuf::from(name), + metadata, } } - fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> Entry { + fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> File { let t: SystemTime = SystemTime::now(); let metadata = Metadata { - atime: t, - ctime: t, - mtime: t, + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: if is_dir { + FileType::Directory + } else { + FileType::File + }, symlink: None, gid: Some(0), uid: Some(0), mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })), size: size as u64, }; - match is_dir { - false => Entry::File(File { - name: name.to_string(), - path: PathBuf::from(name), - extension: None, - metadata, - }), - true => Entry::Directory(Directory { - name: name.to_string(), - path: PathBuf::from(name), - metadata, - }), + File { + path: PathBuf::from(name), + metadata, } } } diff --git a/src/filetransfer/builder.rs b/src/filetransfer/builder.rs index d389bde3..cbd8cdad 100644 --- a/src/filetransfer/builder.rs +++ b/src/filetransfer/builder.rs @@ -30,12 +30,10 @@ use super::{FileTransferProtocol, ProtocolParams}; use crate::system::config_client::ConfigClient; use crate::system::sshkey_storage::SshKeyStorage; -use remotefs::client::{ - aws_s3::AwsS3Fs, - ftp::FtpFs, - ssh::{ScpFs, SftpFs, SshOpts}, -}; use remotefs::RemoteFs; +use remotefs_aws_s3::AwsS3Fs; +use remotefs_ftp::FtpFs; +use remotefs_ssh::{ScpFs, SftpFs, SshOpts}; use std::path::PathBuf; /// Remotefs builder diff --git a/src/host/mod.rs b/src/host/mod.rs index a6c07962..e261db9b 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -28,17 +28,16 @@ // ext #[cfg(target_family = "unix")] use remotefs::fs::UnixPex; -use remotefs::fs::{Directory, Entry, File, Metadata}; +use remotefs::fs::{File, FileType, Metadata}; use std::fs::{self, File as StdFile, OpenOptions}; use std::path::{Path, PathBuf}; -use std::time::SystemTime; use thiserror::Error; use wildmatch::WildMatch; // Metadata ext #[cfg(target_family = "unix")] use std::fs::set_permissions; #[cfg(target_family = "unix")] -use std::os::unix::fs::{MetadataExt, PermissionsExt}; +use std::os::unix::fs::PermissionsExt; // Locals use crate::utils::path; @@ -112,7 +111,7 @@ impl std::fmt::Display for HostError { /// It provides functions to navigate across the local host file system pub struct Localhost { wrkdir: PathBuf, - files: Vec, + files: Vec, } impl Localhost { @@ -157,7 +156,7 @@ impl Localhost { /// List files in current directory #[allow(dead_code)] - pub fn list_dir(&self) -> Vec { + pub fn list_dir(&self) -> Vec { self.files.clone() } @@ -245,71 +244,68 @@ impl Localhost { } /// Remove file entry - pub fn remove(&mut self, entry: &Entry) -> Result<(), HostError> { - match entry { - Entry::Directory(dir) => { - // If file doesn't exist; return error - debug!("Removing directory {}", dir.path.display()); - if !dir.path.as_path().exists() { - error!("Directory doesn't exist"); - return Err(HostError::new( - HostErrorType::NoSuchFileOrDirectory, - None, - dir.path.as_path(), - )); + pub fn remove(&mut self, entry: &File) -> Result<(), HostError> { + if entry.is_dir() { + // If file doesn't exist; return error + debug!("Removing directory {}", entry.path().display()); + if !entry.path().exists() { + error!("Directory doesn't exist"); + return Err(HostError::new( + HostErrorType::NoSuchFileOrDirectory, + None, + entry.path(), + )); + } + // Remove + match std::fs::remove_dir_all(entry.path()) { + Ok(_) => { + // Update dir + self.files = self.scan_dir(self.wrkdir.as_path())?; + info!("Removed directory {}", entry.path().display()); + Ok(()) } - // Remove - match std::fs::remove_dir_all(dir.path.as_path()) { - Ok(_) => { - // Update dir - self.files = self.scan_dir(self.wrkdir.as_path())?; - info!("Removed directory {}", dir.path.display()); - Ok(()) - } - Err(err) => { - error!("Could not remove directory: {}", err); - Err(HostError::new( - HostErrorType::DeleteFailed, - Some(err), - dir.path.as_path(), - )) - } + Err(err) => { + error!("Could not remove directory: {}", err); + Err(HostError::new( + HostErrorType::DeleteFailed, + Some(err), + entry.path(), + )) } } - Entry::File(file) => { - // If file doesn't exist; return error - debug!("Removing file {}", file.path.display()); - if !file.path.as_path().exists() { - error!("File doesn't exist"); - return Err(HostError::new( - HostErrorType::NoSuchFileOrDirectory, - None, - file.path.as_path(), - )); + } else { + // If file doesn't exist; return error + debug!("Removing file {}", entry.path().display()); + if !entry.path().exists() { + error!("File doesn't exist"); + return Err(HostError::new( + HostErrorType::NoSuchFileOrDirectory, + None, + entry.path(), + )); + } + // Remove + match std::fs::remove_file(entry.path()) { + Ok(_) => { + // Update dir + self.files = self.scan_dir(self.wrkdir.as_path())?; + info!("Removed file {}", entry.path().display()); + Ok(()) } - // Remove - match std::fs::remove_file(file.path.as_path()) { - Ok(_) => { - // Update dir - self.files = self.scan_dir(self.wrkdir.as_path())?; - info!("Removed file {}", file.path.display()); - Ok(()) - } - Err(err) => { - error!("Could not remove file: {}", err); - Err(HostError::new( - HostErrorType::DeleteFailed, - Some(err), - file.path.as_path(), - )) - } + Err(err) => { + error!("Could not remove file: {}", err); + Err(HostError::new( + HostErrorType::DeleteFailed, + Some(err), + entry.path(), + )) } } } } /// Rename file or directory to new name - pub fn rename(&mut self, entry: &Entry, dst_path: &Path) -> Result<(), HostError> { + pub fn rename(&mut self, entry: &File, dst_path: &Path) -> Result<(), HostError> { match std::fs::rename(entry.path(), dst_path) { Ok(_) => { // Scan dir @@ -338,7 +334,7 @@ impl Localhost { } /// Copy file to destination path - pub fn copy(&mut self, entry: &Entry, dst: &Path) -> Result<(), HostError> { + pub fn copy(&mut self, entry: &File, dst: &Path) -> Result<(), HostError> { // Get absolute path of dest let dst: PathBuf = self.to_path(dst); info!( @@ -347,46 +343,43 @@ impl Localhost { dst.display() ); // Match entry - match entry { - Entry::File(file) => { - // Copy file - // If destination path is a directory, push file name - let dst: PathBuf = match dst.as_path().is_dir() { - true => { - let mut p: PathBuf = dst.clone(); - p.push(file.name.as_str()); - p - } - false => dst.clone(), - }; - // Copy entry path to dst path - if let Err(err) = std::fs::copy(file.path.as_path(), dst.as_path()) { - error!("Failed to copy file: {}", err); - return Err(HostError::new( - HostErrorType::CouldNotCreateFile, - Some(err), - file.path.as_path(), - )); - } - info!("File copied"); + if entry.is_dir() { + // If destination path doesn't exist, create destination + if !dst.exists() { + debug!("Directory {} doesn't exist; creating it", dst.display()); + self.mkdir(dst.as_path())?; } - Entry::Directory(dir) => { - // If destination path doesn't exist, create destination - if !dst.exists() { - debug!("Directory {} doesn't exist; creating it", dst.display()); - self.mkdir(dst.as_path())?; - } - // Scan dir - let dir_files: Vec = self.scan_dir(dir.path.as_path())?; - // Iterate files - for dir_entry in dir_files.iter() { - // Calculate dst - let mut sub_dst: PathBuf = dst.clone(); - sub_dst.push(dir_entry.name()); - // Call function recursively - self.copy(dir_entry, sub_dst.as_path())?; + // Scan dir + let dir_files: Vec = self.scan_dir(entry.path())?; + // Iterate files + for dir_entry in dir_files.iter() { + // Calculate dst + let mut sub_dst: PathBuf = dst.clone(); + sub_dst.push(dir_entry.name()); + // Call function recursively + self.copy(dir_entry, sub_dst.as_path())?; + } + } else { + // Copy file + // If destination path is a directory, push file name + let dst: PathBuf = match dst.as_path().is_dir() { + true => { + let mut p: PathBuf = dst.clone(); + p.push(entry.name().as_str()); + p } + false => dst.clone(), + }; + // Copy entry path to dst path + if let Err(err) = std::fs::copy(entry.path(), dst.as_path()) { + error!("Failed to copy file: {}", err); + return Err(HostError::new( + HostErrorType::CouldNotCreateFile, + Some(err), + entry.path(), + )); } + info!("File copied"); } // Reload directory if dst is pwd match dst.is_dir() { @@ -412,9 +405,8 @@ impl Localhost { Ok(()) } - /// Stat file and create a Entry - #[cfg(target_family = "unix")] - pub fn stat(&self, path: &Path) -> Result { + /// Stat file and create a File + pub fn stat(&self, path: &Path) -> Result { info!("Stating file {}", path.display()); let path: PathBuf = self.to_path(path); let attr = match fs::metadata(path.as_path()) { @@ -428,90 +420,13 @@ impl Localhost { )); } }; - let name = String::from(path.file_name().unwrap().to_str().unwrap_or("")); - // Match dir / file - let metadata = Metadata { - atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), - ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - gid: Some(attr.gid()), - mode: Some(UnixPex::from(attr.mode())), - mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), - size: if path.is_dir() { - attr.blksize() - } else { - attr.len() - }, - symlink: fs::read_link(path.as_path()).ok(), - uid: Some(attr.uid()), - }; - Ok(match path.is_dir() { - true => Entry::Directory(Directory { - name, - path, - metadata, - }), - false => { - // Is File - let extension = path - .extension() - .map(|s| String::from(s.to_str().unwrap_or(""))); - Entry::File(File { - name, - path, - extension, - metadata, - }) - } - }) - } - - /// Stat file and create a Entry - #[cfg(target_os = "windows")] - pub fn stat(&self, path: &Path) -> Result { - let path: PathBuf = self.to_path(path); - info!("Stating file {}", path.display()); - let attr = match fs::metadata(path.as_path()) { - Ok(metadata) => metadata, - Err(err) => { - error!("Could not read file metadata: {}", err); - return Err(HostError::new( - HostErrorType::FileNotAccessible, - Some(err), - path.as_path(), - )); - } - }; - let name = String::from(path.file_name().unwrap().to_str().unwrap_or("")); - let metadata = Metadata { - atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), - ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), - size: if path.is_dir() { 0 } else { attr.len() }, - symlink: fs::read_link(path.as_path()).ok(), - uid: None, - gid: None, - mode: None, - }; + let mut metadata = Metadata::from(attr); + if let Ok(symlink) = fs::read_link(path.as_path()) { + metadata.set_symlink(symlink); + metadata.file_type = FileType::Symlink; + } // Match dir / file - Ok(match path.is_dir() { - true => Entry::Directory(Directory { - name, - path, - metadata, - }), - false => { - // Is File - let extension = path - .extension() - .map(|s| String::from(s.to_str().unwrap_or(""))); - Entry::File(File { - name, - path, - extension, - metadata, - }) - } - }) + Ok(File { path, metadata }) } /// Execute a command on localhost @@ -644,11 +559,11 @@ impl Localhost { } /// Get content of the current directory as a list of fs entry - pub fn scan_dir(&self, dir: &Path) -> Result, HostError> { + pub fn scan_dir(&self, dir: &Path) -> Result, HostError> { info!("Reading directory {}", dir.display()); match std::fs::read_dir(dir) { Ok(e) => { - let mut fs_entries: Vec = Vec::new(); + let mut fs_entries: Vec = Vec::new(); for entry in e.flatten() { // NOTE: 0.4.1, don't fail if stat for one file fails match self.stat(entry.path().as_path()) { @@ -668,7 +583,7 @@ impl Localhost { /// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course. /// The `search` argument supports wilcards ('*', '?') - pub fn find(&self, search: &str) -> Result, HostError> { + pub fn find(&self, search: &str) -> Result, HostError> { self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search)) } @@ -692,9 +607,9 @@ impl Localhost { /// Recursive call for `find` method. /// Search in current directory for files which match `filter`. /// If a directory is found in current directory, `iter_search` will be called using that dir as argument. - fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result, HostError> { + fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result, HostError> { // Scan directory - let mut drained: Vec = Vec::new(); + let mut drained: Vec = Vec::new(); match self.scan_dir(dir) { Err(err) => Err(err), Ok(entries) => { @@ -705,20 +620,16 @@ impl Localhost { - if is file: check if it matches `filter` - if it matches `filter`: push to to filter */ - for entry in entries.iter() { - match entry { - Entry::Directory(dir) => { - // If directory matches; push directory to drained - if filter.matches(dir.name.as_str()) { - drained.push(Entry::Directory(dir.clone())); - } - drained.append(&mut self.iter_search(dir.path.as_path(), filter)?); - } - Entry::File(file) => { - if filter.matches(file.name.as_str()) { - drained.push(Entry::File(file.clone())); - } + for entry in entries.into_iter() { + if entry.is_dir() { + // If directory matches; push directory to drained + let next_path = entry.path().to_path_buf(); + if filter.matches(entry.name().as_str()) { + drained.push(entry); } + drained.append(&mut self.iter_search(next_path.as_path(), filter)?); + } else if filter.matches(entry.name().as_str()) { + drained.push(entry); } } Ok(drained) @@ -902,37 +813,27 @@ mod tests { .is_ok()); // Get dir let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); // Verify files - let file_0: &Entry = files.get(0).unwrap(); - match file_0 { - Entry::File(file_0) => { - if file_0.name == String::from("foo.txt") { - assert!(file_0.metadata.symlink.is_none()); - } else { - assert_eq!( - file_0.metadata.symlink.as_ref().unwrap(), - &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) - ); - } - } - _ => panic!("expected entry 0 to be file: {:?}", file_0), - }; + let file_0: &File = files.get(0).unwrap(); + if file_0.name() == String::from("foo.txt") { + assert!(file_0.metadata.symlink.is_none()); + } else { + assert_eq!( + file_0.metadata.symlink.as_ref().unwrap(), + &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) + ); + } // Verify simlink - let file_1: &Entry = files.get(1).unwrap(); - match file_1 { - Entry::File(file_1) => { - if file_1.name == String::from("bar.txt") { - assert_eq!( - file_1.metadata.symlink.as_ref().unwrap(), - &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) - ); - } else { - assert!(file_1.metadata.symlink.is_none()); - } - } - _ => panic!("expected entry 0 to be file: {:?}", file_1), - }; + let file_1: &File = files.get(1).unwrap(); + if file_1.name() == String::from("bar.txt") { + assert_eq!( + file_1.metadata.symlink.as_ref().unwrap(), + &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) + ); + } else { + assert!(file_1.metadata.symlink.is_none()); + } } #[test] @@ -940,10 +841,10 @@ mod tests { fn test_host_localhost_mkdir() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 0); // There should be 0 files now assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now // Try to re-create directory assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err()); @@ -967,17 +868,17 @@ mod tests { // Create sample file assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now // Remove file assert!(host.remove(files.get(0).unwrap()).is_ok()); // There should be 0 files now - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 0); // There should be 0 files now // Create directory assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); // Delete directory - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now assert!(host.remove(files.get(0).unwrap()).is_ok()); // Remove unexisting directory @@ -998,7 +899,7 @@ mod tests { PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str()); assert!(StdFile::create(src_path.as_path()).is_ok()); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now assert_eq!(files.get(0).unwrap().name(), "foo.txt"); // Rename file @@ -1008,7 +909,7 @@ mod tests { .rename(files.get(0).unwrap(), dst_path.as_path()) .is_ok()); // There should be still 1 file now, but named bar.txt - let files: Vec = host.list_dir(); + let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 0 files now assert_eq!(files.get(0).unwrap().name(), "bar.txt"); // Fail @@ -1052,7 +953,7 @@ mod tests { file2_path.push("bar.txt"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let file1_entry: Entry = host.files.get(0).unwrap().clone(); + let file1_entry: File = host.files.get(0).unwrap().clone(); assert_eq!(file1_entry.name(), String::from("foo.txt")); // Copy assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); @@ -1081,7 +982,7 @@ mod tests { let file2_path: PathBuf = PathBuf::from("bar.txt"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let file1_entry: Entry = host.files.get(0).unwrap().clone(); + let file1_entry: File = host.files.get(0).unwrap().clone(); assert_eq!(file1_entry.name(), String::from("foo.txt")); // Copy assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); @@ -1108,7 +1009,7 @@ mod tests { dir_dest.push("test_dest_dir/"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let dir_src_entry: Entry = host.files.get(0).unwrap().clone(); + let dir_src_entry: File = host.files.get(0).unwrap().clone(); assert_eq!(dir_src_entry.name(), String::from("test_dir")); // Copy assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); @@ -1138,7 +1039,7 @@ mod tests { let dir_dest: PathBuf = PathBuf::from("test_dest_dir/"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let dir_src_entry: Entry = host.files.get(0).unwrap().clone(); + let dir_src_entry: File = host.files.get(0).unwrap().clone(); assert_eq!(dir_src_entry.name(), String::from("test_dir")); // Copy assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); @@ -1178,8 +1079,8 @@ mod tests { assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok()); let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap(); // Find txt files - let mut result: Vec = host.find("*.txt").ok().unwrap(); - result.sort_by_key(|x: &Entry| x.name().to_lowercase()); + let mut result: Vec = host.find("*.txt").ok().unwrap(); + result.sort_by_key(|x: &File| x.name().to_lowercase()); // There should be 3 entries assert_eq!(result.len(), 3); // Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt) @@ -1187,8 +1088,8 @@ mod tests { assert_eq!(result[1].name(), "omar.txt"); assert_eq!(result[2].name(), "pippo.txt"); // Search for directory - let mut result: Vec = host.find("examples*").ok().unwrap(); - result.sort_by_key(|x: &Entry| x.name().to_lowercase()); + let mut result: Vec = host.find("examples*").ok().unwrap(); + result.sort_by_key(|x: &File| x.name().to_lowercase()); assert_eq!(result.len(), 2); assert_eq!(result[0].name(), "examples"); assert_eq!(result[1].name(), "examples.csv"); diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index a67e244e..d25b7433 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -28,7 +28,7 @@ // Locals use super::config_client::ConfigClient; // Ext -use remotefs::client::ssh::SshKeyStorage as SshKeyStorageT; +use remotefs_ssh::SshKeyStorage as SshKeyStorageT; use std::collections::HashMap; use std::path::{Path, PathBuf}; diff --git a/src/ui/activities/filetransfer/actions/change_dir.rs b/src/ui/activities/filetransfer/actions/change_dir.rs index bc4e6f72..941a6722 100644 --- a/src/ui/activities/filetransfer/actions/change_dir.rs +++ b/src/ui/activities/filetransfer/actions/change_dir.rs @@ -28,7 +28,7 @@ // locals use super::{FileExplorerTab, FileTransferActivity, LogLevel, Msg, PendingActionMsg}; -use remotefs::Directory; +use remotefs::File; use std::path::PathBuf; /// Describes destination for sync browsing @@ -40,18 +40,18 @@ enum SyncBrowsingDestination { impl FileTransferActivity { /// Enter a directory on local host from entry - pub(crate) fn action_enter_local_dir(&mut self, dir: Directory) { - self.local_changedir(dir.path.as_path(), true); + pub(crate) fn action_enter_local_dir(&mut self, dir: File) { + self.local_changedir(dir.path(), true); if self.browser.sync_browsing && self.browser.found().is_none() { - self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name)); + self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name())); } } /// Enter a directory on local host from entry - pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory) { - self.remote_changedir(dir.path.as_path(), true); + pub(crate) fn action_enter_remote_dir(&mut self, dir: File) { + self.remote_changedir(dir.path(), true); if self.browser.sync_browsing && self.browser.found().is_none() { - self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name)); + self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name())); } } diff --git a/src/ui/activities/filetransfer/actions/copy.rs b/src/ui/activities/filetransfer/actions/copy.rs index 7674b9a7..209620db 100644 --- a/src/ui/activities/filetransfer/actions/copy.rs +++ b/src/ui/activities/filetransfer/actions/copy.rs @@ -26,20 +26,20 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload}; +use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; -use remotefs::{Entry, RemoteErrorType}; +use remotefs::{File, RemoteErrorType}; use std::path::{Path, PathBuf}; impl FileTransferActivity { /// Copy file on local pub(crate) fn action_local_copy(&mut self, input: String) { match self.get_local_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { let dest_path: PathBuf = PathBuf::from(input); self.local_copy_file(&entry, dest_path.as_path()); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Try to copy each file to Input/{FILE_NAME} let base_path: PathBuf = PathBuf::from(input); // Iter files @@ -49,18 +49,18 @@ impl FileTransferActivity { self.local_copy_file(entry, dest_path.as_path()); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } /// Copy file on remote pub(crate) fn action_remote_copy(&mut self, input: String) { match self.get_remote_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { let dest_path: PathBuf = PathBuf::from(input); self.remote_copy_file(entry, dest_path.as_path()); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Try to copy each file to Input/{FILE_NAME} let base_path: PathBuf = PathBuf::from(input); // Iter files @@ -70,11 +70,11 @@ impl FileTransferActivity { self.remote_copy_file(entry, dest_path.as_path()); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } - fn local_copy_file(&mut self, entry: &Entry, dest: &Path) { + fn local_copy_file(&mut self, entry: &File, dest: &Path) { match self.host.copy(entry, dest) { Ok(_) => { self.log( @@ -98,7 +98,7 @@ impl FileTransferActivity { } } - fn remote_copy_file(&mut self, entry: Entry, dest: &Path) { + fn remote_copy_file(&mut self, entry: File, dest: &Path) { match self.client.as_mut().copy(entry.path(), dest) { Ok(_) => { self.log( @@ -129,123 +129,121 @@ impl FileTransferActivity { } /// Tricky copy will be used whenever copy command is not available on remote host - pub(super) fn tricky_copy(&mut self, entry: Entry, dest: &Path) -> Result<(), String> { + pub(super) fn tricky_copy(&mut self, entry: File, dest: &Path) -> Result<(), String> { // NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen self.umount_wait(); // match entry - match entry { - Entry::File(entry) => { - // Create tempfile - let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() { - Ok(f) => f, - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!("Copy failed: could not create temporary file: {}", err), - ); - return Err(String::from("Could not create temporary file")); - } - }; - // Download file - let name = entry.name.clone(); - let entry_path = entry.path.clone(); - if let Err(err) = - self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name)) - { + if entry.is_dir() { + let tempdir: tempfile::TempDir = match tempfile::TempDir::new() { + Ok(d) => d, + Err(err) => { self.log_and_alert( LogLevel::Error, - format!("Copy failed: could not download to temporary file: {}", err), + format!("Copy failed: could not create temporary directory: {}", err), ); - return Err(err); + return Err(err.to_string()); } - // Get local fs entry - let tmpfile_entry = match self.host.stat(tmpfile.path()) { - Ok(e) => e.unwrap_file(), - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Copy failed: could not stat \"{}\": {}", - tmpfile.path().display(), - err - ), - ); - return Err(err.to_string()); - } - }; - // Upload file to destination - let wrkdir = self.remote().wrkdir.clone(); - if let Err(err) = self.filetransfer_send( - TransferPayload::File(tmpfile_entry), - wrkdir.as_path(), - Some(String::from(dest.to_string_lossy())), - ) { + }; + // Get path of dest + let mut tempdir_path: PathBuf = tempdir.path().to_path_buf(); + tempdir_path.push(entry.name()); + // Download file + if let Err(err) = + self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None) + { + self.log_and_alert( + LogLevel::Error, + format!("Copy failed: failed to download file: {}", err), + ); + return Err(err); + } + // Stat dir + let tempdir_entry = match self.host.stat(tempdir_path.as_path()) { + Ok(e) => e, + Err(err) => { self.log_and_alert( LogLevel::Error, format!( - "Copy failed: could not write file {}: {}", - entry_path.display(), + "Copy failed: could not stat \"{}\": {}", + tempdir.path().display(), err ), ); - return Err(err); + return Err(err.to_string()); } - Ok(()) + }; + // Upload to destination + let wrkdir: PathBuf = self.remote().wrkdir.clone(); + if let Err(err) = self.filetransfer_send( + TransferPayload::Any(tempdir_entry), + wrkdir.as_path(), + Some(String::from(dest.to_string_lossy())), + ) { + self.log_and_alert( + LogLevel::Error, + format!("Copy failed: failed to send file: {}", err), + ); + return Err(err); } - Entry::Directory(_) => { - let tempdir: tempfile::TempDir = match tempfile::TempDir::new() { - Ok(d) => d, - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!("Copy failed: could not create temporary directory: {}", err), - ); - return Err(err.to_string()); - } - }; - // Get path of dest - let mut tempdir_path: PathBuf = tempdir.path().to_path_buf(); - tempdir_path.push(entry.name()); - // Download file - if let Err(err) = - self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None) - { + Ok(()) + } else { + // Create tempfile + let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() { + Ok(f) => f, + Err(err) => { self.log_and_alert( LogLevel::Error, - format!("Copy failed: failed to download file: {}", err), + format!("Copy failed: could not create temporary file: {}", err), ); - return Err(err); + return Err(String::from("Could not create temporary file")); } - // Stat dir - let tempdir_entry = match self.host.stat(tempdir_path.as_path()) { - Ok(e) => e, - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Copy failed: could not stat \"{}\": {}", - tempdir.path().display(), - err - ), - ); - return Err(err.to_string()); - } - }; - // Upload to destination - let wrkdir: PathBuf = self.remote().wrkdir.clone(); - if let Err(err) = self.filetransfer_send( - TransferPayload::Any(tempdir_entry), - wrkdir.as_path(), - Some(String::from(dest.to_string_lossy())), - ) { + }; + // Download file + let name = entry.name(); + let entry_path = entry.path().to_path_buf(); + if let Err(err) = + self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name)) + { + self.log_and_alert( + LogLevel::Error, + format!("Copy failed: could not download to temporary file: {}", err), + ); + return Err(err); + } + // Get local fs entry + let tmpfile_entry = match self.host.stat(tmpfile.path()) { + Ok(e) if e.is_file() => e, + Ok(_) => panic!("{} is not a file", tmpfile.path().display()), + Err(err) => { self.log_and_alert( LogLevel::Error, - format!("Copy failed: failed to send file: {}", err), + format!( + "Copy failed: could not stat \"{}\": {}", + tmpfile.path().display(), + err + ), ); - return Err(err); + return Err(err.to_string()); } - Ok(()) + }; + // Upload file to destination + let wrkdir = self.remote().wrkdir.clone(); + if let Err(err) = self.filetransfer_send( + TransferPayload::File(tmpfile_entry), + wrkdir.as_path(), + Some(String::from(dest.to_string_lossy())), + ) { + self.log_and_alert( + LogLevel::Error, + format!( + "Copy failed: could not write file {}: {}", + entry_path.display(), + err + ), + ); + return Err(err); } + Ok(()) } } } diff --git a/src/ui/activities/filetransfer/actions/delete.rs b/src/ui/activities/filetransfer/actions/delete.rs index 1890791f..9121dc82 100644 --- a/src/ui/activities/filetransfer/actions/delete.rs +++ b/src/ui/activities/filetransfer/actions/delete.rs @@ -26,46 +26,46 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, LogLevel, SelectedEntry}; +use super::{FileTransferActivity, LogLevel, SelectedFile}; -use remotefs::Entry; +use remotefs::File; impl FileTransferActivity { pub(crate) fn action_local_delete(&mut self) { match self.get_local_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { // Delete file self.local_remove_file(&entry); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Iter files for entry in entries.iter() { // Delete file self.local_remove_file(entry); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } pub(crate) fn action_remote_delete(&mut self) { match self.get_remote_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { // Delete file self.remote_remove_file(&entry); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Iter files for entry in entries.iter() { // Delete file self.remote_remove_file(entry); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } - pub(crate) fn local_remove_file(&mut self, entry: &Entry) { + pub(crate) fn local_remove_file(&mut self, entry: &File) { match self.host.remove(entry) { Ok(_) => { // Log @@ -87,7 +87,7 @@ impl FileTransferActivity { } } - pub(crate) fn remote_remove_file(&mut self, entry: &Entry) { + pub(crate) fn remote_remove_file(&mut self, entry: &File) { match self.client.remove_dir_all(entry.path()) { Ok(_) => { self.log( diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index e9e377f1..efe78270 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -26,10 +26,10 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload}; +use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; // ext -use remotefs::{Entry, File}; +use remotefs::File; use std::fs::OpenOptions; use std::io::Read; use std::path::{Path, PathBuf}; @@ -37,10 +37,10 @@ use std::time::SystemTime; impl FileTransferActivity { pub(crate) fn action_edit_local_file(&mut self) { - let entries: Vec = match self.get_local_selected_entries() { - SelectedEntry::One(entry) => vec![entry], - SelectedEntry::Many(entries) => entries, - SelectedEntry::None => vec![], + let entries: Vec = match self.get_local_selected_entries() { + SelectedFile::One(entry) => vec![entry], + SelectedFile::Many(entries) => entries, + SelectedFile::None => vec![], }; // Edit all entries for entry in entries.iter() { @@ -59,21 +59,21 @@ impl FileTransferActivity { } pub(crate) fn action_edit_remote_file(&mut self) { - let entries: Vec = match self.get_remote_selected_entries() { - SelectedEntry::One(entry) => vec![entry], - SelectedEntry::Many(entries) => entries, - SelectedEntry::None => vec![], + let entries: Vec = match self.get_remote_selected_entries() { + SelectedFile::One(entry) => vec![entry], + SelectedFile::Many(entries) => entries, + SelectedFile::None => vec![], }; // Edit all entries for entry in entries.into_iter() { // Check if file - if let Entry::File(file) = entry { + if entry.is_file() { self.log( LogLevel::Info, - format!("Opening file \"{}\"…", file.path.display()), + format!("Opening file \"{}\"…", entry.path().display()), ); // Edit file - if let Err(err) = self.edit_remote_file(file) { + if let Err(err) = self.edit_remote_file(entry) { self.log_and_alert(LogLevel::Error, err); } } @@ -149,8 +149,8 @@ impl FileTransferActivity { Err(err) => return Err(err), }; // Download file - let file_name = file.name.clone(); - let file_path = file.path.clone(); + let file_name = file.name(); + let file_path = file.path().to_path_buf(); if let Err(err) = self.filetransfer_recv( TransferPayload::File(file), tmpfile.as_path(), @@ -160,7 +160,7 @@ impl FileTransferActivity { } // Get current file modification time let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) { - Ok(e) => e.metadata().mtime, + Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH), Err(err) => { return Err(format!( "Could not stat \"{}\": {}", @@ -174,7 +174,7 @@ impl FileTransferActivity { return Err(err); } // Get local fs entry - let tmpfile_entry: Entry = match self.host.stat(tmpfile.as_path()) { + let tmpfile_entry: File = match self.host.stat(tmpfile.as_path()) { Ok(e) => e, Err(err) => { return Err(format!( @@ -185,7 +185,12 @@ impl FileTransferActivity { } }; // Check if file has changed - match prev_mtime != tmpfile_entry.metadata().mtime { + match prev_mtime + != tmpfile_entry + .metadata() + .modified + .unwrap_or(std::time::UNIX_EPOCH) + { true => { self.log( LogLevel::Info, @@ -196,7 +201,7 @@ impl FileTransferActivity { ); // Get local fs entry let tmpfile_entry = match self.host.stat(tmpfile.as_path()) { - Ok(e) => e.unwrap_file(), + Ok(e) => e, Err(err) => { return Err(format!( "Could not stat \"{}\": {}", diff --git a/src/ui/activities/filetransfer/actions/find.rs b/src/ui/activities/filetransfer/actions/find.rs index c6ffba9a..17fab66c 100644 --- a/src/ui/activities/filetransfer/actions/find.rs +++ b/src/ui/activities/filetransfer/actions/find.rs @@ -27,19 +27,19 @@ */ // locals use super::super::browser::FileExplorerTab; -use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferOpts, TransferPayload}; +use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload}; use std::path::PathBuf; impl FileTransferActivity { - pub(crate) fn action_local_find(&mut self, input: String) -> Result, String> { + pub(crate) fn action_local_find(&mut self, input: String) -> Result, String> { match self.host.find(input.as_str()) { Ok(entries) => Ok(entries), Err(err) => Err(format!("Could not search for files: {}", err)), } } - pub(crate) fn action_remote_find(&mut self, input: String) -> Result, String> { + pub(crate) fn action_remote_find(&mut self, input: String) -> Result, String> { match self.client.as_mut().find(input.as_str()) { Ok(entries) => Ok(entries), Err(err) => Err(format!("Could not search for files: {}", err)), @@ -48,14 +48,15 @@ impl FileTransferActivity { pub(crate) fn action_find_changedir(&mut self) { // Match entry - if let SelectedEntry::One(entry) = self.get_found_selected_entries() { + if let SelectedFile::One(entry) = self.get_found_selected_entries() { // Get path: if a directory, use directory path; if it is a File, get parent path - let path: PathBuf = match entry { - Entry::Directory(dir) => dir.path, - Entry::File(file) => match file.path.parent() { + let path = if entry.is_dir() { + entry.path().to_path_buf() + } else { + match entry.path().parent() { None => PathBuf::from("."), Some(p) => p.to_path_buf(), - }, + } }; // Change directory match self.browser.tab() { @@ -75,13 +76,13 @@ impl FileTransferActivity { FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(), }; match self.get_found_selected_entries() { - SelectedEntry::One(entry) => match self.browser.tab() { + SelectedFile::One(entry) => match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() && self.remote_file_exists(file_to_check.as_path()) && !self.should_replace_file( - opts.save_as.as_deref().unwrap_or_else(|| entry.name()), + opts.save_as.clone().unwrap_or_else(|| entry.name()), ) { // Do not replace @@ -103,7 +104,7 @@ impl FileTransferActivity { if self.config().get_prompt_on_file_replace() && self.local_file_exists(file_to_check.as_path()) && !self.should_replace_file( - opts.save_as.as_deref().unwrap_or_else(|| entry.name()), + opts.save_as.clone().unwrap_or_else(|| entry.name()), ) { // Do not replace @@ -121,7 +122,7 @@ impl FileTransferActivity { } } }, - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // In case of selection: save multiple files in wrkdir/input let mut dest_path: PathBuf = wrkdir; if let Some(save_as) = opts.save_as { @@ -132,7 +133,7 @@ impl FileTransferActivity { FileExplorerTab::FindLocal | FileExplorerTab::Local => { if self.config().get_prompt_on_file_replace() { // Check which file would be replaced - let existing_files: Vec<&Entry> = entries + let existing_files: Vec<&File> = entries .iter() .filter(|x| { self.remote_file_exists( @@ -163,7 +164,7 @@ impl FileTransferActivity { FileExplorerTab::FindRemote | FileExplorerTab::Remote => { if self.config().get_prompt_on_file_replace() { // Check which file would be replaced - let existing_files: Vec<&Entry> = entries + let existing_files: Vec<&File> = entries .iter() .filter(|x| { self.local_file_exists( @@ -191,28 +192,28 @@ impl FileTransferActivity { } } } - SelectedEntry::None => {} + SelectedFile::None => {} } } pub(crate) fn action_find_delete(&mut self) { match self.get_found_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { // Delete file self.remove_found_file(&entry); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Iter files for entry in entries.iter() { // Delete file self.remove_found_file(entry); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } - fn remove_found_file(&mut self, entry: &Entry) { + fn remove_found_file(&mut self, entry: &File) { match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { self.local_remove_file(entry); @@ -225,39 +226,39 @@ impl FileTransferActivity { pub(crate) fn action_find_open(&mut self) { match self.get_found_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { // Open file self.open_found_file(&entry, None); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Iter files for entry in entries.iter() { // Open file self.open_found_file(entry, None); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } pub(crate) fn action_find_open_with(&mut self, with: &str) { match self.get_found_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { // Open file self.open_found_file(&entry, Some(with)); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Iter files for entry in entries.iter() { // Open file self.open_found_file(entry, Some(with)); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } - fn open_found_file(&mut self, entry: &Entry, with: Option<&str>) { + fn open_found_file(&mut self, entry: &File, with: Option<&str>) { match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { self.action_open_local_file(entry, with); diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index e3b73505..a8143368 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -29,7 +29,7 @@ pub(self) use super::{ browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg, TransferOpts, TransferPayload, }; -pub(self) use remotefs::Entry; +pub(self) use remotefs::File; use tuirealm::{State, StateValue}; // actions @@ -49,100 +49,100 @@ pub(crate) mod submit; pub(crate) mod symlink; #[derive(Debug)] -pub(crate) enum SelectedEntry { - One(Entry), - Many(Vec), +pub(crate) enum SelectedFile { + One(File), + Many(Vec), None, } #[derive(Debug)] -enum SelectedEntryIndex { +enum SelectedFileIndex { One(usize), Many(Vec), None, } -impl From> for SelectedEntry { - fn from(opt: Option<&Entry>) -> Self { +impl From> for SelectedFile { + fn from(opt: Option<&File>) -> Self { match opt { - Some(e) => SelectedEntry::One(e.clone()), - None => SelectedEntry::None, + Some(e) => SelectedFile::One(e.clone()), + None => SelectedFile::None, } } } -impl From> for SelectedEntry { - fn from(files: Vec<&Entry>) -> Self { - SelectedEntry::Many(files.into_iter().cloned().collect()) +impl From> for SelectedFile { + fn from(files: Vec<&File>) -> Self { + SelectedFile::Many(files.into_iter().cloned().collect()) } } impl FileTransferActivity { /// Get local file entry - pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry { + pub(crate) fn get_local_selected_entries(&self) -> SelectedFile { match self.get_selected_index(&Id::ExplorerLocal) { - SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)), - SelectedEntryIndex::Many(files) => { - let files: Vec<&Entry> = files + SelectedFileIndex::One(idx) => SelectedFile::from(self.local().get(idx)), + SelectedFileIndex::Many(files) => { + let files: Vec<&File> = files .iter() - .map(|x| self.local().get(*x)) // Usize to Option + .map(|x| self.local().get(*x)) // Usize to Option .flatten() .collect(); - SelectedEntry::from(files) + SelectedFile::from(files) } - SelectedEntryIndex::None => SelectedEntry::None, + SelectedFileIndex::None => SelectedFile::None, } } /// Get remote file entry - pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry { + pub(crate) fn get_remote_selected_entries(&self) -> SelectedFile { match self.get_selected_index(&Id::ExplorerRemote) { - SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)), - SelectedEntryIndex::Many(files) => { - let files: Vec<&Entry> = files + SelectedFileIndex::One(idx) => SelectedFile::from(self.remote().get(idx)), + SelectedFileIndex::Many(files) => { + let files: Vec<&File> = files .iter() - .map(|x| self.remote().get(*x)) // Usize to Option + .map(|x| self.remote().get(*x)) // Usize to Option .flatten() .collect(); - SelectedEntry::from(files) + SelectedFile::from(files) } - SelectedEntryIndex::None => SelectedEntry::None, + SelectedFileIndex::None => SelectedFile::None, } } /// Returns whether only one entry is selected on local host pub(crate) fn is_local_selected_one(&self) -> bool { - matches!(self.get_local_selected_entries(), SelectedEntry::One(_)) + matches!(self.get_local_selected_entries(), SelectedFile::One(_)) } /// Returns whether only one entry is selected on remote host pub(crate) fn is_remote_selected_one(&self) -> bool { - matches!(self.get_remote_selected_entries(), SelectedEntry::One(_)) + matches!(self.get_remote_selected_entries(), SelectedFile::One(_)) } /// Get remote file entry - pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry { + pub(crate) fn get_found_selected_entries(&self) -> SelectedFile { match self.get_selected_index(&Id::ExplorerFind) { - SelectedEntryIndex::One(idx) => { - SelectedEntry::from(self.found().as_ref().unwrap().get(idx)) + SelectedFileIndex::One(idx) => { + SelectedFile::from(self.found().as_ref().unwrap().get(idx)) } - SelectedEntryIndex::Many(files) => { - let files: Vec<&Entry> = files + SelectedFileIndex::Many(files) => { + let files: Vec<&File> = files .iter() - .map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option + .map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option .flatten() .collect(); - SelectedEntry::from(files) + SelectedFile::from(files) } - SelectedEntryIndex::None => SelectedEntry::None, + SelectedFileIndex::None => SelectedFile::None, } } // -- private - fn get_selected_index(&self, id: &Id) -> SelectedEntryIndex { + fn get_selected_index(&self, id: &Id) -> SelectedFileIndex { match self.app.state(id) { - Ok(State::One(StateValue::Usize(idx))) => SelectedEntryIndex::One(idx), + Ok(State::One(StateValue::Usize(idx))) => SelectedFileIndex::One(idx), Ok(State::Vec(files)) => { let list: Vec = files .iter() @@ -151,9 +151,9 @@ impl FileTransferActivity { _ => 0, }) .collect(); - SelectedEntryIndex::Many(list) + SelectedFileIndex::Many(list) } - _ => SelectedEntryIndex::None, + _ => SelectedFileIndex::None, } } } diff --git a/src/ui/activities/filetransfer/actions/newfile.rs b/src/ui/activities/filetransfer/actions/newfile.rs index c8119e4f..29d95da3 100644 --- a/src/ui/activities/filetransfer/actions/newfile.rs +++ b/src/ui/activities/filetransfer/actions/newfile.rs @@ -26,8 +26,8 @@ * SOFTWARE. */ // locals -use super::{Entry, FileTransferActivity, LogLevel}; -use std::fs::File; +use super::{File, FileTransferActivity, LogLevel}; +use std::fs::File as StdFile; use std::path::PathBuf; impl FileTransferActivity { @@ -86,7 +86,7 @@ impl FileTransferActivity { ), Ok(tfile) => { // Stat tempfile - let local_file: Entry = match self.host.stat(tfile.path()) { + let local_file: File = match self.host.stat(tfile.path()) { Err(err) => { self.log_and_alert( LogLevel::Error, @@ -96,9 +96,9 @@ impl FileTransferActivity { } Ok(f) => f, }; - if let Entry::File(local_file) = local_file { + if local_file.is_file() { // Create file - let reader = Box::new(match File::open(tfile.path()) { + let reader = Box::new(match StdFile::open(tfile.path()) { Ok(f) => f, Err(err) => { self.log_and_alert( diff --git a/src/ui/activities/filetransfer/actions/open.rs b/src/ui/activities/filetransfer/actions/open.rs index 4366c4f2..5936fc6c 100644 --- a/src/ui/activities/filetransfer/actions/open.rs +++ b/src/ui/activities/filetransfer/actions/open.rs @@ -26,17 +26,17 @@ * SOFTWARE. */ // locals -use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferPayload}; +use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; // ext use std::path::{Path, PathBuf}; impl FileTransferActivity { /// Open local file pub(crate) fn action_open_local(&mut self) { - let entries: Vec = match self.get_local_selected_entries() { - SelectedEntry::One(entry) => vec![entry], - SelectedEntry::Many(entries) => entries, - SelectedEntry::None => vec![], + let entries: Vec = match self.get_local_selected_entries() { + SelectedFile::One(entry) => vec![entry], + SelectedFile::Many(entries) => entries, + SelectedFile::None => vec![], }; entries .iter() @@ -45,10 +45,10 @@ impl FileTransferActivity { /// Open local file pub(crate) fn action_open_remote(&mut self) { - let entries: Vec = match self.get_remote_selected_entries() { - SelectedEntry::One(entry) => vec![entry], - SelectedEntry::Many(entries) => entries, - SelectedEntry::None => vec![], + let entries: Vec = match self.get_remote_selected_entries() { + SelectedFile::One(entry) => vec![entry], + SelectedFile::Many(entries) => entries, + SelectedFile::None => vec![], }; entries .iter() @@ -56,20 +56,21 @@ impl FileTransferActivity { } /// Perform open lopcal file - pub(crate) fn action_open_local_file(&mut self, entry: &Entry, open_with: Option<&str>) { + pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) { self.open_path_with(entry.path(), open_with); } /// Open remote file. The file is first downloaded to a temporary directory on localhost - pub(crate) fn action_open_remote_file(&mut self, entry: &Entry, open_with: Option<&str>) { + pub(crate) fn action_open_remote_file(&mut self, entry: &File, open_with: Option<&str>) { // Download file - let tmpfile: String = match self.get_cache_tmp_name(entry.name(), entry.extension()) { - None => { - self.log(LogLevel::Error, String::from("Could not create tempdir")); - return; - } - Some(p) => p, - }; + let tmpfile: String = + match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) { + None => { + self.log(LogLevel::Error, String::from("Could not create tempdir")); + return; + } + Some(p) => p, + }; let cache: PathBuf = match self.cache.as_ref() { None => { self.log(LogLevel::Error, String::from("Could not create tempdir")); @@ -101,10 +102,10 @@ impl FileTransferActivity { /// Open selected file with provided application pub(crate) fn action_local_open_with(&mut self, with: &str) { - let entries: Vec = match self.get_local_selected_entries() { - SelectedEntry::One(entry) => vec![entry], - SelectedEntry::Many(entries) => entries, - SelectedEntry::None => vec![], + let entries: Vec = match self.get_local_selected_entries() { + SelectedFile::One(entry) => vec![entry], + SelectedFile::Many(entries) => entries, + SelectedFile::None => vec![], }; // Open all entries entries @@ -114,10 +115,10 @@ impl FileTransferActivity { /// Open selected file with provided application pub(crate) fn action_remote_open_with(&mut self, with: &str) { - let entries: Vec = match self.get_remote_selected_entries() { - SelectedEntry::One(entry) => vec![entry], - SelectedEntry::Many(entries) => entries, - SelectedEntry::None => vec![], + let entries: Vec = match self.get_remote_selected_entries() { + SelectedFile::One(entry) => vec![entry], + SelectedFile::Many(entries) => entries, + SelectedFile::None => vec![], }; // Open all entries entries diff --git a/src/ui/activities/filetransfer/actions/rename.rs b/src/ui/activities/filetransfer/actions/rename.rs index 9bb1cf70..5e095cb4 100644 --- a/src/ui/activities/filetransfer/actions/rename.rs +++ b/src/ui/activities/filetransfer/actions/rename.rs @@ -26,7 +26,7 @@ * SOFTWARE. */ // locals -use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry}; +use super::{File, FileTransferActivity, LogLevel, SelectedFile}; use remotefs::RemoteErrorType; use std::path::{Path, PathBuf}; @@ -34,11 +34,11 @@ use std::path::{Path, PathBuf}; impl FileTransferActivity { pub(crate) fn action_local_rename(&mut self, input: String) { match self.get_local_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { let dest_path: PathBuf = PathBuf::from(input); self.local_rename_file(&entry, dest_path.as_path()); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Try to copy each file to Input/{FILE_NAME} let base_path: PathBuf = PathBuf::from(input); // Iter files @@ -48,17 +48,17 @@ impl FileTransferActivity { self.local_rename_file(entry, dest_path.as_path()); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } pub(crate) fn action_remote_rename(&mut self, input: String) { match self.get_remote_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { let dest_path: PathBuf = PathBuf::from(input); self.remote_rename_file(&entry, dest_path.as_path()); } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // Try to copy each file to Input/{FILE_NAME} let base_path: PathBuf = PathBuf::from(input); // Iter files @@ -68,11 +68,11 @@ impl FileTransferActivity { self.remote_rename_file(entry, dest_path.as_path()); } } - SelectedEntry::None => {} + SelectedFile::None => {} } } - fn local_rename_file(&mut self, entry: &Entry, dest: &Path) { + fn local_rename_file(&mut self, entry: &File, dest: &Path) { match self.host.rename(entry, dest) { Ok(_) => { self.log( @@ -96,7 +96,7 @@ impl FileTransferActivity { } } - fn remote_rename_file(&mut self, entry: &Entry, dest: &Path) { + fn remote_rename_file(&mut self, entry: &File, dest: &Path) { match self.client.as_mut().mov(entry.path(), dest) { Ok(_) => { self.log( @@ -125,7 +125,7 @@ impl FileTransferActivity { /// Tricky move will be used whenever copy command is not available on remote host. /// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`) - fn tricky_move(&mut self, entry: &Entry, dest: &Path) { + fn tricky_move(&mut self, entry: &File, dest: &Path) { debug!( "Using tricky-move to move entry {} to {}", entry.path().display(), diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index 0b6e70db..425760cc 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -27,7 +27,7 @@ */ // locals use super::{ - Entry, FileTransferActivity, LogLevel, Msg, PendingActionMsg, SelectedEntry, TransferOpts, + File, FileTransferActivity, LogLevel, Msg, PendingActionMsg, SelectedFile, TransferOpts, TransferPayload, }; use std::path::{Path, PathBuf}; @@ -52,19 +52,18 @@ impl FileTransferActivity { fn local_send_file(&mut self, opts: TransferOpts) { let wrkdir: PathBuf = self.remote().wrkdir.clone(); match self.get_local_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() && self.remote_file_exists(file_to_check.as_path()) - && !self.should_replace_file( - opts.save_as.as_deref().unwrap_or_else(|| entry.name()), - ) + && !self + .should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name())) { // Do not replace return; } if let Err(err) = self.filetransfer_send( - TransferPayload::Any(entry.clone()), + TransferPayload::Any(entry), wrkdir.as_path(), opts.save_as, ) { @@ -76,7 +75,7 @@ impl FileTransferActivity { } } } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // In case of selection: save multiple files in wrkdir/input let mut dest_path: PathBuf = wrkdir; if let Some(save_as) = opts.save_as { @@ -85,7 +84,7 @@ impl FileTransferActivity { // Iter files if self.config().get_prompt_on_file_replace() { // Check which file would be replaced - let existing_files: Vec<&Entry> = entries + let existing_files: Vec<&File> = entries .iter() .filter(|x| { self.remote_file_exists( @@ -111,25 +110,24 @@ impl FileTransferActivity { } } } - SelectedEntry::None => {} + SelectedFile::None => {} } } fn remote_recv_file(&mut self, opts: TransferOpts) { let wrkdir: PathBuf = self.local().wrkdir.clone(); match self.get_remote_selected_entries() { - SelectedEntry::One(entry) => { + SelectedFile::One(entry) => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() && self.local_file_exists(file_to_check.as_path()) - && !self.should_replace_file( - opts.save_as.as_deref().unwrap_or_else(|| entry.name()), - ) + && !self + .should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name())) { return; } if let Err(err) = self.filetransfer_recv( - TransferPayload::Any(entry.clone()), + TransferPayload::Any(entry), wrkdir.as_path(), opts.save_as, ) { @@ -141,7 +139,7 @@ impl FileTransferActivity { } } } - SelectedEntry::Many(entries) => { + SelectedFile::Many(entries) => { // In case of selection: save multiple files in wrkdir/input let mut dest_path: PathBuf = wrkdir; if let Some(save_as) = opts.save_as { @@ -150,7 +148,7 @@ impl FileTransferActivity { // Iter files if self.config().get_prompt_on_file_replace() { // Check which file would be replaced - let existing_files: Vec<&Entry> = entries + let existing_files: Vec<&File> = entries .iter() .filter(|x| { self.local_file_exists( @@ -176,13 +174,13 @@ impl FileTransferActivity { } } } - SelectedEntry::None => {} + SelectedFile::None => {} } } /// Set pending transfer into storage - pub(crate) fn should_replace_file(&mut self, file_name: &str) -> bool { - self.mount_radio_replace(file_name); + pub(crate) fn should_replace_file(&mut self, file_name: String) -> bool { + self.mount_radio_replace(&file_name); // Wait for answer trace!("Asking user whether he wants to replace file {}", file_name); if self.wait_for_pending_msg(&[ @@ -201,8 +199,8 @@ impl FileTransferActivity { } /// Set pending transfer for many files into storage and mount radio - pub(crate) fn should_replace_files(&mut self, files: Vec<&Entry>) -> bool { - let file_names: Vec<&str> = files.iter().map(|x| x.name()).collect(); + pub(crate) fn should_replace_files(&mut self, files: Vec<&File>) -> bool { + let file_names: Vec = files.iter().map(|x| x.name()).collect(); self.mount_radio_replace_many(file_names.as_slice()); // Wait for answer trace!( @@ -225,14 +223,14 @@ impl FileTransferActivity { } /// Get file to check for path - pub(crate) fn file_to_check(e: &Entry, alt: Option<&String>) -> PathBuf { + pub(crate) fn file_to_check(e: &File, alt: Option<&String>) -> PathBuf { match alt { Some(s) => PathBuf::from(s), None => PathBuf::from(e.name()), } } - pub(crate) fn file_to_check_many(e: &Entry, wrkdir: &Path) -> PathBuf { + pub(crate) fn file_to_check_many(e: &File, wrkdir: &Path) -> PathBuf { let mut p = wrkdir.to_path_buf(); p.push(e.name()); p diff --git a/src/ui/activities/filetransfer/actions/submit.rs b/src/ui/activities/filetransfer/actions/submit.rs index 6aff312c..efa3e996 100644 --- a/src/ui/activities/filetransfer/actions/submit.rs +++ b/src/ui/activities/filetransfer/actions/submit.rs @@ -26,9 +26,7 @@ * SOFTWARE. */ // locals -use super::{Entry, FileTransferActivity}; - -use remotefs::fs::{File, Metadata}; +use super::{File, FileTransferActivity}; enum SubmitAction { ChangeDir, @@ -38,73 +36,59 @@ enum SubmitAction { impl FileTransferActivity { /// Decides which action to perform on submit for local explorer /// Return true whether the directory changed - pub(crate) fn action_submit_local(&mut self, entry: Entry) { - let (action, entry) = match &entry { - Entry::Directory(_) => (SubmitAction::ChangeDir, entry), - Entry::File(File { - path, - metadata: - Metadata { - symlink: Some(symlink), - .. - }, - .. - }) => { - // Stat file - let stat_file = match self.host.stat(symlink.as_path()) { - Ok(e) => e, - Err(err) => { - warn!( - "Could not stat file pointed by {} ({}): {}", - path.display(), - symlink.display(), - err - ); - entry - } - }; - (SubmitAction::ChangeDir, stat_file) - } - Entry::File(_) => (SubmitAction::None, entry), + pub(crate) fn action_submit_local(&mut self, entry: File) { + let (action, entry) = if entry.is_dir() { + (SubmitAction::ChangeDir, entry) + } else if entry.metadata().symlink.is_some() { + // Stat file + let symlink = entry.metadata().symlink.as_ref().unwrap(); + let stat_file = match self.host.stat(symlink.as_path()) { + Ok(e) => e, + Err(err) => { + warn!( + "Could not stat file pointed by {} ({}): {}", + entry.path().display(), + symlink.display(), + err + ); + entry + } + }; + (SubmitAction::ChangeDir, stat_file) + } else { + (SubmitAction::None, entry) }; - if let (SubmitAction::ChangeDir, Entry::Directory(dir)) = (action, entry) { - self.action_enter_local_dir(dir) + if let (SubmitAction::ChangeDir, entry) = (action, entry) { + self.action_enter_local_dir(entry) } } /// Decides which action to perform on submit for remote explorer /// Return true whether the directory changed - pub(crate) fn action_submit_remote(&mut self, entry: Entry) { - let (action, entry) = match &entry { - Entry::Directory(_) => (SubmitAction::ChangeDir, entry), - Entry::File(File { - path, - metadata: - Metadata { - symlink: Some(symlink), - .. - }, - .. - }) => { - // Stat file - let stat_file = match self.client.stat(symlink.as_path()) { - Ok(e) => e, - Err(err) => { - warn!( - "Could not stat file pointed by {} ({}): {}", - path.display(), - symlink.display(), - err - ); - entry - } - }; - (SubmitAction::ChangeDir, stat_file) - } - Entry::File(_) => (SubmitAction::None, entry), + pub(crate) fn action_submit_remote(&mut self, entry: File) { + let (action, entry) = if entry.is_dir() { + (SubmitAction::ChangeDir, entry) + } else if entry.metadata().symlink.is_some() { + // Stat file + let symlink = entry.metadata().symlink.as_ref().unwrap(); + let stat_file = match self.client.stat(symlink.as_path()) { + Ok(e) => e, + Err(err) => { + warn!( + "Could not stat file pointed by {} ({}): {}", + entry.path().display(), + symlink.display(), + err + ); + entry + } + }; + (SubmitAction::ChangeDir, stat_file) + } else { + (SubmitAction::None, entry) }; - if let (SubmitAction::ChangeDir, Entry::Directory(dir)) = (action, entry) { - self.action_enter_remote_dir(dir) + if let (SubmitAction::ChangeDir, entry) = (action, entry) { + self.action_enter_remote_dir(entry) } } } diff --git a/src/ui/activities/filetransfer/actions/symlink.rs b/src/ui/activities/filetransfer/actions/symlink.rs index 3006944a..04e25a90 100644 --- a/src/ui/activities/filetransfer/actions/symlink.rs +++ b/src/ui/activities/filetransfer/actions/symlink.rs @@ -26,7 +26,7 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, LogLevel, SelectedEntry}; +use super::{FileTransferActivity, LogLevel, SelectedFile}; use std::path::PathBuf; @@ -34,7 +34,7 @@ impl FileTransferActivity { /// Create symlink on localhost #[cfg(target_family = "unix")] pub(crate) fn action_local_symlink(&mut self, name: String) { - if let SelectedEntry::One(entry) = self.get_local_selected_entries() { + if let SelectedFile::One(entry) = self.get_local_selected_entries() { match self .host .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) @@ -66,7 +66,7 @@ impl FileTransferActivity { /// Copy file on remote pub(crate) fn action_remote_symlink(&mut self, name: String) { - if let SelectedEntry::One(entry) = self.get_remote_selected_entries() { + if let SelectedFile::One(entry) = self.get_remote_selected_entries() { match self .client .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index 17bf2421..762ad06b 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -31,7 +31,8 @@ use crate::explorer::FileSorting; use crate::utils::fmt::fmt_time; use bytesize::ByteSize; -use remotefs::Entry; +use remotefs::File; +use std::time::UNIX_EPOCH; use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span}; use tuirealm::command::{Cmd, CmdResult, Direction, Position}; @@ -399,7 +400,7 @@ pub struct FileInfoPopup { } impl FileInfoPopup { - pub fn new(file: &Entry) -> Self { + pub fn new(file: &File) -> Self { let mut texts: TableBuilder = TableBuilder::default(); // Abs path let real_path = file.metadata().symlink.as_deref(); @@ -422,9 +423,18 @@ impl FileInfoPopup { .add_row() .add_col(TextSpan::from("Size: ")) .add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan)); - let atime: String = fmt_time(file.metadata().atime, "%b %d %Y %H:%M:%S"); - let ctime: String = fmt_time(file.metadata().ctime, "%b %d %Y %H:%M:%S"); - let mtime: String = fmt_time(file.metadata().mtime, "%b %d %Y %H:%M:%S"); + let atime: String = fmt_time( + file.metadata().accessed.unwrap_or(UNIX_EPOCH), + "%b %d %Y %H:%M:%S", + ); + let ctime: String = fmt_time( + file.metadata().created.unwrap_or(UNIX_EPOCH), + "%b %d %Y %H:%M:%S", + ); + let mtime: String = fmt_time( + file.metadata().modified.unwrap_or(UNIX_EPOCH), + "%b %d %Y %H:%M:%S", + ); texts .add_row() .add_col(TextSpan::from("Creation time: ")) @@ -1373,7 +1383,7 @@ pub struct ReplacingFilesListPopup { } impl ReplacingFilesListPopup { - pub fn new(files: &[&str], color: Color) -> Self { + pub fn new(files: &[String], color: Color) -> Self { Self { component: List::default() .borders( diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index 3661d6b8..ec3e0777 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -28,7 +28,7 @@ use crate::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs}; use crate::system::config_client::ConfigClient; -use remotefs::Entry; +use remotefs::File; use std::path::Path; /// File explorer tab @@ -92,7 +92,7 @@ impl Browser { self.found.as_mut().map(|x| &mut x.1) } - pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec, wrkdir: &Path) { + pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec, wrkdir: &Path) { let mut explorer = Self::build_found_explorer(wrkdir); explorer.set_files(files); self.found = Some((tab, explorer)); diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 836a8e2b..5c02a6d1 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -191,7 +191,8 @@ impl FileTransferActivity { TransferPayload::File(file) => { format!( "File \"{}\" has been successfully transferred ({})", - file.name, transfer_stats + file.name(), + transfer_stats ) } TransferPayload::Any(entry) => { diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 651a1580..72a3f809 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -32,7 +32,7 @@ use crate::utils::fmt::fmt_millis; // Ext use bytesize::ByteSize; -use remotefs::fs::{Entry, File, UnixPex, Welcome}; +use remotefs::fs::{File, ReadStream, UnixPex, Welcome, WriteStream}; use remotefs::{RemoteError, RemoteErrorType}; use std::fs::File as StdFile; use std::io::{Read, Seek, Write}; @@ -59,13 +59,13 @@ enum TransferErrorReason { /// Represents the entity to send or receive during a transfer. /// - File: describes an individual `File` to send -/// - Any: Can be any kind of `Entry`, but just one -/// - Many: a list of `Entry` +/// - Any: Can be any kind of `File`, but just one +/// - Many: a list of `File` #[derive(Debug)] pub(super) enum TransferPayload { File(File), - Any(Entry), - Many(Vec), + Any(File), + Many(Vec), } impl FileTransferActivity { @@ -224,7 +224,7 @@ impl FileTransferActivity { // Mount progress bar self.mount_progress_bar(format!("Uploading {}…", file.path.display())); // Get remote path - let file_name: String = file.name.clone(); + let file_name: String = file.name(); let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); let remote_file_name: PathBuf = match dst_name { Some(s) => PathBuf::from(s.as_str()), @@ -242,7 +242,7 @@ impl FileTransferActivity { /// Send a `TransferPayload` of type `Any` fn filetransfer_send_any( &mut self, - entry: &Entry, + entry: &File, curr_remote_path: &Path, dst_name: Option, ) -> Result<(), String> { @@ -263,7 +263,7 @@ impl FileTransferActivity { /// Send many entries to remote fn filetransfer_send_many( &mut self, - entries: &[Entry], + entries: &[File], curr_remote_path: &Path, ) -> Result<(), String> { // Reset states @@ -289,15 +289,12 @@ impl FileTransferActivity { fn filetransfer_send_recurse( &mut self, - entry: &Entry, + entry: &File, curr_remote_path: &Path, dst_name: Option, ) -> Result<(), String> { // Write popup - let file_name: String = match entry { - Entry::Directory(dir) => dir.name.clone(), - Entry::File(file) => file.name.clone(), - }; + let file_name = entry.name(); // Get remote path let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); let remote_file_name: PathBuf = match dst_name { @@ -306,107 +303,104 @@ impl FileTransferActivity { }; remote_path.push(remote_file_name); // Match entry - let result: Result<(), String> = match entry { - Entry::File(file) => { - match self.filetransfer_send_one(file, remote_path.as_path(), file_name) { - Err(err) => { - // If transfer was abrupted or there was an IO error on remote, remove file - if matches!( - err, - TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_) - ) { - // Stat file on remote and remove it if exists - match self.client.stat(remote_path.as_path()) { - Err(err) => self.log( - LogLevel::Error, - format!( - "Could not remove created file {}: {}", - remote_path.display(), - err - ), - ), - Ok(entry) => { - if let Err(err) = self.client.remove_file(entry.path()) { - self.log( - LogLevel::Error, - format!( - "Could not remove created file {}: {}", - remote_path.display(), - err - ), - ); - } - } - } + let result: Result<(), String> = if entry.is_dir() { + // Create directory on remote first + match self + .client + .create_dir(remote_path.as_path(), UnixPex::from(0o755)) + { + Ok(_) => { + self.log( + LogLevel::Info, + format!("Created directory \"{}\"", remote_path.display()), + ); + } + Err(err) if err.kind == RemoteErrorType::DirectoryAlreadyExists => { + self.log( + LogLevel::Info, + format!( + "Directory \"{}\" already exists on remote", + remote_path.display() + ), + ); + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Failed to create directory \"{}\": {}", + remote_path.display(), + err + ), + ); + return Err(err.to_string()); + } + } + // Get files in dir + match self.host.scan_dir(entry.path()) { + Ok(entries) => { + // Iterate over files + for entry in entries.iter() { + // If aborted; break + if self.transfer.aborted() { + break; + } + // Send entry; name is always None after first call + if let Err(err) = + self.filetransfer_send_recurse(entry, remote_path.as_path(), None) + { + return Err(err); } - Err(err.to_string()) } - Ok(_) => Ok(()), + Ok(()) + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not scan directory \"{}\": {}", + entry.path().display(), + err + ), + ); + Err(err.to_string()) } } - Entry::Directory(dir) => { - // Create directory on remote first - match self - .client - .create_dir(remote_path.as_path(), UnixPex::from(0o755)) - { - Ok(_) => { - self.log( - LogLevel::Info, - format!("Created directory \"{}\"", remote_path.display()), - ); - } - Err(err) if err.kind == RemoteErrorType::DirectoryAlreadyExists => { - self.log( - LogLevel::Info, - format!( - "Directory \"{}\" already exists on remote", - remote_path.display() - ), - ); - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Failed to create directory \"{}\": {}", - remote_path.display(), - err + } else { + match self.filetransfer_send_one(entry, remote_path.as_path(), file_name) { + Err(err) => { + // If transfer was abrupted or there was an IO error on remote, remove file + if matches!( + err, + TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_) + ) { + // Stat file on remote and remove it if exists + match self.client.stat(remote_path.as_path()) { + Err(err) => self.log( + LogLevel::Error, + format!( + "Could not remove created file {}: {}", + remote_path.display(), + err + ), ), - ); - return Err(err.to_string()); - } - } - // Get files in dir - match self.host.scan_dir(dir.path.as_path()) { - Ok(entries) => { - // Iterate over files - for entry in entries.iter() { - // If aborted; break - if self.transfer.aborted() { - break; - } - // Send entry; name is always None after first call - if let Err(err) = - self.filetransfer_send_recurse(entry, remote_path.as_path(), None) - { - return Err(err); + Ok(entry) => { + if let Err(err) = self.client.remove_file(entry.path()) { + self.log( + LogLevel::Error, + format!( + "Could not remove created file {}: {}", + remote_path.display(), + err + ), + ); + } } } - Ok(()) - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Could not scan directory \"{}\": {}", - dir.path.display(), - err - ), - ); - Err(err.to_string()) } + Err(err.to_string()) } + Ok(_) => Ok(()), } }; // Scan dir on remote @@ -458,7 +452,7 @@ impl FileTransferActivity { remote: &Path, file_name: String, mut reader: StdFile, - mut writer: Box, + mut writer: WriteStream, ) -> Result<(), TransferErrorReason> { // Write file let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; @@ -632,7 +626,7 @@ impl FileTransferActivity { /// If entry is a directory, this applies to directory only fn filetransfer_recv_any( &mut self, - entry: &Entry, + entry: &File, local_path: &Path, dst_name: Option, ) -> Result<(), String> { @@ -660,7 +654,7 @@ impl FileTransferActivity { // Mount progress bar self.mount_progress_bar(format!("Downloading {}…", entry.path.display())); // Receive - let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone()); + let result = self.filetransfer_recv_one(local_path, entry, entry.name()); // Umount progress bar self.umount_progress_bar(); // Return result @@ -670,7 +664,7 @@ impl FileTransferActivity { /// Send many entries to remote fn filetransfer_recv_many( &mut self, - entries: &[Entry], + entries: &[File], curr_remote_path: &Path, ) -> Result<(), String> { // Reset states @@ -696,142 +690,132 @@ impl FileTransferActivity { fn filetransfer_recv_recurse( &mut self, - entry: &Entry, + entry: &File, local_path: &Path, dst_name: Option, ) -> Result<(), String> { // Write popup - let file_name: String = match entry { - Entry::Directory(dir) => dir.name.clone(), - Entry::File(file) => file.name.clone(), - }; + let file_name = entry.name(); // Match entry - let result: Result<(), String> = match entry { - Entry::File(file) => { - // Get local file - let mut local_file_path: PathBuf = PathBuf::from(local_path); - let local_file_name: String = match dst_name { - Some(n) => n, - None => file.name.clone(), - }; - local_file_path.push(local_file_name.as_str()); - // Download file - if let Err(err) = - self.filetransfer_recv_one(local_file_path.as_path(), file, file_name) - { - // If transfer was abrupted or there was an IO error on remote, remove file - if matches!( - err, - TransferErrorReason::Abrupted | TransferErrorReason::LocalIoError(_) - ) { - // Stat file - match self.host.stat(local_file_path.as_path()) { - Err(err) => self.log( + let result: Result<(), String> = if entry.is_dir() { + // Get dir name + let mut local_dir_path: PathBuf = PathBuf::from(local_path); + match dst_name { + Some(name) => local_dir_path.push(name), + None => local_dir_path.push(entry.name()), + } + // Create directory on local + match self.host.mkdir_ex(local_dir_path.as_path(), true) { + Ok(_) => { + // Apply file mode to directory + #[cfg(any(target_family = "unix", target_os = "macos", target_os = "linux"))] + if let Some(mode) = entry.metadata().mode { + if let Err(err) = self.host.chmod(local_dir_path.as_path(), mode) { + self.log( LogLevel::Error, format!( - "Could not remove created file {}: {}", - local_file_path.display(), + "Could not apply file mode {:o} to \"{}\": {}", + u32::from(mode), + local_dir_path.display(), err ), - ), - Ok(entry) => { - if let Err(err) = self.host.remove(&entry) { - self.log( - LogLevel::Error, - format!( - "Could not remove created file {}: {}", - local_file_path.display(), - err - ), - ); + ); + } + } + self.log( + LogLevel::Info, + format!("Created directory \"{}\"", local_dir_path.display()), + ); + // Get files in dir + match self.client.list_dir(entry.path()) { + Ok(entries) => { + // Iterate over files + for entry in entries.iter() { + // If transfer has been aborted; break + if self.transfer.aborted() { + break; + } + // Receive entry; name is always None after first call + // Local path becomes local_dir_path + if let Err(err) = self.filetransfer_recv_recurse( + entry, + local_dir_path.as_path(), + None, + ) { + return Err(err); } } + Ok(()) + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not scan directory \"{}\": {}", + entry.path().display(), + err + ), + ); + Err(err.to_string()) } } + } + Err(err) => { + self.log( + LogLevel::Error, + format!( + "Failed to create directory \"{}\": {}", + local_dir_path.display(), + err + ), + ); Err(err.to_string()) - } else { - Ok(()) } } - Entry::Directory(dir) => { - // Get dir name - let mut local_dir_path: PathBuf = PathBuf::from(local_path); - match dst_name { - Some(name) => local_dir_path.push(name), - None => local_dir_path.push(dir.name.as_str()), - } - // Create directory on local - match self.host.mkdir_ex(local_dir_path.as_path(), true) { - Ok(_) => { - // Apply file mode to directory - #[cfg(any( - target_family = "unix", - target_os = "macos", - target_os = "linux" - ))] - if let Some(mode) = dir.metadata.mode { - if let Err(err) = self.host.chmod(local_dir_path.as_path(), mode) { + } else { + // Get local file + let mut local_file_path: PathBuf = PathBuf::from(local_path); + let local_file_name: String = match dst_name { + Some(n) => n, + None => entry.name(), + }; + local_file_path.push(local_file_name.as_str()); + // Download file + if let Err(err) = + self.filetransfer_recv_one(local_file_path.as_path(), entry, file_name) + { + // If transfer was abrupted or there was an IO error on remote, remove file + if matches!( + err, + TransferErrorReason::Abrupted | TransferErrorReason::LocalIoError(_) + ) { + // Stat file + match self.host.stat(local_file_path.as_path()) { + Err(err) => self.log( + LogLevel::Error, + format!( + "Could not remove created file {}: {}", + local_file_path.display(), + err + ), + ), + Ok(entry) => { + if let Err(err) = self.host.remove(&entry) { self.log( LogLevel::Error, format!( - "Could not apply file mode {:o} to \"{}\": {}", - u32::from(mode), - local_dir_path.display(), - err - ), - ); - } - } - self.log( - LogLevel::Info, - format!("Created directory \"{}\"", local_dir_path.display()), - ); - // Get files in dir - match self.client.list_dir(dir.path.as_path()) { - Ok(entries) => { - // Iterate over files - for entry in entries.iter() { - // If transfer has been aborted; break - if self.transfer.aborted() { - break; - } - // Receive entry; name is always None after first call - // Local path becomes local_dir_path - if let Err(err) = self.filetransfer_recv_recurse( - entry, - local_dir_path.as_path(), - None, - ) { - return Err(err); - } - } - Ok(()) - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Could not scan directory \"{}\": {}", - dir.path.display(), + "Could not remove created file {}: {}", + local_file_path.display(), err ), ); - Err(err.to_string()) } } } - Err(err) => { - self.log( - LogLevel::Error, - format!( - "Failed to create directory \"{}\": {}", - local_dir_path.display(), - err - ), - ); - Err(err.to_string()) - } } + Err(err.to_string()) + } else { + Ok(()) } }; // Reload directory on local @@ -872,13 +856,13 @@ impl FileTransferActivity { } } - /// Receive an `Entry` from remote using stream + /// Receive an `File` from remote using stream fn filetransfer_recv_one_with_stream( &mut self, local: &Path, remote: &File, file_name: String, - mut reader: Box, + mut reader: ReadStream, mut writer: StdFile, ) -> Result<(), TransferErrorReason> { let mut total_bytes_written: usize = 0; @@ -979,7 +963,7 @@ impl FileTransferActivity { Ok(()) } - /// Receive an `Entry` from remote without using stream + /// Receive an `File` from remote without using stream fn filetransfer_recv_one_wno_stream( &mut self, local: &Path, @@ -1098,7 +1082,7 @@ impl FileTransferActivity { let tmpfile: PathBuf = match self.cache.as_ref() { Some(cache) => { let mut p: PathBuf = cache.path().to_path_buf(); - p.push(file.name.as_str()); + p.push(file.name()); p } None => { @@ -1111,7 +1095,7 @@ impl FileTransferActivity { match self.filetransfer_recv( TransferPayload::File(file.clone()), tmpfile.as_path(), - Some(file.name.clone()), + Some(file.name()), ) { Err(err) => Err(format!( "Could not download {} to temporary file: {}", @@ -1125,48 +1109,54 @@ impl FileTransferActivity { // -- transfer sizes /// Get total size of transfer for localhost - fn get_total_transfer_size_local(&mut self, entry: &Entry) -> usize { - match entry { - Entry::File(file) => file.metadata.size as usize, - Entry::Directory(dir) => { - // List dir - match self.host.scan_dir(dir.path.as_path()) { - Ok(files) => files - .iter() - .map(|x| self.get_total_transfer_size_local(x)) - .sum(), - Err(err) => { - self.log( - LogLevel::Error, - format!("Could not list directory {}: {}", dir.path.display(), err), - ); - 0 - } + fn get_total_transfer_size_local(&mut self, entry: &File) -> usize { + if entry.is_dir() { + // List dir + match self.host.scan_dir(entry.path()) { + Ok(files) => files + .iter() + .map(|x| self.get_total_transfer_size_local(x)) + .sum(), + Err(err) => { + self.log( + LogLevel::Error, + format!( + "Could not list directory {}: {}", + entry.path().display(), + err + ), + ); + 0 } } + } else { + entry.metadata.size as usize } } /// Get total size of transfer for remote host - fn get_total_transfer_size_remote(&mut self, entry: &Entry) -> usize { - match entry { - Entry::File(file) => file.metadata.size as usize, - Entry::Directory(dir) => { - // List directory - match self.client.list_dir(dir.path.as_path()) { - Ok(files) => files - .iter() - .map(|x| self.get_total_transfer_size_remote(x)) - .sum(), - Err(err) => { - self.log( - LogLevel::Error, - format!("Could not list directory {}: {}", dir.path.display(), err), - ); - 0 - } + fn get_total_transfer_size_remote(&mut self, entry: &File) -> usize { + if entry.is_dir() { + // List directory + match self.client.list_dir(entry.path()) { + Ok(files) => files + .iter() + .map(|x| self.get_total_transfer_size_remote(x)) + .sum(), + Err(err) => { + self.log( + LogLevel::Error, + format!( + "Could not list directory {}: {}", + entry.path().display(), + err + ), + ); + 0 } } + } else { + entry.metadata.size as usize } } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 0924447f..0da73558 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -27,12 +27,12 @@ */ // locals use super::{ - actions::SelectedEntry, + actions::SelectedFile, browser::{FileExplorerTab, FoundExplorerTab}, ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg, }; // externals -use remotefs::fs::Entry; +use remotefs::fs::File; use tuirealm::{ props::{AttrValue, Attribute}, State, StateValue, Update, @@ -121,7 +121,7 @@ impl FileTransferActivity { } } TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Local => { - if let SelectedEntry::One(entry) = self.get_local_selected_entries() { + if let SelectedFile::One(entry) = self.get_local_selected_entries() { self.action_submit_local(entry); // Update file list if sync if self.browser.sync_browsing && self.browser.found().is_none() { @@ -131,7 +131,7 @@ impl FileTransferActivity { } } TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => { - if let SelectedEntry::One(entry) = self.get_remote_selected_entries() { + if let SelectedFile::One(entry) = self.get_remote_selected_entries() { self.action_submit_remote(entry); // Update file list if sync if self.browser.sync_browsing && self.browser.found().is_none() { @@ -296,7 +296,7 @@ impl FileTransferActivity { // Mount wait self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str()); // Find - let res: Result, String> = match self.browser.tab() { + let res: Result, String> = match self.browser.tab() { FileExplorerTab::Local => self.action_local_find(search.clone()), FileExplorerTab::Remote => self.action_remote_find(search.clone()), _ => panic!("Trying to search for files, while already in a find result"), @@ -449,17 +449,17 @@ impl FileTransferActivity { UiMsg::ShowDisconnectPopup => self.mount_disconnect(), UiMsg::ShowExecPopup => self.mount_exec(), UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Local => { - if let SelectedEntry::One(file) = self.get_local_selected_entries() { + if let SelectedFile::One(file) = self.get_local_selected_entries() { self.mount_file_info(&file); } } UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Remote => { - if let SelectedEntry::One(file) = self.get_remote_selected_entries() { + if let SelectedFile::One(file) = self.get_remote_selected_entries() { self.mount_file_info(&file); } } UiMsg::ShowFileInfoPopup => { - if let SelectedEntry::One(file) = self.get_found_selected_entries() { + if let SelectedFile::One(file) = self.get_found_selected_entries() { self.mount_file_info(&file); } } diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index a3ae2915..64de25b9 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -33,7 +33,7 @@ use super::{ use crate::explorer::FileSorting; use crate::utils::ui::draw_area_in; // Ext -use remotefs::fs::Entry; +use remotefs::fs::File; use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::tui::widgets::Clear; @@ -719,7 +719,7 @@ impl FileTransferActivity { assert!(self.app.active(&Id::ReplacePopup).is_ok()); } - pub(super) fn mount_radio_replace_many(&mut self, files: &[&str]) { + pub(super) fn mount_radio_replace_many(&mut self, files: &[String]) { let warn_color = self.theme().misc_warn_dialog; assert!(self .app @@ -750,7 +750,7 @@ impl FileTransferActivity { let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway } - pub(super) fn mount_file_info(&mut self, file: &Entry) { + pub(super) fn mount_file_info(&mut self, file: &File) { assert!(self .app .remount( diff --git a/src/utils/test_helpers.rs b/src/utils/test_helpers.rs index 52ca22cb..091206c7 100644 --- a/src/utils/test_helpers.rs +++ b/src/utils/test_helpers.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use remotefs::fs::{Directory, Entry, File, Metadata}; +use remotefs::fs::{File, FileType, Metadata}; // ext use std::fs::File as StdFile; use std::io::Write; @@ -37,14 +37,7 @@ pub fn create_sample_file_entry() -> (File, NamedTempFile) { let tmpfile = create_sample_file(); ( File { - name: tmpfile - .path() - .file_name() - .unwrap() - .to_string_lossy() - .to_string(), path: tmpfile.path().to_path_buf(), - extension: None, metadata: Metadata::default(), }, tmpfile, @@ -85,20 +78,15 @@ pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> { std::fs::create_dir(p.as_path()) } -/// Create a Entry at specified path -pub fn make_fsentry>(path: P, is_dir: bool) -> Entry { +/// Create a File at specified path +pub fn make_fsentry>(path: P, is_dir: bool) -> File { let path: PathBuf = path.as_ref().to_path_buf(); - match is_dir { - true => Entry::Directory(Directory { - name: path.file_name().unwrap().to_string_lossy().to_string(), - path, - metadata: Metadata::default(), - }), - false => Entry::File(File { - name: path.file_name().unwrap().to_string_lossy().to_string(), - path, - extension: None, - metadata: Metadata::default(), + File { + path, + metadata: Metadata::default().file_type(if is_dir { + FileType::Directory + } else { + FileType::File }), } } @@ -127,15 +115,13 @@ mod test { fn test_utils_test_helpers_make_fsentry() { assert_eq!( make_fsentry(PathBuf::from("/tmp/omar.txt"), false) - .unwrap_file() - .name + .name() .as_str(), "omar.txt" ); assert_eq!( make_fsentry(PathBuf::from("/tmp/cards"), true) - .unwrap_dir() - .name + .name() .as_str(), "cards" ); From dbe4b6b59833ff10e2cf664c59e513d4f1f45dac Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 4 Jan 2022 15:23:17 +0100 Subject: [PATCH 37/45] Changed buffer size to 65535 --- CHANGELOG.md | 1 + src/ui/activities/filetransfer/session.rs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 955f7d76..95e42c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Released on FIXME: - **Tui-realm migration**: - migrated application to tui-realm 1.x - Improved application performance + - Changed the buffer size to **65535** (was 65536) for transfer I/O - **SSH Config** - Added `ssh config` parameter in configuration - It is now possible to specify the ssh configuration file to use diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 72a3f809..e7a2ce7d 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -40,6 +40,9 @@ use std::path::{Path, PathBuf}; use std::time::Instant; use thiserror::Error; +/// Buffer size for remote I/O +const BUFSIZE: usize = 65535; + /// Describes the reason that caused an error during a file transfer #[derive(Error, Debug)] enum TransferErrorReason { @@ -483,7 +486,7 @@ impl FileTransferActivity { last_input_event_fetch = Some(Instant::now()); } // Read till you can - let mut buffer: [u8; 65536] = [0; 65536]; + let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE]; let delta: usize = match reader.read(&mut buffer) { Ok(bytes_read) => { total_bytes_written += bytes_read; @@ -888,7 +891,7 @@ impl FileTransferActivity { last_input_event_fetch = Some(Instant::now()); } // Read till you can - let mut buffer: [u8; 65536] = [0; 65536]; + let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE]; let delta: usize = match reader.read(&mut buffer) { Ok(bytes_read) => { total_bytes_written += bytes_read; From 8c45c7ebd7830aace2561cd25ccb261e11e00b9a Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Tue, 4 Jan 2022 18:31:58 +0100 Subject: [PATCH 38/45] Aws s3 connection parameters extension (#89) * Aws s3 connection parameters extension * Changed 'save password?' popup to 'change secrets?' * missing docs --- CHANGELOG.md | 5 + Cargo.lock | 6 +- docs/de/man.md | 9 +- docs/es/man.md | 9 +- docs/fr/man.md | 9 +- docs/it/man.md | 13 +- docs/man.md | 9 +- docs/zh-CN/man.md | 8 +- src/config/bookmarks.rs | 22 +- src/config/serialization.rs | 6 + src/filetransfer/builder.rs | 22 +- src/filetransfer/params.rs | 55 ++- src/main.rs | 2 + src/system/bookmarks_client.rs | 130 ++++++- src/system/sshkey_storage.rs | 55 +-- src/ui/activities/auth/bookmarks.rs | 4 + .../activities/auth/components/bookmarks.rs | 2 +- src/ui/activities/auth/components/form.rs | 357 +++++++++++++++++- src/ui/activities/auth/components/mod.rs | 3 +- src/ui/activities/auth/misc.rs | 32 +- src/ui/activities/auth/mod.rs | 12 + src/ui/activities/auth/update.rs | 28 +- src/ui/activities/auth/view.rs | 142 ++++++- src/ui/activities/setup/components/ssh.rs | 4 +- src/ui/activities/setup/components/theme.rs | 2 +- 25 files changed, 831 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e42c65..929c3b26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ Released on FIXME: - migrated application to tui-realm 1.x - Improved application performance - Changed the buffer size to **65535** (was 65536) for transfer I/O +- **Aws s3 connection parameters extension** 🦊: + - Added `Access Key` to Aws-s3 connection parameters + - Added `Security Access Key` to Aws-s3 connection parameters + - Added `Security token` to Aws-s3 connection parameters + - Added `Session token` to Aws-s3 connection parameters - **SSH Config** - Added `ssh config` parameter in configuration - It is now possible to specify the ssh configuration file to use diff --git a/Cargo.lock b/Cargo.lock index 1aa09008..801117f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2021,7 +2021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", - "parking_lot 0.11.2", + "parking_lot 0.10.2", "serial_test_derive", ] @@ -2479,9 +2479,9 @@ dependencies = [ [[package]] name = "tuirealm" -version = "1.3.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e5c7137a0bd92feadea98033a1849fe51c83d23f7761b866e8700a3d6f1de7" +checksum = "8b1919cf08ce9eda3b968870ecab04267d684adc22f2cdc13bb4a3846c89eab4" dependencies = [ "bitflags 1.3.2", "crossterm", diff --git a/docs/de/man.md b/docs/de/man.md index 0dafeb2f..99340444 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -112,11 +112,14 @@ Password can be basically provided through 3 ways when address argument is provi ## Aws S3 credentials 🦊 In order to connect to an Aws S3 bucket you must obviously provide some credentials. -There are basically two ways to achieve this, and as you've probably already noticed you **can't** do that via the authentication form. +There are basically three ways to achieve this. So these are the ways you can provide the credentials for s3: -1. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form. -2. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below: +1. Authentication form: + 1. You can provide the `access_key` (should be mandatory), the `secret_access_key` (should be mandatory), `security_token` and the `session_token` + 2. If you save the s3 connection as a bookmark, these credentials will be saved as an encrypted AES-256/BASE64 string in your bookmarks file (except for the security token and session token which are meant to be temporary credentials). +2. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form. +3. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below: These should always be mandatory: diff --git a/docs/es/man.md b/docs/es/man.md index 67c97a33..e60155f9 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -112,11 +112,14 @@ La contraseña se puede proporcionar básicamente a través de 3 formas cuando s ## Credenciales de AWS S3 🦊 Para conectarse a un bucket de Aws S3, obviamente debe proporcionar algunas credenciales. -Básicamente, hay dos formas de lograr esto, y como probablemente ya hayas notado, **no puedes** hacerlo a través del formulario de autenticación. +Básicamente, hay tres formas de lograr esto. Entonces, estas son las formas en que puede proporcionar las credenciales para s3: -1. Use su archivo de credenciales: simplemente configure la cli de AWS a través de `aws configure` y sus credenciales ya deberían estar ubicadas en`~/.aws/credentials`. En caso de que esté usando un perfil diferente al "predeterminado", simplemente proporciónelo en el campo de perfil en el formulario de autenticación. -2. **Variables de entorno**: siempre puede proporcionar sus credenciales como variables de entorno. Tenga en cuenta que estas credenciales **siempre anularán** las credenciales ubicadas en el archivo `credentials`. Vea cómo configurar el entorno a continuación: +1. Authentication form: + 1. Puede proporcionar la `access_key` (debería ser obligatoria), la `secret_access_kedy` (debería ser obligatoria), el `security_token` y el `session_token` + 2. Si guarda la conexión s3 como marcador, estas credenciales se guardarán como una cadena AES-256 / BASE64 cifrada en su archivo de marcadores (excepto el token de seguridad y el token de sesión, que deben ser credenciales temporales). +2. Use su archivo de credenciales: simplemente configure la cli de AWS a través de `aws configure` y sus credenciales ya deberían estar ubicadas en`~/.aws/credentials`. En caso de que esté usando un perfil diferente al "predeterminado", simplemente proporciónelo en el campo de perfil en el formulario de autenticación. +3. **Variables de entorno**: siempre puede proporcionar sus credenciales como variables de entorno. Tenga en cuenta que estas credenciales **siempre anularán** las credenciales ubicadas en el archivo `credentials`. Vea cómo configurar el entorno a continuación: Estos siempre deben ser obligatorios: diff --git a/docs/fr/man.md b/docs/fr/man.md index b84bd9a5..5db0c965 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -110,11 +110,14 @@ Le mot de passe peut être fourni de 3 manières lorsque l'argument d'adresse es ## Identifiants AWS S3 🦊 Afin de vous connecter à un compartiment Aws S3, vous devez évidemment fournir des informations d'identification. -Il existe essentiellement deux manières d'y parvenir, et comme vous l'avez probablement déjà remarqué, vous ne pouvez **pas** le faire via le formulaire d'authentification. +Il existe essentiellement trois manières d'y parvenir. Voici donc les moyens de fournir les informations d'identification pour s3 : -1. Utilisez votre fichier d'informations d'identification : configurez simplement l'AWS cli via `aws configure` et vos informations d'identification doivent déjà se trouver dans `~/.aws/credentials`. Si vous utilisez un profil différent de "default", fournissez-le simplement dans le champ profile du formulaire d'authentification. -2. **Variables d'environnement** : vous pouvez toujours fournir vos informations d'identification en tant que variables d'environnement. Gardez à l'esprit que ces informations d'identification **remplaceront toujours** les informations d'identification situées dans le fichier « credentials ». Voir comment configurer l'environnement ci-dessous : +1. Authentication form: + 1. Vous pouvez fournir le `access_key` (devrait être obligatoire), le `secret_access_key` (devrait être obligatoire), `security_token` et le `session_token` + 2. Si vous enregistrez la connexion s3 en tant que signet, ces informations d'identification seront enregistrées en tant que chaîne AES-256/BASE64 cryptée dans votre fichier de signets (à l'exception du jeton de sécurité et du jeton de session qui sont censés être des informations d'identification temporaires). +2. Utilisez votre fichier d'informations d'identification : configurez simplement l'AWS cli via `aws configure` et vos informations d'identification doivent déjà se trouver dans `~/.aws/credentials`. Si vous utilisez un profil différent de "default", fournissez-le simplement dans le champ profile du formulaire d'authentification. +3. **Variables d'environnement** : vous pouvez toujours fournir vos informations d'identification en tant que variables d'environnement. Gardez à l'esprit que ces informations d'identification **remplaceront toujours** les informations d'identification situées dans le fichier « credentials ». Voir comment configurer l'environnement ci-dessous : Ceux-ci devraient toujours être obligatoires: diff --git a/docs/it/man.md b/docs/it/man.md index b90264c0..7a0cd427 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -107,11 +107,14 @@ Quando si usa l'argomento indirizzo non è possibile fornire la password diretta ## Credenziali Aws S3 🦊 Per connettersi ad un bucket S3 devi come già saprai fornire le credenziali fornite da AWS. -Ci sono due modi per passare queste credenziali a termscp e come avrai già notato **non puoi** farlo dal form di autenticazione. -Questi sono quindi i due modi per passare le chiavi: - -1. Utilizza il file delle credenziali s3: configurando aws via `aws configure` le tue credenziali dovrebbero già venir salvate in `~/.aws/credentials`. Nel caso tu debba usare un profile diverso da `default`, puoi fornire un profilo diverso nell'authentication form. -2. **Variabili d'ambiente**: nel caso il primo metodo non sia utilizzabile, puoi comunque fornirle come variabili d'ambiente. Considera però che queste variabili sovrascriveranno sempre le credenziali situate nel file credentials. Vediamo come impostarle: +Ci sono tre modi per passare queste credenziali a termscp. +Questi sono quindi i tre modi per passare le chiavi: + +1. Form di autenticazione: + 1. Puoi fornire la `access_key` (dovrebbe essere obbligatoria), la `secret_access_key` (dovrebbe essere obbligatoria), il `security_token` ed il `session_token` + 2. Se salvi la connessione s3 come segnalibro e decidi di salvare la password, questi parametri verranno salvati nel file dei segnalibri criptati con AES-256/BASE64; ad eccezion fatta per i due token, che dovrebbero essere credenziali temporanee, quindi inutili da salvare. +2. Utilizza il file delle credenziali s3: configurando aws via `aws configure` le tue credenziali dovrebbero già venir salvate in `~/.aws/credentials`. Nel caso tu debba usare un profile diverso da `default`, puoi fornire un profilo diverso nell'authentication form. +3. **Variabili d'ambiente**: nel caso il primo metodo non sia utilizzabile, puoi comunque fornirle come variabili d'ambiente. Considera però che queste variabili sovrascriveranno sempre le credenziali situate nel file credentials. Vediamo come impostarle: Queste sono sempre obbligatorie: diff --git a/docs/man.md b/docs/man.md index 4eef1e06..6f454fff 100644 --- a/docs/man.md +++ b/docs/man.md @@ -110,11 +110,14 @@ Password can be basically provided through 3 ways when address argument is provi ## Aws S3 credentials 🦊 In order to connect to an Aws S3 bucket you must obviously provide some credentials. -There are basically two ways to achieve this, and as you've probably already noticed you **can't** do that via the authentication form. +There are basically three ways to achieve this: So these are the ways you can provide the credentials for s3: -1. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form. -2. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below: +1. Authentication form: + 1. You can provide the `access_key` (should be mandatory), the `secret_access_key` (should be mandatory), `security_token` and the `session_token` + 2. If you save the s3 connection as a bookmark, these credentials will be saved as an encrypted AES-256/BASE64 string in your bookmarks file (except for the security token and session token which are meant to be temporary credentials). +2. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form. +3. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below: These should always be mandatory: diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index 470f9b91..1effb417 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -108,11 +108,13 @@ s3://buckethead@eu-central-1:default:/assets ## Aws S3 凭证 为了连接到 Aws S3 存储桶,您显然必须提供一些凭据。 -基本上有两种方法可以实现这一点,而且您可能已经注意到您**不能**通过身份验证表单来做到这一点。 因此,您可以通过以下方式为 s3 提供凭据: -1. 使用您的凭证文件:只需通过`aws configure` 配置AWS cli,您的凭证应该已经位于`~/.aws/credentials`。 如果您使用的配置文件不同于“默认”,只需在身份验证表单的配置文件字段中提供它。 -2. **环境变量**: 您始终可以将您的凭据作为环境变量提供。 请记住,这些凭据**将始终覆盖**位于 `credentials` 文件中的凭据。 下面看看如何配置环境: +1. 认证形式: + 1. 您可以提供 `access_key`(应该是强制性的)、`secret_access_key`(应该是强制性的)、`security_token` 和`session_token` + 2. 如果您将 s3 连接保存为书签,这些凭据将在您的书签文件中保存为加密的 AES-256/BASE64 字符串(安全令牌和会话令牌除外,它们是临时凭据)。. +2. 使用您的凭证文件:只需通过`aws configure` 配置AWS cli,您的凭证应该已经位于`~/.aws/credentials`。 如果您使用的配置文件不同于“默认”,只需在身份验证表单的配置文件字段中提供它。 +3. **环境变量**: 您始终可以将您的凭据作为环境变量提供。 请记住,这些凭据**将始终覆盖**位于 `credentials` 文件中的凭据。 下面看看如何配置环境: 这些应该始终是强制性的: diff --git a/src/config/bookmarks.rs b/src/config/bookmarks.rs index 86a01b71..7c76af22 100644 --- a/src/config/bookmarks.rs +++ b/src/config/bookmarks.rs @@ -66,6 +66,8 @@ pub struct S3Params { pub bucket: String, pub region: String, pub profile: Option, + pub access_key: Option, + pub secret_access_key: Option, } // -- impls @@ -122,6 +124,8 @@ impl From for S3Params { bucket: params.bucket_name, region: params.region, profile: params.profile, + access_key: params.access_key, + secret_access_key: params.secret_access_key, } } } @@ -129,6 +133,8 @@ impl From for S3Params { impl From for AwsS3Params { fn from(params: S3Params) -> Self { AwsS3Params::new(params.bucket, params.region, params.profile) + .access_key(params.access_key) + .secret_access_key(params.secret_access_key) } } @@ -227,7 +233,11 @@ mod tests { #[test] fn bookmark_from_s3_ftparams() { - let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test"))); + let params = ProtocolParams::AwsS3( + AwsS3Params::new("omar", "eu-west-1", Some("test")) + .access_key(Some("pippo")) + .secret_access_key(Some("pluto")), + ); let params: FileTransferParams = FileTransferParams::new(FileTransferProtocol::AwsS3, params); let bookmark = Bookmark::from(params); @@ -240,6 +250,8 @@ mod tests { assert_eq!(s3.bucket.as_str(), "omar"); assert_eq!(s3.region.as_str(), "eu-west-1"); assert_eq!(s3.profile.as_deref().unwrap(), "test"); + assert_eq!(s3.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto"); } #[test] @@ -272,7 +284,9 @@ mod tests { s3: Some(S3Params { bucket: String::from("veeso"), region: String::from("eu-west-1"), - profile: None, + profile: Some(String::from("default")), + access_key: Some(String::from("pippo")), + secret_access_key: Some(String::from("pluto")), }), }; let params = FileTransferParams::from(bookmark); @@ -280,6 +294,8 @@ mod tests { let gparams = params.params.s3_params().unwrap(); assert_eq!(gparams.bucket_name.as_str(), "veeso"); assert_eq!(gparams.region.as_str(), "eu-west-1"); - assert_eq!(gparams.profile, None); + assert_eq!(gparams.profile.as_deref().unwrap(), "default"); + assert_eq!(gparams.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(gparams.secret_access_key.as_deref().unwrap(), "pluto"); } } diff --git a/src/config/serialization.rs b/src/config/serialization.rs index 84198c95..038f4c19 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -421,6 +421,8 @@ mod tests { assert_eq!(s3.bucket.as_str(), "veeso"); assert_eq!(s3.region.as_str(), "eu-west-1"); assert_eq!(s3.profile.as_deref().unwrap(), "default"); + assert_eq!(s3.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto"); } #[test] @@ -470,6 +472,8 @@ mod tests { bucket: "veeso".to_string(), region: "eu-west-1".to_string(), profile: None, + access_key: None, + secret_access_key: None, }), }, ); @@ -531,6 +535,8 @@ mod tests { bucket = "veeso" region = "eu-west-1" profile = "default" + access_key = "pippo" + secret_access_key = "pluto" [recents] ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" } diff --git a/src/filetransfer/builder.rs b/src/filetransfer/builder.rs index cbd8cdad..aa906bbd 100644 --- a/src/filetransfer/builder.rs +++ b/src/filetransfer/builder.rs @@ -77,6 +77,18 @@ impl Builder { if let Some(profile) = params.profile { client = client.profile(profile); } + if let Some(access_key) = params.access_key { + client = client.access_key(access_key); + } + if let Some(secret_access_key) = params.secret_access_key { + client = client.secret_access_key(secret_access_key); + } + if let Some(security_token) = params.security_token { + client = client.security_token(security_token); + } + if let Some(session_token) = params.session_token { + client = client.session_token(session_token); + } client } @@ -124,7 +136,7 @@ impl Builder { /// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded) fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage { - SshKeyStorage::storage_from_config(config_client) + SshKeyStorage::from(config_client) } } @@ -138,7 +150,13 @@ mod test { #[test] fn should_build_aws_s3_fs() { - let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test"))); + let params = ProtocolParams::AwsS3( + AwsS3Params::new("omar", "eu-west-1", Some("test")) + .access_key(Some("pippo")) + .secret_access_key(Some("pluto")) + .security_token(Some("omar")) + .session_token(Some("gerry-scotti")), + ); let config_client = get_config_client(); let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client); } diff --git a/src/filetransfer/params.rs b/src/filetransfer/params.rs index f60c4358..01683673 100644 --- a/src/filetransfer/params.rs +++ b/src/filetransfer/params.rs @@ -61,6 +61,10 @@ pub struct AwsS3Params { pub bucket_name: String, pub region: String, pub profile: Option, + pub access_key: Option, + pub secret_access_key: Option, + pub security_token: Option, + pub session_token: Option, } impl FileTransferParams { @@ -102,6 +106,7 @@ impl ProtocolParams { } } + /// Get a mutable reference to the inner generic protocol params pub fn mut_generic_params(&mut self) -> Option<&mut GenericProtocolParams> { match self { ProtocolParams::Generic(params) => Some(params), @@ -167,8 +172,36 @@ impl AwsS3Params { bucket_name: bucket.as_ref().to_string(), region: region.as_ref().to_string(), profile: profile.map(|x| x.as_ref().to_string()), + access_key: None, + secret_access_key: None, + security_token: None, + session_token: None, } } + + /// Construct aws s3 params with provided access key + pub fn access_key>(mut self, key: Option) -> Self { + self.access_key = key.map(|x| x.as_ref().to_string()); + self + } + + /// Construct aws s3 params with provided secret_access_key + pub fn secret_access_key>(mut self, key: Option) -> Self { + self.secret_access_key = key.map(|x| x.as_ref().to_string()); + self + } + + /// Construct aws s3 params with provided security_token + pub fn security_token>(mut self, key: Option) -> Self { + self.security_token = key.map(|x| x.as_ref().to_string()); + self + } + + /// Construct aws s3 params with provided session_token + pub fn session_token>(mut self, key: Option) -> Self { + self.session_token = key.map(|x| x.as_ref().to_string()); + self + } } #[cfg(test)] @@ -206,11 +239,31 @@ mod test { } #[test] - fn params_aws_s3() { + fn should_init_aws_s3_params() { let params: AwsS3Params = AwsS3Params::new("omar", "eu-west-1", Some("test")); assert_eq!(params.bucket_name.as_str(), "omar"); assert_eq!(params.region.as_str(), "eu-west-1"); assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert!(params.access_key.is_none()); + assert!(params.secret_access_key.is_none()); + assert!(params.security_token.is_none()); + assert!(params.session_token.is_none()); + } + + #[test] + fn should_init_aws_s3_params_with_optionals() { + let params: AwsS3Params = AwsS3Params::new("omar", "eu-west-1", Some("test")) + .access_key(Some("pippo")) + .secret_access_key(Some("pluto")) + .security_token(Some("omar")) + .session_token(Some("gerry-scotti")); + assert_eq!(params.bucket_name.as_str(), "omar"); + assert_eq!(params.region.as_str(), "eu-west-1"); + assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert_eq!(params.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(params.secret_access_key.as_deref().unwrap(), "pluto"); + assert_eq!(params.security_token.as_deref().unwrap(), "omar"); + assert_eq!(params.session_token.as_deref().unwrap(), "gerry-scotti"); } #[test] diff --git a/src/main.rs b/src/main.rs index 15794742..ba62d0b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -227,7 +227,9 @@ fn parse_args(args: Args) -> Result { fn read_password(run_opts: &mut RunOpts) -> Result<(), String> { // Initialize client if necessary if let Some(remote) = run_opts.remote.as_mut() { + // Ask password for generic params if let Some(mut params) = remote.params.mut_generic_params() { + // Ask password only if generic protocol params if params.password.is_none() { // Ask password if unspecified params.password = match rpassword::read_password_from_tty(Some("Password: ")) { diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index 9b9f0d94..132985c8 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -167,7 +167,38 @@ impl BookmarksClient { *pwd = decrypted_pwd; } Err(err) => { - error!("Failed to decrypt password for bookmark: {}", err); + error!("Failed to decrypt `password` for bookmark {}: {}", key, err); + } + } + } + // Decrypt AWS-S3 params + if let Some(s3) = entry.s3.as_mut() { + // Access key + if let Some(access_key) = s3.access_key.as_mut() { + match self.decrypt_str(access_key.as_str()) { + Ok(plain) => { + *access_key = plain; + } + Err(err) => { + error!( + "Failed to decrypt `access_key` for bookmark {}: {}", + key, err + ); + } + } + } + // Secret access key + if let Some(secret_access_key) = s3.secret_access_key.as_mut() { + match self.decrypt_str(secret_access_key.as_str()) { + Ok(plain) => { + *secret_access_key = plain; + } + Err(err) => { + error!( + "Failed to decrypt `secret_access_key` for bookmark {}: {}", + key, err + ); + } } } } @@ -190,9 +221,13 @@ impl BookmarksClient { // Make bookmark info!("Added bookmark {}", name); let mut host: Bookmark = self.make_bookmark(params); - // If not save_password, set password to `None` + // If not save_password, set secrets to `None` if !save_password { host.password = None; + if let Some(s3) = host.s3.as_mut() { + s3.access_key = None; + s3.secret_access_key = None; + } } self.hosts.bookmarks.insert(name, host); } @@ -221,6 +256,10 @@ impl BookmarksClient { let mut host: Bookmark = self.make_bookmark(params); // Null password for recents host.password = None; + if let Some(s3) = host.s3.as_mut() { + s3.access_key = None; + s3.secret_access_key = None; + } // Check if duplicated for (key, value) in &self.hosts.recents { if *value == host { @@ -321,6 +360,15 @@ impl BookmarksClient { if let Some(pwd) = bookmark.password { bookmark.password = Some(self.encrypt_str(pwd.as_str())); } + // Encrypt aws s3 params + if let Some(s3) = bookmark.s3.as_mut() { + if let Some(access_key) = s3.access_key.as_mut() { + *access_key = self.encrypt_str(access_key.as_str()); + } + if let Some(secret_access_key) = s3.secret_access_key.as_mut() { + *secret_access_key = self.encrypt_str(secret_access_key.as_str()); + } + } bookmark } @@ -346,7 +394,7 @@ impl BookmarksClient { mod tests { use super::*; - use crate::filetransfer::params::GenericProtocolParams; + use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams}; use crate::filetransfer::{FileTransferProtocol, ProtocolParams}; use pretty_assertions::assert_eq; @@ -441,6 +489,69 @@ mod tests { assert_eq!(bookmark.4, None); } + #[test] + fn should_make_s3_bookmark_with_secrets() { + let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); + // Add s3 bookmark + client.add_bookmark("my-bucket", make_s3_ftparams(), true); + // Verify bookmark + let bookmark = client.get_bookmark("my-bucket").unwrap(); + assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3); + let params = bookmark.params.s3_params().unwrap(); + assert_eq!(params.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert_eq!(params.secret_access_key.as_deref().unwrap(), "pluto"); + assert_eq!(params.bucket_name.as_str(), "omar"); + assert_eq!(params.region.as_str(), "eu-west-1"); + } + + #[test] + fn should_make_s3_bookmark_without_secrets() { + let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); + // Add s3 bookmark + client.add_bookmark("my-bucket", make_s3_ftparams(), false); + // Verify bookmark + let bookmark = client.get_bookmark("my-bucket").unwrap(); + assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3); + let params = bookmark.params.s3_params().unwrap(); + assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert_eq!(params.bucket_name.as_str(), "omar"); + assert_eq!(params.region.as_str(), "eu-west-1"); + // secrets + assert_eq!(params.access_key, None); + assert_eq!(params.secret_access_key, None); + } + + #[test] + fn should_make_s3_recent() { + let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); + // Add s3 bookmark + client.add_recent(make_s3_ftparams()); + // Verify bookmark + let bookmark = client.iter_recents().next().unwrap(); + let bookmark = client.get_recent(bookmark).unwrap(); + assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3); + let params = bookmark.params.s3_params().unwrap(); + assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert_eq!(params.bucket_name.as_str(), "omar"); + assert_eq!(params.region.as_str(), "eu-west-1"); + // secrets + assert_eq!(params.access_key, None); + assert_eq!(params.secret_access_key, None); + } + #[test] fn test_system_bookmarks_manipulate_bookmarks() { @@ -734,6 +845,19 @@ mod tests { FileTransferParams::new(protocol, params) } + fn make_s3_ftparams() -> FileTransferParams { + FileTransferParams::new( + FileTransferProtocol::AwsS3, + ProtocolParams::AwsS3( + AwsS3Params::new("omar", "eu-west-1", Some("test")) + .access_key(Some("pippo")) + .secret_access_key(Some("pluto")) + .security_token(Some("omar")) + .session_token(Some("gerry-scotti")), + ), + ) + } + fn ftparams_to_tup( params: FileTransferParams, ) -> (String, u16, FileTransferProtocol, String, Option) { diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index d25b7433..0924beef 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -37,32 +37,6 @@ pub struct SshKeyStorage { } impl SshKeyStorage { - /// Create a `SshKeyStorage` starting from a `ConfigClient` - pub fn storage_from_config(cfg_client: &ConfigClient) -> Self { - let mut hosts: HashMap = - HashMap::with_capacity(cfg_client.iter_ssh_keys().count()); - debug!("Setting up SSH key storage"); - // Iterate over keys - for key in cfg_client.iter_ssh_keys() { - match cfg_client.get_ssh_key(key) { - Ok(host) => match host { - Some((addr, username, rsa_key_path)) => { - let key_name: String = Self::make_mapkey(&addr, &username); - hosts.insert(key_name, rsa_key_path); - } - None => continue, - }, - Err(err) => { - error!("Failed to get SSH key for {}: {}", key, err); - continue; - } - } - info!("Got SSH key for {}", key); - } - // Return storage - SshKeyStorage { hosts } - } - /// Create an empty ssh key storage; used in case `ConfigClient` is not available #[cfg(test)] pub fn empty() -> Self { @@ -92,6 +66,33 @@ impl SshKeyStorageT for SshKeyStorage { } } +impl From<&ConfigClient> for SshKeyStorage { + fn from(cfg_client: &ConfigClient) -> Self { + let mut hosts: HashMap = + HashMap::with_capacity(cfg_client.iter_ssh_keys().count()); + debug!("Setting up SSH key storage"); + // Iterate over keys + for key in cfg_client.iter_ssh_keys() { + match cfg_client.get_ssh_key(key) { + Ok(host) => match host { + Some((addr, username, rsa_key_path)) => { + let key_name: String = Self::make_mapkey(&addr, &username); + hosts.insert(key_name, rsa_key_path); + } + None => continue, + }, + Err(err) => { + error!("Failed to get SSH key for {}: {}", key, err); + continue; + } + } + info!("Got SSH key for {}", key); + } + // Return storage + SshKeyStorage { hosts } + } +} + #[cfg(test)] mod tests { @@ -113,7 +114,7 @@ mod tests { .add_ssh_key("192.168.1.31", "pi", "piroporopero") .is_ok()); // Create ssh key storage - let storage: SshKeyStorage = SshKeyStorage::storage_from_config(&client); + let storage: SshKeyStorage = SshKeyStorage::from(&client); // Verify key exists let mut exp_key_path: PathBuf = key_path.clone(); exp_key_path.push("pi@192.168.1.31.key"); diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index d3ac9bb7..4f89b8f0 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -229,5 +229,9 @@ impl AuthActivity { self.mount_s3_bucket(params.bucket_name.as_str()); self.mount_s3_region(params.region.as_str()); self.mount_s3_profile(params.profile.as_deref().unwrap_or("")); + self.mount_s3_access_key(params.access_key.as_deref().unwrap_or("")); + self.mount_s3_secret_access_key(params.secret_access_key.as_deref().unwrap_or("")); + self.mount_s3_security_token(params.security_token.as_deref().unwrap_or("")); + self.mount_s3_session_token(params.session_token.as_deref().unwrap_or("")); } } diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs index 8ad9661f..75c162fd 100644 --- a/src/ui/activities/auth/components/bookmarks.rs +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -346,7 +346,7 @@ impl BookmarkSavePassword { .value(0) .rewind(true) .foreground(color) - .title("Save password?", Alignment::Center), + .title("Save secrets?", Alignment::Center), } } } diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs index d45150ed..2847b906 100644 --- a/src/ui/activities/auth/components/form.rs +++ b/src/ui/activities/auth/components/form.rs @@ -177,7 +177,7 @@ impl Component for InputAddress { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -263,7 +263,7 @@ impl Component for InputPort { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -348,7 +348,7 @@ impl Component for InputUsername { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -432,7 +432,7 @@ impl Component for InputPassword { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -517,7 +517,7 @@ impl Component for InputS3Bucket { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -537,7 +537,7 @@ impl Component for InputS3Bucket { } } -// -- s3 bucket +// -- s3 region #[derive(MockComponent)] pub struct InputS3Region { @@ -602,7 +602,7 @@ impl Component for InputS3Region { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -622,7 +622,7 @@ impl Component for InputS3Region { } } -// -- s3 bucket +// -- s3 profile #[derive(MockComponent)] pub struct InputS3Profile { @@ -687,7 +687,7 @@ impl Component for InputS3Profile { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -708,3 +708,342 @@ impl Component for InputS3Profile { } } } + +// -- s3 access key + +#[derive(MockComponent)] +pub struct InputS3AccessKey { + component: Input, +} + +impl InputS3AccessKey { + pub fn new(access_key: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("AKIA...", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Access key", Alignment::Left) + .input_type(InputType::Text) + .value(access_key), + } + } +} + +impl Component for InputS3AccessKey { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ui(UiMsg::S3AccessKeyBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3AccessKeyBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct InputS3SecretAccessKey { + component: Input, +} + +impl InputS3SecretAccessKey { + pub fn new(secret_access_key: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Secret access key", Alignment::Left) + .input_type(InputType::Password('*')) + .value(secret_access_key), + } + } +} + +impl Component for InputS3SecretAccessKey { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ui(UiMsg::S3SecretAccessKeyBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3SecretAccessKeyBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct InputS3SecurityToken { + component: Input, +} + +impl InputS3SecurityToken { + pub fn new(security_token: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Security token", Alignment::Left) + .input_type(InputType::Password('*')) + .value(security_token), + } + } +} + +impl Component for InputS3SecurityToken { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ui(UiMsg::S3SecurityTokenBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3SecurityTokenBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct InputS3SessionToken { + component: Input, +} + +impl InputS3SessionToken { + pub fn new(session_token: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Session token", Alignment::Left) + .input_type(InputType::Password('*')) + .value(session_token), + } + } +} + +impl Component for InputS3SessionToken { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ui(UiMsg::S3SessionTokenBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3SessionTokenBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index bb014ae2..d42bf60c 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -37,7 +37,8 @@ pub use bookmarks::{ RecentsList, }; pub use form::{ - InputAddress, InputPassword, InputPort, InputS3Bucket, InputS3Profile, InputS3Region, + InputAddress, InputPassword, InputPort, InputS3AccessKey, InputS3Bucket, InputS3Profile, + InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputUsername, ProtocolRadio, }; pub use popup::{ diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index 29b2164f..a00d2dc6 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -26,7 +26,7 @@ * SOFTWARE. */ use super::{AuthActivity, FileTransferParams, FileTransferProtocol}; -use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; +use crate::filetransfer::params::ProtocolParams; use crate::system::auto_update::{Release, Update, UpdateStatus}; use crate::system::notifications::Notification; @@ -68,46 +68,32 @@ impl AuthActivity { &self, protocol: FileTransferProtocol, ) -> Result { - let (address, port, username, password): (String, u16, String, String) = - self.get_generic_params_input(); - if address.is_empty() { + let params = self.get_generic_params_input(); + if params.address.is_empty() { return Err("Invalid host"); } - if port == 0 { + if params.port == 0 { return Err("Invalid port"); } Ok(FileTransferParams { protocol, - params: ProtocolParams::Generic( - GenericProtocolParams::default() - .address(address) - .port(port) - .username(match username.is_empty() { - true => None, - false => Some(username), - }) - .password(match password.is_empty() { - true => None, - false => Some(password), - }), - ), + params: ProtocolParams::Generic(params), entry_directory: None, }) } /// Get input values from fields or return an error if fields are invalid to work as aws s3 pub(super) fn collect_s3_host_params(&self) -> Result { - let (bucket, region, profile): (String, String, Option) = - self.get_s3_params_input(); - if bucket.is_empty() { + let params = self.get_s3_params_input(); + if params.bucket_name.is_empty() { return Err("Invalid bucket"); } - if region.is_empty() { + if params.region.is_empty() { return Err("Invalid region"); } Ok(FileTransferParams { protocol: FileTransferProtocol::AwsS3, - params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)), + params: ProtocolParams::AwsS3(params), entry_directory: None, }) } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 08ee9c35..7d2ea8aa 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -66,9 +66,13 @@ pub enum Id { Protocol, QuitPopup, RecentsList, + S3AccessKey, S3Bucket, S3Profile, S3Region, + S3SecretAccessKey, + S3SecurityToken, + S3SessionToken, Subtitle, Title, Username, @@ -119,12 +123,20 @@ pub enum UiMsg { ProtocolBlurDown, ProtocolBlurUp, RececentsListBlur, + S3AccessKeyBlurDown, + S3AccessKeyBlurUp, S3BucketBlurDown, S3BucketBlurUp, S3ProfileBlurDown, S3ProfileBlurUp, S3RegionBlurDown, S3RegionBlurUp, + S3SecretAccessKeyBlurDown, + S3SecretAccessKeyBlurUp, + S3SecurityTokenBlurDown, + S3SecurityTokenBlurUp, + S3SessionTokenBlurDown, + S3SessionTokenBlurUp, BookmarkNameBlur, SaveBookmarkPasswordBlur, ShowDeleteBookmarkPopup, diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index bbe62d86..76124c2b 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -203,7 +203,7 @@ impl AuthActivity { .app .active(match self.input_mask() { InputMask::Generic => &Id::Password, - InputMask::AwsS3 => &Id::S3Profile, + InputMask::AwsS3 => &Id::S3SessionToken, }) .is_ok()); } @@ -223,11 +223,35 @@ impl AuthActivity { assert!(self.app.active(&Id::S3Bucket).is_ok()); } UiMsg::S3ProfileBlurDown => { - assert!(self.app.active(&Id::Protocol).is_ok()); + assert!(self.app.active(&Id::S3AccessKey).is_ok()); } UiMsg::S3ProfileBlurUp => { assert!(self.app.active(&Id::S3Region).is_ok()); } + UiMsg::S3AccessKeyBlurDown => { + assert!(self.app.active(&Id::S3SecretAccessKey).is_ok()); + } + UiMsg::S3AccessKeyBlurUp => { + assert!(self.app.active(&Id::S3Profile).is_ok()); + } + UiMsg::S3SecretAccessKeyBlurDown => { + assert!(self.app.active(&Id::S3SecurityToken).is_ok()); + } + UiMsg::S3SecretAccessKeyBlurUp => { + assert!(self.app.active(&Id::S3AccessKey).is_ok()); + } + UiMsg::S3SecurityTokenBlurDown => { + assert!(self.app.active(&Id::S3SessionToken).is_ok()); + } + UiMsg::S3SecurityTokenBlurUp => { + assert!(self.app.active(&Id::S3SecretAccessKey).is_ok()); + } + UiMsg::S3SessionTokenBlurDown => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + UiMsg::S3SessionTokenBlurUp => { + assert!(self.app.active(&Id::S3SecurityToken).is_ok()); + } UiMsg::SaveBookmarkPasswordBlur => { assert!(self.app.active(&Id::BookmarkName).is_ok()); } diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 29ce8588..322dcd05 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -27,7 +27,7 @@ */ // Locals use super::{components, AuthActivity, Context, FileTransferProtocol, Id, InputMask}; -use crate::filetransfer::params::ProtocolParams; +use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; use crate::filetransfer::FileTransferParams; use crate::utils::ui::draw_area_in; @@ -74,6 +74,10 @@ impl AuthActivity { self.mount_s3_bucket(""); self.mount_s3_profile(""); self.mount_s3_region(""); + self.mount_s3_access_key(""); + self.mount_s3_secret_access_key(""); + self.mount_s3_security_token(""); + self.mount_s3_session_token(""); // Version notice if let Some(version) = self .context() @@ -158,6 +162,7 @@ impl AuthActivity { Constraint::Length(3), // bucket Constraint::Length(3), // region Constraint::Length(3), // profile + Constraint::Length(3), // access_key ] .as_ref(), ) @@ -190,9 +195,11 @@ impl AuthActivity { // Render input mask match self.input_mask() { InputMask::AwsS3 => { - self.app.view(&Id::S3Bucket, f, input_mask[0]); - self.app.view(&Id::S3Region, f, input_mask[1]); - self.app.view(&Id::S3Profile, f, input_mask[2]); + let s3_view_ids = self.get_s3_view(); + self.app.view(&s3_view_ids[0], f, input_mask[0]); + self.app.view(&s3_view_ids[1], f, input_mask[1]); + self.app.view(&s3_view_ids[2], f, input_mask[2]); + self.app.view(&s3_view_ids[3], f, input_mask[3]); } InputMask::Generic => { self.app.view(&Id::Address, f, input_mask[0]); @@ -653,23 +660,83 @@ impl AuthActivity { .is_ok()); } + pub(crate) fn mount_s3_access_key(&mut self, key: &str) { + let password_color = self.theme().auth_password; + assert!(self + .app + .remount( + Id::S3AccessKey, + Box::new(components::InputS3AccessKey::new(key, password_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_s3_secret_access_key(&mut self, key: &str) { + let addr_color = self.theme().auth_address; + assert!(self + .app + .remount( + Id::S3SecretAccessKey, + Box::new(components::InputS3SecretAccessKey::new(key, addr_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_s3_security_token(&mut self, token: &str) { + let port_color = self.theme().auth_port; + assert!(self + .app + .remount( + Id::S3SecurityToken, + Box::new(components::InputS3SecurityToken::new(token, port_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_s3_session_token(&mut self, token: &str) { + let username_color = self.theme().auth_username; + assert!(self + .app + .remount( + Id::S3SessionToken, + Box::new(components::InputS3SessionToken::new(token, username_color)), + vec![] + ) + .is_ok()); + } + // -- query /// Collect input values from view - pub(super) fn get_generic_params_input(&self) -> (String, u16, String, String) { + pub(super) fn get_generic_params_input(&self) -> GenericProtocolParams { let addr: String = self.get_input_addr(); let port: u16 = self.get_input_port(); - let username: String = self.get_input_username(); - let password: String = self.get_input_password(); - (addr, port, username, password) + let username = self.get_input_username(); + let password = self.get_input_password(); + GenericProtocolParams::default() + .address(addr) + .port(port) + .username(username) + .password(password) } /// Collect s3 input values from view - pub(super) fn get_s3_params_input(&self) -> (String, String, Option) { + pub(super) fn get_s3_params_input(&self) -> AwsS3Params { let bucket: String = self.get_input_s3_bucket(); let region: String = self.get_input_s3_region(); let profile: Option = self.get_input_s3_profile(); - (bucket, region, profile) + let access_key = self.get_input_s3_access_key(); + let secret_access_key = self.get_input_s3_secret_access_key(); + let security_token = self.get_input_s3_security_token(); + let session_token = self.get_input_s3_session_token(); + AwsS3Params::new(bucket, region, profile) + .access_key(access_key) + .secret_access_key(secret_access_key) + .security_token(security_token) + .session_token(session_token) } pub(super) fn get_input_addr(&self) -> String { @@ -689,17 +756,17 @@ impl AuthActivity { } } - pub(super) fn get_input_username(&self) -> String { + pub(super) fn get_input_username(&self) -> Option { match self.app.state(&Id::Username) { - Ok(State::One(StateValue::String(x))) => x, - _ => String::new(), + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, } } - pub(super) fn get_input_password(&self) -> String { + pub(super) fn get_input_password(&self) -> Option { match self.app.state(&Id::Password) { - Ok(State::One(StateValue::String(x))) => x, - _ => String::new(), + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, } } @@ -724,6 +791,34 @@ impl AuthActivity { } } + pub(super) fn get_input_s3_access_key(&self) -> Option { + match self.app.state(&Id::S3AccessKey) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, + } + } + + pub(super) fn get_input_s3_secret_access_key(&self) -> Option { + match self.app.state(&Id::S3SecretAccessKey) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, + } + } + + pub(super) fn get_input_s3_security_token(&self) -> Option { + match self.app.state(&Id::S3SecurityToken) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, + } + } + + pub(super) fn get_input_s3_session_token(&self) -> Option { + match self.app.state(&Id::S3SessionToken) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, + } + } + /// Get new bookmark params pub(super) fn get_new_bookmark(&self) -> (String, bool) { let name = match self.app.state(&Id::BookmarkName) { @@ -745,7 +840,7 @@ impl AuthActivity { /// Returns the input mask size based on current input mask pub(super) fn input_mask_size(&self) -> u16 { match self.input_mask() { - InputMask::AwsS3 => 9, + InputMask::AwsS3 => 12, InputMask::Generic => 12, } } @@ -785,6 +880,19 @@ impl AuthActivity { } } + /// Get the visible element in the aws-s3 form, based on current focus + fn get_s3_view(&self) -> [Id; 4] { + match self.app.focus() { + Some(&Id::S3SecretAccessKey | &Id::S3SecurityToken | &Id::S3SessionToken) => [ + Id::S3AccessKey, + Id::S3SecretAccessKey, + Id::S3SecurityToken, + Id::S3SessionToken, + ], + _ => [Id::S3Bucket, Id::S3Region, Id::S3Profile, Id::S3AccessKey], + } + } + fn init_global_listener(&mut self) { use tuirealm::event::{Key, KeyEvent, KeyModifiers}; assert!(self diff --git a/src/ui/activities/setup/components/ssh.rs b/src/ui/activities/setup/components/ssh.rs index cbd43d54..c53adae3 100644 --- a/src/ui/activities/setup/components/ssh.rs +++ b/src/ui/activities/setup/components/ssh.rs @@ -240,7 +240,7 @@ impl Component for SshHost { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -319,7 +319,7 @@ impl Component for SshUsername { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) diff --git a/src/ui/activities/setup/components/theme.rs b/src/ui/activities/setup/components/theme.rs index 3b937247..872534e2 100644 --- a/src/ui/activities/setup/components/theme.rs +++ b/src/ui/activities/setup/components/theme.rs @@ -895,7 +895,7 @@ impl Component for InputColor { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { let result = self.perform(Cmd::Type(ch)); self.update_color(result) From f1cc912432aac66ecc37318ebeff94af629c0e76 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 4 Jan 2022 18:41:40 +0100 Subject: [PATCH 39/45] re-use code with handle input ev --- src/ui/activities/auth/components/form.rs | 740 ++++------------------ 1 file changed, 126 insertions(+), 614 deletions(-) diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs index 2847b906..4822af4d 100644 --- a/src/ui/activities/auth/components/form.rs +++ b/src/ui/activities/auth/components/form.rs @@ -139,61 +139,12 @@ impl InputAddress { impl Component for InputAddress { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::AddressBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::AddressBlurUp)), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::AddressBlurDown), + Msg::Ui(UiMsg::AddressBlurUp), + ) } } @@ -225,61 +176,12 @@ impl InputPort { impl Component for InputPort { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::PortBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::PortBlurUp)), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::PortBlurDown), + Msg::Ui(UiMsg::PortBlurUp), + ) } } @@ -310,61 +212,12 @@ impl InputUsername { impl Component for InputUsername { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::UsernameBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::UsernameBlurUp)), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::UsernameBlurDown), + Msg::Ui(UiMsg::UsernameBlurUp), + ) } } @@ -394,61 +247,12 @@ impl InputPassword { impl Component for InputPassword { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::PasswordBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::PasswordBlurUp)), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::PasswordBlurDown), + Msg::Ui(UiMsg::PasswordBlurUp), + ) } } @@ -479,61 +283,12 @@ impl InputS3Bucket { impl Component for InputS3Bucket { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::S3BucketBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::S3BucketBlurUp)), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::S3BucketBlurDown), + Msg::Ui(UiMsg::S3BucketBlurUp), + ) } } @@ -564,61 +319,12 @@ impl InputS3Region { impl Component for InputS3Region { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::S3RegionBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::Ui(UiMsg::S3RegionBlurUp)), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::S3RegionBlurDown), + Msg::Ui(UiMsg::S3RegionBlurUp), + ) } } @@ -649,63 +355,12 @@ impl InputS3Profile { impl Component for InputS3Profile { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::S3ProfileBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - Some(Msg::Ui(UiMsg::S3ProfileBlurUp)) - } - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::S3ProfileBlurDown), + Msg::Ui(UiMsg::S3ProfileBlurUp), + ) } } @@ -736,63 +391,12 @@ impl InputS3AccessKey { impl Component for InputS3AccessKey { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::S3AccessKeyBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - Some(Msg::Ui(UiMsg::S3AccessKeyBlurUp)) - } - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::S3AccessKeyBlurDown), + Msg::Ui(UiMsg::S3AccessKeyBlurUp), + ) } } @@ -820,63 +424,12 @@ impl InputS3SecretAccessKey { impl Component for InputS3SecretAccessKey { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::S3SecretAccessKeyBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - Some(Msg::Ui(UiMsg::S3SecretAccessKeyBlurUp)) - } - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::S3SecretAccessKeyBlurDown), + Msg::Ui(UiMsg::S3SecretAccessKeyBlurUp), + ) } } @@ -904,63 +457,12 @@ impl InputS3SecurityToken { impl Component for InputS3SecurityToken { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::S3SecurityTokenBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - Some(Msg::Ui(UiMsg::S3SecurityTokenBlurUp)) - } - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, - } + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::S3SecurityTokenBlurDown), + Msg::Ui(UiMsg::S3SecurityTokenBlurUp), + ) } } @@ -988,62 +490,72 @@ impl InputS3SessionToken { impl Component for InputS3SessionToken { fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::Connect)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::S3SessionTokenBlurDown)), - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - Some(Msg::Ui(UiMsg::S3SessionTokenBlurUp)) - } - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) - } - _ => None, + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::S3SessionTokenBlurDown), + Msg::Ui(UiMsg::S3SessionTokenBlurUp), + ) + } +} + +fn handle_input_ev( + component: &mut dyn Component, + ev: Event, + on_key_down: Msg, + on_key_up: Msg, +) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + component.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + component.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + component.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + component.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + component.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + component.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + component.perform(Cmd::Type(ch)); + Some(Msg::None) } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(on_key_down), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::Ui(UiMsg::ParamsFormBlur)), + _ => None, } } From 721af3d3d5b564185bc4f39bfeec442556613ff2 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 4 Jan 2022 21:16:06 +0100 Subject: [PATCH 40/45] Error popup message height is now calculated based on the content it must display --- CHANGELOG.md | 4 ++- Cargo.lock | 1 + Cargo.toml | 1 + src/ui/activities/filetransfer/view.rs | 48 ++++++++++++++++++++++++-- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 929c3b26..59a7c186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Released on FIXME: - **Ui**: - Transfer abortion is now more responsive - Selected files will now be rendered with **Reversed, underlined and italic** text modifiers instead of being prepended with `*`. + - Error popup message height is now calculated based on the content it must display. - **Midnight commander keys** - ``: Show help - ``: Save file as (actually I invented this) @@ -74,7 +75,8 @@ Released on FIXME: - By default the log level is now set to `INFO` - It is now possible to enable the `TRACE` level with the `-D` CLI option. - Dependencies: - - Updated `tui-realm` to `1.3.0` + - Added `unicode-width 0.1.8` + - Updated `tui-realm` to `1.4.2` - Updated `tui-realm-stdlib` to `1.1.4` - Removed `rust-s3`, `ssh2`, `suppaftp`; replaced by `remotefs 0.2.0`, `remotefs-aws-s3 0.1.0`, `remotefs-ftp 0.1.0` and `remotefs-ssh 0.1.0` - Removed `crossterm` (since bridged by tui-realm) diff --git a/Cargo.lock b/Cargo.lock index 801117f9..368ef585 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2288,6 +2288,7 @@ dependencies = [ "toml", "tui-realm-stdlib", "tuirealm", + "unicode-width", "users", "whoami", "wildmatch", diff --git a/Cargo.toml b/Cargo.toml index 9acac0ca..aa1c5951 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ thiserror = "^1.0.0" toml = "0.5.8" tui-realm-stdlib = "^1.1.0" tuirealm = "^1.2.0" +unicode-width = "^0.1.8" whoami = "1.1.1" wildmatch = "2.0.0" diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 64de25b9..52d1cece 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -38,6 +38,7 @@ use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::tui::widgets::Clear; use tuirealm::{Sub, SubClause, SubEventClause}; +use unicode_width::UnicodeWidthStr; impl FileTransferActivity { // -- init @@ -292,12 +293,22 @@ impl FileTransferActivity { // make popup self.app.view(&Id::SortingPopup, f, popup); } else if self.app.mounted(&Id::ErrorPopup) { - let popup = draw_area_in(f.size(), 50, 10); + // TODO: inject dynamic height here + let popup = draw_area_in( + f.size(), + 50, + self.calc_popup_height(Id::ErrorPopup, f.size().width, f.size().height), + ); f.render_widget(Clear, popup); // make popup self.app.view(&Id::ErrorPopup, f, popup); } else if self.app.mounted(&Id::FatalPopup) { - let popup = draw_area_in(f.size(), 50, 10); + // TODO: inject dynamic height here + let popup = draw_area_in( + f.size(), + 50, + self.calc_popup_height(Id::FatalPopup, f.size().width, f.size().height), + ); f.render_widget(Clear, popup); // make popup self.app.view(&Id::FatalPopup, f, popup); @@ -854,6 +865,39 @@ impl FileTransferActivity { let _ = self.app.umount(&Id::KeybindingsPopup); } + // -- dynamic size + + /// Given the id of the component to display and the width and height of the total area, + /// returns the height in percentage to the entire area height, that the popup should have + fn calc_popup_height(&self, id: Id, width: u16, height: u16) -> u16 { + // Get current text width + let text_width = self + .app + .query(&id, tuirealm::Attribute::Text) + .ok() + .flatten() + .map(|x| { + x.unwrap_payload() + .unwrap_vec() + .into_iter() + .map(|x| x.unwrap_text_span().content) + .collect::>() + .join("") + .width() as u16 + }) + .unwrap_or(0); + // Calc real width of a row in the popup + let row_width = (width / 2).saturating_sub(2); + // Calc row height in percentage (1 : height = x : 100) + let row_height_p = (100.0 / (height as f64)).ceil() as u16; + // Get amount of required rows NOTE: + 2 because of margins + let display_rows = ((text_width as f64) / (row_width as f64)).ceil() as u16 + 2; + // Return height (row_height_p * display_rows) + display_rows * row_height_p + } + + // -- global listener + fn mount_global_listener(&mut self) { assert!(self .app From b899f6080e52664db5416bada80e69a76bbeda33 Mon Sep 17 00:00:00 2001 From: veeso Date: Wed, 5 Jan 2022 12:40:41 +0100 Subject: [PATCH 41/45] 2022-01-05 review --- docs/developer.md | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 11023b62..d78f0de8 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -1,11 +1,13 @@ # Developer Manual Document audience: developers +Revision: 2022-01-05 - [Developer Manual](#developer-manual) - [How termscp works](#how-termscp-works) - [Activities](#activities) - [The Context](#the-context) + - [Achieving an abstract file transfer client](#achieving-an-abstract-file-transfer-client) Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at This chapter describes how termscp works and the guide lines to implement stuff such as file transfers and add features to the user interface. @@ -14,19 +16,18 @@ This chapter describes how termscp works and the guide lines to implement stuff ## How termscp works -termscp is basically made up of 4 components: +termscp is basically made up of 3 core modules: -- the **filetransfer**: the filetransfer takes care of managing the remote file system; it provides function to establish a connection with the remote, operating on the remote server file system (e.g. remove files, make directories, rename files, ...), read files and write files. The FileTransfer, as we'll see later, is actually a trait, and for each protocol a FileTransfer must be implement the trait. - the **host**: the host module provides functions to interact with the local host file system. - the **ui**: this module contains the implementation of the user interface, as we'll see in the next chapter, this is achieved through **activities**. - the **activity_manager**: the activity manager takes care of managing activities, basically it runs the activities of the user interface, and chooses, based on their state, when is the moment to terminate the current activity and which activity to run after the current one. -In addition to the 4 main components, other have been added through the time: +In addition to the 3 core modules, other have been added through the time: - **config**: this module provides the configuration schema and serialization methods for it. -- **fs**: this modules exposes the FsFile entity and the explorers. The explorers are structs which hold the content of the current directory; they also they take of filtering files up to your preferences and format file entries based on your configuration. -- **system**: the system module provides a way to actually interact with the configuration, the ssh key storage and with the bookmarks. -- **utils**: contains the utilities used by pretty much all the project. +- **explorer**: this modules exposes the explorer structures, which are used to handle the file explorer in the ui. So, basically they store the current directory model and the view states (e.g. sorting, whether to display hidden files, ...). +- **system**: the system module provides a way to interact with the configuration, with the ssh key storage and with the bookmarks. +- **utils**: contains the utilities used by pretty much all of the project. ## Activities @@ -35,10 +36,10 @@ I think there are many ways to implement a user interface and I've worked with d My approach was this: -- **Activities on top**: each "page" is an Activity and an `Activity Manager` handles them. I got inspired by Android for this case. I think that's a good way to implement the ui in case like this, where you have different pages, each one with their view, their components and their logics. Activities work with the `Context`, which is a data holder for different data, which are shared and common between the activities. -- **Activities display Views**: Each activity can show different views. A view is basically a list of **components**, each one with its properties. The view is a facade to the components and also handles the focus, which is the current active component. You cannot have more than one component active, so you need to handle this; but at the same time you also have to give focus to the previously active component if the current one is destroyed. So basically view takes care of all this stuff. -- **Components**: I've decided to write around `tui` in order to re-use widgets. To do so I've implemented the `Component` trait. To implement traits I got inspired by [React](https://reactjs.org/). Each component has its *Properties* and can have its *States*. Then each component must be able to handle input events and to be updated with new properties. Last but not least, each component must provide a method to **render** itself. -- **Messages: an Elm based approach**: I was really satisfied with my implementation choices; the problem at this point was solving one of the biggest teardrops I've ever had with this project: **events**. Input events were really a pain to handle, since I had to handle states in the activity to handle which component was enabled etc. To solve this I got inspired by a wonderful language I had recently studied, which is [Elm](https://elm-lang.org/). Basically in Elm you implement your ui using three basic functions: **update**, **view** and **init**. View and init were pretty much already implemented here, but at this point I decided to implement also something like the **elm update function**. I came out with a huge match case to handle events inside a recursive function, which you can basically find in the `update.rs` file inside each activity. This match case handles a tuple, made out of the **component id** and the **input event** received from the view. It matches the two propeties against the input event we want to handle for each component *et voilà*. +- **Activities on top**: each "view" is an Activity and an `Activity Manager` handles them. I got inspired by Android for this case. I think that's a good way to implement the ui in case like this, where you have different views, each one with their view, their components and their logic. Activities work with the `Context`, which is a data holder to share data between the activities. +- **Activities display Applications**: Each activity can show different **Applications**. An application, contains a **View** is basically a list of **components**, each one with its properties. The view is a facade to the components and also handles the focus, which is the current active component. You cannot have more than one component active, so you need to handle this; but at the same time you also have to give focus to the previously active component if the current one is destroyed. So basically **Application** takes care of all this stuff. If you're interested on how this works, you can read more on . +- **Components**: I've decided to write around `tui` in order to re-use widgets. To do so I've implemented the `Component` trait. To implement traits I got inspired by [React](https://reactjs.org/). Each component has its *Properties* and can have its *States*. Then each component must be able to handle input events and to be updated with new properties. Last but not least, each component must provide a method to **render** itself. At the beginning this was implemented inside of termscp, but now this has been moved to [tui-realm](https://github.com/veeso/tui-realm). +- **Messages: an Elm based approach**: I was really satisfied with my implementation choices; the problem at this point was solving one of the biggest teardrops I've ever had with this project: **events**. Input events were really a pain to handle, since I had to handle states in the activity to handle which component was enabled etc. To solve this I got inspired by a wonderful language I had recently studied, which is [Elm](https://elm-lang.org/). Basically in Elm you implement your ui using three basic functions: **update**, **view** and **init**. View and init were pretty much already implemented here, but at this point I decided to implement also something like the **elm update function**. I came out with a huge match case to handle messages inside a recursive function, which you can basically find in the `update.rs` file inside each activity. This match case handles the messages produced by the components in front of an incoming input event. It matches the messages causing the activity to change its states *et voilà*. I've implemented a Trait called `Activity`, which, is a very very reduced version of the Android activity of course. This trait provides only 3 methods: @@ -57,7 +58,16 @@ The context basically holds the following data: - The **File Transfer Params**: the current parameters set to connect to the remote - The **Config Client**: the configuration client is a structure which provides functions to access the user configuration - The **Store**: the store is a key-value storage which can hold any kind of data. This can be used to store states to share between activities or to keep persistence for heavy/slow tasks (such as checking for updates). -- The **Input handler**: the input handler is used to read input events from the keyboard - The **Terminal**: the terminal is used to view the tui on the terminal --- + +## Achieving an abstract file transfer client + +When I started to implement termscp, in december 2020, the file transfer was at the core of my implementation focus, since, for obvious reasons, it is at the heart of termscp. +The first implementation consisted of a `filetransfer` module, which exposed a trait called `FileTransfer`, which exposed different methods to generically interact with the remote file system. +This thing has changed over the last year, since different users has asked me to implement a dedicated library to implement this. +So in the last quarter of 2021, I dedicated part of my time in implementing an abstract library to work with remote device file systems, and this is how [remotefs](https://github.com/veeso/remotefs-rs) was born. +Remotefs provides a `RemoteFs` trait which exposes all of the core file-system functionalities and this has since 0.8.0 version, replaced the `FileTransfer` trait. + +The file transfer module, still exists though, but its only task is to create a builder from the "file transfer parameters" into the `RemoteFs` client implementation. From 56dfe0ba725d605c7588e04d4b5e209cc18b0722 Mon Sep 17 00:00:00 2001 From: veeso Date: Wed, 5 Jan 2022 12:59:32 +0100 Subject: [PATCH 42/45] keyring 1.0.0 --- CHANGELOG.md | 4 +- Cargo.lock | 994 ++++++++++++++++++------------ Cargo.toml | 6 +- src/system/keys/keyringstorage.rs | 27 +- 4 files changed, 606 insertions(+), 425 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a7c186..f1555099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ ## 0.8.0 -Released on FIXME: +Released on 06/01/2022 > ❄️ Winter update 2022 ⛄ @@ -76,6 +76,8 @@ Released on FIXME: - It is now possible to enable the `TRACE` level with the `-D` CLI option. - Dependencies: - Added `unicode-width 0.1.8` + - Updated `argh` to `0.1.7` + - Updated `keyring` to `1.0.0` - Updated `tui-realm` to `1.4.2` - Updated `tui-realm-stdlib` to `1.1.4` - Removed `rust-s3`, `ssh2`, `suppaftp`; replaced by `remotefs 0.2.0`, `remotefs-aws-s3 0.1.0`, `remotefs-ftp 0.1.0` and `remotefs-ssh 0.1.0` diff --git a/Cargo.lock b/Cargo.lock index 368ef585..f3ef875e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,19 @@ checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" dependencies = [ "aes-soft", "aesni", - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.3.0", + "cpufeatures", + "opaque-debug", ] [[package]] @@ -25,7 +37,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -35,7 +47,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -65,15 +77,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.44" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" [[package]] name = "argh" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f023c76cd7975f9969f8e29f0e461decbdc7f51048ce43427107a3d192f1c9bf" +checksum = "dbb41d85d92dfab96cb95ab023c265c5e4261bb956c0fb49ca06d90c570f1958" dependencies = [ "argh_derive", "argh_shared", @@ -81,22 +93,22 @@ dependencies = [ [[package]] name = "argh_derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ad219abc0c06ca788aface2e3a1970587e3413ab70acd20e54b6ec524c1f8f" +checksum = "be69f70ef5497dd6ab331a50bd95c6ac6b8f7f17a7967838332743fbd58dc3b5" dependencies = [ "argh_shared", "heck", "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", ] [[package]] name = "argh_shared" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33" +checksum = "e6f8c380fa28aa1b36107cd97f0196474bb7241bb95a453c5c01a15ac74b2eac" [[package]] name = "arrayref" @@ -110,15 +122,34 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + [[package]] name = "async-trait" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", ] [[package]] @@ -161,9 +192,9 @@ dependencies = [ [[package]] name = "aws-region" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2884b8f2aaeb4a4bf80b219b4fe1d340139ca9331679c57e0fd4a24f571a78bd" +checksum = "b1efb67b8f201dd0deea4e1240bce7da0366f8df90509a7df054973604b20b34" dependencies = [ "anyhow", ] @@ -174,12 +205,6 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" -[[package]] -name = "bitflags" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" - [[package]] name = "bitflags" version = "1.3.2" @@ -220,7 +245,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0" dependencies = [ "block-padding", - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding", + "cipher 0.3.0", ] [[package]] @@ -231,9 +266,9 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bumpalo" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byteorder" @@ -253,6 +288,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + [[package]] name = "cassowary" version = "0.3.0" @@ -261,9 +302,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.70" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cfg-if" @@ -300,23 +341,32 @@ dependencies = [ ] [[package]] -name = "cloudabi" -version = "0.0.3" +name = "cipher" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "bitflags 1.3.2", + "generic-array", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", ] [[package]] name = "console" -version = "0.14.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" dependencies = [ "encode_unicode", - "lazy_static", "libc", + "once_cell", "regex", "terminal_size", "unicode-width", @@ -340,35 +390,19 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys 0.7.0", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" dependencies = [ - "core-foundation-sys 0.8.2", + "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" - -[[package]] -name = "core-foundation-sys" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" @@ -381,18 +415,18 @@ dependencies = [ [[package]] name = "crc-any" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c268ef4daf6fe48c6794ffb5e138afb6ea405b7987ec8f8501b817a84a56d6" +checksum = "073375684a58dece169afbdc9879a027f3698118ad3814938316c6002b7aa921" dependencies = [ "debug-helper", ] [[package]] name = "crc32fast" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" dependencies = [ "cfg-if 1.0.0", ] @@ -413,11 +447,11 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d" dependencies = [ - "bitflags 1.3.2", + "bitflags", "crossterm_winapi", "libc", "mio", - "parking_lot 0.11.2", + "parking_lot", "signal-hook", "signal-hook-mio", "winapi", @@ -458,24 +492,15 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" dependencies = [ - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", ] [[package]] name = "dbus" -version = "0.2.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a0c10ea61042b7555729ab0608727bbbb06ce709c11e6047cfa4e10f6d052d" -dependencies = [ - "libc", -] - -[[package]] -name = "dbus" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b225f88dd3718253526c38bc333b9986b547a7580abf81186c9461647b2487" +checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce" dependencies = [ "libc", "libdbus-sys", @@ -488,14 +513,25 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76fbd10dce159c002b9c688ae8ab7cd531151e185e0ad360f4bfea3b0eede3a8" +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "des" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b24e7c748888aa2fa8bce21d8c64a52efc810663285315ac7476f7197a982fae" +checksum = "ac41dd49fb554432020d52c875fc290e110113f864c6b1b525cd62c7e7747a5d" dependencies = [ "byteorder", - "cipher", + "cipher 0.3.0", "opaque-debug", ] @@ -551,7 +587,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" dependencies = [ - "rand 0.8.4", + "rand", ] [[package]] @@ -578,13 +614,43 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.28" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enumflags2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fastrand" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +dependencies = [ + "instant", +] + [[package]] name = "filetime" version = "0.2.15" @@ -640,48 +706,103 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" + +[[package]] +name = "futures-executor" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" dependencies = [ - "autocfg", + "futures-channel", "futures-core", "futures-io", + "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -691,9 +812,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -723,9 +844,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.4" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472" +checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd" dependencies = [ "bytes", "fnv", @@ -822,20 +943,20 @@ dependencies = [ [[package]] name = "http" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.1", ] [[package]] name = "http-body" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", "http", @@ -850,15 +971,15 @@ checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.13" +version = "0.14.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593" +checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" dependencies = [ "bytes", "futures-channel", @@ -869,7 +990,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 0.4.8", "pin-project-lite", "socket2", "tokio", @@ -926,9 +1047,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", ] @@ -945,6 +1066,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "js-sys" version = "0.3.55" @@ -956,13 +1083,13 @@ dependencies = [ [[package]] name = "keyring" -version = "0.10.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bcd64f48199f69993c705fd2f76882e53969db93bc6345021bc8bb6462a9ffa" +checksum = "4dadaf714ce3f9d7e1c8c05aa8d070eb6e9ddd40cd7be3aca2f21fa372405104" dependencies = [ "byteorder", "secret-service", - "security-framework 0.4.4", + "security-framework", "winapi", ] @@ -974,9 +1101,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.102" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" [[package]] name = "libdbus-sys" @@ -989,9 +1116,9 @@ dependencies = [ [[package]] name = "libssh2-sys" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" dependencies = [ "cc", "libc", @@ -1013,15 +1140,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "lock_api" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" -dependencies = [ - "scopeguard", -] - [[package]] name = "lock_api" version = "0.4.5" @@ -1054,13 +1172,13 @@ dependencies = [ [[package]] name = "magic-crypt" -version = "3.1.8" +version = "3.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3c94f1281833c690f81e6a00c545063b4f034509d3af6d29b58d48e39aa64c9" +checksum = "0f89f3c9a23ba052e4fc602770944c7ef16096ade1ca110a5c722efb16da7395" dependencies = [ - "aes-soft", + "aes 0.7.5", "base64", - "block-modes", + "block-modes 0.8.1", "crc-any", "des", "digest", @@ -1097,8 +1215,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6" dependencies = [ "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", ] [[package]] @@ -1142,9 +1260,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", @@ -1175,18 +1293,41 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.4.2", - "security-framework-sys 2.4.2", + "security-framework", + "security-framework-sys", "tempfile", ] +[[package]] +name = "nb-connect" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1bb540dc6ef51cfe1916ec038ce7a620daf3a111e2502d745197cd53d6bca15" +dependencies = [ + "libc", + "socket2", +] + +[[package]] +name = "nix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + [[package]] name = "notify-rust" -version = "4.5.3" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b2d5d72d16b6abdb6fa2c364d9363e23d6ed7c20c1a1e85fd8cd880144442c" +checksum = "ca6ebab865e67efdd7182a88d76cadbdd2a8d02d1c7a4e16bb7c234016a12cac" dependencies = [ - "dbus 0.9.4", + "dbus", "mac-notification-sys", "winrt-notification", ] @@ -1278,9 +1419,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -1323,9 +1464,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "opaque-debug" @@ -1335,9 +1476,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46b233de7d83bc167fe43ae2dda3b5b84e80e09cceba581e4decb958a4896bf" +checksum = "176ee4b630d174d2da8241336763bb459281dddc0f4d87f72c3b1efc9a6109b7" dependencies = [ "pathdiff", "winapi", @@ -1345,11 +1486,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.36" +version = "0.10.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ - "bitflags 1.3.2", + "bitflags", "cfg-if 1.0.0", "foreign-types", "libc", @@ -1365,9 +1506,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.66" +version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ "autocfg", "cc", @@ -1396,14 +1537,10 @@ dependencies = [ ] [[package]] -name = "parking_lot" -version = "0.10.2" +name = "parking" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" -dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.7.2", -] +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" [[package]] name = "parking_lot" @@ -1412,22 +1549,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", - "lock_api 0.4.5", - "parking_lot_core 0.8.5", -] - -[[package]] -name = "parking_lot_core" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" -dependencies = [ - "cfg-if 0.1.10", - "cloudabi", - "libc", - "redox_syscall 0.1.57", - "smallvec", - "winapi", + "lock_api", + "parking_lot_core", ] [[package]] @@ -1452,9 +1575,9 @@ checksum = "3cacbb3c4ff353b534a67fb8d7524d00229da4cb1dc8c79f4db96e375ab5b619" [[package]] name = "pathdiff" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" @@ -1473,9 +1596,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -1485,15 +1608,28 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "polling" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "log", + "wepoll-ffi", + "winapi", +] [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "pretty_assertions" @@ -1508,49 +1644,49 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.29" +name = "proc-macro-crate" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "unicode-xid 0.2.2", + "toml", ] [[package]] -name = "quick-xml" -version = "0.20.0" +name = "proc-macro-crate" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26aab6b48e2590e4a64d1ed808749ba06257882b461d01ca71baeb747074a6dd" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" dependencies = [ - "memchr", + "thiserror", + "toml", ] [[package]] -name = "quote" -version = "0.3.15" +name = "proc-macro2" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] [[package]] -name = "quote" -version = "1.0.9" +name = "quick-xml" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "26aab6b48e2590e4a64d1ed808749ba06257882b461d01ca71baeb747074a6dd" dependencies = [ - "proc-macro2", + "memchr", ] [[package]] -name = "rand" -version = "0.7.3" +name = "quote" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", + "proc-macro2", ] [[package]] @@ -1560,19 +1696,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.3", - "rand_hc 0.3.1", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", + "rand_hc", ] [[package]] @@ -1582,16 +1708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -1603,22 +1720,13 @@ dependencies = [ "getrandom 0.2.3", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "rand_hc" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ - "rand_core 0.6.3", + "rand_core", ] [[package]] @@ -1633,7 +1741,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] @@ -1742,9 +1850,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.4" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +checksum = "7c4e0a76dc12a116108933f6301b95e83634e0c47b0afbed6abbaa0601e99258" dependencies = [ "base64", "bytes", @@ -1838,9 +1946,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "schannel" @@ -1852,6 +1960,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1860,31 +1974,22 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "secret-service" -version = "1.1.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d752040301c251d653aa740dec847e95767ce312cfc469bee85eb13cbf81d8a" +checksum = "2400fb1bf2a87b303ada204946294f932ade4929477e9e2bf66d7b49a66656ec" dependencies = [ - "aes", - "block-modes", - "dbus 0.2.3", + "aes 0.6.0", + "block-modes 0.7.0", "hkdf", "lazy_static", "num", - "rand 0.7.3", + "rand", + "serde", "sha2", -] - -[[package]] -name = "security-framework" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.7.0", - "core-foundation-sys 0.7.0", - "libc", - "security-framework-sys 0.4.3", + "zbus", + "zbus_macros", + "zvariant", + "zvariant_derive", ] [[package]] @@ -1893,21 +1998,11 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.1", - "core-foundation-sys 0.8.2", - "libc", - "security-framework-sys 2.4.2", -] - -[[package]] -name = "security-framework-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" -dependencies = [ - "core-foundation-sys 0.7.0", + "bitflags", + "core-foundation", + "core-foundation-sys", "libc", + "security-framework-sys", ] [[package]] @@ -1916,7 +2011,7 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" dependencies = [ - "core-foundation-sys 0.8.2", + "core-foundation-sys", "libc", ] @@ -1961,9 +2056,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" dependencies = [ "serde_derive", ] @@ -1977,31 +2072,42 @@ dependencies = [ "log", "serde", "thiserror", - "xml-rs 0.8.4", + "xml-rs", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", ] [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" dependencies = [ - "itoa", + "itoa 1.0.1", "ryu", "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.0" @@ -2009,7 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ "form_urlencoded", - "itoa", + "itoa 0.4.8", "ryu", "serde", ] @@ -2021,7 +2127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", - "parking_lot 0.10.2", + "parking_lot", "serial_test_derive", ] @@ -2032,8 +2138,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" dependencies = [ "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", ] [[package]] @@ -2051,9 +2157,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" dependencies = [ "libc", "signal-hook-registry", @@ -2081,9 +2187,9 @@ dependencies = [ [[package]] name = "simplelog" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0" +checksum = "85d04ae642154220ef00ee82c36fb07853c10a4f2a0ca6719f9991211d2eb959" dependencies = [ "chrono", "log", @@ -2092,15 +2198,15 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "smawk" @@ -2120,14 +2226,14 @@ dependencies = [ [[package]] name = "ssh2" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d876d4d57f6bbf2245d43f7ec53759461f801a446d3693704aa6d27b257844d7" +checksum = "269343e64430067a14937ae0e3c4ec604c178fb896dde0964b1acd22b3e2eeb1" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", "libssh2-sys", - "parking_lot 0.10.2", + "parking_lot", ] [[package]] @@ -2141,20 +2247,31 @@ dependencies = [ "wildmatch", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strum" -version = "0.8.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca6e4730f517e041e547ffe23d29daab8de6b73af4b6ae2a002108169f5e7da" +checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" -version = "0.8.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3384590878eb0cab3b128e844412e2d010821e7e091211b9d87324173ada7db8" +checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb" dependencies = [ - "quote 0.3.15", - "syn 0.11.11", + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2179,40 +2296,20 @@ dependencies = [ [[package]] name = "syn" -version = "0.11.11" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" -dependencies = [ - "quote 0.3.15", - "synom", - "unicode-xid 0.0.4", -] - -[[package]] -name = "syn" -version = "1.0.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" +checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" dependencies = [ "proc-macro2", - "quote 1.0.9", - "unicode-xid 0.2.2", -] - -[[package]] -name = "synom" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" -dependencies = [ - "unicode-xid 0.0.4", + "quote", + "unicode-xid", ] [[package]] name = "tar" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f5515d3add52e0bbdcad7b83c388bb36ba7b754dda3b5f5bc2d38640cdba5c" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" dependencies = [ "filetime", "libc", @@ -2227,7 +2324,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand 0.8.4", + "rand", "redox_syscall 0.2.10", "remove_dir_all", "winapi", @@ -2257,7 +2354,7 @@ name = "termscp" version = "0.8.0" dependencies = [ "argh", - "bitflags 1.3.2", + "bitflags", "bytesize", "chrono", "content_inspector", @@ -2271,7 +2368,7 @@ dependencies = [ "notify-rust", "open", "pretty_assertions", - "rand 0.8.4", + "rand", "regex", "remotefs", "remotefs-aws-s3", @@ -2307,22 +2404,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", ] [[package]] @@ -2349,9 +2446,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5241dd6f21443a3606b432718b166d3cedc962fd4b8bea54a8bc7f514ebda986" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -2364,11 +2461,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.11.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -2390,9 +2486,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ "futures-core", "pin-project-lite", @@ -2401,9 +2497,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", @@ -2430,9 +2526,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -2441,9 +2537,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" dependencies = [ "lazy_static", ] @@ -2460,7 +2556,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" dependencies = [ - "bitflags 1.3.2", + "bitflags", "cassowary", "crossterm", "unicode-segmentation", @@ -2469,9 +2565,9 @@ dependencies = [ [[package]] name = "tui-realm-stdlib" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6444ac3cf88c6cbee4267b6999775aa65ef4ddf556587d2154631d74b5d65fc" +checksum = "21740681b37a84d5a2a9cf00eec596bf6893c5e92689e11dd5b7456e284c5d7a" dependencies = [ "textwrap", "tuirealm", @@ -2484,7 +2580,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b1919cf08ce9eda3b968870ecab04267d684adc22f2cdc13bb4a3846c89eab4" dependencies = [ - "bitflags 1.3.2", + "bitflags", "crossterm", "lazy_static", "regex", @@ -2500,15 +2596,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0adcdaf59881626555558eae08f8a53003c8a1961723b4d7a10c51599abbc81" dependencies = [ "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", ] [[package]] name = "typenum" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" @@ -2518,9 +2614,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "unicode-bidi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-linebreak" @@ -2552,12 +2648,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" -[[package]] -name = "unicode-xid" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" - [[package]] name = "unicode-xid" version = "0.2.2" @@ -2594,9 +2684,21 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "waker-fn" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "want" @@ -2627,8 +2729,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if 1.0.0", - "serde", - "serde_json", "wasm-bindgen-macro", ] @@ -2642,8 +2742,8 @@ dependencies = [ "lazy_static", "log", "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", "wasm-bindgen-shared", ] @@ -2665,7 +2765,7 @@ version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ - "quote 1.0.9", + "quote", "wasm-bindgen-macro-support", ] @@ -2676,8 +2776,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2", - "quote 1.0.9", - "syn 1.0.76", + "quote", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2698,6 +2798,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "which" version = "4.2.2" @@ -2711,9 +2820,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.1.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7741161a40200a867c96dfa5574544efa4178cf4c8f770b62dd1cc0362d7ae1" +checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" dependencies = [ "wasm-bindgen", "web-sys", @@ -2757,34 +2866,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "winreg" -version = "0.7.0" +name = "windows" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "a9f39345ae0c8ab072c0ac7fe8a8b411636aa34f89be19ddd0d9226544f13944" dependencies = [ - "winapi", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", ] [[package]] -name = "winrt" -version = "0.4.0" +name = "windows_i686_gnu" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0866510a3eca9aed73a077490bbbf03e5eaac4e1fd70849d89539e5830501fd" + +[[package]] +name = "windows_i686_msvc" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0ffed56b7e9369a29078d2ab3aaeceea48eb58999d2cff3aa2494a275b95c6" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384a173630588044205a2993b6864a2f56e5a8c1e7668c07b93ec18cf4888dc4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30cba82e22b083dc5a422c2ee77e20dc7927271a0dc981360c57c1453cb48d" +checksum = "9bd8f062d8ca5446358159d79a90be12c543b3a965c847c8f3eedf14b321d399" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ "winapi", ] [[package]] name = "winrt-notification" -version = "0.2.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57790eb281688a4682dab44df2a1ba8b78373233bd71cb291c3e75fecb1a01c4" +checksum = "eda101fb8e034a25f3d50a0714d7ca4f234a4fc7bc57427f6d81040db0ccbe6a" dependencies = [ "strum", - "strum_macros", - "winapi", - "winrt", - "xml-rs 0.6.1", + "windows", + "xml-rs", ] [[package]] @@ -2798,18 +2932,44 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.6.1" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "zbus" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1945e12e16b951721d7976520b0832496ef79c31602c7a29d950de79ba74621" +checksum = "2326acc379a3ac4e34b794089f5bdb17086bf29a5fdf619b7b4cc772dc2e9dad" dependencies = [ - "bitflags 0.9.1", + "async-io", + "byteorder", + "derivative", + "enumflags2", + "fastrand", + "futures", + "nb-connect", + "nix", + "once_cell", + "polling", + "scoped-tls", + "serde", + "serde_repr", + "zbus_macros", + "zvariant", ] [[package]] -name = "xml-rs" -version = "0.8.4" +name = "zbus_macros" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "a482c56029e48681b89b92b5db3c446db0915e8dd1052c0328a574eda38d5f93" +dependencies = [ + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zip" @@ -2823,3 +2983,29 @@ dependencies = [ "thiserror", "time", ] + +[[package]] +name = "zvariant" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c7b55f2074489b7e8e07d2d0a6ee6b4f233867a653c664d8020ba53692525" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ca5e22593eb4212382d60d26350065bf2a02c34b85bc850474a74b589a3de9" +dependencies = [ + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index aa1c5951..e8bb5126 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ termscp = { path = "/usr/bin/termscp" } [package.metadata.deb] maintainer = "Christian Visintin " -copyright = "2021, Christian Visintin " +copyright = "2022, Christian Visintin " extended-description-file = "docs/misc/README.deb.txt" [[bin]] @@ -32,7 +32,7 @@ name = "termscp" path = "src/main.rs" [dependencies] -argh = "0.1.6" +argh = "0.1.7" bitflags = "1.3.2" bytesize = "1.1.0" chrono = "0.4.19" @@ -40,7 +40,7 @@ content_inspector = "0.2.4" dirs = "4.0.0" edit = "0.1.3" hostname = "0.3.1" -keyring = { version = "0.10.1", optional = true } +keyring = { version = "1.0.0", optional = true } lazy_static = "1.4.0" log = "0.4.14" magic-crypt = "3.1.7" diff --git a/src/system/keys/keyringstorage.rs b/src/system/keys/keyringstorage.rs index 5226d90e..c2c201dc 100644 --- a/src/system/keys/keyringstorage.rs +++ b/src/system/keys/keyringstorage.rs @@ -28,7 +28,7 @@ // Local use super::{KeyStorage, KeyStorageError}; // Ext -use keyring::{Keyring, KeyringError}; +use keyring::{Entry as Keyring, Error as KeyringError}; /// provides a `KeyStorage` implementation using the keyring crate pub struct KeyringStorage { @@ -53,15 +53,13 @@ impl KeyStorage for KeyringStorage { match storage.get_password() { Ok(s) => Ok(s), Err(e) => match e { - KeyringError::NoPasswordFound => Err(KeyStorageError::NoSuchKey), - #[cfg(target_os = "windows")] - KeyringError::WindowsVaultError => Err(KeyStorageError::NoSuchKey), - #[cfg(target_os = "macos")] - KeyringError::MacOsKeychainError(_) => Err(KeyStorageError::NoSuchKey), - #[cfg(target_os = "linux")] - KeyringError::SecretServiceError(_) => Err(KeyStorageError::ProviderError), - KeyringError::Parse(_) => Err(KeyStorageError::BadSytax), - _ => Err(KeyStorageError::ProviderError), + KeyringError::NoEntry => Err(KeyStorageError::NoSuchKey), + KeyringError::PlatformFailure(_) + | KeyringError::NoStorageAccess(_) + | KeyringError::WrongCredentialPlatform => Err(KeyStorageError::ProviderError), + KeyringError::BadEncoding(_) | KeyringError::TooLong(_, _) => { + Err(KeyStorageError::BadSytax) + } }, } } @@ -84,13 +82,8 @@ impl KeyStorage for KeyringStorage { // Check what kind of error is returned match storage.get_password() { Ok(_) => true, - #[cfg(not(target_os = "linux"))] - Err(err) => !matches!(err, KeyringError::NoBackendFound), - #[cfg(target_os = "linux")] - Err(err) => !matches!( - err, - KeyringError::NoBackendFound | KeyringError::SecretServiceError(_) - ), + Err(KeyringError::NoStorageAccess(_) | KeyringError::PlatformFailure(_)) => false, + Err(_) => true, } } } From 874ba308472434de5107238643349d167ede269c Mon Sep 17 00:00:00 2001 From: veeso Date: Wed, 5 Jan 2022 13:13:41 +0100 Subject: [PATCH 43/45] Updated deps --- CHANGELOG.md | 11 ++++++++++- Cargo.lock | 19 ++++++++----------- Cargo.toml | 23 +++++++++++------------ 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1555099..bdb3a689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,10 +78,19 @@ Released on 06/01/2022 - Added `unicode-width 0.1.8` - Updated `argh` to `0.1.7` - Updated `keyring` to `1.0.0` + - Updated `magic-crypt` to `3.1.9` + - Updated `open` to `2.0.2` + - Updated `notify-rust` to `4.5.5` + - Updated `self_update` to `0.28.0` + - Updated `simplelog` to `0.11.1` + - Updated `tempfile` to `3.2.0` - Updated `tui-realm` to `1.4.2` - - Updated `tui-realm-stdlib` to `1.1.4` + - Updated `tui-realm-stdlib` to `1.1.5` + - Updated `whoami` to `1.2.1` + - Updated `wildmatch` to `2.1.0` - Removed `rust-s3`, `ssh2`, `suppaftp`; replaced by `remotefs 0.2.0`, `remotefs-aws-s3 0.1.0`, `remotefs-ftp 0.1.0` and `remotefs-ssh 0.1.0` - Removed `crossterm` (since bridged by tui-realm) + - Removed `textwrap` (unused) ## 0.7.0 diff --git a/Cargo.lock b/Cargo.lock index f3ef875e..57ec86b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,9 +367,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "regex", "terminal_size", - "unicode-width", "winapi", ] @@ -1035,9 +1033,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.15.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7baab56125e25686df467fe470785512329883aab42696d661247aca2a2896e4" +checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" dependencies = [ "console", "lazy_static", @@ -1429,9 +1427,9 @@ dependencies = [ [[package]] name = "number_prefix" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "objc" @@ -2017,9 +2015,9 @@ dependencies = [ [[package]] name = "self_update" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb85f1802f7b987237b8525c0fde86ea86f31c957c1875467c727d5b921179c" +checksum = "5bc3e89793fe56c82104ddc103c998e4e94713cb975202207829e61031eb4be6" dependencies = [ "either", "flate2", @@ -2187,9 +2185,9 @@ dependencies = [ [[package]] name = "simplelog" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85d04ae642154220ef00ee82c36fb07853c10a4f2a0ca6719f9991211d2eb959" +checksum = "ecabc0118918611790b8615670ab79296272cbe09496b6884b02b1e929c20886" dependencies = [ "chrono", "log", @@ -2380,7 +2378,6 @@ dependencies = [ "serial_test", "simplelog", "tempfile", - "textwrap", "thiserror", "toml", "tui-realm-stdlib", diff --git a/Cargo.toml b/Cargo.toml index e8bb5126..39dfe22f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,9 +43,9 @@ hostname = "0.3.1" keyring = { version = "1.0.0", optional = true } lazy_static = "1.4.0" log = "0.4.14" -magic-crypt = "3.1.7" -notify-rust = { version = "4.5.3", default-features = false, features = [ "d" ] } -open = "2.0.1" +magic-crypt = "3.1.9" +notify-rust = { version = "4.5.5", default-features = false, features = [ "d" ] } +open = "2.0.2" rand = "0.8.4" regex = "1.5.4" remotefs = "^0.2.0" @@ -53,18 +53,17 @@ remotefs-aws-s3 = "^0.1.0" remotefs-ftp = { version = "^0.1.0", features = [ "secure" ] } remotefs-ssh = "^0.1.0" rpassword = "5.0.1" -self_update = { version = "0.27.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] } +self_update = { version = "0.28.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] } serde = { version = "^1.0.0", features = [ "derive" ] } -simplelog = "0.10.0" -tempfile = "3.1.0" -textwrap = "0.14.2" +simplelog = "0.11.1" +tempfile = "3.2.0" thiserror = "^1.0.0" toml = "0.5.8" -tui-realm-stdlib = "^1.1.0" -tuirealm = "^1.2.0" -unicode-width = "^0.1.8" -whoami = "1.1.1" -wildmatch = "2.0.0" +tui-realm-stdlib = "1.1.5" +tuirealm = "1.4.2" +unicode-width = "0.1.8" +whoami = "1.2.1" +wildmatch = "2.1.0" [dev-dependencies] pretty_assertions = "0.7.2" From febd3ac1eff8db0b51cddc273fdc6da91bac3035 Mon Sep 17 00:00:00 2001 From: veeso Date: Wed, 5 Jan 2022 13:20:10 +0100 Subject: [PATCH 44/45] readme --- README.md | 20 ++++++-------------- docs/de/README.md | 9 +-------- docs/es/README.md | 9 +-------- docs/fr/README.md | 9 +-------- docs/it/README.md | 9 +-------- docs/misc/README.deb.txt | 1 - docs/zh-CN/README.md | 9 +-------- 7 files changed, 11 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 203b8592..fe3507b6 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@

Developed by @veeso

-

Current version: 0.8.0 (12/10/2021)

+

Current version: 0.8.0 (06/01/2022)

Entwickelt von @veeso

-

Aktuelle Version: 0.8.0 (12/10/2021)

+

Aktuelle Version: 0.8.0 (06/01/2022)

Desarrollado por @veeso

-

Versión actual: 0.8.0 (12/10/2021)

+

Versión actual: 0.8.0 (06/01/2022)

Développé par @veeso

-

Version actuelle: 0.8.0 (12/10/2021)

+

Version actuelle: 0.8.0 (06/01/2022)

--- -## Problèmes connus 🧻 - -- `NoSuchFileOrDirectory` à la connexion (WSL1): Je connais ce problème et c'est un problème de WSL je suppose. Ne te inquiéte pas, déplace simplement l'exécutable termscp dans un autre emplacement PATH, tel que `/usr/bin`, ou installe-le via le format de package approprié (par exemple, deb). - ---- - ## Contribution et enjeux 🤝🏻 Les contributions, les rapports de bugs, les nouvelles fonctionnalités et les questions sont les bienvenus ! 😉 @@ -266,7 +260,6 @@ termscp est soutenu par ces projets impressionnants: - [self_update](https://github.com/jaemk/self_update) - [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) - [suppaftp](https://github.com/veeso/suppaftp) -- [textwrap](https://github.com/mgeisler/textwrap) - [tui-rs](https://github.com/fdehau/tui-rs) - [tui-realm](https://github.com/veeso/tui-realm) - [whoami](https://github.com/libcala/whoami) diff --git a/docs/it/README.md b/docs/it/README.md index d5f107d2..b72fe15d 100644 --- a/docs/it/README.md +++ b/docs/it/README.md @@ -63,7 +63,7 @@

Sviluppato da @veeso

-

Versione corrente: 0.8.0 (12/10/2021)

+

Versione corrente: 0.8.0 (06/01/2022)

@veeso 开发

-

当前版本: 0.8.0 (12/10/2021)

+

当前版本: 0.8.0 (06/01/2022)

Date: Wed, 5 Jan 2022 13:38:10 +0100 Subject: [PATCH 45/45] use branch in docker build --- Cargo.toml | 3 --- dist/build/docker.sh | 4 ++-- dist/build/x86_64_centos7/Dockerfile | 4 +++- dist/build/x86_64_debian9/Dockerfile | 4 +++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 39dfe22f..4210953c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,8 +78,5 @@ with-keyring = [ "keyring" ] [target."cfg(target_family = \"unix\")".dependencies] users = "0.11.0" -[profile.release] -incremental = true - [profile.dev] incremental = true diff --git a/dist/build/docker.sh b/dist/build/docker.sh index 330b1867..c39dd54c 100755 --- a/dist/build/docker.sh +++ b/dist/build/docker.sh @@ -16,7 +16,7 @@ cd - mkdir -p ${PKGS_DIR}/ # Build x86_64_deb cd x86_64_debian9/ -docker build --tag termscp-${VERSION}-x86_64_debian9 . +docker build --build-arg branch=${VERSION} --tag termscp-${VERSION}-x86_64_debian9 . cd - mkdir -p ${PKGS_DIR}/deb/ mkdir -p ${PKGS_DIR}/x86_64-unknown-linux-gnu/ @@ -30,7 +30,7 @@ rm termscp cd - # Build x86_64_centos7 cd x86_64_centos7/ -docker build --tag termscp-${VERSION}-x86_64_centos7 . +docker build --build-arg branch=${VERSION} --tag termscp-${VERSION}-x86_64_centos7 . cd - mkdir -p ${PKGS_DIR}/rpm/ CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_centos7 /bin/bash) diff --git a/dist/build/x86_64_centos7/Dockerfile b/dist/build/x86_64_centos7/Dockerfile index f11ec5f0..7e7d830f 100644 --- a/dist/build/x86_64_centos7/Dockerfile +++ b/dist/build/x86_64_centos7/Dockerfile @@ -1,5 +1,7 @@ FROM centos:centos7 as builder +ARG branch +ENV branch=$branch WORKDIR /usr/src/ # Install dependencies RUN yum -y install \ @@ -15,7 +17,7 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && chmod +x /tmp/rust.sh && \ /tmp/rust.sh -y # Clone repository -RUN git clone https://github.com/veeso/termscp.git +RUN git clone --branch $branch https://github.com/veeso/termscp.git # Set workdir to termscp WORKDIR /usr/src/termscp/ # Install cargo arxch diff --git a/dist/build/x86_64_debian9/Dockerfile b/dist/build/x86_64_debian9/Dockerfile index dbe1ebdd..c418bfc6 100644 --- a/dist/build/x86_64_debian9/Dockerfile +++ b/dist/build/x86_64_debian9/Dockerfile @@ -1,5 +1,7 @@ FROM debian:stretch +ARG branch +ENV branch=$branch WORKDIR /usr/src/ # Install dependencies RUN apt update && apt install -y \ @@ -17,7 +19,7 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && chmod +x /tmp/rust.sh && \ /tmp/rust.sh -y # Clone repository -RUN git clone https://github.com/veeso/termscp.git +RUN git clone --branch $branch https://github.com/veeso/termscp.git # Set workdir to termscp WORKDIR /usr/src/termscp/ # Install cargo deb