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