Merge pull request #55 from veeso/theme-provider

Theme provider and '-t' and '-c' CLI options
This commit is contained in:
Christian Visintin 2021-07-07 14:38:22 +02:00 committed by GitHub
commit 2231727adb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 5453 additions and 1835 deletions

View file

@ -3,12 +3,13 @@ ignore-not-existing: true
llvm: true
output-type: lcov
ignore:
- "/*"
- "C:/*"
- "../*"
- src/main.rs
- src/lib.rs
- src/activity_manager.rs
- "src/ui/activities/*"
- src/ui/context.rs
- src/ui/input.rs
- "/*"
- "C:/*"
- "../*"
- src/main.rs
- src/lib.rs
- src/activity_manager.rs
- src/support.rs
- "src/ui/activities/*"
- src/ui/context.rs
- src/ui/input.rs

View file

@ -28,6 +28,11 @@ Released on FIXME: ??
- **Open any file** in explorer:
- Open file with default program for file type with `<V>`
- Open file with a specific program with `<W>`
- **Themes**:
- You can now set colors for 25 elements in the application
- Colors can be any RGB, also supports **CSS colors** syntax
- Configure theme from settings or import from CLI using the `-t <theme file>` argument
- You can find several themes in the `themes/` directory
- **Keyring support for Linux**
- From now on keyring will be available for Linux only
- Read the manual to find out if your system supports the keyring and how you can enable it
@ -39,6 +44,7 @@ Released on FIXME: ??
- Just press `<CTRL+R>` when a new version is available from the auth activity to read the release notes
- **Installation script**:
- From now on, in case cargo is used to install termscp, all the cargo dependencies will be installed
- **Start termscp from configuration**: Start termscp with `-c` or `--config` to start termscp from configuration page
- Bugfix:
- Fixed broken input cursor when typing UTF8 characters (tui-realm 0.3.2)
- Dependencies:

View file

@ -43,6 +43,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
- 💁 SFTP/SCP authentication through SSH keys and username/password
- 🐧 Compatible with Windows, Linux, BSD and MacOS
- ✏ Customizable
- Themes
- Custom file explorer format
- Customizable text editor
- Customizable file sorting

BIN
assets/images/themes.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

View file

@ -1,5 +1,30 @@
# User manual 🎓
- [User manual 🎓](#user-manual-)
- [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-)
- [How Password can be provided 🔐](#how-password-can-be-provided-)
- [File explorer 📂](#file-explorer-)
- [Keybindings ⌨](#keybindings-)
- [Work on multiple files 🥷](#work-on-multiple-files-)
- [Synchronized browsing ⏲️](#synchronized-browsing-)
- [Open and Open With 🚪](#open-and-open-with-)
- [Bookmarks ⭐](#bookmarks-)
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
- [Linux Keyring](#linux-keyring)
- [KeepassXC setup for termscp](#keepassxc-setup-for-termscp)
- [Configuration ⚙️](#configuration-)
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [File Explorer Format](#file-explorer-format)
- [Themes 🎨](#themes-)
- [Styles 💈](#styles-)
- [Authentication page](#authentication-page)
- [Transfer page](#transfer-page)
- [Misc](#misc)
- [Text Editor ✏](#text-editor-)
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
- [Logging 🩺](#logging-)
## Usage ❓
termscp can be started with the following options:
@ -7,7 +32,9 @@ termscp can be started with the following options:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
- `-P, --password <password>` if address is provided, password will be this argument
- `-c, --config` Open termscp starting from the configuration page
- `-q, --quiet` Disable logging
- `-t, --theme <path>` Import specified theme
- `-v, --version` Print version info
- `-h, --help` Print help page
@ -281,6 +308,73 @@ If left empty, the default formatter syntax will be used: `{NAME:24} {PEX} {USER
---
## Themes 🎨
Termscp provides you with an awesome feature: the possibility to set the colors for several components in the application.
If you want to customize termscp there are two available ways to do so:
- From the **configuration menu**
- Importing a **theme file**
In order to create your own customization from termscp, all you have to do so is to enter the configuration from the auth activity, pressing `<CTRL+C>` and then `<TAB>` twice. You should have now moved to the `themes` panel.
Here you can move with `<UP>` and `<DOWN>` to change the style you want to change, as shown in the gif below:
![Themes](../assets/images/themes.gif)
termscp supports both the traditional explicit hex (`#rrggbb`) and rgb `rgb(r, g, b)` syntax to provide colors, but also **[css colors](https://www.w3schools.com/cssref/css_colors.asp)** (such as `crimson`) are accepted 😉. There is also a special keywork which is `Default`. Default means that the color used will be the default foreground or background color based on the situation (foreground for texts and lines, background for well, guess what).
As said before, you can also import theme files. You can take inspiration from or directly use one of the themes provided along with termscp, located in the `themes/` directory of this repository and import them running termscp as `termscp -t <theme_file>`. If everything was fine, it should tell you the theme has successfully been imported.
### Styles 💈
You can find in the table below, the description for each style field.
Please, notice that **styles won't apply to configuration page**, in order to make it always accessible in case you mess everything up
#### Authentication page
| Key | Description |
|----------------|------------------------------------------|
| auth_address | Color of the input field for IP address |
| auth_bookmarks | Color of the bookmarks panel |
| auth_password | Color of the input field for password |
| auth_port | Color of the input field for port number |
| auth_protocol | Color of the radio group for protocol |
| auth_recents | Color of the recents panel |
| auth_username | Color of the input field for username |
#### Transfer page
| Key | Description |
|--------------------------------------|---------------------------------------------------------------------------|
| transfer_local_explorer_background | Background color of localhost explorer |
| transfer_local_explorer_foreground | Foreground coloor of localhost explorer |
| transfer_local_explorer_highlighted | Border and highlighted color for localhost explorer |
| transfer_remote_explorer_background | Background color of remote explorer |
| transfer_remote_explorer_foreground | Foreground coloor of remote explorer |
| transfer_remote_explorer_highlighted | Border and highlighted color for remote explorer |
| transfer_log_background | Background color for log panel |
| transfer_log_window | Window color for log panel |
| transfer_progress_bar | Progress bar color |
| transfer_status_hidden | Color for status bar "hidden" label |
| transfer_status_sorting | Color for status bar "sorting" label; applies also to file sorting dialog |
| transfer_status_sync_browsing | Color for status bar "sync browsing" label |
#### Misc
These styles applie to different part of the application.
| Key | Description |
|-------------------|---------------------------------------------|
| misc_error_dialog | Color for error messages |
| misc_input_dialog | Color for input dialogs (such as copy file) |
| misc_keys | Color of text for key strokes |
| misc_quit_dialog | Color for quit dialogs |
| misc_save_dialog | Color for save dialogs |
| misc_warn_dialog | Color for warn dialogs |
---
## Text Editor ✏
termscp has, as you might have noticed, many features, one of these is the possibility to view and edit text file. It doesn't matter if the file is located on the local host or on the remote host, termscp provides the possibility to open a file in your favourite text editor.

View file

@ -30,6 +30,7 @@ use crate::filetransfer::FileTransferProtocol;
use crate::host::{HostError, Localhost};
use crate::system::config_client::ConfigClient;
use crate::system::environment;
use crate::system::theme_provider::ThemeProvider;
use crate::ui::activities::{
auth::AuthActivity, filetransfer::FileTransferActivity, setup::SetupActivity, Activity,
ExitReason,
@ -74,7 +75,8 @@ impl ActivityManager {
(None, Some(err))
}
};
let ctx: Context = Context::new(config_client, error);
let theme_provider: ThemeProvider = Self::init_theme_provider();
let ctx: Context = Context::new(config_client, theme_provider, error);
Ok(ActivityManager {
context: Some(ctx),
local_dir: local_dir.to_path_buf(),
@ -306,7 +308,7 @@ impl ActivityManager {
}
}
None => Err(String::from(
"Your system doesn't support configuration paths",
"Your system doesn't provide a configuration directory",
)),
}
}
@ -316,4 +318,32 @@ impl ActivityManager {
)),
}
}
fn init_theme_provider() -> ThemeProvider {
match environment::init_config_dir() {
Ok(config_dir) => {
match config_dir {
Some(config_dir) => {
// Get config client paths
let theme_path: PathBuf = environment::get_theme_path(config_dir.as_path());
match ThemeProvider::new(theme_path.as_path()) {
Ok(provider) => provider,
Err(err) => {
error!("Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode", theme_path.display(), err);
ThemeProvider::degraded()
}
}
}
None => {
error!("This system doesn't provide a configuration directory; using theme provider in degraded mode");
ThemeProvider::degraded()
}
}
}
Err(err) => {
error!("Could not initialize configuration directory: {}; using theme provider in degraded mode", err);
ThemeProvider::degraded()
}
}
}
}

View file

@ -1,228 +0,0 @@
//! ## Serializer
//!
//! `serializer` is the module which provides the serializer/deserializer for bookmarks
/**
* 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::{SerializerError, SerializerErrorKind, UserHosts};
use std::io::{Read, Write};
pub struct BookmarkSerializer;
impl BookmarkSerializer {
/// ### serialize
///
/// Serialize `UserHosts` into TOML and write content to writable
pub fn serialize(
&self,
mut writable: Box<dyn Write>,
hosts: &UserHosts,
) -> Result<(), SerializerError> {
// Serialize content
let data: String = match toml::ser::to_string(hosts) {
Ok(dt) => dt,
Err(err) => {
return Err(SerializerError::new_ex(
SerializerErrorKind::SerializationError,
err.to_string(),
))
}
};
trace!("Serialized new bookmarks data: {}", data);
// Write file
match writable.write_all(data.as_bytes()) {
Ok(_) => Ok(()),
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
}
}
/// ### deserialize
///
/// Read data from readable and deserialize its content as TOML
pub fn deserialize(&self, mut readable: Box<dyn Read>) -> Result<UserHosts, SerializerError> {
// Read file content
let mut data: String = String::new();
if let Err(err) = readable.read_to_string(&mut data) {
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
));
}
trace!("Read bookmarks from file: {}", data);
// Deserialize
match toml::de::from_str(data.as_str()) {
Ok(bookmarks) => {
debug!("Read bookmarks from file {:?}", bookmarks);
Ok(bookmarks)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::SyntaxError,
err.to_string(),
)),
}
}
}
// Tests
#[cfg(test)]
mod tests {
use super::super::Bookmark;
use super::*;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::io::{Seek, SeekFrom};
#[test]
fn test_bookmarks_serializer_deserialize_ok() {
let toml_file: tempfile::NamedTempFile = create_good_toml();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let deserializer: BookmarkSerializer = BookmarkSerializer {};
let hosts = deserializer.deserialize(Box::new(toml_file));
assert!(hosts.is_ok());
let hosts: UserHosts = hosts.ok().unwrap();
// Verify hosts
// Verify recents
assert_eq!(hosts.recents.len(), 1);
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
assert_eq!(host.address, String::from("172.16.104.10"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SCP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(host.password, None);
// Verify bookmarks
assert_eq!(hosts.bookmarks.len(), 3);
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
assert_eq!(host.address, String::from("192.168.1.31"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
assert_eq!(host.address, String::from("192.168.1.30"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("cvisintin"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
assert_eq!(host.address, String::from("51.23.67.12"));
assert_eq!(host.port, 21);
assert_eq!(host.protocol, String::from("FTPS"));
assert_eq!(host.username, String::from("aws001"));
assert_eq!(host.password, None);
}
#[test]
fn test_bookmarks_serializer_deserialize_nok() {
let toml_file: tempfile::NamedTempFile = create_bad_toml();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let deserializer: BookmarkSerializer = BookmarkSerializer {};
assert!(deserializer.deserialize(Box::new(toml_file)).is_err());
}
#[test]
fn test_bookmarks_serializer_serialize() {
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
// Push two samples
bookmarks.insert(
String::from("raspberrypi2"),
Bookmark {
address: String::from("192.168.1.31"),
port: 22,
protocol: String::from("SFTP"),
username: String::from("root"),
password: None,
},
);
bookmarks.insert(
String::from("msi-estrem"),
Bookmark {
address: String::from("192.168.1.30"),
port: 4022,
protocol: String::from("SFTP"),
username: String::from("cvisintin"),
password: Some(String::from("password")),
},
);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(
String::from("ISO20201215T094000Z"),
Bookmark {
address: String::from("192.168.1.254"),
port: 3022,
protocol: String::from("SCP"),
username: String::from("omar"),
password: Some(String::from("aaa")),
},
);
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
// Serialize
let deserializer: BookmarkSerializer = BookmarkSerializer {};
let hosts: UserHosts = UserHosts { bookmarks, recents };
assert!(deserializer.serialize(Box::new(tmpfile), &hosts).is_ok());
}
fn create_good_toml() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents]
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
tmpfile
}
fn create_bad_toml() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents]
ISO20201215T094000Z = { address = "172.16.104.10", protocol = "SCP", username = "root", port = 22 }
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
}

View file

@ -25,11 +25,8 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
pub mod serializer;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserHosts
@ -53,66 +50,15 @@ pub struct Bookmark {
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password
}
// Errors
/// ## SerializerError
///
/// Contains the error for serializer/deserializer
#[derive(std::fmt::Debug)]
pub struct SerializerError {
kind: SerializerErrorKind,
msg: Option<String>,
}
/// ## SerializerErrorKind
///
/// Describes the kind of error for the serializer/deserializer
#[derive(Error, Debug)]
pub enum SerializerErrorKind {
#[error("IO error")]
IoError,
#[error("Serialization error")]
SerializationError,
#[error("Syntax error")]
SyntaxError,
}
impl Default for UserHosts {
fn default() -> Self {
UserHosts {
Self {
bookmarks: HashMap::new(),
recents: HashMap::new(),
}
}
}
impl SerializerError {
/// ### new
///
/// Instantiate a new `SerializerError`
pub fn new(kind: SerializerErrorKind) -> SerializerError {
SerializerError { kind, msg: None }
}
/// ### new_ex
///
/// Instantiates a new `SerializerError` with description message
pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError {
let mut err: SerializerError = SerializerError::new(kind);
err.msg = Some(msg);
err
}
}
impl std::fmt::Display for SerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.msg {
Some(msg) => write!(f, "{} ({})", self.kind, msg),
None => write!(f, "{}", self.kind),
}
}
}
// Tests
#[cfg(test)]
@ -121,6 +67,13 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_bookmarks_default() {
let bookmarks: UserHosts = UserHosts::default();
assert_eq!(bookmarks.bookmarks.len(), 0);
assert_eq!(bookmarks.recents.len(), 0);
}
#[test]
fn test_bookmarks_bookmark_new() {
let bookmark: Bookmark = Bookmark {
@ -168,30 +121,4 @@ mod tests {
String::from("password")
);
}
#[test]
fn test_bookmarks_bookmark_errors() {
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
assert!(error.msg.is_none());
assert_eq!(format!("{}", error), String::from("Syntax error"));
let error: SerializerError =
SerializerError::new_ex(SerializerErrorKind::SyntaxError, String::from("bad syntax"));
assert!(error.msg.is_some());
assert_eq!(
format!("{}", error),
String::from("Syntax error (bad syntax)")
);
// Fmt
assert_eq!(
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
String::from("IO error")
);
assert_eq!(
format!(
"{}",
SerializerError::new(SerializerErrorKind::SerializationError)
),
String::from("Serialization error")
);
}
}

View file

@ -1,6 +1,6 @@
//! ## Config
//!
//! `config` is the module which provides access to termscp configuration
//! `config` is the module which provides access to all the termscp configurations
/**
* MIT License
@ -25,237 +25,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Modules
pub mod serializer;
// export
pub use params::*;
// Locals
use crate::filetransfer::FileTransferProtocol;
// Ext
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserConfig
///
/// UserConfig contains all the configurations for the user,
/// supported by termscp
pub struct UserConfig {
pub user_interface: UserInterfaceConfig,
pub remote: RemoteConfig,
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserInterfaceConfig
///
/// UserInterfaceConfig provides all the keys to configure the user interface
pub struct UserInterfaceConfig {
pub text_editor: PathBuf,
pub default_protocol: String,
pub show_hidden_files: bool,
pub check_for_updates: Option<bool>, // @! Since 0.3.3
pub group_dirs: Option<String>,
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## RemoteConfig
///
/// Contains configuratio related to remote hosts
pub struct RemoteConfig {
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
}
impl Default for UserConfig {
fn default() -> Self {
UserConfig {
user_interface: UserInterfaceConfig::default(),
remote: RemoteConfig::default(),
}
}
}
impl Default for UserInterfaceConfig {
fn default() -> Self {
UserInterfaceConfig {
text_editor: match edit::get_editor() {
Ok(p) => p,
Err(_) => PathBuf::from("nano"), // Default to nano
},
default_protocol: FileTransferProtocol::Sftp.to_string(),
show_hidden_files: false,
check_for_updates: Some(true),
group_dirs: None,
file_fmt: None,
remote_file_fmt: None,
}
}
}
impl Default for RemoteConfig {
fn default() -> Self {
RemoteConfig {
ssh_keys: HashMap::new(),
}
}
}
// Errors
/// ## SerializerError
///
/// Contains the error for serializer/deserializer
#[derive(std::fmt::Debug)]
pub struct SerializerError {
kind: SerializerErrorKind,
msg: Option<String>,
}
/// ## SerializerErrorKind
///
/// Describes the kind of error for the serializer/deserializer
#[derive(Error, Debug)]
pub enum SerializerErrorKind {
#[error("IO error")]
IoError,
#[error("Serialization error")]
SerializationError,
#[error("Syntax error")]
SyntaxError,
}
impl SerializerError {
/// ### new
///
/// Instantiate a new `SerializerError`
pub fn new(kind: SerializerErrorKind) -> SerializerError {
SerializerError { kind, msg: None }
}
/// ### new_ex
///
/// Instantiates a new `SerializerError` with description message
pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError {
let mut err: SerializerError = SerializerError::new(kind);
err.msg = Some(msg);
err
}
}
impl std::fmt::Display for SerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.msg {
Some(msg) => write!(f, "{} ({})", self.kind, msg),
None => write!(f, "{}", self.kind),
}
}
}
// Tests
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::env;
#[test]
fn test_config_mod_new() {
let mut keys: HashMap<String, PathBuf> = HashMap::with_capacity(1);
keys.insert(
String::from("192.168.1.31"),
PathBuf::from("/tmp/private.key"),
);
let remote: RemoteConfig = RemoteConfig { ssh_keys: keys };
let ui: UserInterfaceConfig = UserInterfaceConfig {
default_protocol: String::from("SFTP"),
text_editor: PathBuf::from("nano"),
show_hidden_files: true,
check_for_updates: Some(true),
group_dirs: Some(String::from("first")),
file_fmt: Some(String::from("{NAME}")),
remote_file_fmt: Some(String::from("{USER}")),
};
assert_eq!(ui.default_protocol, String::from("SFTP"));
assert_eq!(ui.text_editor, PathBuf::from("nano"));
assert_eq!(ui.show_hidden_files, true);
assert_eq!(ui.check_for_updates, Some(true));
assert_eq!(ui.group_dirs, Some(String::from("first")));
assert_eq!(ui.file_fmt, Some(String::from("{NAME}")));
let cfg: UserConfig = UserConfig {
user_interface: ui,
remote: remote,
};
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/tmp/private.key")
);
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates, Some(true));
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
assert_eq!(
cfg.user_interface.remote_file_fmt,
Some(String::from("{USER}"))
);
}
#[test]
fn test_config_mod_new_default() {
// Force vim editor
env::set_var(String::from("EDITOR"), String::from("vim"));
// Get default
let cfg: UserConfig = UserConfig::default();
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
// Text editor
#[cfg(target_os = "windows")]
assert_eq!(
PathBuf::from(cfg.user_interface.text_editor.file_name().unwrap()), // NOTE: since edit 0.1.3 real path is used
PathBuf::from("vim.EXE")
);
#[cfg(target_family = "unix")]
assert_eq!(
PathBuf::from(cfg.user_interface.text_editor.file_name().unwrap()), // NOTE: since edit 0.1.3 real path is used
PathBuf::from("vim")
);
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.remote.ssh_keys.len(), 0);
assert!(cfg.user_interface.file_fmt.is_none());
assert!(cfg.user_interface.remote_file_fmt.is_none());
}
#[test]
fn test_config_mod_errors() {
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
assert!(error.msg.is_none());
assert_eq!(format!("{}", error), String::from("Syntax error"));
let error: SerializerError =
SerializerError::new_ex(SerializerErrorKind::SyntaxError, String::from("bad syntax"));
assert!(error.msg.is_some());
assert_eq!(
format!("{}", error),
String::from("Syntax error (bad syntax)")
);
// Fmt
assert_eq!(
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
String::from("IO error")
);
assert_eq!(
format!(
"{}",
SerializerError::new(SerializerErrorKind::SerializationError)
),
String::from("Serialization error")
);
}
}
pub mod bookmarks;
pub mod params;
pub mod serialization;
pub mod themes;

155
src/config/params.rs Normal file
View file

@ -0,0 +1,155 @@
//! ## Config
//!
//! `config` is the module which provides access to termscp configuration
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use crate::filetransfer::FileTransferProtocol;
// Ext
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserConfig
///
/// UserConfig contains all the configurations for the user,
/// supported by termscp
pub struct UserConfig {
pub user_interface: UserInterfaceConfig,
pub remote: RemoteConfig,
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserInterfaceConfig
///
/// UserInterfaceConfig provides all the keys to configure the user interface
pub struct UserInterfaceConfig {
pub text_editor: PathBuf,
pub default_protocol: String,
pub show_hidden_files: bool,
pub check_for_updates: Option<bool>, // @! Since 0.3.3
pub group_dirs: Option<String>,
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## RemoteConfig
///
/// Contains configuratio related to remote hosts
pub struct RemoteConfig {
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
}
impl Default for UserConfig {
fn default() -> Self {
UserConfig {
user_interface: UserInterfaceConfig::default(),
remote: RemoteConfig::default(),
}
}
}
impl Default for UserInterfaceConfig {
fn default() -> Self {
UserInterfaceConfig {
text_editor: match edit::get_editor() {
Ok(p) => p,
Err(_) => PathBuf::from("nano"), // Default to nano
},
default_protocol: FileTransferProtocol::Sftp.to_string(),
show_hidden_files: false,
check_for_updates: Some(true),
group_dirs: None,
file_fmt: None,
remote_file_fmt: None,
}
}
}
impl Default for RemoteConfig {
fn default() -> Self {
RemoteConfig {
ssh_keys: HashMap::new(),
}
}
}
// Tests
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_config_mod_new() {
let mut keys: HashMap<String, PathBuf> = HashMap::with_capacity(1);
keys.insert(
String::from("192.168.1.31"),
PathBuf::from("/tmp/private.key"),
);
let remote: RemoteConfig = RemoteConfig { ssh_keys: keys };
let ui: UserInterfaceConfig = UserInterfaceConfig {
default_protocol: String::from("SFTP"),
text_editor: PathBuf::from("nano"),
show_hidden_files: true,
check_for_updates: Some(true),
group_dirs: Some(String::from("first")),
file_fmt: Some(String::from("{NAME}")),
remote_file_fmt: Some(String::from("{USER}")),
};
assert_eq!(ui.default_protocol, String::from("SFTP"));
assert_eq!(ui.text_editor, PathBuf::from("nano"));
assert_eq!(ui.show_hidden_files, true);
assert_eq!(ui.check_for_updates, Some(true));
assert_eq!(ui.group_dirs, Some(String::from("first")));
assert_eq!(ui.file_fmt, Some(String::from("{NAME}")));
let cfg: UserConfig = UserConfig {
user_interface: ui,
remote: remote,
};
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/tmp/private.key")
);
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates, Some(true));
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
assert_eq!(
cfg.user_interface.remote_file_fmt,
Some(String::from("{USER}"))
);
}
}

574
src/config/serialization.rs Normal file
View file

@ -0,0 +1,574 @@
//! ## Serialization
//!
//! `serialization` provides serialization and deserialization for configurations
/**
* 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 serde::{de::DeserializeOwned, Serialize};
use std::io::{Read, Write};
use thiserror::Error;
/// ## SerializerError
///
/// Contains the error for serializer/deserializer
#[derive(std::fmt::Debug)]
pub struct SerializerError {
kind: SerializerErrorKind,
msg: Option<String>,
}
/// ## SerializerErrorKind
///
/// Describes the kind of error for the serializer/deserializer
#[derive(Error, Debug)]
pub enum SerializerErrorKind {
#[error("Operation failed")]
GenericError,
#[error("IO error")]
IoError,
#[error("Serialization error")]
SerializationError,
#[error("Syntax error")]
SyntaxError,
}
impl SerializerError {
/// ### new
///
/// Instantiate a new `SerializerError`
pub fn new(kind: SerializerErrorKind) -> SerializerError {
SerializerError { kind, msg: None }
}
/// ### new_ex
///
/// Instantiates a new `SerializerError` with description message
pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError {
let mut err: SerializerError = SerializerError::new(kind);
err.msg = Some(msg);
err
}
}
impl std::fmt::Display for SerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.msg {
Some(msg) => write!(f, "{} ({})", self.kind, msg),
None => write!(f, "{}", self.kind),
}
}
}
/// ### serialize
///
/// Serialize `UserHosts` into TOML and write content to writable
pub fn serialize<S>(serializable: &S, mut writable: Box<dyn Write>) -> Result<(), SerializerError>
where
S: Serialize + Sized,
{
// Serialize content
let data: String = match toml::ser::to_string(serializable) {
Ok(dt) => dt,
Err(err) => {
return Err(SerializerError::new_ex(
SerializerErrorKind::SerializationError,
err.to_string(),
))
}
};
trace!("Serialized new bookmarks data: {}", data);
// Write file
match writable.write_all(data.as_bytes()) {
Ok(_) => Ok(()),
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
}
}
/// ### deserialize
///
/// Read data from readable and deserialize its content as TOML
pub fn deserialize<S>(mut readable: Box<dyn Read>) -> Result<S, SerializerError>
where
S: DeserializeOwned + Sized + std::fmt::Debug,
{
// Read file content
let mut data: String = String::new();
if let Err(err) = readable.read_to_string(&mut data) {
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
));
}
trace!("Read bookmarks from file: {}", data);
// Deserialize
match toml::de::from_str(data.as_str()) {
Ok(deserialized) => {
debug!("Read bookmarks from file {:?}", deserialized);
Ok(deserialized)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::SyntaxError,
err.to_string(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::io::{Seek, SeekFrom};
use std::path::PathBuf;
use tuirealm::tui::style::Color;
use crate::config::bookmarks::{Bookmark, UserHosts};
use crate::config::params::UserConfig;
use crate::config::themes::Theme;
use crate::utils::test_helpers::create_file_ioers;
#[test]
fn test_config_serialization_errors() {
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
assert!(error.msg.is_none());
assert_eq!(format!("{}", error), String::from("Syntax error"));
let error: SerializerError =
SerializerError::new_ex(SerializerErrorKind::SyntaxError, String::from("bad syntax"));
assert!(error.msg.is_some());
assert_eq!(
format!("{}", error),
String::from("Syntax error (bad syntax)")
);
// Fmt
assert_eq!(
format!(
"{}",
SerializerError::new(SerializerErrorKind::GenericError)
),
String::from("Operation failed")
);
assert_eq!(
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
String::from("IO error")
);
assert_eq!(
format!(
"{}",
SerializerError::new(SerializerErrorKind::SerializationError)
),
String::from("Serialization error")
);
}
// -- Serialization of params
#[test]
fn test_config_serialization_params_deserialize_ok() {
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks_params();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let cfg = deserialize(Box::new(toml_file));
assert!(cfg.is_ok());
let cfg: UserConfig = cfg.ok().unwrap();
// Verify configuration
// Verify ui
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
assert_eq!(
cfg.user_interface.file_fmt,
Some(String::from("{NAME} {PEX}"))
);
assert_eq!(
cfg.user_interface.remote_file_fmt,
Some(String::from("{NAME} {USER}")),
);
// Verify keys
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/raspberry.key")
);
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.32"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/beaglebone.key")
);
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
}
#[test]
fn test_config_serialization_params_deserialize_ok_no_opts() {
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks_params_no_opts();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let cfg = deserialize(Box::new(toml_file));
assert!(cfg.is_ok());
let cfg: UserConfig = cfg.ok().unwrap();
// Verify configuration
// Verify ui
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.group_dirs, None);
assert!(cfg.user_interface.check_for_updates.is_none());
assert!(cfg.user_interface.file_fmt.is_none());
assert!(cfg.user_interface.remote_file_fmt.is_none());
// Verify keys
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/raspberry.key")
);
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.32"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/beaglebone.key")
);
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
}
#[test]
fn test_config_serialization_params_deserialize_nok() {
let toml_file: tempfile::NamedTempFile = create_bad_toml_bookmarks_params();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
assert!(deserialize::<UserConfig>(Box::new(toml_file)).is_err());
}
#[test]
fn test_config_serialization_params_serialize() {
let mut cfg: UserConfig = UserConfig::default();
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
// Insert key
cfg.remote.ssh_keys.insert(
String::from("192.168.1.31"),
PathBuf::from("/home/omar/.ssh/id_rsa"),
);
// Serialize
let writer: Box<dyn Write> = Box::new(std::fs::File::create(toml_file.path()).unwrap());
assert!(serialize(&cfg, writer).is_ok());
// Reload configuration and check if it's ok
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
assert!(deserialize::<UserConfig>(Box::new(toml_file)).is_ok());
}
#[test]
fn test_config_serialization_params_fail_write() {
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
let writer: Box<dyn Write> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
// Try to write unexisting file
let cfg: UserConfig = UserConfig::default();
assert!(serialize(&cfg, writer).is_err());
}
#[test]
fn test_config_serialization_params_fail_read() {
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
let reader: Box<dyn Read> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
// Try to write unexisting file
assert!(deserialize::<UserConfig>(reader).is_err());
}
fn create_good_toml_bookmarks_params() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SCP"
text_editor = "vim"
show_hidden_files = true
check_for_updates = true
group_dirs = "last"
file_fmt = "{NAME} {PEX}"
remote_file_fmt = "{NAME} {USER}"
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_good_toml_bookmarks_params_no_opts() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SCP"
text_editor = "vim"
show_hidden_files = true
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_bad_toml_bookmarks_params() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SFTP"
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
// -- bookmarks
#[test]
fn test_config_serializer_bookmarks_serializer_deserialize_ok() {
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let hosts = deserialize(Box::new(toml_file));
assert!(hosts.is_ok());
let hosts: UserHosts = hosts.ok().unwrap();
// Verify hosts
// Verify recents
assert_eq!(hosts.recents.len(), 1);
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
assert_eq!(host.address, String::from("172.16.104.10"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SCP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(host.password, None);
// Verify bookmarks
assert_eq!(hosts.bookmarks.len(), 3);
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
assert_eq!(host.address, String::from("192.168.1.31"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
assert_eq!(host.address, String::from("192.168.1.30"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("cvisintin"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
assert_eq!(host.address, String::from("51.23.67.12"));
assert_eq!(host.port, 21);
assert_eq!(host.protocol, String::from("FTPS"));
assert_eq!(host.username, String::from("aws001"));
assert_eq!(host.password, None);
}
#[test]
fn test_config_serializer_bookmarks_serializer_deserialize_nok() {
let toml_file: tempfile::NamedTempFile = create_bad_toml_bookmarks();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
assert!(deserialize::<UserHosts>(Box::new(toml_file)).is_err());
}
#[test]
fn test_config_serializer_bookmarks_serializer_serialize() {
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
// Push two samples
bookmarks.insert(
String::from("raspberrypi2"),
Bookmark {
address: String::from("192.168.1.31"),
port: 22,
protocol: String::from("SFTP"),
username: String::from("root"),
password: None,
},
);
bookmarks.insert(
String::from("msi-estrem"),
Bookmark {
address: String::from("192.168.1.30"),
port: 4022,
protocol: String::from("SFTP"),
username: String::from("cvisintin"),
password: Some(String::from("password")),
},
);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(
String::from("ISO20201215T094000Z"),
Bookmark {
address: String::from("192.168.1.254"),
port: 3022,
protocol: String::from("SCP"),
username: String::from("omar"),
password: Some(String::from("aaa")),
},
);
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
// Serialize
let hosts: UserHosts = UserHosts { bookmarks, recents };
assert!(serialize(&hosts, Box::new(tmpfile)).is_ok());
}
#[test]
fn test_config_serialization_theme_serialize() {
let mut theme: Theme = Theme::default();
theme.auth_address = Color::Rgb(240, 240, 240);
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let (reader, writer) = create_file_ioers(tmpfile.path());
assert!(serialize(&theme, Box::new(writer)).is_ok());
// Try to deserialize
let deserialized_theme: Theme = deserialize(Box::new(reader)).ok().unwrap();
assert_eq!(theme, deserialized_theme);
}
#[test]
fn test_config_serialization_theme_deserialize() {
let toml_file = create_good_toml_theme();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
let toml_file = create_bad_toml_theme();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
}
fn create_good_toml_bookmarks() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents]
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
tmpfile
}
fn create_bad_toml_bookmarks() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents]
ISO20201215T094000Z = { address = "172.16.104.10", protocol = "SCP", username = "root", port = 22 }
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_good_toml_theme() -> tempfile::NamedTempFile {
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r##"auth_address = "Yellow"
auth_bookmarks = "LightGreen"
auth_password = "LightBlue"
auth_port = "LightCyan"
auth_protocol = "LightGreen"
auth_recents = "LightBlue"
auth_username = "LightMagenta"
misc_error_dialog = "Red"
misc_input_dialog = "240,240,240"
misc_keys = "Cyan"
misc_quit_dialog = "Yellow"
misc_save_dialog = "Cyan"
misc_warn_dialog = "LightRed"
transfer_local_explorer_background = "rgb(240, 240, 240)"
transfer_local_explorer_foreground = "rgb(60, 60, 60)"
transfer_local_explorer_highlighted = "Yellow"
transfer_log_background = "255, 255, 255"
transfer_log_window = "LightGreen"
transfer_progress_bar = "Green"
transfer_remote_explorer_background = "#f0f0f0"
transfer_remote_explorer_foreground = "rgb(40, 40, 40)"
transfer_remote_explorer_highlighted = "LightBlue"
transfer_status_hidden = "LightBlue"
transfer_status_sorting = "LightYellow"
transfer_status_sync_browsing = "LightGreen"
"##;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_bad_toml_theme() -> tempfile::NamedTempFile {
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
auth_address = "Yellow"
auth_bookmarks = "LightGreen"
auth_password = "LightBlue"
auth_port = "LightCyan"
auth_protocol = "LightGreen"
auth_recents = "LightBlue"
auth_username = "LightMagenta"
misc_error_dialog = "Red"
misc_input_dialog = "240,240,240"
misc_keys = "Cyan"
misc_quit_dialog = "Yellow"
misc_warn_dialog = "LightRed"
transfer_local_explorer_text = "rgb(240, 240, 240)"
transfer_local_explorer_window = "Yellow"
transfer_log_text = "255, 255, 255"
transfer_log_window = "LightGreen"
transfer_progress_bar = "Green"
transfer_remote_explorer_text = "verdazzurro"
transfer_remote_explorer_window = "LightBlue"
transfer_status_hidden = "LightBlue"
transfer_status_sorting = "LightYellow"
transfer_status_sync_browsing = "LightGreen"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
}

View file

@ -1,281 +0,0 @@
//! ## Serializer
//!
//! `serializer` is the module which provides the serializer/deserializer for configuration
/**
* 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::{SerializerError, SerializerErrorKind, UserConfig};
use std::io::{Read, Write};
pub struct ConfigSerializer;
impl ConfigSerializer {
/// ### serialize
///
/// Serialize `UserConfig` into TOML and write content to writable
pub fn serialize(
&self,
mut writable: Box<dyn Write>,
cfg: &UserConfig,
) -> Result<(), SerializerError> {
// Serialize content
let data: String = match toml::ser::to_string(cfg) {
Ok(dt) => dt,
Err(err) => {
return Err(SerializerError::new_ex(
SerializerErrorKind::SerializationError,
err.to_string(),
))
}
};
trace!("Serialized new configuration data: {}", data);
// Write file
match writable.write_all(data.as_bytes()) {
Ok(_) => Ok(()),
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
}
}
/// ### deserialize
///
/// Read data from readable and deserialize its content as TOML
pub fn deserialize(&self, mut readable: Box<dyn Read>) -> Result<UserConfig, SerializerError> {
// Read file content
let mut data: String = String::new();
if let Err(err) = readable.read_to_string(&mut data) {
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
));
}
trace!("Read configuration from file: {}", data);
// Deserialize
match toml::de::from_str(data.as_str()) {
Ok(config) => {
debug!("Read config from file {:?}", config);
Ok(config)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::SyntaxError,
err.to_string(),
)),
}
}
}
// Tests
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::io::{Seek, SeekFrom};
use std::path::PathBuf;
#[test]
fn test_config_serializer_deserialize_ok() {
let toml_file: tempfile::NamedTempFile = create_good_toml();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let deserializer: ConfigSerializer = ConfigSerializer {};
let cfg = deserializer.deserialize(Box::new(toml_file));
assert!(cfg.is_ok());
let cfg: UserConfig = cfg.ok().unwrap();
// Verify configuration
// Verify ui
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
assert_eq!(
cfg.user_interface.file_fmt,
Some(String::from("{NAME} {PEX}"))
);
assert_eq!(
cfg.user_interface.remote_file_fmt,
Some(String::from("{NAME} {USER}")),
);
// Verify keys
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/raspberry.key")
);
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.32"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/beaglebone.key")
);
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
}
#[test]
fn test_config_serializer_deserialize_ok_no_opts() {
let toml_file: tempfile::NamedTempFile = create_good_toml_no_opts();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let deserializer: ConfigSerializer = ConfigSerializer {};
let cfg = deserializer.deserialize(Box::new(toml_file));
assert!(cfg.is_ok());
let cfg: UserConfig = cfg.ok().unwrap();
// Verify configuration
// Verify ui
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.group_dirs, None);
assert!(cfg.user_interface.check_for_updates.is_none());
assert!(cfg.user_interface.file_fmt.is_none());
assert!(cfg.user_interface.remote_file_fmt.is_none());
// Verify keys
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/raspberry.key")
);
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.32"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/beaglebone.key")
);
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
}
#[test]
fn test_config_serializer_deserialize_nok() {
let toml_file: tempfile::NamedTempFile = create_bad_toml();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let deserializer: ConfigSerializer = ConfigSerializer {};
assert!(deserializer.deserialize(Box::new(toml_file)).is_err());
}
#[test]
fn test_config_serializer_serialize() {
let mut cfg: UserConfig = UserConfig::default();
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
// Insert key
cfg.remote.ssh_keys.insert(
String::from("192.168.1.31"),
PathBuf::from("/home/omar/.ssh/id_rsa"),
);
// Serialize
let serializer: ConfigSerializer = ConfigSerializer {};
let writer: Box<dyn Write> = Box::new(std::fs::File::create(toml_file.path()).unwrap());
assert!(serializer.serialize(writer, &cfg).is_ok());
// Reload configuration and check if it's ok
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
assert!(serializer.deserialize(Box::new(toml_file)).is_ok());
}
#[test]
fn test_config_serializer_fail_write() {
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
let writer: Box<dyn Write> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
// Try to write unexisting file
let serializer: ConfigSerializer = ConfigSerializer {};
let cfg: UserConfig = UserConfig::default();
assert!(serializer.serialize(writer, &cfg).is_err());
}
#[test]
fn test_config_serializer_fail_read() {
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
let reader: Box<dyn Read> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
// Try to write unexisting file
let serializer: ConfigSerializer = ConfigSerializer {};
assert!(serializer.deserialize(reader).is_err());
}
fn create_good_toml() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SCP"
text_editor = "vim"
show_hidden_files = true
check_for_updates = true
group_dirs = "last"
file_fmt = "{NAME} {PEX}"
remote_file_fmt = "{NAME} {USER}"
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_good_toml_no_opts() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SCP"
text_editor = "vim"
show_hidden_files = true
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_bad_toml() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SFTP"
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
}

260
src/config/themes.rs Normal file
View file

@ -0,0 +1,260 @@
//! ## Themes
//!
//! `themes` is the module which provides the themes configurations and the serializers
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use crate::utils::fmt::fmt_color;
use crate::utils::parser::parse_color;
// ext
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
use tuirealm::tui::style::Color;
/// ### Theme
///
/// Theme contains all the colors lookup table for termscp
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Theme {
// -- auth
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_address: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_bookmarks: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_password: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_port: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_protocol: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_recents: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_username: Color,
// -- misc
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_error_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_input_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_keys: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_quit_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_save_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_warn_dialog: Color,
// -- transfer
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_background: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_foreground: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_highlighted: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_log_background: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_log_window: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_progress_bar: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_background: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_foreground: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_highlighted: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_hidden: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_sorting: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_sync_browsing: Color,
}
impl Default for Theme {
fn default() -> Self {
Self {
auth_address: Color::Yellow,
auth_bookmarks: Color::LightGreen,
auth_password: Color::LightBlue,
auth_port: Color::LightCyan,
auth_protocol: Color::LightGreen,
auth_recents: Color::LightBlue,
auth_username: Color::LightMagenta,
misc_error_dialog: Color::Red,
misc_input_dialog: Color::Reset,
misc_keys: Color::Cyan,
misc_quit_dialog: Color::Yellow,
misc_save_dialog: Color::LightCyan,
misc_warn_dialog: Color::LightRed,
transfer_local_explorer_background: Color::Reset,
transfer_local_explorer_foreground: Color::Reset,
transfer_local_explorer_highlighted: Color::Yellow,
transfer_log_background: Color::Reset,
transfer_log_window: Color::LightGreen,
transfer_progress_bar: Color::Green,
transfer_remote_explorer_background: Color::Reset,
transfer_remote_explorer_foreground: Color::Reset,
transfer_remote_explorer_highlighted: Color::LightBlue,
transfer_status_hidden: Color::LightBlue,
transfer_status_sorting: Color::LightYellow,
transfer_status_sync_browsing: Color::LightGreen,
}
}
}
// -- deserializer
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: Deserializer<'de>,
{
let s: &str = Deserialize::deserialize(deserializer)?;
// Parse color
match parse_color(s) {
None => Err(DeError::custom("Invalid color")),
Some(color) => Ok(color),
}
}
fn serialize_color<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Convert color to string
let s: String = fmt_color(color);
serializer.serialize_str(s.as_str())
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_config_themes_default() {
let theme: Theme = Theme::default();
assert_eq!(theme.auth_address, Color::Yellow);
assert_eq!(theme.auth_bookmarks, Color::LightGreen);
assert_eq!(theme.auth_password, Color::LightBlue);
assert_eq!(theme.auth_port, Color::LightCyan);
assert_eq!(theme.auth_protocol, Color::LightGreen);
assert_eq!(theme.auth_recents, Color::LightBlue);
assert_eq!(theme.auth_username, Color::LightMagenta);
assert_eq!(theme.misc_error_dialog, Color::Red);
assert_eq!(theme.misc_input_dialog, Color::Reset);
assert_eq!(theme.misc_keys, Color::Cyan);
assert_eq!(theme.misc_quit_dialog, Color::Yellow);
assert_eq!(theme.misc_save_dialog, Color::LightCyan);
assert_eq!(theme.misc_warn_dialog, Color::LightRed);
assert_eq!(theme.transfer_local_explorer_background, Color::Reset);
assert_eq!(theme.transfer_local_explorer_foreground, Color::Reset);
assert_eq!(theme.transfer_local_explorer_highlighted, Color::Yellow);
assert_eq!(theme.transfer_log_background, Color::Reset);
assert_eq!(theme.transfer_log_window, Color::LightGreen);
assert_eq!(theme.transfer_progress_bar, Color::Green);
assert_eq!(theme.transfer_remote_explorer_background, Color::Reset);
assert_eq!(theme.transfer_remote_explorer_foreground, Color::Reset);
assert_eq!(theme.transfer_remote_explorer_highlighted, Color::LightBlue);
assert_eq!(theme.transfer_status_hidden, Color::LightBlue);
assert_eq!(theme.transfer_status_sorting, Color::LightYellow);
assert_eq!(theme.transfer_status_sync_browsing, Color::LightGreen);
}
}

View file

@ -64,11 +64,11 @@ extern crate whoami;
extern crate wildmatch;
pub mod activity_manager;
pub mod bookmarks;
pub mod config;
pub mod filetransfer;
pub mod fs;
pub mod host;
pub mod support;
pub mod system;
pub mod ui;
pub mod utils;

View file

@ -40,16 +40,16 @@ extern crate rpassword;
// External libs
use getopts::Options;
use std::env;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::time::Duration;
// Include
mod activity_manager;
mod bookmarks;
mod config;
mod filetransfer;
mod fs;
mod host;
mod support;
mod system;
mod ui;
mod utils;
@ -83,11 +83,14 @@ fn main() {
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
let mut ticks: Duration = Duration::from_millis(10);
let mut log_enabled: bool = true;
let mut start_activity: NextActivity = NextActivity::Authentication;
//Process options
let mut opts = Options::new();
opts.optflag("c", "config", "Open termscp configuration");
opts.optflag("q", "quiet", "Disable logging");
opts.optopt("t", "theme", "Import specified theme", "<path>");
opts.optopt("P", "password", "Provide password from CLI", "<password>");
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
opts.optflag("q", "quiet", "Disable logging");
opts.optflag("v", "version", "");
opts.optflag("h", "help", "Print this menu");
let matches = match opts.parse(&args[1..]) {
@ -110,6 +113,10 @@ fn main() {
);
std::process::exit(255);
}
// Setup activity?
if matches.opt_present("c") {
start_activity = NextActivity::SetupActivity;
}
// Logging
if matches.opt_present("q") {
log_enabled = false;
@ -129,6 +136,20 @@ fn main() {
}
}
}
// @! extra modes
if let Some(theme) = matches.opt_str("t") {
match support::import_theme(Path::new(theme.as_str())) {
Ok(_) => {
println!("Theme has been successfully imported!");
std::process::exit(0)
}
Err(err) => {
eprintln!("{}", err);
std::process::exit(1);
}
}
}
// @! Ordinary mode
// Check free args
let extra_args: Vec<String> = matches.free;
// Remote argument
@ -172,7 +193,6 @@ fn main() {
}
info!("termscp {} started!", TERMSCP_VERSION);
// Initialize client if necessary
let mut start_activity: NextActivity = NextActivity::Authentication;
if address.is_some() {
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", address, port, protocol, username, utils::fmt::shadow_password(password.as_deref().unwrap_or("")));
if password.is_none() {

68
src/support.rs Normal file
View file

@ -0,0 +1,68 @@
//! ## Support
//!
//! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// mod
use crate::system::{environment, theme_provider::ThemeProvider};
use std::fs;
use std::path::{Path, PathBuf};
/// ### import_theme
///
/// Import theme at provided path into termscp
pub fn import_theme(p: &Path) -> Result<(), String> {
if !p.exists() {
return Err(String::from(
"Could not import theme: No such file or directory",
));
}
// Validate theme file
ThemeProvider::new(p).map_err(|e| format!("Invalid theme error: {}", e))?;
// get config dir
let cfg_dir: PathBuf = get_config_dir()?;
// Get theme directory
let theme_file: PathBuf = environment::get_theme_path(cfg_dir.as_path());
// Copy theme to theme_dir
fs::copy(p, theme_file.as_path())
.map(|_| ())
.map_err(|e| format!("Could not import theme: {}", e))
}
/// ### get_config_dir
///
/// Get configuration directory
fn get_config_dir() -> Result<PathBuf, String> {
match environment::init_config_dir() {
Ok(Some(config_dir)) => Ok(config_dir),
Ok(None) => Err(String::from(
"Your system doesn't provide a configuration directory",
)),
Err(err) => Err(format!(
"Could not initialize configuration directory: {}",
err
)),
}
}

View file

@ -30,8 +30,10 @@
use super::keys::keyringstorage::KeyringStorage;
use super::keys::{filestorage::FileStorage, KeyStorage, KeyStorageError};
// Local
use crate::bookmarks::serializer::BookmarkSerializer;
use crate::bookmarks::{Bookmark, SerializerError, SerializerErrorKind, UserHosts};
use crate::config::{
bookmarks::{Bookmark, UserHosts},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
};
use crate::filetransfer::FileTransferProtocol;
use crate::utils::crypto;
use crate::utils::fmt::fmt_time;
@ -65,7 +67,7 @@ impl BookmarksClient {
recents_size: usize,
) -> Result<BookmarksClient, SerializerError> {
// Create default hosts
let default_hosts: UserHosts = Default::default();
let default_hosts: UserHosts = UserHosts::default();
debug!("Setting up bookmarks client...");
// Make a key storage (with-keyring)
#[cfg(feature = "with-keyring")]
@ -322,10 +324,7 @@ impl BookmarksClient {
.truncate(true)
.open(self.bookmarks_file.as_path())
{
Ok(writer) => {
let serializer: BookmarkSerializer = BookmarkSerializer {};
serializer.serialize(Box::new(writer), &self.hosts)
}
Ok(writer) => serialize(&self.hosts, Box::new(writer)),
Err(err) => {
error!("Failed to write bookmarks: {}", err);
Err(SerializerError::new_ex(
@ -348,8 +347,7 @@ impl BookmarksClient {
{
Ok(reader) => {
// Deserialize
let deserializer: BookmarkSerializer = BookmarkSerializer {};
match deserializer.deserialize(Box::new(reader)) {
match deserialize(Box::new(reader)) {
Ok(hosts) => {
self.hosts = hosts;
Ok(())
@ -448,7 +446,7 @@ mod tests {
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "netbsd"
target_os = "openbsd"
))]
fn test_system_bookmarks_new_err() {
assert!(BookmarksClient::new(
@ -710,7 +708,6 @@ mod tests {
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
client.key = "MYSUPERSECRETKEY".to_string();
let input: &str = "Hello world!";
assert_eq!(
client.decrypt_str("z4Z6LpcpYqBW4+bkIok+5A==").ok().unwrap(),
"Hello world!"

View file

@ -26,8 +26,10 @@
* SOFTWARE.
*/
// Locals
use crate::config::serializer::ConfigSerializer;
use crate::config::{SerializerError, SerializerErrorKind, UserConfig};
use crate::config::{
params::UserConfig,
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
// Ext
@ -323,10 +325,7 @@ impl ConfigClient {
.truncate(true)
.open(self.config_path.as_path())
{
Ok(writer) => {
let serializer: ConfigSerializer = ConfigSerializer {};
serializer.serialize(Box::new(writer), &self.config)
}
Ok(writer) => serialize(&self.config, Box::new(writer)),
Err(err) => {
error!("Failed to write configuration file: {}", err);
Err(SerializerError::new_ex(
@ -348,8 +347,7 @@ impl ConfigClient {
{
Ok(reader) => {
// Deserialize
let deserializer: ConfigSerializer = ConfigSerializer {};
match deserializer.deserialize(Box::new(reader)) {
match deserialize(Box::new(reader)) {
Ok(config) => {
self.config = config;
Ok(())

View file

@ -93,6 +93,17 @@ pub fn get_log_paths(config_dir: &Path) -> PathBuf {
log_file
}
/// ### get_theme_path
///
/// Get paths for theme provider
/// Returns: path of theme.toml
pub fn get_theme_path(config_dir: &Path) -> PathBuf {
// Prepare paths
let mut theme_file: PathBuf = PathBuf::from(config_dir);
theme_file.push("theme.toml");
theme_file
}
#[cfg(test)]
mod tests {
@ -157,4 +168,12 @@ mod tests {
PathBuf::from("/home/omar/.config/termscp/termscp.log"),
);
}
#[test]
fn test_system_environment_get_theme_path() {
assert_eq!(
get_theme_path(&Path::new("/home/omar/.config/termscp/")),
PathBuf::from("/home/omar/.config/termscp/theme.toml"),
);
}
}

View file

@ -29,6 +29,7 @@
pub mod bookmarks_client;
pub mod config_client;
pub mod environment;
pub(crate) mod keys;
pub(self) mod keys;
pub mod logging;
pub mod sshkey_storage;
pub mod theme_provider;

View file

@ -0,0 +1,246 @@
//! ## ThemeProvider
//!
//! `theme_provider` is the module which provides an API between the theme configuration and the system
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use crate::config::{
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
themes::Theme,
};
// Ext
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::string::ToString;
/// ## ThemeProvider
///
/// ThemeProvider provides a high level API to communicate with the termscp theme
pub struct ThemeProvider {
theme: Theme, // Theme loaded
theme_path: PathBuf, // Theme TOML Path
degraded: bool, // Fallback mode; won't work with file system
}
impl ThemeProvider {
/// ### new
///
/// Instantiates a new `ThemeProvider`
pub fn new(theme_path: &Path) -> Result<Self, SerializerError> {
let default_theme: Theme = Theme::default();
info!(
"Setting up theme provider with thene path {} ",
theme_path.display(),
);
// Create provider
let mut provider: ThemeProvider = ThemeProvider {
theme: default_theme,
theme_path: theme_path.to_path_buf(),
degraded: false,
};
// If Config file doesn't exist, create it
if !theme_path.exists() {
if let Err(err) = provider.save() {
error!("Couldn't write theme file: {}", err);
return Err(err);
}
debug!("Theme file didn't exist; created file");
} else {
// otherwise Load configuration from file
if let Err(err) = provider.load() {
error!("Couldn't read thene file: {}", err);
return Err(err);
}
debug!("Read theme file");
}
Ok(provider)
}
/// ### degraded
///
/// Create a new theme provider which won't work with file system.
/// This is done in order to prevent a lot of `unwrap_or` on Ui
pub fn degraded() -> Self {
Self {
theme: Theme::default(),
theme_path: PathBuf::default(),
degraded: true,
}
}
// -- getters
/// ### theme
///
/// Returns theme as reference
pub fn theme(&self) -> &Theme {
&self.theme
}
/// ### theme_mut
///
/// Returns a mutable reference to the theme
pub fn theme_mut(&mut self) -> &mut Theme {
&mut self.theme
}
// -- io
/// ### load
///
/// Load theme from file
pub fn load(&mut self) -> Result<(), SerializerError> {
if self.degraded {
warn!("Configuration won't be loaded, since degraded; reloading default...");
self.theme = Theme::default();
return Err(SerializerError::new_ex(
SerializerErrorKind::GenericError,
String::from("Can't access theme file"),
));
}
// Open theme file for read
debug!("Loading theme from file...");
match OpenOptions::new()
.read(true)
.open(self.theme_path.as_path())
{
Ok(reader) => {
// Deserialize
match deserialize(Box::new(reader)) {
Ok(theme) => {
self.theme = theme;
Ok(())
}
Err(err) => Err(err),
}
}
Err(err) => {
error!("Failed to read theme: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
}
/// ### save
///
/// Save theme to file
pub fn save(&self) -> Result<(), SerializerError> {
if self.degraded {
warn!("Configuration won't be saved, since in degraded mode");
return Err(SerializerError::new_ex(
SerializerErrorKind::GenericError,
String::from("Can't access theme file"),
));
}
// Open file
debug!("Writing theme");
match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(self.theme_path.as_path())
{
Ok(writer) => serialize(self.theme(), Box::new(writer)),
Err(err) => {
error!("Failed to write theme: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tuirealm::tui::style::Color;
#[test]
fn test_system_theme_provider_new() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let theme_path: PathBuf = get_theme_path(tmp_dir.path());
// Initialize a new bookmarks client
let mut provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
// Verify client
assert_eq!(provider.theme().auth_address, Color::Yellow);
assert_eq!(provider.theme_path, theme_path);
assert_eq!(provider.degraded, false);
// Mutation
provider.theme_mut().auth_address = Color::Green;
assert_eq!(provider.theme().auth_address, Color::Green);
}
#[test]
fn test_system_theme_provider_load_and_save() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let theme_path: PathBuf = get_theme_path(tmp_dir.path());
// Initialize a new bookmarks client
let mut provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
// Write
provider.theme_mut().auth_address = Color::Green;
assert!(provider.save().is_ok());
provider.theme_mut().auth_address = Color::Blue;
// Reload
assert!(provider.load().is_ok());
// Unchanged
assert_eq!(provider.theme().auth_address, Color::Green);
// Instantiate a new provider
let provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
assert_eq!(provider.theme().auth_address, Color::Green); // Unchanged
}
#[test]
fn test_system_theme_provider_degraded() {
let mut provider: ThemeProvider = ThemeProvider::degraded();
assert_eq!(provider.theme().auth_address, Color::Yellow);
assert_eq!(provider.degraded, true);
provider.theme_mut().auth_address = Color::Green;
assert!(provider.load().is_err());
assert_eq!(provider.theme().auth_address, Color::Yellow);
assert!(provider.save().is_err());
}
#[test]
fn test_system_theme_provider_err() {
assert!(ThemeProvider::new(Path::new("/tmp/oifoif/omar")).is_err());
}
/// ### get_theme_path
///
/// Get paths for theme file
fn get_theme_path(dir: &Path) -> PathBuf {
let mut p: PathBuf = PathBuf::from(dir);
p.push("theme.toml");
p
}
}

View file

@ -33,6 +33,7 @@ mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::FileTransferProtocol;
use crate::system::bookmarks_client::BookmarksClient;
use crate::ui::context::FileTransferParams;
@ -154,6 +155,13 @@ impl AuthActivity {
}
}
}
/// ### theme
///
/// Returns a reference to theme
fn theme(&self) -> &Theme {
self.context.as_ref().unwrap().theme_provider.theme()
}
}
impl Activity for AuthActivity {

View file

@ -56,6 +56,14 @@ 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;
// Headers
self.view.mount(
super::COMPONENT_TEXT_H1,
@ -86,14 +94,14 @@ impl AuthActivity {
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
TextSpanBuilder::new(" to show keybindings; ")
.bold()
.build(),
TextSpanBuilder::new("<CTRL+C>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
TextSpanBuilder::new(" to enter setup").bold().build(),
])
@ -111,9 +119,9 @@ impl AuthActivity {
super::COMPONENT_RADIO_PROTOCOL,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightGreen)
.with_color(protocol_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_borders(Borders::ALL, BorderType::Rounded, protocol_color)
.with_options(
Some(String::from("Protocol")),
vec![
@ -132,8 +140,8 @@ impl AuthActivity {
super::COMPONENT_INPUT_ADDR,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::Yellow)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_foreground(addr_color)
.with_borders(Borders::ALL, BorderType::Rounded, addr_color)
.with_label(String::from("Remote address"))
.build(),
)),
@ -143,8 +151,8 @@ impl AuthActivity {
super::COMPONENT_INPUT_PORT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightCyan)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_foreground(port_color)
.with_borders(Borders::ALL, BorderType::Rounded, port_color)
.with_label(String::from("Port number"))
.with_input(InputType::Number)
.with_input_len(5)
@ -157,8 +165,8 @@ impl AuthActivity {
super::COMPONENT_INPUT_USERNAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightMagenta)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
.with_foreground(username_color)
.with_borders(Borders::ALL, BorderType::Rounded, username_color)
.with_label(String::from("Username"))
.build(),
)),
@ -168,8 +176,8 @@ impl AuthActivity {
super::COMPONENT_INPUT_PASSWORD,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightBlue)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
.with_foreground(password_color)
.with_borders(Borders::ALL, BorderType::Rounded, password_color)
.with_label(String::from("Password"))
.with_input(InputType::Password)
.build(),
@ -202,26 +210,27 @@ impl AuthActivity {
super::COMPONENT_BOOKMARKS_LIST,
Box::new(BookmarkList::new(
BookmarkListPropsBuilder::default()
.with_background(Color::LightGreen)
.with_background(bookmarks_color)
.with_foreground(Color::Black)
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.with_borders(Borders::ALL, BorderType::Plain, bookmarks_color)
.with_bookmarks(Some(String::from("Bookmarks")), vec![])
.build(),
)),
);
let _ = self.view_bookmarks();
// Recents
self.view.mount(
super::COMPONENT_RECENTS_LIST,
Box::new(BookmarkList::new(
BookmarkListPropsBuilder::default()
.with_background(Color::LightBlue)
.with_background(recents_color)
.with_foreground(Color::Black)
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
.with_borders(Borders::ALL, BorderType::Plain, recents_color)
.with_bookmarks(Some(String::from("Recent connections")), vec![])
.build(),
)),
);
// Load bookmarks
let _ = self.view_bookmarks();
let _ = self.view_recent_connections();
// Active protocol
self.view.active(super::COMPONENT_RADIO_PROTOCOL);
@ -475,12 +484,13 @@ impl AuthActivity {
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
// Mount
let err_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(MsgBox::new(
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.with_borders(Borders::ALL, BorderType::Thick, Color::Red)
.with_foreground(err_color)
.with_borders(Borders::ALL, BorderType::Thick, err_color)
.bold()
.with_texts(None, vec![TextSpan::from(text)])
.build(),
@ -502,12 +512,13 @@ impl AuthActivity {
/// Mount size error
pub(super) fn mount_size_err(&mut self) {
// Mount
let err_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_SIZE_ERR,
Box::new(MsgBox::new(
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.with_borders(Borders::ALL, BorderType::Thick, Color::Red)
.with_foreground(err_color)
.with_borders(Borders::ALL, BorderType::Thick, err_color)
.bold()
.with_texts(
None,
@ -534,12 +545,13 @@ impl AuthActivity {
/// Mount quit popup
pub(super) fn mount_quit(&mut self) {
// Protocol
let quit_color = self.theme().misc_quit_dialog;
self.view.mount(
super::COMPONENT_RADIO_QUIT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_color(quit_color)
.with_borders(Borders::ALL, BorderType::Rounded, quit_color)
.with_inverted_color(Color::Black)
.with_options(
Some(String::from("Quit termscp?")),
@ -562,13 +574,14 @@ impl AuthActivity {
///
/// Mount bookmark delete dialog
pub(super) fn mount_bookmark_del_dialog(&mut self) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
.with_options(
Some(String::from("Delete bookmark?")),
vec![String::from("Yes"), String::from("No")],
@ -594,13 +607,14 @@ impl AuthActivity {
///
/// Mount recent delete dialog
pub(super) fn mount_recent_del_dialog(&mut self) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
.with_options(
Some(String::from("Delete bookmark?")),
vec![String::from("Yes"), String::from("No")],
@ -624,11 +638,13 @@ impl AuthActivity {
///
/// Mount bookmark save dialog
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(Color::LightCyan)
.with_foreground(save_color)
.with_label(String::from("Save bookmark as..."))
.with_borders(
Borders::TOP | Borders::RIGHT | Borders::LEFT,
@ -642,7 +658,7 @@ impl AuthActivity {
super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Red)
.with_color(warn_color)
.with_borders(
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
BorderType::Rounded,
@ -671,6 +687,7 @@ impl AuthActivity {
///
/// Mount help
pub(super) fn mount_help(&mut self) {
let key_color = self.theme().misc_keys;
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Scrolltable::new(
@ -685,7 +702,7 @@ impl AuthActivity {
.add_col(
TextSpanBuilder::new("<ESC>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Quit termscp"))
@ -693,7 +710,7 @@ impl AuthActivity {
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Switch from form and bookmarks"))
@ -701,7 +718,7 @@ impl AuthActivity {
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Switch bookmark tab"))
@ -709,7 +726,7 @@ impl AuthActivity {
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Move up/down in current tab"))
@ -717,7 +734,7 @@ impl AuthActivity {
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Connect/Load bookmark"))
@ -725,7 +742,7 @@ impl AuthActivity {
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Delete selected bookmark"))
@ -733,7 +750,7 @@ impl AuthActivity {
.add_col(
TextSpanBuilder::new("<CTRL+C>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Enter setup"))
@ -741,7 +758,7 @@ impl AuthActivity {
.add_col(
TextSpanBuilder::new("<CTRL+S>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Save bookmark"))

View file

@ -35,6 +35,7 @@ pub(self) mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
use crate::filetransfer::scp_transfer::ScpFileTransfer;
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
@ -165,34 +166,34 @@ impl FileTransferActivity {
}
}
pub(crate) fn local(&self) -> &FileExplorer {
fn local(&self) -> &FileExplorer {
self.browser.local()
}
pub(crate) fn local_mut(&mut self) -> &mut FileExplorer {
fn local_mut(&mut self) -> &mut FileExplorer {
self.browser.local_mut()
}
pub(crate) fn remote(&self) -> &FileExplorer {
fn remote(&self) -> &FileExplorer {
self.browser.remote()
}
pub(crate) fn remote_mut(&mut self) -> &mut FileExplorer {
fn remote_mut(&mut self) -> &mut FileExplorer {
self.browser.remote_mut()
}
pub(crate) fn found(&self) -> Option<&FileExplorer> {
fn found(&self) -> Option<&FileExplorer> {
self.browser.found()
}
pub(crate) fn found_mut(&mut self) -> Option<&mut FileExplorer> {
fn found_mut(&mut self) -> Option<&mut FileExplorer> {
self.browser.found_mut()
}
/// ### get_cache_tmp_name
///
/// Get file name for a file in cache
pub(crate) fn get_cache_tmp_name(&self, name: &str, file_type: Option<&str>) -> Option<String> {
fn get_cache_tmp_name(&self, name: &str, file_type: Option<&str>) -> Option<String> {
self.cache.as_ref().map(|_| {
let base: String = format!(
"{}-{}",
@ -208,6 +209,13 @@ impl FileTransferActivity {
}
})
}
/// ### theme
///
/// Get a reference to `Theme`
fn theme(&self) -> &Theme {
self.context.as_ref().unwrap().theme_provider.theme()
}
}
/**

View file

@ -65,13 +65,22 @@ impl FileTransferActivity {
/// Initialize file transfer activity's view
pub(super) fn init(&mut self) {
// Mount local file explorer
let local_explorer_background = self.theme().transfer_local_explorer_background;
let local_explorer_foreground = self.theme().transfer_local_explorer_foreground;
let local_explorer_highlighted = self.theme().transfer_local_explorer_highlighted;
let remote_explorer_background = self.theme().transfer_remote_explorer_background;
let remote_explorer_foreground = self.theme().transfer_remote_explorer_foreground;
let remote_explorer_highlighted = self.theme().transfer_remote_explorer_highlighted;
let log_panel = self.theme().transfer_log_window;
let log_background = self.theme().transfer_log_background;
self.view.mount(
super::COMPONENT_EXPLORER_LOCAL,
Box::new(FileList::new(
FileListPropsBuilder::default()
.with_background(Color::Yellow)
.with_foreground(Color::Yellow)
.with_borders(Borders::ALL, BorderType::Plain, Color::Yellow)
.with_highlight_color(local_explorer_highlighted)
.with_background(local_explorer_background)
.with_foreground(local_explorer_foreground)
.with_borders(Borders::ALL, BorderType::Plain, local_explorer_highlighted)
.build(),
)),
);
@ -80,9 +89,10 @@ impl FileTransferActivity {
super::COMPONENT_EXPLORER_REMOTE,
Box::new(FileList::new(
FileListPropsBuilder::default()
.with_background(Color::LightBlue)
.with_foreground(Color::LightBlue)
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
.with_highlight_color(remote_explorer_highlighted)
.with_background(remote_explorer_background)
.with_foreground(remote_explorer_foreground)
.with_borders(Borders::ALL, BorderType::Plain, remote_explorer_highlighted)
.build(),
)),
);
@ -91,7 +101,8 @@ impl FileTransferActivity {
super::COMPONENT_LOG_BOX,
Box::new(LogBox::new(
LogboxPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.with_background(log_background)
.with_borders(Borders::ALL, BorderType::Plain, log_panel)
.build(),
)),
);
@ -369,12 +380,13 @@ impl FileTransferActivity {
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
// Mount
let error_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(MsgBox::new(
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
.with_foreground(error_color)
.with_borders(Borders::ALL, BorderType::Rounded, error_color)
.bold()
.with_texts(None, vec![TextSpan::from(text)])
.build(),
@ -393,12 +405,13 @@ impl FileTransferActivity {
pub(super) fn mount_fatal(&mut self, text: &str) {
// Mount
let error_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_FATAL,
Box::new(MsgBox::new(
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
.with_foreground(error_color)
.with_borders(Borders::ALL, BorderType::Rounded, error_color)
.bold()
.with_texts(None, vec![TextSpan::from(text)])
.build(),
@ -434,13 +447,14 @@ impl FileTransferActivity {
/// Mount quit popup
pub(super) fn mount_quit(&mut self) {
// Protocol
let quit_color = self.theme().misc_quit_dialog;
self.view.mount(
super::COMPONENT_RADIO_QUIT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_color(quit_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_borders(Borders::ALL, BorderType::Rounded, quit_color)
.with_options(
Some(String::from("Are you sure you want to quit?")),
vec![String::from("Yes"), String::from("No")],
@ -463,13 +477,14 @@ impl FileTransferActivity {
/// Mount disconnect popup
pub(super) fn mount_disconnect(&mut self) {
// Protocol
let quit_color = self.theme().misc_quit_dialog;
self.view.mount(
super::COMPONENT_RADIO_DISCONNECT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_color(quit_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_borders(Borders::ALL, BorderType::Rounded, quit_color)
.with_options(
Some(String::from("Are you sure you want to disconnect?")),
vec![String::from("Yes"), String::from("No")],
@ -488,11 +503,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_copy(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_COPY,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("Copy file(s) to..."))
.build(),
)),
@ -505,11 +522,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_exec(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_EXEC,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Plain, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("Execute command"))
.build(),
)),
@ -523,9 +542,17 @@ impl FileTransferActivity {
pub(super) fn mount_find(&mut self, search: &str) {
// Get color
let color: Color = match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => Color::Yellow,
FileExplorerTab::Remote | FileExplorerTab::FindRemote => Color::LightBlue,
let (bg, fg, hg): (Color, Color, Color) = match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => (
self.theme().transfer_local_explorer_background,
self.theme().transfer_local_explorer_foreground,
self.theme().transfer_local_explorer_highlighted,
),
FileExplorerTab::Remote | FileExplorerTab::FindRemote => (
self.theme().transfer_remote_explorer_background,
self.theme().transfer_remote_explorer_foreground,
self.theme().transfer_remote_explorer_highlighted,
),
};
// Mount component
self.view.mount(
@ -533,9 +560,10 @@ impl FileTransferActivity {
Box::new(FileList::new(
FileListPropsBuilder::default()
.with_files(Some(format!("Search results for \"{}\"", search)), vec![])
.with_borders(Borders::ALL, BorderType::Plain, color)
.with_background(color)
.with_foreground(color)
.with_borders(Borders::ALL, BorderType::Plain, hg)
.with_highlight_color(hg)
.with_background(bg)
.with_foreground(fg)
.build(),
)),
);
@ -548,11 +576,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_find_input(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_FIND,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("Search files by name"))
.build(),
)),
@ -567,11 +597,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_goto(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_GOTO,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("Change working directory"))
.build(),
)),
@ -584,11 +616,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_mkdir(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_MKDIR,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("Insert directory name"))
.build(),
)),
@ -601,11 +635,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_newfile(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_NEWFILE,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("New file name"))
.build(),
)),
@ -618,11 +654,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_openwith(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_OPEN_WITH,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("Open file with..."))
.build(),
)),
@ -635,11 +673,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_rename(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_RENAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("Move file(s) to..."))
.build(),
)),
@ -652,11 +692,13 @@ impl FileTransferActivity {
}
pub(super) fn mount_saveas(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_SAVEAS,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label(String::from("Save as..."))
.build(),
)),
@ -669,11 +711,12 @@ impl FileTransferActivity {
}
pub(super) fn mount_progress_bar(&mut self, root_name: String) {
let prog_color = self.theme().transfer_progress_bar;
self.view.mount(
super::COMPONENT_PROGRESS_BAR_FULL,
Box::new(ProgressBar::new(
ProgressBarPropsBuilder::default()
.with_progbar_color(Color::Green)
.with_progbar_color(prog_color)
.with_background(Color::Black)
.with_borders(
Borders::TOP | Borders::RIGHT | Borders::LEFT,
@ -688,7 +731,7 @@ impl FileTransferActivity {
super::COMPONENT_PROGRESS_BAR_PARTIAL,
Box::new(ProgressBar::new(
ProgressBarPropsBuilder::default()
.with_progbar_color(Color::Green)
.with_progbar_color(prog_color)
.with_background(Color::Black)
.with_borders(
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
@ -708,6 +751,7 @@ impl FileTransferActivity {
}
pub(super) fn mount_file_sorting(&mut self) {
let sorting_color = self.theme().transfer_status_sorting;
let sorting: FileSorting = match self.browser.tab() {
FileExplorerTab::Local => self.local().get_file_sorting(),
FileExplorerTab::Remote => self.remote().get_file_sorting(),
@ -723,9 +767,9 @@ impl FileTransferActivity {
super::COMPONENT_RADIO_SORTING,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightMagenta)
.with_color(sorting_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
.with_borders(Borders::ALL, BorderType::Rounded, sorting_color)
.with_options(
Some(String::from("Sort files by")),
vec![
@ -747,13 +791,14 @@ impl FileTransferActivity {
}
pub(super) fn mount_radio_delete(&mut self) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
super::COMPONENT_RADIO_DELETE,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Red)
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Plain, Color::Red)
.with_borders(Borders::ALL, BorderType::Plain, warn_color)
.with_options(
Some(String::from("Delete file")),
vec![String::from("Yes"), String::from("No")],
@ -881,21 +926,23 @@ impl FileTransferActivity {
}
pub(super) fn refresh_local_status_bar(&mut self) {
let sorting_color = self.theme().transfer_status_sorting;
let hidden_color = self.theme().transfer_status_hidden;
let local_bar_spans: Vec<TextSpan> = vec![
TextSpanBuilder::new("File sorting: ")
.with_foreground(Color::LightYellow)
.with_foreground(sorting_color)
.build(),
TextSpanBuilder::new(Self::get_file_sorting_str(self.local().get_file_sorting()))
.with_foreground(Color::LightYellow)
.with_foreground(sorting_color)
.reversed()
.build(),
TextSpanBuilder::new(" Hidden files: ")
.with_foreground(Color::LightBlue)
.with_foreground(hidden_color)
.build(),
TextSpanBuilder::new(Self::get_hidden_files_str(
self.local().hidden_files_visible(),
))
.with_foreground(Color::LightBlue)
.with_foreground(hidden_color)
.reversed()
.build(),
];
@ -910,31 +957,34 @@ impl FileTransferActivity {
}
pub(super) fn refresh_remote_status_bar(&mut self) {
let sorting_color = self.theme().transfer_status_sorting;
let hidden_color = self.theme().transfer_status_hidden;
let sync_color = self.theme().transfer_status_sync_browsing;
let remote_bar_spans: Vec<TextSpan> = vec![
TextSpanBuilder::new("File sorting: ")
.with_foreground(Color::LightYellow)
.with_foreground(sorting_color)
.build(),
TextSpanBuilder::new(Self::get_file_sorting_str(self.remote().get_file_sorting()))
.with_foreground(Color::LightYellow)
.with_foreground(sorting_color)
.reversed()
.build(),
TextSpanBuilder::new(" Hidden files: ")
.with_foreground(Color::LightBlue)
.with_foreground(hidden_color)
.build(),
TextSpanBuilder::new(Self::get_hidden_files_str(
self.remote().hidden_files_visible(),
))
.with_foreground(Color::LightBlue)
.with_foreground(hidden_color)
.reversed()
.build(),
TextSpanBuilder::new(" Sync Browsing: ")
.with_foreground(Color::LightGreen)
.with_foreground(sync_color)
.build(),
TextSpanBuilder::new(match self.browser.sync_browsing {
true => "ON ",
false => "OFF",
})
.with_foreground(Color::LightGreen)
.with_foreground(sync_color)
.reversed()
.build(),
];
@ -952,6 +1002,7 @@ impl FileTransferActivity {
///
/// Mount help
pub(super) fn mount_help(&mut self) {
let key_color = self.theme().misc_keys;
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Scrolltable::new(
@ -966,7 +1017,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<ESC>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Disconnect"))
@ -974,7 +1025,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(
@ -984,7 +1035,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<BACKSPACE>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Go to previous directory"))
@ -992,7 +1043,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Change explorer tab"))
@ -1000,7 +1051,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Move up/down in list"))
@ -1008,7 +1059,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Enter directory"))
@ -1016,7 +1067,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<SPACE>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Upload/Download file"))
@ -1024,7 +1075,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<A>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Toggle hidden files"))
@ -1032,7 +1083,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<B>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Change file sorting mode"))
@ -1040,7 +1091,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<C>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Copy"))
@ -1048,7 +1099,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<D>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Make directory"))
@ -1056,7 +1107,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<G>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Go to path"))
@ -1064,7 +1115,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<H>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Show help"))
@ -1072,7 +1123,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<I>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Show info about selected file"))
@ -1080,7 +1131,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<L>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Reload directory content"))
@ -1088,7 +1139,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<M>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Select file"))
@ -1096,7 +1147,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<N>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Create new file"))
@ -1104,7 +1155,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<O>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(
@ -1114,7 +1165,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<Q>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Quit termscp"))
@ -1122,7 +1173,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<R>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Rename file"))
@ -1130,7 +1181,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<S>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Save file as"))
@ -1138,7 +1189,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<U>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Go to parent directory"))
@ -1146,7 +1197,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<V>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(
@ -1156,7 +1207,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<W>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(
@ -1166,7 +1217,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<X>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Execute shell command"))
@ -1174,7 +1225,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<Y>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Toggle synchronized browsing"))
@ -1182,7 +1233,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Delete selected file"))
@ -1190,7 +1241,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<CTRL+A>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Select all files"))
@ -1198,7 +1249,7 @@ impl FileTransferActivity {
.add_col(
TextSpanBuilder::new("<CTRL+C>")
.bold()
.with_foreground(Color::Cyan)
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::from(" Interrupt file transfer"))

View file

@ -29,18 +29,24 @@
// Locals
use super::SetupActivity;
// Ext
use crate::config::themes::Theme;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::env;
use tuirealm::tui::style::Color;
use tuirealm::{Payload, Value};
impl SetupActivity {
/// ### action_save_config
///
/// Save configuration
pub(super) fn action_save_config(&mut self) -> Result<(), String> {
pub(super) fn action_save_all(&mut self) -> Result<(), String> {
// Collect input values
self.collect_input_values();
self.save_config()
self.save_config()?;
// save theme
self.collect_styles()
.map_err(|e| format!("'{}' has an invalid color", e))?;
self.save_theme()
}
/// ### action_reset_config
@ -56,6 +62,19 @@ impl SetupActivity {
}
}
/// ### action_reset_theme
///
/// Reset configuration input fields
pub(super) fn action_reset_theme(&mut self) -> Result<(), String> {
match self.reset_theme_changes() {
Err(err) => Err(err),
Ok(_) => {
self.load_styles();
Ok(())
}
}
}
/// ### action_delete_ssh_key
///
/// delete of a ssh key
@ -159,4 +178,89 @@ impl SetupActivity {
}
}
}
/// ### set_color
///
/// Given a component and a color, save the color into the theme
pub(super) fn action_save_color(&mut self, component: &str, color: Color) {
let theme: &mut Theme = self.theme_mut();
match component {
super::COMPONENT_COLOR_AUTH_ADDR => {
theme.auth_address = color;
}
super::COMPONENT_COLOR_AUTH_BOOKMARKS => {
theme.auth_bookmarks = color;
}
super::COMPONENT_COLOR_AUTH_PASSWORD => {
theme.auth_password = color;
}
super::COMPONENT_COLOR_AUTH_PORT => {
theme.auth_port = color;
}
super::COMPONENT_COLOR_AUTH_PROTOCOL => {
theme.auth_protocol = color;
}
super::COMPONENT_COLOR_AUTH_RECENTS => {
theme.auth_recents = color;
}
super::COMPONENT_COLOR_AUTH_USERNAME => {
theme.auth_username = color;
}
super::COMPONENT_COLOR_MISC_ERROR => {
theme.misc_error_dialog = color;
}
super::COMPONENT_COLOR_MISC_INPUT => {
theme.misc_input_dialog = color;
}
super::COMPONENT_COLOR_MISC_KEYS => {
theme.misc_keys = color;
}
super::COMPONENT_COLOR_MISC_QUIT => {
theme.misc_quit_dialog = color;
}
super::COMPONENT_COLOR_MISC_SAVE => {
theme.misc_save_dialog = color;
}
super::COMPONENT_COLOR_MISC_WARN => {
theme.misc_warn_dialog = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG => {
theme.transfer_local_explorer_background = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG => {
theme.transfer_local_explorer_foreground = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG => {
theme.transfer_local_explorer_highlighted = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG => {
theme.transfer_remote_explorer_background = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG => {
theme.transfer_remote_explorer_foreground = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG => {
theme.transfer_remote_explorer_highlighted = color;
}
super::COMPONENT_COLOR_TRANSFER_LOG_BG => {
theme.transfer_log_background = color;
}
super::COMPONENT_COLOR_TRANSFER_LOG_WIN => {
theme.transfer_log_window = color;
}
super::COMPONENT_COLOR_TRANSFER_PROG_BAR => {
theme.transfer_progress_bar = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN => {
theme.transfer_status_hidden = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING => {
theme.transfer_status_sorting = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC => {
theme.transfer_status_sync_browsing = color;
}
_ => {}
}
}
}

View file

@ -60,6 +60,24 @@ impl SetupActivity {
}
}
/// ### save_theme
///
/// Save theme to file
pub(super) fn save_theme(&mut self) -> Result<(), String> {
self.theme_provider()
.save()
.map_err(|e| format!("Could not save theme: {}", e))
}
/// ### reset_theme_changes
///
/// Reset changes committed to theme
pub(super) fn reset_theme_changes(&mut self) -> Result<(), String> {
self.theme_provider()
.load()
.map_err(|e| format!("Could not restore theme: {}", e))
}
/// ### delete_ssh_key
///
/// Delete ssh key from config cli

View file

@ -34,16 +34,21 @@ mod view;
// Locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::system::theme_provider::ThemeProvider;
// Ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::{Update, View};
// -- components
// -- common
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SAVE: &str = "RADIO_SAVE";
const COMPONENT_RADIO_TAB: &str = "RADIO_TAB";
// -- config
const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR";
const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL";
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
@ -51,11 +56,47 @@ const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
const COMPONENT_RADIO_TAB: &str = "RADIO_TAB";
// -- ssh keys
const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS";
const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST";
const COMPONENT_INPUT_SSH_USERNAME: &str = "INPUT_SSH_USERNAME";
const COMPONENT_RADIO_DEL_SSH_KEY: &str = "RADIO_DEL_SSH_KEY";
// -- theme
const COMPONENT_COLOR_AUTH_TITLE: &str = "COMPONENT_COLOR_AUTH_TITLE";
const COMPONENT_COLOR_MISC_TITLE: &str = "COMPONENT_COLOR_MISC_TITLE";
const COMPONENT_COLOR_TRANSFER_TITLE: &str = "COMPONENT_COLOR_TRANSFER_TITLE";
const COMPONENT_COLOR_TRANSFER_TITLE_2: &str = "COMPONENT_COLOR_TRANSFER_TITLE_2";
const COMPONENT_COLOR_AUTH_ADDR: &str = "COMPONENT_COLOR_AUTH_ADDR";
const COMPONENT_COLOR_AUTH_BOOKMARKS: &str = "COMPONENT_COLOR_AUTH_BOOKMARKS";
const COMPONENT_COLOR_AUTH_PASSWORD: &str = "COMPONENT_COLOR_AUTH_PASSWORD";
const COMPONENT_COLOR_AUTH_PORT: &str = "COMPONENT_COLOR_AUTH_PORT";
const COMPONENT_COLOR_AUTH_PROTOCOL: &str = "COMPONENT_COLOR_AUTH_PROTOCOL";
const COMPONENT_COLOR_AUTH_RECENTS: &str = "COMPONENT_COLOR_AUTH_RECENTS";
const COMPONENT_COLOR_AUTH_USERNAME: &str = "COMPONENT_COLOR_AUTH_USERNAME";
const COMPONENT_COLOR_MISC_ERROR: &str = "COMPONENT_COLOR_MISC_ERROR";
const COMPONENT_COLOR_MISC_INPUT: &str = "COMPONENT_COLOR_MISC_INPUT";
const COMPONENT_COLOR_MISC_KEYS: &str = "COMPONENT_COLOR_MISC_KEYS";
const COMPONENT_COLOR_MISC_QUIT: &str = "COMPONENT_COLOR_MISC_QUIT";
const COMPONENT_COLOR_MISC_SAVE: &str = "COMPONENT_COLOR_MISC_SAVE";
const COMPONENT_COLOR_MISC_WARN: &str = "COMPONENT_COLOR_MISC_WARN";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG";
const COMPONENT_COLOR_TRANSFER_PROG_BAR: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR";
const COMPONENT_COLOR_TRANSFER_LOG_BG: &str = "COMPONENT_COLOR_TRANSFER_LOG_BG";
const COMPONENT_COLOR_TRANSFER_LOG_WIN: &str = "COMPONENT_COLOR_TRANSFER_LOG_WIN";
const COMPONENT_COLOR_TRANSFER_STATUS_SORTING: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SORTING";
const COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN: &str = "COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN";
const COMPONENT_COLOR_TRANSFER_STATUS_SYNC: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SYNC";
/// ### ViewLayout
///
@ -64,6 +105,7 @@ const COMPONENT_RADIO_DEL_SSH_KEY: &str = "RADIO_DEL_SSH_KEY";
enum ViewLayout {
SetupForm,
SshKeys,
Theme,
}
/// ## SetupActivity
@ -89,6 +131,20 @@ impl Default for SetupActivity {
}
}
impl SetupActivity {
fn theme(&self) -> &Theme {
self.context.as_ref().unwrap().theme_provider.theme()
}
fn theme_mut(&mut self) -> &mut Theme {
self.context.as_mut().unwrap().theme_provider.theme_mut()
}
fn theme_provider(&mut self) -> &mut ThemeProvider {
&mut self.context.as_mut().unwrap().theme_provider
}
}
impl Activity for SetupActivity {
/// ### on_create
///
@ -105,7 +161,7 @@ impl Activity for SetupActivity {
error!("Failed to enter raw mode: {}", err);
}
// Init view
self.init_setup();
self.init(ViewLayout::SetupForm);
// Verify error state from context
if let Some(err) = self.context.as_mut().unwrap().get_error() {
self.mount_error(err.as_str());

View file

@ -28,13 +28,25 @@
*/
// locals
use super::{
SetupActivity, COMPONENT_INPUT_LOCAL_FILE_FMT, COMPONENT_INPUT_REMOTE_FILE_FMT,
COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR,
COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY,
COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES, COMPONENT_RADIO_QUIT,
COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
SetupActivity, ViewLayout, COMPONENT_COLOR_AUTH_ADDR, COMPONENT_COLOR_AUTH_BOOKMARKS,
COMPONENT_COLOR_AUTH_PASSWORD, COMPONENT_COLOR_AUTH_PORT, COMPONENT_COLOR_AUTH_PROTOCOL,
COMPONENT_COLOR_AUTH_RECENTS, COMPONENT_COLOR_AUTH_USERNAME, COMPONENT_COLOR_MISC_ERROR,
COMPONENT_COLOR_MISC_INPUT, COMPONENT_COLOR_MISC_KEYS, COMPONENT_COLOR_MISC_QUIT,
COMPONENT_COLOR_MISC_SAVE, COMPONENT_COLOR_MISC_WARN,
COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
COMPONENT_COLOR_TRANSFER_LOG_BG, COMPONENT_COLOR_TRANSFER_LOG_WIN,
COMPONENT_COLOR_TRANSFER_PROG_BAR, COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
COMPONENT_COLOR_TRANSFER_STATUS_SORTING, COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
COMPONENT_INPUT_LOCAL_FILE_FMT, COMPONENT_INPUT_REMOTE_FILE_FMT, COMPONENT_INPUT_SSH_HOST,
COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS,
COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS,
COMPONENT_RADIO_HIDDEN_FILES, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE,
COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
};
use crate::ui::keymap::*;
use crate::utils::parser::parse_color;
// ext
use tuirealm::{Msg, Payload, Update, Value};
@ -45,6 +57,16 @@ impl Update for SetupActivity {
/// Update auth activity model based on msg
/// The function exits when returns None
fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
match self.layout {
ViewLayout::SetupForm => self.update_setup(msg),
ViewLayout::SshKeys => self.update_ssh_keys(msg),
ViewLayout::Theme => self.update_theme(msg),
}
}
}
impl SetupActivity {
fn update_setup(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
// Match msg
match ref_msg {
@ -118,7 +140,100 @@ impl Update for SetupActivity {
// Exit
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Save changes
if let Err(err) = self.action_save_config() {
if let Err(err) = self.action_save_all() {
self.mount_error(err.as_str());
}
// Exit
self.exit_reason = Some(super::ExitReason::Quit);
None
}
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
// Quit
self.exit_reason = Some(super::ExitReason::Quit);
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => {
// Umount popup
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, _) => None,
// Close help
(COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => {
// Umount help
self.umount_help();
None
}
(COMPONENT_TEXT_HELP, _) => None,
// Save popup
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Save config
if let Err(err) = self.action_save_all() {
self.mount_error(err.as_str());
}
self.umount_save_popup();
None
}
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => {
// Umount radio save
self.umount_save_popup();
None
}
(COMPONENT_RADIO_SAVE, _) => None,
// <CTRL+H> Show help
(_, &MSG_KEY_CTRL_H) => {
// Show help
self.mount_help();
None
}
(_, &MSG_KEY_TAB) => {
// Change view
self.init(ViewLayout::SshKeys);
None
}
// <CTRL+R> Revert changes
(_, &MSG_KEY_CTRL_R) => {
// Revert changes
if let Err(err) = self.action_reset_config() {
self.mount_error(err.as_str());
}
None
}
// <CTRL+S> Save
(_, &MSG_KEY_CTRL_S) => {
// Show save
self.mount_save_popup();
None
}
// <ESC>
(_, &MSG_KEY_ESC) => {
// Mount quit prompt
self.mount_quit();
None
}
(_, _) => None, // Nothing to do
},
}
}
fn update_ssh_keys(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
// Match msg
match ref_msg {
None => None,
Some(msg) => match msg {
// Error <ENTER> or <ESC>
(COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => {
// Umount text error
self.umount_error();
None
}
(COMPONENT_TEXT_ERROR, _) => None,
// Exit
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Save changes
if let Err(err) = self.action_save_all() {
self.mount_error(err.as_str());
}
// Exit
@ -163,7 +278,7 @@ impl Update for SetupActivity {
// Save popup
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Save config
if let Err(err) = self.action_save_config() {
if let Err(err) = self.action_save_all() {
self.mount_error(err.as_str());
}
self.umount_save_popup();
@ -176,12 +291,6 @@ impl Update for SetupActivity {
}
(COMPONENT_RADIO_SAVE, _) => None,
// Edit SSH Key
// <TAB> Change view
(COMPONENT_LIST_SSH_KEYS, &MSG_KEY_TAB) => {
// Change view
self.init_setup();
None
}
// <CTRL+H> Show help
(_, &MSG_KEY_CTRL_H) => {
// Show help
@ -247,7 +356,7 @@ impl Update for SetupActivity {
}
(_, &MSG_KEY_TAB) => {
// Change view
self.init_ssh_keys();
self.init(ViewLayout::Theme);
None
}
// <CTRL+R> Revert changes
@ -274,4 +383,312 @@ impl Update for SetupActivity {
},
}
}
fn update_theme(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
// Match msg
match ref_msg {
None => None,
Some(msg) => match msg {
// Input fields
(COMPONENT_COLOR_AUTH_PROTOCOL, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_AUTH_ADDR);
None
}
(COMPONENT_COLOR_AUTH_ADDR, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_AUTH_PORT);
None
}
(COMPONENT_COLOR_AUTH_PORT, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_AUTH_USERNAME);
None
}
(COMPONENT_COLOR_AUTH_USERNAME, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_AUTH_PASSWORD);
None
}
(COMPONENT_COLOR_AUTH_PASSWORD, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS);
None
}
(COMPONENT_COLOR_AUTH_BOOKMARKS, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_AUTH_RECENTS);
None
}
(COMPONENT_COLOR_AUTH_RECENTS, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_MISC_ERROR);
None
}
(COMPONENT_COLOR_MISC_ERROR, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_MISC_INPUT);
None
}
(COMPONENT_COLOR_MISC_INPUT, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_MISC_KEYS);
None
}
(COMPONENT_COLOR_MISC_KEYS, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_MISC_QUIT);
None
}
(COMPONENT_COLOR_MISC_QUIT, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_MISC_SAVE);
None
}
(COMPONENT_COLOR_MISC_SAVE, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_MISC_WARN);
None
}
(COMPONENT_COLOR_MISC_WARN, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, &MSG_KEY_DOWN) => {
self.view
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, &MSG_KEY_DOWN) => {
self.view
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, &MSG_KEY_DOWN) => {
self.view
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR);
None
}
(COMPONENT_COLOR_TRANSFER_PROG_BAR, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG);
None
}
(COMPONENT_COLOR_TRANSFER_LOG_BG, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN);
None
}
(COMPONENT_COLOR_TRANSFER_LOG_WIN, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING);
None
}
(COMPONENT_COLOR_TRANSFER_STATUS_SORTING, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN);
None
}
(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC);
None
}
(COMPONENT_COLOR_TRANSFER_STATUS_SYNC, &MSG_KEY_DOWN) => {
self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL);
None
}
(COMPONENT_COLOR_AUTH_PROTOCOL, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC);
None
}
(COMPONENT_COLOR_AUTH_ADDR, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL);
None
}
(COMPONENT_COLOR_AUTH_PORT, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_AUTH_ADDR);
None
}
(COMPONENT_COLOR_AUTH_USERNAME, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_AUTH_PORT);
None
}
(COMPONENT_COLOR_AUTH_PASSWORD, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_AUTH_USERNAME);
None
}
(COMPONENT_COLOR_AUTH_BOOKMARKS, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_AUTH_PASSWORD);
None
}
(COMPONENT_COLOR_AUTH_RECENTS, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS);
None
}
(COMPONENT_COLOR_MISC_ERROR, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_AUTH_RECENTS);
None
}
(COMPONENT_COLOR_MISC_INPUT, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_MISC_ERROR);
None
}
(COMPONENT_COLOR_MISC_KEYS, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_MISC_INPUT);
None
}
(COMPONENT_COLOR_MISC_QUIT, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_MISC_KEYS);
None
}
(COMPONENT_COLOR_MISC_SAVE, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_MISC_QUIT);
None
}
(COMPONENT_COLOR_MISC_WARN, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_MISC_SAVE);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_MISC_WARN);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, &MSG_KEY_UP) => {
self.view
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG);
None
}
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, &MSG_KEY_UP) => {
self.view
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG);
None
}
(COMPONENT_COLOR_TRANSFER_PROG_BAR, &MSG_KEY_UP) => {
self.view
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG);
None
}
(COMPONENT_COLOR_TRANSFER_LOG_BG, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR);
None
}
(COMPONENT_COLOR_TRANSFER_LOG_WIN, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG);
None
}
(COMPONENT_COLOR_TRANSFER_STATUS_SORTING, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN);
None
}
(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING);
None
}
(COMPONENT_COLOR_TRANSFER_STATUS_SYNC, &MSG_KEY_UP) => {
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN);
None
}
// On color change
(component, Msg::OnChange(Payload::One(Value::Str(color)))) => {
if let Some(color) = parse_color(color) {
self.action_save_color(component, color);
}
None
}
// Error <ENTER> or <ESC>
(COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => {
// Umount text error
self.umount_error();
None
}
(COMPONENT_TEXT_ERROR, _) => None,
// Exit
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Save changes
if let Err(err) = self.action_save_all() {
self.mount_error(err.as_str());
}
// Exit
self.exit_reason = Some(super::ExitReason::Quit);
None
}
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
// Quit
self.exit_reason = Some(super::ExitReason::Quit);
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => {
// Umount popup
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, _) => None,
// Close help
(COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => {
// Umount help
self.umount_help();
None
}
(COMPONENT_TEXT_HELP, _) => None,
// Save popup
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Save config
if let Err(err) = self.action_save_all() {
self.mount_error(err.as_str());
}
self.umount_save_popup();
None
}
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => {
// Umount radio save
self.umount_save_popup();
None
}
(COMPONENT_RADIO_SAVE, _) => None,
// Edit SSH Key
// <CTRL+H> Show help
(_, &MSG_KEY_CTRL_H) => {
// Show help
self.mount_help();
None
}
(_, &MSG_KEY_TAB) => {
// Change view
self.init(ViewLayout::SetupForm);
None
}
// <CTRL+R> Revert changes
(_, &MSG_KEY_CTRL_R) => {
// Revert changes
if let Err(err) = self.action_reset_theme() {
self.mount_error(err.as_str());
}
None
}
// <CTRL+S> Save
(_, &MSG_KEY_CTRL_S) => {
// Show save
self.mount_save_popup();
None
}
// <ESC>
(_, &MSG_KEY_ESC) => {
// Mount quit prompt
self.mount_quit();
None
}
(_, _) => None, // Nothing to do
},
}
}
}

View file

@ -1,808 +0,0 @@
//! ## SetupActivity
//!
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
//! work on termscp configuration
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity, ViewLayout};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
use crate::ui::components::{
bookmark_list::{BookmarkList, BookmarkListPropsBuilder},
msgbox::{MsgBox, MsgBoxPropsBuilder},
};
use crate::utils::ui::draw_area_in;
// Ext
use std::path::PathBuf;
use tuirealm::components::{
input::{Input, InputPropsBuilder},
radio::{Radio, RadioPropsBuilder},
scrolltable::{ScrollTablePropsBuilder, Scrolltable},
span::{Span, SpanPropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
Payload, Value, View,
};
impl SetupActivity {
// -- view
/// ### init_setup
///
/// Initialize setup view
pub(super) fn init_setup(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.view.mount(
super::COMPONENT_RADIO_TAB,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::White)
.with_options(
None,
vec![String::from("User Interface"), String::from("SSH Keys")],
)
.with_value(0)
.build(),
)),
);
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
])
.build(),
)),
);
// Input fields
self.view.mount(
super::COMPONENT_INPUT_TEXT_EDITOR,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightGreen)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_label(String::from("Text editor"))
.build(),
)),
);
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
self.view.mount(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightCyan)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_options(
Some(String::from("Default file transfer protocol")),
vec![
String::from("SFTP"),
String::from("SCP"),
String::from("FTP"),
String::from("FTPS"),
],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_HIDDEN_FILES,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Show hidden files (by default)")),
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_UPDATES,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_options(
Some(String::from("Check for updates?")),
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_GROUP_DIRS,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightMagenta)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
.with_options(
Some(String::from("Group directories")),
vec![
String::from("Display first"),
String::from("Display Last"),
String::from("No"),
],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightBlue)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
.with_label(String::from("File formatter syntax (local)"))
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightGreen)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_label(String::from("File formatter syntax (remote)"))
.build(),
)),
);
// Load values
self.load_input_values();
// Set view
self.layout = ViewLayout::SetupForm;
}
/// ### init_ssh_keys
///
/// Initialize ssh keys view
pub(super) fn init_ssh_keys(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.view.mount(
super::COMPONENT_RADIO_TAB,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::LightYellow)
.with_options(
None,
vec![String::from("User Interface"), String::from("SSH Keys")],
)
.with_value(1)
.build(),
)),
);
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
])
.build(),
)),
);
self.view.mount(
super::COMPONENT_LIST_SSH_KEYS,
Box::new(BookmarkList::new(
BookmarkListPropsBuilder::default()
.with_bookmarks(Some(String::from("SSH Keys")), vec![])
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.with_background(Color::LightGreen)
.with_foreground(Color::Black)
.build(),
)),
);
// Give focus
self.view.active(super::COMPONENT_LIST_SSH_KEYS);
// Load keys
self.reload_ssh_keys();
// Set view
self.layout = ViewLayout::SshKeys;
}
/// ### view
///
/// View gui
pub(super) fn view(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal.draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(3), // Current tab
Constraint::Percentage(90), // Main body
Constraint::Length(3), // Help footer
]
.as_ref(),
)
.split(f.size());
// Render common widget
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
match self.layout {
ViewLayout::SetupForm => {
// Make chunks
let ui_cfg_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Text editor
Constraint::Length(3), // Protocol tab
Constraint::Length(3), // Hidden files
Constraint::Length(3), // Updates tab
Constraint::Length(3), // Group dirs
Constraint::Length(3), // Local Format input
Constraint::Length(3), // Remote Format input
Constraint::Length(1), // Empty ?
]
.as_ref(),
)
.split(chunks[1]);
self.view
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks[0]);
self.view
.render(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, f, ui_cfg_chunks[1]);
self.view
.render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]);
self.view
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]);
self.view
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]);
self.view
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[5]);
self.view
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[6]);
}
ViewLayout::SshKeys => {
let sshcfg_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(100)].as_ref())
.split(chunks[1]);
self.view
.render(super::COMPONENT_LIST_SSH_KEYS, f, sshcfg_chunks[0]);
}
}
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 20);
f.render_widget(Clear, popup);
let popup_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Host
Constraint::Length(3), // Username
]
.as_ref(),
)
.split(popup);
self.view
.render(super::COMPONENT_INPUT_SSH_HOST, f, popup_chunks[0]);
self.view
.render(super::COMPONENT_INPUT_SSH_USERNAME, f, popup_chunks[1]);
}
}
});
// Put context back to context
self.context = Some(ctx);
}
// -- mount
/// ### mount_error
///
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
// Mount
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(MsgBox::new(
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.bold()
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
.with_texts(None, vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_ERROR);
}
/// ### umount_error
///
/// Umount error message
pub(super) fn umount_error(&mut self) {
self.view.umount(super::COMPONENT_TEXT_ERROR);
}
/// ### mount_del_ssh_key
///
/// Mount delete ssh key component
pub(super) fn mount_del_ssh_key(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_DEL_SSH_KEY,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Delete key?")),
vec![String::from("Yes"), String::from("No")],
)
.with_value(1) // Default: No
.build(),
)),
);
// Active
self.view.active(super::COMPONENT_RADIO_DEL_SSH_KEY);
}
/// ### umount_del_ssh_key
///
/// Umount delete ssh key
pub(super) fn umount_del_ssh_key(&mut self) {
self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY);
}
/// ### mount_new_ssh_key
///
/// Mount new ssh key prompt
pub(super) fn mount_new_ssh_key(&mut self) {
self.view.mount(
super::COMPONENT_INPUT_SSH_HOST,
Box::new(Input::new(
InputPropsBuilder::default()
.with_label(String::from("Hostname or address"))
.with_borders(
Borders::TOP | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_SSH_USERNAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_label(String::from("Username"))
.with_borders(
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
self.view.active(super::COMPONENT_INPUT_SSH_HOST);
}
/// ### umount_new_ssh_key
///
/// Umount new ssh key prompt
pub(super) fn umount_new_ssh_key(&mut self) {
self.view.umount(super::COMPONENT_INPUT_SSH_HOST);
self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME);
}
/// ### mount_quit
///
/// Mount quit popup
pub(super) fn mount_quit(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_QUIT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Exit setup?")),
vec![
String::from("Save"),
String::from("Don't save"),
String::from("Cancel"),
],
)
.build(),
)),
);
// Active
self.view.active(super::COMPONENT_RADIO_QUIT);
}
/// ### umount_quit
///
/// Umount quit
pub(super) fn umount_quit(&mut self) {
self.view.umount(super::COMPONENT_RADIO_QUIT);
}
/// ### mount_save_popup
///
/// Mount save popup
pub(super) fn mount_save_popup(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_SAVE,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_options(
Some(String::from("Save changes?")),
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
// Active
self.view.active(super::COMPONENT_RADIO_SAVE);
}
/// ### umount_quit
///
/// Umount quit
pub(super) fn umount_save_popup(&mut self) {
self.view.umount(super::COMPONENT_RADIO_SAVE);
}
/// ### mount_help
///
/// Mount help
pub(super) fn mount_help(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Scrolltable::new(
ScrollTablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_highlighted_str(Some("?"))
.with_max_scroll_step(8)
.bold()
.with_table(
Some(String::from("Help")),
TableBuilder::default()
.add_col(
TextSpanBuilder::new("<ESC>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Exit setup"))
.add_row()
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change setup page"))
.add_row()
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change cursor"))
.add_row()
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change input field"))
.add_row()
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Select / Dismiss popup"))
.add_row()
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Delete SSH key"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+N>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" New SSH key"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+R>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Revert changes"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+S>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Save configuration"))
.build(),
)
.build(),
)),
);
// Active help
self.view.active(super::COMPONENT_TEXT_HELP);
}
/// ### umount_help
///
/// Umount help
pub(super) fn umount_help(&mut self) {
self.view.umount(super::COMPONENT_TEXT_HELP);
}
/// ### load_input_values
///
/// Load values from configuration into input fields
pub(super) fn load_input_values(&mut self) {
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
// Text editor
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
let text_editor: String =
String::from(cli.get_text_editor().as_path().to_string_lossy());
let props = InputPropsBuilder::from(props)
.with_value(text_editor)
.build();
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
}
// Protocol
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
let protocol: usize = match cli.get_default_protocol() {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
};
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
}
// Hidden files
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
let hidden: usize = match cli.get_show_hidden_files() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
}
// Updates
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
let updates: usize = match cli.get_check_for_updates() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
}
// Group dirs
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
let dirs: usize = match cli.get_group_dirs() {
Some(GroupDirs::First) => 0,
Some(GroupDirs::Last) => 1,
None => 2,
};
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
}
// Local File Fmt
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) {
let file_fmt: String = cli.get_local_file_fmt().unwrap_or_default();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props);
}
// Remote File Fmt
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) {
let file_fmt: String = cli.get_remote_file_fmt().unwrap_or_default();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
}
}
}
/// ### collect_input_values
///
/// Collect values from input and put them into the configuration
pub(super) fn collect_input_values(&mut self) {
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
if let Some(Payload::One(Value::Str(editor))) =
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
{
cli.set_text_editor(PathBuf::from(editor.as_str()));
}
if let Some(Payload::One(Value::Usize(protocol))) =
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
{
let protocol: FileTransferProtocol = match protocol {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
_ => FileTransferProtocol::Sftp,
};
cli.set_default_protocol(protocol);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
{
let show: bool = matches!(opt, 0);
cli.set_show_hidden_files(show);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
{
let check: bool = matches!(opt, 0);
cli.set_check_for_updates(check);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
{
cli.set_local_file_fmt(fmt);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT)
{
cli.set_remote_file_fmt(fmt);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
{
let dirs: Option<GroupDirs> = match opt {
0 => Some(GroupDirs::First),
1 => Some(GroupDirs::Last),
_ => None,
};
cli.set_group_dirs(dirs);
}
}
}
/// ### reload_ssh_keys
///
/// Reload ssh keys
pub(super) fn reload_ssh_keys(&mut self) {
if let Some(cli) = self.context.as_ref().unwrap().config_client.as_ref() {
// get props
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
// Create texts
let keys: Vec<String> = cli
.iter_ssh_keys()
.map(|x| {
let (addr, username, _) = cli.get_ssh_key(x).ok().unwrap().unwrap();
format!("{} at {}", addr, username)
})
.collect();
let props = BookmarkListPropsBuilder::from(props)
.with_bookmarks(Some(String::from("SSH Keys")), keys)
.build();
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
}
}
}
}

View file

@ -0,0 +1,265 @@
//! ## SetupActivity
//!
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
//! work on termscp configuration
/**
* 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.
*/
pub mod setup;
pub mod ssh_keys;
pub mod theme;
use super::*;
pub use setup::*;
pub use ssh_keys::*;
pub use theme::*;
// Locals
use crate::ui::components::msgbox::{MsgBox, MsgBoxPropsBuilder};
// Ext
use tuirealm::components::{
radio::{Radio, RadioPropsBuilder},
scrolltable::{ScrollTablePropsBuilder, Scrolltable},
};
use tuirealm::props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder};
use tuirealm::tui::{
style::Color,
widgets::{BorderType, Borders},
};
impl SetupActivity {
// -- view
pub(super) fn init(&mut self, layout: ViewLayout) {
self.layout = layout;
match self.layout {
ViewLayout::SetupForm => self.init_setup(),
ViewLayout::SshKeys => self.init_ssh_keys(),
ViewLayout::Theme => self.init_theme(),
}
}
/// ### view
///
/// View gui
pub(super) fn view(&mut self) {
match self.layout {
ViewLayout::SetupForm => self.view_setup(),
ViewLayout::SshKeys => self.view_ssh_keys(),
ViewLayout::Theme => self.view_theme(),
}
}
// -- mount
/// ### mount_error
///
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
// Mount
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(MsgBox::new(
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.bold()
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
.with_texts(None, vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_ERROR);
}
/// ### umount_error
///
/// Umount error message
pub(super) fn umount_error(&mut self) {
self.view.umount(super::COMPONENT_TEXT_ERROR);
}
/// ### mount_quit
///
/// Mount quit popup
pub(super) fn mount_quit(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_QUIT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Exit setup?")),
vec![
String::from("Save"),
String::from("Don't save"),
String::from("Cancel"),
],
)
.build(),
)),
);
// Active
self.view.active(super::COMPONENT_RADIO_QUIT);
}
/// ### umount_quit
///
/// Umount quit
pub(super) fn umount_quit(&mut self) {
self.view.umount(super::COMPONENT_RADIO_QUIT);
}
/// ### mount_save_popup
///
/// Mount save popup
pub(super) fn mount_save_popup(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_SAVE,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_options(
Some(String::from("Save changes?")),
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
// Active
self.view.active(super::COMPONENT_RADIO_SAVE);
}
/// ### umount_quit
///
/// Umount quit
pub(super) fn umount_save_popup(&mut self) {
self.view.umount(super::COMPONENT_RADIO_SAVE);
}
/// ### mount_help
///
/// Mount help
pub(super) fn mount_help(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Scrolltable::new(
ScrollTablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_highlighted_str(Some("?"))
.with_max_scroll_step(8)
.bold()
.with_table(
Some(String::from("Help")),
TableBuilder::default()
.add_col(
TextSpanBuilder::new("<ESC>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Exit setup"))
.add_row()
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change setup page"))
.add_row()
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change cursor"))
.add_row()
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change input field"))
.add_row()
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Select / Dismiss popup"))
.add_row()
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Delete SSH key"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+N>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" New SSH key"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+R>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Revert changes"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+S>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Save configuration"))
.build(),
)
.build(),
)),
);
// Active help
self.view.active(super::COMPONENT_TEXT_HELP);
}
/// ### umount_help
///
/// Umount help
pub(super) fn umount_help(&mut self) {
self.view.umount(super::COMPONENT_TEXT_HELP);
}
}

View file

@ -0,0 +1,414 @@
//! ## SetupActivity
//!
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
//! work on termscp configuration
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
use crate::utils::ui::draw_area_in;
// Ext
use std::path::PathBuf;
use tuirealm::components::{
input::{Input, InputPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{PropsBuilder, TextSpanBuilder},
Payload, Value, View,
};
impl SetupActivity {
// -- view
/// ### init_setup
///
/// Initialize setup view
pub(super) fn init_setup(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.view.mount(
super::COMPONENT_RADIO_TAB,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::White)
.with_options(
None,
vec![
String::from("User Interface"),
String::from("SSH Keys"),
String::from("Theme"),
],
)
.with_value(0)
.build(),
)),
);
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
])
.build(),
)),
);
// Input fields
self.view.mount(
super::COMPONENT_INPUT_TEXT_EDITOR,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightGreen)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_label(String::from("Text editor"))
.build(),
)),
);
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
self.view.mount(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightCyan)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_options(
Some(String::from("Default file transfer protocol")),
vec![
String::from("SFTP"),
String::from("SCP"),
String::from("FTP"),
String::from("FTPS"),
],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_HIDDEN_FILES,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Show hidden files (by default)")),
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_UPDATES,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_options(
Some(String::from("Check for updates?")),
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_GROUP_DIRS,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightMagenta)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
.with_options(
Some(String::from("Group directories")),
vec![
String::from("Display first"),
String::from("Display Last"),
String::from("No"),
],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightBlue)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
.with_label(String::from("File formatter syntax (local)"))
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightGreen)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_label(String::from("File formatter syntax (remote)"))
.build(),
)),
);
// Load values
self.load_input_values();
}
pub(super) fn view_setup(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal.draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(3), // Current tab
Constraint::Length(21), // Main body
Constraint::Length(3), // Help footer
]
.as_ref(),
)
.split(f.size());
// Render common widget
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
// Make chunks
let ui_cfg_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Text editor
Constraint::Length(3), // Protocol tab
Constraint::Length(3), // Hidden files
Constraint::Length(3), // Updates tab
Constraint::Length(3), // Group dirs
Constraint::Length(3), // Local Format input
Constraint::Length(3), // Remote Format input
]
.as_ref(),
)
.split(chunks[1]);
self.view
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks[0]);
self.view
.render(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, f, ui_cfg_chunks[1]);
self.view
.render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]);
self.view
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]);
self.view
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]);
self.view
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[5]);
self.view
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[6]);
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
}
}
});
// Put context back to context
self.context = Some(ctx);
}
/// ### load_input_values
///
/// Load values from configuration into input fields
pub(crate) fn load_input_values(&mut self) {
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
// Text editor
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
let text_editor: String =
String::from(cli.get_text_editor().as_path().to_string_lossy());
let props = InputPropsBuilder::from(props)
.with_value(text_editor)
.build();
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
}
// Protocol
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
let protocol: usize = match cli.get_default_protocol() {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
};
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
}
// Hidden files
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
let hidden: usize = match cli.get_show_hidden_files() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
}
// Updates
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
let updates: usize = match cli.get_check_for_updates() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
}
// Group dirs
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
let dirs: usize = match cli.get_group_dirs() {
Some(GroupDirs::First) => 0,
Some(GroupDirs::Last) => 1,
None => 2,
};
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
}
// Local File Fmt
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) {
let file_fmt: String = cli.get_local_file_fmt().unwrap_or_default();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props);
}
// Remote File Fmt
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) {
let file_fmt: String = cli.get_remote_file_fmt().unwrap_or_default();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
}
}
}
/// ### collect_input_values
///
/// Collect values from input and put them into the configuration
pub(crate) fn collect_input_values(&mut self) {
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
if let Some(Payload::One(Value::Str(editor))) =
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
{
cli.set_text_editor(PathBuf::from(editor.as_str()));
}
if let Some(Payload::One(Value::Usize(protocol))) =
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
{
let protocol: FileTransferProtocol = match protocol {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
_ => FileTransferProtocol::Sftp,
};
cli.set_default_protocol(protocol);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
{
let show: bool = matches!(opt, 0);
cli.set_show_hidden_files(show);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
{
let check: bool = matches!(opt, 0);
cli.set_check_for_updates(check);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
{
cli.set_local_file_fmt(fmt);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT)
{
cli.set_remote_file_fmt(fmt);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
{
let dirs: Option<GroupDirs> = match opt {
0 => Some(GroupDirs::First),
1 => Some(GroupDirs::Last),
_ => None,
};
cli.set_group_dirs(dirs);
}
}
}
}

View file

@ -0,0 +1,296 @@
//! ## SetupActivity
//!
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
//! work on termscp configuration
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity};
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use crate::utils::ui::draw_area_in;
// Ext
use tuirealm::components::{
input::{Input, InputPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{PropsBuilder, TextSpanBuilder},
View,
};
impl SetupActivity {
// -- view
/// ### init_ssh_keys
///
/// Initialize ssh keys view
pub(super) fn init_ssh_keys(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.view.mount(
super::COMPONENT_RADIO_TAB,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::LightYellow)
.with_options(
None,
vec![
String::from("User Interface"),
String::from("SSH Keys"),
String::from("Theme"),
],
)
.with_value(1)
.build(),
)),
);
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
])
.build(),
)),
);
self.view.mount(
super::COMPONENT_LIST_SSH_KEYS,
Box::new(BookmarkList::new(
BookmarkListPropsBuilder::default()
.with_bookmarks(Some(String::from("SSH Keys")), vec![])
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.with_background(Color::LightGreen)
.with_foreground(Color::Black)
.build(),
)),
);
// Give focus
self.view.active(super::COMPONENT_LIST_SSH_KEYS);
// Load keys
self.reload_ssh_keys();
}
pub(crate) fn view_ssh_keys(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal.draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(3), // Current tab
Constraint::Percentage(90), // Main body
Constraint::Length(3), // Help footer
]
.as_ref(),
)
.split(f.size());
// Render common widget
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
self.view
.render(super::COMPONENT_LIST_SSH_KEYS, f, chunks[1]);
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 20);
f.render_widget(Clear, popup);
let popup_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Host
Constraint::Length(3), // Username
]
.as_ref(),
)
.split(popup);
self.view
.render(super::COMPONENT_INPUT_SSH_HOST, f, popup_chunks[0]);
self.view
.render(super::COMPONENT_INPUT_SSH_USERNAME, f, popup_chunks[1]);
}
}
});
// Put context back to context
self.context = Some(ctx);
}
// -- mount
/// ### mount_del_ssh_key
///
/// Mount delete ssh key component
pub(crate) fn mount_del_ssh_key(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_DEL_SSH_KEY,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Delete key?")),
vec![String::from("Yes"), String::from("No")],
)
.with_value(1) // Default: No
.build(),
)),
);
// Active
self.view.active(super::COMPONENT_RADIO_DEL_SSH_KEY);
}
/// ### umount_del_ssh_key
///
/// Umount delete ssh key
pub(crate) fn umount_del_ssh_key(&mut self) {
self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY);
}
/// ### mount_new_ssh_key
///
/// Mount new ssh key prompt
pub(crate) fn mount_new_ssh_key(&mut self) {
self.view.mount(
super::COMPONENT_INPUT_SSH_HOST,
Box::new(Input::new(
InputPropsBuilder::default()
.with_label(String::from("Hostname or address"))
.with_borders(
Borders::TOP | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_SSH_USERNAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_label(String::from("Username"))
.with_borders(
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
self.view.active(super::COMPONENT_INPUT_SSH_HOST);
}
/// ### umount_new_ssh_key
///
/// Umount new ssh key prompt
pub(crate) fn umount_new_ssh_key(&mut self) {
self.view.umount(super::COMPONENT_INPUT_SSH_HOST);
self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME);
}
/// ### reload_ssh_keys
///
/// Reload ssh keys
pub(crate) fn reload_ssh_keys(&mut self) {
if let Some(cli) = self.context.as_ref().unwrap().config_client.as_ref() {
// get props
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
// Create texts
let keys: Vec<String> = cli
.iter_ssh_keys()
.map(|x| {
let (addr, username, _) = cli.get_ssh_key(x).ok().unwrap().unwrap();
format!("{} at {}", addr, username)
})
.collect();
let props = BookmarkListPropsBuilder::from(props)
.with_bookmarks(Some(String::from("SSH Keys")), keys)
.build();
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
}
}
}
}

View file

@ -0,0 +1,656 @@
//! ## SetupActivity
//!
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
//! work on termscp configuration
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity};
use crate::config::themes::Theme;
use crate::ui::components::color_picker::{ColorPicker, ColorPickerPropsBuilder};
use crate::utils::parser::parse_color;
use crate::utils::ui::draw_area_in;
// Ext
use tuirealm::components::{
label::{Label, LabelPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{PropsBuilder, TextSpanBuilder},
Payload, Value, View,
};
impl SetupActivity {
// -- view
/// ### init_theme
///
/// Initialize thene view
pub(super) fn init_theme(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.view.mount(
super::COMPONENT_RADIO_TAB,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::White)
.with_options(
None,
vec![
String::from("User Interface"),
String::from("SSH Keys"),
String::from("Theme"),
],
)
.with_value(2)
.build(),
)),
);
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
])
.build(),
)),
);
// auth colors
self.mount_title(super::COMPONENT_COLOR_AUTH_TITLE, "Authentication styles");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PROTOCOL, "Protocol");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_ADDR, "Ip address");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PORT, "Port");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_USERNAME, "Username");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PASSWORD, "Password");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_BOOKMARKS, "Bookmarks");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_RECENTS, "Recent connections");
// Misc
self.mount_title(super::COMPONENT_COLOR_MISC_TITLE, "Misc styles");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_ERROR, "Error");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INPUT, "Input fields");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_KEYS, "Key strokes");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_QUIT, "Quit dialogs");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_SAVE, "Save confirmations");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_WARN, "Warnings");
// Transfer (1)
self.mount_title(super::COMPONENT_COLOR_TRANSFER_TITLE, "Transfer styles");
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
"Local explorer background",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
"Local explorer foreground",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
"Local explorer highlighted",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
"Remote explorer background",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
"Remote explorer foreground",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
"Remote explorer highlighted",
);
self.mount_color_picker(super::COMPONENT_COLOR_TRANSFER_PROG_BAR, "Progress bar");
// Transfer (2)
self.mount_title(
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
"Transfer styles (2)",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
"Log window background",
);
self.mount_color_picker(super::COMPONENT_COLOR_TRANSFER_LOG_WIN, "Log window");
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
"File sorting",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
"Hidden files",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
"Synchronized browsing",
);
// Load styles
self.load_styles();
// Active first field
self.view.active(super::COMPONENT_COLOR_AUTH_PROTOCOL);
}
pub(super) fn view_theme(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal.draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(3), // Current tab
Constraint::Length(22), // Main body
Constraint::Length(3), // Help footer
]
.as_ref(),
)
.split(f.size());
// Render common widget
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
// Make chunks
let colors_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(chunks[1]);
let auth_colors_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1), // Title
Constraint::Length(3), // Protocol
Constraint::Length(3), // Addr
Constraint::Length(3), // Port
Constraint::Length(3), // Username
Constraint::Length(3), // Password
Constraint::Length(3), // Bookmarks
Constraint::Length(3), // Recents
]
.as_ref(),
)
.split(colors_layout[0]);
self.view
.render(super::COMPONENT_COLOR_AUTH_TITLE, f, auth_colors_layout[0]);
self.view.render(
super::COMPONENT_COLOR_AUTH_PROTOCOL,
f,
auth_colors_layout[1],
);
self.view
.render(super::COMPONENT_COLOR_AUTH_ADDR, f, auth_colors_layout[2]);
self.view
.render(super::COMPONENT_COLOR_AUTH_PORT, f, auth_colors_layout[3]);
self.view.render(
super::COMPONENT_COLOR_AUTH_USERNAME,
f,
auth_colors_layout[4],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_PASSWORD,
f,
auth_colors_layout[5],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_BOOKMARKS,
f,
auth_colors_layout[6],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_RECENTS,
f,
auth_colors_layout[7],
);
let misc_colors_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1), // Title
Constraint::Length(3), // Error
Constraint::Length(3), // Input
Constraint::Length(3), // Keys
Constraint::Length(3), // Quit
Constraint::Length(3), // Save
Constraint::Length(3), // Warn
Constraint::Length(3), // Empty
]
.as_ref(),
)
.split(colors_layout[1]);
self.view
.render(super::COMPONENT_COLOR_MISC_TITLE, f, misc_colors_layout[0]);
self.view
.render(super::COMPONENT_COLOR_MISC_ERROR, f, misc_colors_layout[1]);
self.view
.render(super::COMPONENT_COLOR_MISC_INPUT, f, misc_colors_layout[2]);
self.view
.render(super::COMPONENT_COLOR_MISC_KEYS, f, misc_colors_layout[3]);
self.view
.render(super::COMPONENT_COLOR_MISC_QUIT, f, misc_colors_layout[4]);
self.view
.render(super::COMPONENT_COLOR_MISC_SAVE, f, misc_colors_layout[5]);
self.view
.render(super::COMPONENT_COLOR_MISC_WARN, f, misc_colors_layout[6]);
let transfer_colors_layout_col1 = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1), // Title
Constraint::Length(3), // local explorer bg
Constraint::Length(3), // local explorer fg
Constraint::Length(3), // local explorer hg
Constraint::Length(3), // remote explorer bg
Constraint::Length(3), // remote explorer fg
Constraint::Length(3), // remote explorer hg
Constraint::Length(3), // prog bar
]
.as_ref(),
)
.split(colors_layout[2]);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_TITLE,
f,
transfer_colors_layout_col1[0],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
f,
transfer_colors_layout_col1[1],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
f,
transfer_colors_layout_col1[2],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
f,
transfer_colors_layout_col1[3],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
f,
transfer_colors_layout_col1[4],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
f,
transfer_colors_layout_col1[5],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
f,
transfer_colors_layout_col1[6],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR,
f,
transfer_colors_layout_col1[7],
);
let transfer_colors_layout_col2 = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1), // Title
Constraint::Length(3), // log bg
Constraint::Length(3), // log window
Constraint::Length(3), // status sorting
Constraint::Length(3), // status hidden
Constraint::Length(3), // sync browsing
Constraint::Length(3), // Empty
Constraint::Length(3), // Empty
]
.as_ref(),
)
.split(colors_layout[3]);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
f,
transfer_colors_layout_col2[0],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
f,
transfer_colors_layout_col2[1],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
f,
transfer_colors_layout_col2[2],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
f,
transfer_colors_layout_col2[3],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
f,
transfer_colors_layout_col2[4],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
f,
transfer_colors_layout_col2[5],
);
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
}
}
});
// Put context back to context
self.context = Some(ctx);
}
/// ### load_styles
///
/// Load values from theme into input fields
pub(crate) fn load_styles(&mut self) {
let theme: Theme = self.theme().clone();
self.update_color(super::COMPONENT_COLOR_AUTH_ADDR, theme.auth_address);
self.update_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS, theme.auth_bookmarks);
self.update_color(super::COMPONENT_COLOR_AUTH_PASSWORD, theme.auth_password);
self.update_color(super::COMPONENT_COLOR_AUTH_PORT, theme.auth_port);
self.update_color(super::COMPONENT_COLOR_AUTH_PROTOCOL, theme.auth_protocol);
self.update_color(super::COMPONENT_COLOR_AUTH_RECENTS, theme.auth_recents);
self.update_color(super::COMPONENT_COLOR_AUTH_USERNAME, theme.auth_username);
self.update_color(super::COMPONENT_COLOR_MISC_ERROR, theme.misc_error_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_INPUT, theme.misc_input_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_KEYS, theme.misc_keys);
self.update_color(super::COMPONENT_COLOR_MISC_QUIT, theme.misc_quit_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_SAVE, theme.misc_save_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_WARN, theme.misc_warn_dialog);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
theme.transfer_local_explorer_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
theme.transfer_local_explorer_foreground,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
theme.transfer_local_explorer_highlighted,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
theme.transfer_remote_explorer_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
theme.transfer_remote_explorer_foreground,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
theme.transfer_remote_explorer_highlighted,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR,
theme.transfer_progress_bar,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
theme.transfer_log_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
theme.transfer_log_window,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
theme.transfer_status_sorting,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
theme.transfer_status_hidden,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
theme.transfer_status_sync_browsing,
);
}
/// ### collect_styles
///
/// Collect values from input and put them into the theme.
/// If a component has an invalid color, returns Err(component_id)
pub(crate) fn collect_styles(&mut self) -> Result<(), &'static str> {
// auth
let auth_address: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_ADDR)
.map_err(|_| super::COMPONENT_COLOR_AUTH_ADDR)?;
let auth_bookmarks: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS)
.map_err(|_| super::COMPONENT_COLOR_AUTH_BOOKMARKS)?;
let auth_password: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PASSWORD)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PASSWORD)?;
let auth_port: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PORT)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PORT)?;
let auth_protocol: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PROTOCOL)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PROTOCOL)?;
let auth_recents: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_RECENTS)
.map_err(|_| super::COMPONENT_COLOR_AUTH_RECENTS)?;
let auth_username: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_USERNAME)
.map_err(|_| super::COMPONENT_COLOR_AUTH_USERNAME)?;
// misc
let misc_error_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_ERROR)
.map_err(|_| super::COMPONENT_COLOR_MISC_ERROR)?;
let misc_input_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_INPUT)
.map_err(|_| super::COMPONENT_COLOR_MISC_INPUT)?;
let misc_keys: Color = self
.get_color(super::COMPONENT_COLOR_MISC_KEYS)
.map_err(|_| super::COMPONENT_COLOR_MISC_KEYS)?;
let misc_quit_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_QUIT)
.map_err(|_| super::COMPONENT_COLOR_MISC_QUIT)?;
let misc_save_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_SAVE)
.map_err(|_| super::COMPONENT_COLOR_MISC_SAVE)?;
let misc_warn_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_WARN)
.map_err(|_| super::COMPONENT_COLOR_MISC_WARN)?;
// transfer
let transfer_local_explorer_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)?;
let transfer_local_explorer_foreground: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)?;
let transfer_local_explorer_highlighted: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)?;
let transfer_remote_explorer_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)?;
let transfer_remote_explorer_foreground: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)?;
let transfer_remote_explorer_highlighted: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)?;
let transfer_log_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_BG)?;
let transfer_log_window: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_WIN)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_WIN)?;
let transfer_progress_bar: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR)?;
let transfer_status_hidden: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)?;
let transfer_status_sorting: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)?;
let transfer_status_sync_browsing: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)?;
// Update theme
let mut theme: &mut Theme = self.theme_mut();
theme.auth_address = auth_address;
theme.auth_bookmarks = auth_bookmarks;
theme.auth_password = auth_password;
theme.auth_port = auth_port;
theme.auth_protocol = auth_protocol;
theme.auth_recents = auth_recents;
theme.auth_username = auth_username;
theme.misc_error_dialog = misc_error_dialog;
theme.misc_input_dialog = misc_input_dialog;
theme.misc_keys = misc_keys;
theme.misc_quit_dialog = misc_quit_dialog;
theme.misc_save_dialog = misc_save_dialog;
theme.misc_warn_dialog = misc_warn_dialog;
theme.transfer_local_explorer_background = transfer_local_explorer_background;
theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground;
theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted;
theme.transfer_remote_explorer_background = transfer_remote_explorer_background;
theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground;
theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted;
theme.transfer_log_background = transfer_log_background;
theme.transfer_log_window = transfer_log_window;
theme.transfer_progress_bar = transfer_progress_bar;
theme.transfer_status_hidden = transfer_status_hidden;
theme.transfer_status_sorting = transfer_status_sorting;
theme.transfer_status_sync_browsing = transfer_status_sync_browsing;
Ok(())
}
/// ### update_color
///
/// Update color for provided component
fn update_color(&mut self, component: &str, color: Color) {
if let Some(props) = self.view.get_props(component) {
self.view.update(
component,
ColorPickerPropsBuilder::from(props)
.with_color(&color)
.build(),
);
}
}
/// ### get_color
///
/// Get color from component
fn get_color(&self, component: &str) -> Result<Color, ()> {
match self.view.get_state(component) {
Some(Payload::One(Value::Str(color))) => match parse_color(color.as_str()) {
Some(c) => Ok(c),
None => Err(()),
},
_ => Err(()),
}
}
/// ### mount_color_picker
///
/// Mount color picker with provided data
fn mount_color_picker(&mut self, id: &str, label: &str) {
self.view.mount(
id,
Box::new(ColorPicker::new(
ColorPickerPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::Reset)
.with_label(label.to_string())
.build(),
)),
);
}
/// ### mount_title
///
/// Mount title
fn mount_title(&mut self, id: &str, text: &str) {
self.view.mount(
id,
Box::new(Label::new(
LabelPropsBuilder::default()
.bold()
.with_text(text.to_string())
.build(),
)),
);
}
}

View file

@ -0,0 +1,300 @@
//! ## ColorPicker
//!
//! `ColorPicker` component extends an `Input` component in order to provide some extra features
//! for the color picker.
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use crate::utils::fmt::fmt_color;
use crate::utils::parser::parse_color;
// ext
use tuirealm::components::input::{Input, InputPropsBuilder};
use tuirealm::event::Event;
use tuirealm::props::{Props, PropsBuilder};
use tuirealm::tui::{
layout::Rect,
style::Color,
widgets::{BorderType, Borders},
};
use tuirealm::{Canvas, Component, Msg, Payload, Value};
// -- props
/// ## ColorPickerPropsBuilder
///
/// A wrapper around an `InputPropsBuilder`
pub struct ColorPickerPropsBuilder {
puppet: InputPropsBuilder,
}
impl Default for ColorPickerPropsBuilder {
fn default() -> Self {
Self {
puppet: InputPropsBuilder::default(),
}
}
}
impl PropsBuilder for ColorPickerPropsBuilder {
fn build(&mut self) -> Props {
self.puppet.build()
}
fn hidden(&mut self) -> &mut Self {
self.puppet.hidden();
self
}
fn visible(&mut self) -> &mut Self {
self.puppet.visible();
self
}
}
impl From<Props> for ColorPickerPropsBuilder {
fn from(props: Props) -> Self {
ColorPickerPropsBuilder {
puppet: InputPropsBuilder::from(props),
}
}
}
impl ColorPickerPropsBuilder {
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
self.puppet.with_borders(borders, variant, color);
self
}
/// ### with_label
///
/// Set input label
pub fn with_label(&mut self, label: String) -> &mut Self {
self.puppet.with_label(label);
self
}
/// ### with_color
///
/// Set initial value for component
pub fn with_color(&mut self, color: &Color) -> &mut Self {
self.puppet.with_value(fmt_color(color));
self
}
}
// -- component
/// ## ColorPicker
///
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
pub struct ColorPicker {
input: Input,
}
impl ColorPicker {
/// ### new
///
/// Instantiate a new `ColorPicker`
pub fn new(props: Props) -> Self {
// Instantiate a new color picker using input
Self {
input: Input::new(props),
}
}
/// ### update_colors
///
/// Update colors to match selected color, with provided one
fn update_colors(&mut self, color: Color) {
let mut props = self.get_props();
props.foreground = color;
props.borders.color = color;
let _ = self.input.update(props);
}
}
impl Component for ColorPicker {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
self.input.render(render, area);
}
/// ### update
///
/// Update component properties
/// Properties should first be retrieved through `get_props` which creates a builder from
/// existing properties and then edited before calling update.
/// Returns a Msg to the view
fn update(&mut self, props: Props) -> Msg {
let msg: Msg = self.input.update(props);
match msg {
Msg::OnChange(Payload::One(Value::Str(input))) => match parse_color(input.as_str()) {
Some(color) => {
// Update color and return OK
self.update_colors(color);
Msg::OnChange(Payload::One(Value::Str(input)))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
},
msg => msg,
}
}
/// ### get_props
///
/// Returns a props builder starting from component properties.
/// This returns a prop builder in order to make easier to create
/// new properties for the element.
fn get_props(&self) -> Props {
self.input.get_props()
}
/// ### on
///
/// Handle input event and update internal states.
/// Returns a Msg to the view
fn on(&mut self, ev: Event) -> Msg {
// Capture message from input
match self.input.on(ev) {
Msg::OnChange(Payload::One(Value::Str(input))) => {
// Capture color and validate
match parse_color(input.as_str()) {
Some(color) => {
// Update color and return OK
self.update_colors(color);
Msg::OnChange(Payload::One(Value::Str(input)))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
}
}
Msg::OnSubmit(_) => Msg::None,
msg => msg,
}
}
/// ### get_state
///
/// Get current state from component
/// For this component returns Unsigned if the input type is a number, otherwise a text
/// The value is always the current input.
fn get_state(&self) -> Payload {
match self.input.get_state() {
Payload::One(Value::Str(color)) => match parse_color(color.as_str()) {
None => Payload::None,
Some(_) => Payload::One(Value::Str(color)),
},
_ => Payload::None,
}
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.input.blur();
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.input.active();
}
}
#[cfg(test)]
mod test {
use super::*;
use crossterm::event::{KeyCode, KeyEvent};
use pretty_assertions::assert_eq;
#[test]
fn test_ui_components_color_picker() {
let mut component: ColorPicker = ColorPicker::new(
ColorPickerPropsBuilder::default()
.visible()
.with_color(&Color::Rgb(204, 170, 0))
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
.build(),
);
// Focus
component.blur();
component.active();
// Get value
assert_eq!(
component.get_state(),
Payload::One(Value::Str(String::from("#ccaa00")))
);
// Set an invalid color
let props = InputPropsBuilder::from(component.get_props())
.with_value(String::from("#pippo1"))
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.get_state(), Payload::None);
// Reset color
let props = ColorPickerPropsBuilder::from(component.get_props())
.with_color(&Color::Rgb(204, 170, 0))
.hidden()
.build();
assert_eq!(
component.update(props),
Msg::OnChange(Payload::One(Value::Str("#ccaa00".to_string())))
);
// Backspace (invalid)
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::None
);
// Press '1'
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('1')))),
Msg::OnChange(Payload::One(Value::Str(String::from("#ccaa01"))))
);
}
}

View file

@ -28,7 +28,9 @@
// ext
use tuirealm::components::utils::get_block;
use tuirealm::event::{Event, KeyCode, KeyModifiers};
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
use tuirealm::props::{
BordersProps, PropPayload, PropValue, Props, PropsBuilder, TextParts, TextSpan,
};
use tuirealm::tui::{
layout::{Corner, Rect},
style::{Color, Style},
@ -39,6 +41,8 @@ use tuirealm::{Canvas, Component, Msg, Payload, Value};
// -- props
const PROP_HIGHLIGHT_COLOR: &str = "props-highlight-color";
pub struct FileListPropsBuilder {
props: Option<Props>,
}
@ -98,6 +102,19 @@ impl FileListPropsBuilder {
self
}
/// ### with_highlight_color
///
/// Set highlighted color
pub fn with_highlight_color(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.own.insert(
PROP_HIGHLIGHT_COLOR,
PropPayload::One(PropValue::Color(color)),
);
}
self
}
/// ### with_borders
///
/// Set component borders style
@ -306,9 +323,13 @@ impl Component for FileList {
})
.collect(),
};
let (fg, bg): (Color, Color) = match self.states.focus {
true => (Color::Black, self.props.background),
false => (self.props.foreground, Color::Reset),
let highlighted_color: Color = match self.props.own.get(PROP_HIGHLIGHT_COLOR) {
Some(PropPayload::One(PropValue::Color(c))) => *c,
_ => Color::Reset,
};
let (h_fg, h_bg): (Color, Color) = match self.states.focus {
true => (Color::Black, highlighted_color),
false => (highlighted_color, self.props.background),
};
// Render
let mut state: ListState = ListState::default();
@ -321,10 +342,15 @@ impl Component for FileList {
self.states.focus,
))
.start_corner(Corner::TopLeft)
.style(
Style::default()
.fg(self.props.foreground)
.bg(self.props.background),
)
.highlight_style(
Style::default()
.bg(bg)
.fg(fg)
.bg(h_bg)
.fg(h_fg)
.add_modifier(self.props.modifiers),
),
area,
@ -523,6 +549,7 @@ mod tests {
.visible()
.with_foreground(Color::Red)
.with_background(Color::Blue)
.with_highlight_color(Color::LightRed)
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_files(
Some(String::from("files")),
@ -530,6 +557,10 @@ mod tests {
)
.build(),
);
assert_eq!(
*component.props.own.get(PROP_HIGHLIGHT_COLOR).unwrap(),
PropPayload::One(PropValue::Color(Color::LightRed))
);
assert_eq!(component.props.foreground, Color::Red);
assert_eq!(component.props.background, Color::Blue);
assert_eq!(component.props.visible, true);

View file

@ -96,6 +96,16 @@ impl LogboxPropsBuilder {
self
}
/// ### with_background
///
/// Set background color for area
pub fn with_background(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.background = color;
}
self
}
pub fn with_log(&mut self, title: Option<String>, table: TextTable) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.texts = TextParts::table(title, table);
@ -219,6 +229,7 @@ impl Component for LogBox {
))
.start_corner(Corner::BottomLeft)
.highlight_symbol(">> ")
.style(Style::default().bg(self.props.background))
.highlight_style(Style::default().add_modifier(self.props.modifiers));
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
@ -311,6 +322,7 @@ mod tests {
.hidden()
.visible()
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_background(Color::Blue)
.with_log(
Some(String::from("Log")),
TableBuilder::default()
@ -324,6 +336,7 @@ mod tests {
.build(),
);
assert_eq!(component.props.visible, true);
assert_eq!(component.props.background, Color::Blue);
assert_eq!(
component.props.texts.title.as_ref().unwrap().as_str(),
"Log"

View file

@ -27,6 +27,7 @@
*/
// exports
pub mod bookmark_list;
pub mod color_picker;
pub mod file_list;
pub mod logbox;
pub mod msgbox;

View file

@ -30,6 +30,7 @@ use super::input::InputHandler;
use super::store::Store;
use crate::filetransfer::FileTransferProtocol;
use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider;
// Includes
use crossterm::event::DisableMouseCapture;
@ -49,6 +50,7 @@ pub struct Context {
pub(crate) store: Store,
pub(crate) input_hnd: InputHandler,
pub(crate) terminal: Terminal<CrosstermBackend<Stdout>>,
pub(crate) theme_provider: ThemeProvider,
error: Option<String>,
}
@ -68,7 +70,11 @@ impl Context {
/// ### new
///
/// Instantiates a new Context
pub fn new(config_client: Option<ConfigClient>, error: Option<String>) -> Context {
pub fn new(
config_client: Option<ConfigClient>,
theme_provider: ThemeProvider,
error: Option<String>,
) -> Context {
// Create terminal
let mut stdout = stdout();
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
@ -78,6 +84,7 @@ impl Context {
store: Store::init(),
input_hnd: InputHandler::new(),
terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(),
theme_provider,
error,
}
}
@ -172,27 +179,4 @@ mod tests {
assert!(params.username.is_none());
assert!(params.password.is_none());
}
#[test]
#[cfg(not(feature = "github-actions"))]
fn test_ui_context() {
// Prepare stuff
let mut ctx: Context = Context::new(None, Some(String::from("alles kaput")));
assert!(ctx.error.is_some());
assert_eq!(ctx.get_error().unwrap().as_str(), "alles kaput");
assert!(ctx.error.is_none());
assert!(ctx.get_error().is_none());
ctx.set_error(String::from("err"));
assert!(ctx.error.is_some());
assert!(ctx.get_error().is_some());
assert!(ctx.get_error().is_none());
// Try other methods
#[cfg(not(target_os = "windows"))]
{
ctx.enter_alternate_screen();
ctx.clear_screen();
ctx.leave_alternate_screen();
}
drop(ctx);
}
}

View file

@ -28,6 +28,7 @@
use chrono::prelude::*;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use tuirealm::tui::style::Color;
/// ### fmt_pex
///
@ -149,6 +150,174 @@ pub fn fmt_path_elide(p: &Path, width: usize) -> String {
}
}
/// ### fmt_color
///
/// Format color
pub fn fmt_color(color: &Color) -> String {
match color {
Color::Black => "Black".to_string(),
Color::Blue => "Blue".to_string(),
Color::Cyan => "Cyan".to_string(),
Color::DarkGray => "DarkGray".to_string(),
Color::Gray => "Gray".to_string(),
Color::Green => "Green".to_string(),
Color::LightBlue => "LightBlue".to_string(),
Color::LightCyan => "LightCyan".to_string(),
Color::LightGreen => "LightGreen".to_string(),
Color::LightMagenta => "LightMagenta".to_string(),
Color::LightRed => "LightRed".to_string(),
Color::LightYellow => "LightYellow".to_string(),
Color::Magenta => "Magenta".to_string(),
Color::Red => "Red".to_string(),
Color::Reset => "Default".to_string(),
Color::White => "White".to_string(),
Color::Yellow => "Yellow".to_string(),
Color::Indexed(_) => "Default".to_string(),
// -- css colors
Color::Rgb(240, 248, 255) => "aliceblue".to_string(),
Color::Rgb(250, 235, 215) => "antiquewhite".to_string(),
Color::Rgb(0, 255, 255) => "aqua".to_string(),
Color::Rgb(127, 255, 212) => "aquamarine".to_string(),
Color::Rgb(240, 255, 255) => "azure".to_string(),
Color::Rgb(245, 245, 220) => "beige".to_string(),
Color::Rgb(255, 228, 196) => "bisque".to_string(),
Color::Rgb(0, 0, 0) => "black".to_string(),
Color::Rgb(255, 235, 205) => "blanchedalmond".to_string(),
Color::Rgb(0, 0, 255) => "blue".to_string(),
Color::Rgb(138, 43, 226) => "blueviolet".to_string(),
Color::Rgb(165, 42, 42) => "brown".to_string(),
Color::Rgb(222, 184, 135) => "burlywood".to_string(),
Color::Rgb(95, 158, 160) => "cadetblue".to_string(),
Color::Rgb(127, 255, 0) => "chartreuse".to_string(),
Color::Rgb(210, 105, 30) => "chocolate".to_string(),
Color::Rgb(255, 127, 80) => "coral".to_string(),
Color::Rgb(100, 149, 237) => "cornflowerblue".to_string(),
Color::Rgb(255, 248, 220) => "cornsilk".to_string(),
Color::Rgb(220, 20, 60) => "crimson".to_string(),
Color::Rgb(0, 0, 139) => "darkblue".to_string(),
Color::Rgb(0, 139, 139) => "darkcyan".to_string(),
Color::Rgb(184, 134, 11) => "darkgoldenrod".to_string(),
Color::Rgb(169, 169, 169) => "darkgray".to_string(),
Color::Rgb(0, 100, 0) => "darkgreen".to_string(),
Color::Rgb(189, 183, 107) => "darkkhaki".to_string(),
Color::Rgb(139, 0, 139) => "darkmagenta".to_string(),
Color::Rgb(85, 107, 47) => "darkolivegreen".to_string(),
Color::Rgb(255, 140, 0) => "darkorange".to_string(),
Color::Rgb(153, 50, 204) => "darkorchid".to_string(),
Color::Rgb(139, 0, 0) => "darkred".to_string(),
Color::Rgb(233, 150, 122) => "darksalmon".to_string(),
Color::Rgb(143, 188, 143) => "darkseagreen".to_string(),
Color::Rgb(72, 61, 139) => "darkslateblue".to_string(),
Color::Rgb(47, 79, 79) => "darkslategray".to_string(),
Color::Rgb(0, 206, 209) => "darkturquoise".to_string(),
Color::Rgb(148, 0, 211) => "darkviolet".to_string(),
Color::Rgb(255, 20, 147) => "deeppink".to_string(),
Color::Rgb(0, 191, 255) => "deepskyblue".to_string(),
Color::Rgb(105, 105, 105) => "dimgray".to_string(),
Color::Rgb(30, 144, 255) => "dodgerblue".to_string(),
Color::Rgb(178, 34, 34) => "firebrick".to_string(),
Color::Rgb(255, 250, 240) => "floralwhite".to_string(),
Color::Rgb(34, 139, 34) => "forestgreen".to_string(),
Color::Rgb(255, 0, 255) => "fuchsia".to_string(),
Color::Rgb(220, 220, 220) => "gainsboro".to_string(),
Color::Rgb(248, 248, 255) => "ghostwhite".to_string(),
Color::Rgb(255, 215, 0) => "gold".to_string(),
Color::Rgb(218, 165, 32) => "goldenrod".to_string(),
Color::Rgb(128, 128, 128) => "gray".to_string(),
Color::Rgb(0, 128, 0) => "green".to_string(),
Color::Rgb(173, 255, 47) => "greenyellow".to_string(),
Color::Rgb(240, 255, 240) => "honeydew".to_string(),
Color::Rgb(255, 105, 180) => "hotpink".to_string(),
Color::Rgb(205, 92, 92) => "indianred".to_string(),
Color::Rgb(75, 0, 130) => "indigo".to_string(),
Color::Rgb(255, 255, 240) => "ivory".to_string(),
Color::Rgb(240, 230, 140) => "khaki".to_string(),
Color::Rgb(230, 230, 250) => "lavender".to_string(),
Color::Rgb(255, 240, 245) => "lavenderblush".to_string(),
Color::Rgb(124, 252, 0) => "lawngreen".to_string(),
Color::Rgb(255, 250, 205) => "lemonchiffon".to_string(),
Color::Rgb(173, 216, 230) => "lightblue".to_string(),
Color::Rgb(240, 128, 128) => "lightcoral".to_string(),
Color::Rgb(224, 255, 255) => "lightcyan".to_string(),
Color::Rgb(250, 250, 210) => "lightgoldenrodyellow".to_string(),
Color::Rgb(211, 211, 211) => "lightgray".to_string(),
Color::Rgb(144, 238, 144) => "lightgreen".to_string(),
Color::Rgb(255, 182, 193) => "lightpink".to_string(),
Color::Rgb(255, 160, 122) => "lightsalmon".to_string(),
Color::Rgb(32, 178, 170) => "lightseagreen".to_string(),
Color::Rgb(135, 206, 250) => "lightskyblue".to_string(),
Color::Rgb(119, 136, 153) => "lightslategray".to_string(),
Color::Rgb(176, 196, 222) => "lightsteelblue".to_string(),
Color::Rgb(255, 255, 224) => "lightyellow".to_string(),
Color::Rgb(0, 255, 0) => "lime".to_string(),
Color::Rgb(50, 205, 50) => "limegreen".to_string(),
Color::Rgb(250, 240, 230) => "linen".to_string(),
Color::Rgb(128, 0, 0) => "maroon".to_string(),
Color::Rgb(102, 205, 170) => "mediumaquamarine".to_string(),
Color::Rgb(0, 0, 205) => "mediumblue".to_string(),
Color::Rgb(186, 85, 211) => "mediumorchid".to_string(),
Color::Rgb(147, 112, 219) => "mediumpurple".to_string(),
Color::Rgb(60, 179, 113) => "mediumseagreen".to_string(),
Color::Rgb(123, 104, 238) => "mediumslateblue".to_string(),
Color::Rgb(0, 250, 154) => "mediumspringgreen".to_string(),
Color::Rgb(72, 209, 204) => "mediumturquoise".to_string(),
Color::Rgb(199, 21, 133) => "mediumvioletred".to_string(),
Color::Rgb(25, 25, 112) => "midnightblue".to_string(),
Color::Rgb(245, 255, 250) => "mintcream".to_string(),
Color::Rgb(255, 228, 225) => "mistyrose".to_string(),
Color::Rgb(255, 228, 181) => "moccasin".to_string(),
Color::Rgb(255, 222, 173) => "navajowhite".to_string(),
Color::Rgb(0, 0, 128) => "navy".to_string(),
Color::Rgb(253, 245, 230) => "oldlace".to_string(),
Color::Rgb(128, 128, 0) => "olive".to_string(),
Color::Rgb(107, 142, 35) => "olivedrab".to_string(),
Color::Rgb(255, 165, 0) => "orange".to_string(),
Color::Rgb(255, 69, 0) => "orangered".to_string(),
Color::Rgb(218, 112, 214) => "orchid".to_string(),
Color::Rgb(238, 232, 170) => "palegoldenrod".to_string(),
Color::Rgb(152, 251, 152) => "palegreen".to_string(),
Color::Rgb(175, 238, 238) => "paleturquoise".to_string(),
Color::Rgb(219, 112, 147) => "palevioletred".to_string(),
Color::Rgb(255, 239, 213) => "papayawhip".to_string(),
Color::Rgb(255, 218, 185) => "peachpuff".to_string(),
Color::Rgb(205, 133, 63) => "peru".to_string(),
Color::Rgb(255, 192, 203) => "pink".to_string(),
Color::Rgb(221, 160, 221) => "plum".to_string(),
Color::Rgb(176, 224, 230) => "powderblue".to_string(),
Color::Rgb(128, 0, 128) => "purple".to_string(),
Color::Rgb(102, 51, 153) => "rebeccapurple".to_string(),
Color::Rgb(255, 0, 0) => "red".to_string(),
Color::Rgb(188, 143, 143) => "rosybrown".to_string(),
Color::Rgb(65, 105, 225) => "royalblue".to_string(),
Color::Rgb(139, 69, 19) => "saddlebrown".to_string(),
Color::Rgb(250, 128, 114) => "salmon".to_string(),
Color::Rgb(244, 164, 96) => "sandybrown".to_string(),
Color::Rgb(46, 139, 87) => "seagreen".to_string(),
Color::Rgb(255, 245, 238) => "seashell".to_string(),
Color::Rgb(160, 82, 45) => "sienna".to_string(),
Color::Rgb(192, 192, 192) => "silver".to_string(),
Color::Rgb(135, 206, 235) => "skyblue".to_string(),
Color::Rgb(106, 90, 205) => "slateblue".to_string(),
Color::Rgb(112, 128, 144) => "slategray".to_string(),
Color::Rgb(255, 250, 250) => "snow".to_string(),
Color::Rgb(0, 255, 127) => "springgreen".to_string(),
Color::Rgb(70, 130, 180) => "steelblue".to_string(),
Color::Rgb(210, 180, 140) => "tan".to_string(),
Color::Rgb(0, 128, 128) => "teal".to_string(),
Color::Rgb(216, 191, 216) => "thistle".to_string(),
Color::Rgb(255, 99, 71) => "tomato".to_string(),
Color::Rgb(64, 224, 208) => "turquoise".to_string(),
Color::Rgb(238, 130, 238) => "violet".to_string(),
Color::Rgb(245, 222, 179) => "wheat".to_string(),
Color::Rgb(255, 255, 255) => "white".to_string(),
Color::Rgb(245, 245, 245) => "whitesmoke".to_string(),
Color::Rgb(255, 255, 0) => "yellow".to_string(),
Color::Rgb(154, 205, 50) => "yellowgreen".to_string(),
// -- others
Color::Rgb(r, g, b) => format!("#{:02x}{:02x}{:02x}", r, g, b),
}
}
/// ### shadow_password
///
/// Return a string with the same length of input string, but each character is replaced by '*'
@ -224,6 +393,263 @@ mod tests {
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
}
#[test]
fn test_utils_fmt_color() {
assert_eq!(fmt_color(&Color::Black).as_str(), "Black");
assert_eq!(fmt_color(&Color::Blue).as_str(), "Blue");
assert_eq!(fmt_color(&Color::Cyan).as_str(), "Cyan");
assert_eq!(fmt_color(&Color::DarkGray).as_str(), "DarkGray");
assert_eq!(fmt_color(&Color::Gray).as_str(), "Gray");
assert_eq!(fmt_color(&Color::Green).as_str(), "Green");
assert_eq!(fmt_color(&Color::LightBlue).as_str(), "LightBlue");
assert_eq!(fmt_color(&Color::LightCyan).as_str(), "LightCyan");
assert_eq!(fmt_color(&Color::LightGreen).as_str(), "LightGreen");
assert_eq!(fmt_color(&Color::LightMagenta).as_str(), "LightMagenta");
assert_eq!(fmt_color(&Color::LightRed).as_str(), "LightRed");
assert_eq!(fmt_color(&Color::LightYellow).as_str(), "LightYellow");
assert_eq!(fmt_color(&Color::Magenta).as_str(), "Magenta");
assert_eq!(fmt_color(&Color::Red).as_str(), "Red");
assert_eq!(fmt_color(&Color::Reset).as_str(), "Default");
assert_eq!(fmt_color(&Color::White).as_str(), "White");
assert_eq!(fmt_color(&Color::Yellow).as_str(), "Yellow");
assert_eq!(fmt_color(&Color::Indexed(16)).as_str(), "Default");
assert_eq!(fmt_color(&Color::Rgb(204, 170, 22)).as_str(), "#ccaa16");
assert_eq!(fmt_color(&Color::Rgb(204, 170, 0)).as_str(), "#ccaa00");
// css colors
assert_eq!(fmt_color(&Color::Rgb(240, 248, 255)).as_str(), "aliceblue");
assert_eq!(
fmt_color(&Color::Rgb(250, 235, 215)).as_str(),
"antiquewhite"
);
assert_eq!(fmt_color(&Color::Rgb(0, 255, 255)).as_str(), "aqua");
assert_eq!(fmt_color(&Color::Rgb(127, 255, 212)).as_str(), "aquamarine");
assert_eq!(fmt_color(&Color::Rgb(240, 255, 255)).as_str(), "azure");
assert_eq!(fmt_color(&Color::Rgb(245, 245, 220)).as_str(), "beige");
assert_eq!(fmt_color(&Color::Rgb(255, 228, 196)).as_str(), "bisque");
assert_eq!(fmt_color(&Color::Rgb(0, 0, 0)).as_str(), "black");
assert_eq!(
fmt_color(&Color::Rgb(255, 235, 205)).as_str(),
"blanchedalmond"
);
assert_eq!(fmt_color(&Color::Rgb(0, 0, 255)).as_str(), "blue");
assert_eq!(fmt_color(&Color::Rgb(138, 43, 226)).as_str(), "blueviolet");
assert_eq!(fmt_color(&Color::Rgb(165, 42, 42)).as_str(), "brown");
assert_eq!(fmt_color(&Color::Rgb(222, 184, 135)).as_str(), "burlywood");
assert_eq!(fmt_color(&Color::Rgb(95, 158, 160)).as_str(), "cadetblue");
assert_eq!(fmt_color(&Color::Rgb(127, 255, 0)).as_str(), "chartreuse");
assert_eq!(fmt_color(&Color::Rgb(210, 105, 30)).as_str(), "chocolate");
assert_eq!(fmt_color(&Color::Rgb(255, 127, 80)).as_str(), "coral");
assert_eq!(
fmt_color(&Color::Rgb(100, 149, 237)).as_str(),
"cornflowerblue"
);
assert_eq!(fmt_color(&Color::Rgb(255, 248, 220)).as_str(), "cornsilk");
assert_eq!(fmt_color(&Color::Rgb(220, 20, 60)).as_str(), "crimson");
assert_eq!(fmt_color(&Color::Rgb(0, 0, 139)).as_str(), "darkblue");
assert_eq!(fmt_color(&Color::Rgb(0, 139, 139)).as_str(), "darkcyan");
assert_eq!(
fmt_color(&Color::Rgb(184, 134, 11)).as_str(),
"darkgoldenrod"
);
assert_eq!(fmt_color(&Color::Rgb(169, 169, 169)).as_str(), "darkgray");
assert_eq!(fmt_color(&Color::Rgb(0, 100, 0)).as_str(), "darkgreen");
assert_eq!(fmt_color(&Color::Rgb(189, 183, 107)).as_str(), "darkkhaki");
assert_eq!(fmt_color(&Color::Rgb(139, 0, 139)).as_str(), "darkmagenta");
assert_eq!(
fmt_color(&Color::Rgb(85, 107, 47)).as_str(),
"darkolivegreen"
);
assert_eq!(fmt_color(&Color::Rgb(255, 140, 0)).as_str(), "darkorange");
assert_eq!(fmt_color(&Color::Rgb(153, 50, 204)).as_str(), "darkorchid");
assert_eq!(fmt_color(&Color::Rgb(139, 0, 0)).as_str(), "darkred");
assert_eq!(fmt_color(&Color::Rgb(233, 150, 122)).as_str(), "darksalmon");
assert_eq!(
fmt_color(&Color::Rgb(143, 188, 143)).as_str(),
"darkseagreen"
);
assert_eq!(
fmt_color(&Color::Rgb(72, 61, 139)).as_str(),
"darkslateblue"
);
assert_eq!(fmt_color(&Color::Rgb(47, 79, 79)).as_str(), "darkslategray");
assert_eq!(
fmt_color(&Color::Rgb(0, 206, 209)).as_str(),
"darkturquoise"
);
assert_eq!(fmt_color(&Color::Rgb(148, 0, 211)).as_str(), "darkviolet");
assert_eq!(fmt_color(&Color::Rgb(255, 20, 147)).as_str(), "deeppink");
assert_eq!(fmt_color(&Color::Rgb(0, 191, 255)).as_str(), "deepskyblue");
assert_eq!(fmt_color(&Color::Rgb(105, 105, 105)).as_str(), "dimgray");
assert_eq!(fmt_color(&Color::Rgb(30, 144, 255)).as_str(), "dodgerblue");
assert_eq!(fmt_color(&Color::Rgb(178, 34, 34)).as_str(), "firebrick");
assert_eq!(
fmt_color(&Color::Rgb(255, 250, 240)).as_str(),
"floralwhite"
);
assert_eq!(fmt_color(&Color::Rgb(34, 139, 34)).as_str(), "forestgreen");
assert_eq!(fmt_color(&Color::Rgb(255, 0, 255)).as_str(), "fuchsia");
assert_eq!(fmt_color(&Color::Rgb(220, 220, 220)).as_str(), "gainsboro");
assert_eq!(fmt_color(&Color::Rgb(248, 248, 255)).as_str(), "ghostwhite");
assert_eq!(fmt_color(&Color::Rgb(255, 215, 0)).as_str(), "gold");
assert_eq!(fmt_color(&Color::Rgb(218, 165, 32)).as_str(), "goldenrod");
assert_eq!(fmt_color(&Color::Rgb(128, 128, 128)).as_str(), "gray");
assert_eq!(fmt_color(&Color::Rgb(0, 128, 0)).as_str(), "green");
assert_eq!(fmt_color(&Color::Rgb(173, 255, 47)).as_str(), "greenyellow");
assert_eq!(fmt_color(&Color::Rgb(240, 255, 240)).as_str(), "honeydew");
assert_eq!(fmt_color(&Color::Rgb(255, 105, 180)).as_str(), "hotpink");
assert_eq!(fmt_color(&Color::Rgb(205, 92, 92)).as_str(), "indianred");
assert_eq!(fmt_color(&Color::Rgb(75, 0, 130)).as_str(), "indigo");
assert_eq!(fmt_color(&Color::Rgb(255, 255, 240)).as_str(), "ivory");
assert_eq!(fmt_color(&Color::Rgb(240, 230, 140)).as_str(), "khaki");
assert_eq!(fmt_color(&Color::Rgb(230, 230, 250)).as_str(), "lavender");
assert_eq!(
fmt_color(&Color::Rgb(255, 240, 245)).as_str(),
"lavenderblush"
);
assert_eq!(fmt_color(&Color::Rgb(124, 252, 0)).as_str(), "lawngreen");
assert_eq!(
fmt_color(&Color::Rgb(255, 250, 205)).as_str(),
"lemonchiffon"
);
assert_eq!(fmt_color(&Color::Rgb(173, 216, 230)).as_str(), "lightblue");
assert_eq!(fmt_color(&Color::Rgb(240, 128, 128)).as_str(), "lightcoral");
assert_eq!(fmt_color(&Color::Rgb(224, 255, 255)).as_str(), "lightcyan");
assert_eq!(
fmt_color(&Color::Rgb(250, 250, 210)).as_str(),
"lightgoldenrodyellow"
);
assert_eq!(fmt_color(&Color::Rgb(211, 211, 211)).as_str(), "lightgray");
assert_eq!(fmt_color(&Color::Rgb(144, 238, 144)).as_str(), "lightgreen");
assert_eq!(fmt_color(&Color::Rgb(255, 182, 193)).as_str(), "lightpink");
assert_eq!(
fmt_color(&Color::Rgb(255, 160, 122)).as_str(),
"lightsalmon"
);
assert_eq!(
fmt_color(&Color::Rgb(32, 178, 170)).as_str(),
"lightseagreen"
);
assert_eq!(
fmt_color(&Color::Rgb(135, 206, 250)).as_str(),
"lightskyblue"
);
assert_eq!(
fmt_color(&Color::Rgb(119, 136, 153)).as_str(),
"lightslategray"
);
assert_eq!(
fmt_color(&Color::Rgb(176, 196, 222)).as_str(),
"lightsteelblue"
);
assert_eq!(
fmt_color(&Color::Rgb(255, 255, 224)).as_str(),
"lightyellow"
);
assert_eq!(fmt_color(&Color::Rgb(0, 255, 0)).as_str(), "lime");
assert_eq!(fmt_color(&Color::Rgb(50, 205, 50)).as_str(), "limegreen");
assert_eq!(fmt_color(&Color::Rgb(250, 240, 230)).as_str(), "linen");
assert_eq!(fmt_color(&Color::Rgb(128, 0, 0)).as_str(), "maroon");
assert_eq!(
fmt_color(&Color::Rgb(102, 205, 170)).as_str(),
"mediumaquamarine"
);
assert_eq!(fmt_color(&Color::Rgb(0, 0, 205)).as_str(), "mediumblue");
assert_eq!(
fmt_color(&Color::Rgb(186, 85, 211)).as_str(),
"mediumorchid"
);
assert_eq!(
fmt_color(&Color::Rgb(147, 112, 219)).as_str(),
"mediumpurple"
);
assert_eq!(
fmt_color(&Color::Rgb(60, 179, 113)).as_str(),
"mediumseagreen"
);
assert_eq!(
fmt_color(&Color::Rgb(123, 104, 238)).as_str(),
"mediumslateblue"
);
assert_eq!(
fmt_color(&Color::Rgb(0, 250, 154)).as_str(),
"mediumspringgreen"
);
assert_eq!(
fmt_color(&Color::Rgb(72, 209, 204)).as_str(),
"mediumturquoise"
);
assert_eq!(
fmt_color(&Color::Rgb(199, 21, 133)).as_str(),
"mediumvioletred"
);
assert_eq!(fmt_color(&Color::Rgb(25, 25, 112)).as_str(), "midnightblue");
assert_eq!(fmt_color(&Color::Rgb(245, 255, 250)).as_str(), "mintcream");
assert_eq!(fmt_color(&Color::Rgb(255, 228, 225)).as_str(), "mistyrose");
assert_eq!(fmt_color(&Color::Rgb(255, 228, 181)).as_str(), "moccasin");
assert_eq!(
fmt_color(&Color::Rgb(255, 222, 173)).as_str(),
"navajowhite"
);
assert_eq!(fmt_color(&Color::Rgb(0, 0, 128)).as_str(), "navy");
assert_eq!(fmt_color(&Color::Rgb(253, 245, 230)).as_str(), "oldlace");
assert_eq!(fmt_color(&Color::Rgb(128, 128, 0)).as_str(), "olive");
assert_eq!(fmt_color(&Color::Rgb(107, 142, 35)).as_str(), "olivedrab");
assert_eq!(fmt_color(&Color::Rgb(255, 165, 0)).as_str(), "orange");
assert_eq!(fmt_color(&Color::Rgb(255, 69, 0)).as_str(), "orangered");
assert_eq!(fmt_color(&Color::Rgb(218, 112, 214)).as_str(), "orchid");
assert_eq!(
fmt_color(&Color::Rgb(238, 232, 170)).as_str(),
"palegoldenrod"
);
assert_eq!(fmt_color(&Color::Rgb(152, 251, 152)).as_str(), "palegreen");
assert_eq!(
fmt_color(&Color::Rgb(175, 238, 238)).as_str(),
"paleturquoise"
);
assert_eq!(
fmt_color(&Color::Rgb(219, 112, 147)).as_str(),
"palevioletred"
);
assert_eq!(fmt_color(&Color::Rgb(255, 239, 213)).as_str(), "papayawhip");
assert_eq!(fmt_color(&Color::Rgb(255, 218, 185)).as_str(), "peachpuff");
assert_eq!(fmt_color(&Color::Rgb(205, 133, 63)).as_str(), "peru");
assert_eq!(fmt_color(&Color::Rgb(255, 192, 203)).as_str(), "pink");
assert_eq!(fmt_color(&Color::Rgb(221, 160, 221)).as_str(), "plum");
assert_eq!(fmt_color(&Color::Rgb(176, 224, 230)).as_str(), "powderblue");
assert_eq!(fmt_color(&Color::Rgb(128, 0, 128)).as_str(), "purple");
assert_eq!(
fmt_color(&Color::Rgb(102, 51, 153)).as_str(),
"rebeccapurple"
);
assert_eq!(fmt_color(&Color::Rgb(255, 0, 0)).as_str(), "red");
assert_eq!(fmt_color(&Color::Rgb(188, 143, 143)).as_str(), "rosybrown");
assert_eq!(fmt_color(&Color::Rgb(65, 105, 225)).as_str(), "royalblue");
assert_eq!(fmt_color(&Color::Rgb(139, 69, 19)).as_str(), "saddlebrown");
assert_eq!(fmt_color(&Color::Rgb(250, 128, 114)).as_str(), "salmon");
assert_eq!(fmt_color(&Color::Rgb(244, 164, 96)).as_str(), "sandybrown");
assert_eq!(fmt_color(&Color::Rgb(46, 139, 87)).as_str(), "seagreen");
assert_eq!(fmt_color(&Color::Rgb(255, 245, 238)).as_str(), "seashell");
assert_eq!(fmt_color(&Color::Rgb(160, 82, 45)).as_str(), "sienna");
assert_eq!(fmt_color(&Color::Rgb(192, 192, 192)).as_str(), "silver");
assert_eq!(fmt_color(&Color::Rgb(135, 206, 235)).as_str(), "skyblue");
assert_eq!(fmt_color(&Color::Rgb(106, 90, 205)).as_str(), "slateblue");
assert_eq!(fmt_color(&Color::Rgb(112, 128, 144)).as_str(), "slategray");
assert_eq!(fmt_color(&Color::Rgb(255, 250, 250)).as_str(), "snow");
assert_eq!(fmt_color(&Color::Rgb(0, 255, 127)).as_str(), "springgreen");
assert_eq!(fmt_color(&Color::Rgb(70, 130, 180)).as_str(), "steelblue");
assert_eq!(fmt_color(&Color::Rgb(210, 180, 140)).as_str(), "tan");
assert_eq!(fmt_color(&Color::Rgb(0, 128, 128)).as_str(), "teal");
assert_eq!(fmt_color(&Color::Rgb(216, 191, 216)).as_str(), "thistle");
assert_eq!(fmt_color(&Color::Rgb(255, 99, 71)).as_str(), "tomato");
assert_eq!(fmt_color(&Color::Rgb(64, 224, 208)).as_str(), "turquoise");
assert_eq!(fmt_color(&Color::Rgb(238, 130, 238)).as_str(), "violet");
assert_eq!(fmt_color(&Color::Rgb(245, 222, 179)).as_str(), "wheat");
assert_eq!(fmt_color(&Color::Rgb(255, 255, 255)).as_str(), "white");
assert_eq!(fmt_color(&Color::Rgb(245, 245, 245)).as_str(), "whitesmoke");
assert_eq!(fmt_color(&Color::Rgb(255, 255, 0)).as_str(), "yellow");
assert_eq!(fmt_color(&Color::Rgb(154, 205, 50)).as_str(), "yellowgreen");
}
#[test]
fn test_utils_fmt_shadow_password() {
assert_eq!(shadow_password("foobar"), String::from("******"));

View file

@ -39,6 +39,7 @@ use regex::Regex;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use tuirealm::tui::style::Color;
// Regex
lazy_static! {
@ -58,6 +59,20 @@ lazy_static! {
* v0.4.0 => 0.4.0
*/
static ref SEMVER_REGEX: Regex = Regex::new(r".*(:?[0-9]\.[0-9]\.[0-9])").unwrap();
/**
* Regex matches:
* - group 1: Red
* - group 2: Green
* - group 3: Blue
*/
static ref COLOR_HEX_REGEX: Regex = Regex::new(r"#(:?[0-9a-fA-F]{2})(:?[0-9a-fA-F]{2})(:?[0-9a-fA-F]{2})").unwrap();
/**
* Regex matches:
* - group 2: Red
* - group 4: Green
* - group 6: blue
*/
static ref COLOR_RGB_REGEX: Regex = Regex::new(r"^(rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(([01]?\d\d?|2[0-4]\d|25[0-5])\)?)").unwrap();
}
pub struct RemoteOptions {
@ -219,6 +234,237 @@ pub fn parse_semver(haystack: &str) -> Option<String> {
}
}
/// ### parse_color
///
/// Parse color from string into a `Color` enum.
///
/// Color may be in different format:
///
/// 1. color name:
/// - Black,
/// - Blue,
/// - Cyan,
/// - DarkGray,
/// - Gray,
/// - Green,
/// - LightBlue,
/// - LightCyan,
/// - LightGreen,
/// - LightMagenta,
/// - LightRed,
/// - LightYellow,
/// - Magenta,
/// - Red,
/// - Reset,
/// - White,
/// - Yellow,
/// 2. Hex format:
/// - #f0ab05
/// - #AA33BC
/// 3. Rgb format:
/// - rgb(255, 64, 32)
/// - rgb(255,64,32)
/// - 255, 64, 32
pub fn parse_color(color: &str) -> Option<Color> {
match color.to_lowercase().as_str() {
// -- lib colors
"black" => Some(Color::Black),
"blue" => Some(Color::Blue),
"cyan" => Some(Color::Cyan),
"darkgray" | "darkgrey" => Some(Color::DarkGray),
"default" => Some(Color::Reset),
"gray" => Some(Color::Gray),
"green" => Some(Color::Green),
"lightblue" => Some(Color::LightBlue),
"lightcyan" => Some(Color::LightCyan),
"lightgreen" => Some(Color::LightGreen),
"lightmagenta" => Some(Color::LightMagenta),
"lightred" => Some(Color::LightRed),
"lightyellow" => Some(Color::LightYellow),
"magenta" => Some(Color::Magenta),
"red" => Some(Color::Red),
"white" => Some(Color::White),
"yellow" => Some(Color::Yellow),
// -- css colors
"aliceblue" => Some(Color::Rgb(240, 248, 255)),
"antiquewhite" => Some(Color::Rgb(250, 235, 215)),
"aqua" => Some(Color::Rgb(0, 255, 255)),
"aquamarine" => Some(Color::Rgb(127, 255, 212)),
"azure" => Some(Color::Rgb(240, 255, 255)),
"beige" => Some(Color::Rgb(245, 245, 220)),
"bisque" => Some(Color::Rgb(255, 228, 196)),
"blanchedalmond" => Some(Color::Rgb(255, 235, 205)),
"blueviolet" => Some(Color::Rgb(138, 43, 226)),
"brown" => Some(Color::Rgb(165, 42, 42)),
"burlywood" => Some(Color::Rgb(222, 184, 135)),
"cadetblue" => Some(Color::Rgb(95, 158, 160)),
"chartreuse" => Some(Color::Rgb(127, 255, 0)),
"chocolate" => Some(Color::Rgb(210, 105, 30)),
"coral" => Some(Color::Rgb(255, 127, 80)),
"cornflowerblue" => Some(Color::Rgb(100, 149, 237)),
"cornsilk" => Some(Color::Rgb(255, 248, 220)),
"crimson" => Some(Color::Rgb(220, 20, 60)),
"darkblue" => Some(Color::Rgb(0, 0, 139)),
"darkcyan" => Some(Color::Rgb(0, 139, 139)),
"darkgoldenrod" => Some(Color::Rgb(184, 134, 11)),
"darkgreen" => Some(Color::Rgb(0, 100, 0)),
"darkkhaki" => Some(Color::Rgb(189, 183, 107)),
"darkmagenta" => Some(Color::Rgb(139, 0, 139)),
"darkolivegreen" => Some(Color::Rgb(85, 107, 47)),
"darkorange" => Some(Color::Rgb(255, 140, 0)),
"darkorchid" => Some(Color::Rgb(153, 50, 204)),
"darkred" => Some(Color::Rgb(139, 0, 0)),
"darksalmon" => Some(Color::Rgb(233, 150, 122)),
"darkseagreen" => Some(Color::Rgb(143, 188, 143)),
"darkslateblue" => Some(Color::Rgb(72, 61, 139)),
"darkslategray" | "darkslategrey" => Some(Color::Rgb(47, 79, 79)),
"darkturquoise" => Some(Color::Rgb(0, 206, 209)),
"darkviolet" => Some(Color::Rgb(148, 0, 211)),
"deeppink" => Some(Color::Rgb(255, 20, 147)),
"deepskyblue" => Some(Color::Rgb(0, 191, 255)),
"dimgray" | "dimgrey" => Some(Color::Rgb(105, 105, 105)),
"dodgerblue" => Some(Color::Rgb(30, 144, 255)),
"firebrick" => Some(Color::Rgb(178, 34, 34)),
"floralwhite" => Some(Color::Rgb(255, 250, 240)),
"forestgreen" => Some(Color::Rgb(34, 139, 34)),
"fuchsia" => Some(Color::Rgb(255, 0, 255)),
"gainsboro" => Some(Color::Rgb(220, 220, 220)),
"ghostwhite" => Some(Color::Rgb(248, 248, 255)),
"gold" => Some(Color::Rgb(255, 215, 0)),
"goldenrod" => Some(Color::Rgb(218, 165, 32)),
"greenyellow" => Some(Color::Rgb(173, 255, 47)),
"grey" => Some(Color::Rgb(128, 128, 128)),
"honeydew" => Some(Color::Rgb(240, 255, 240)),
"hotpink" => Some(Color::Rgb(255, 105, 180)),
"indianred" => Some(Color::Rgb(205, 92, 92)),
"indigo" => Some(Color::Rgb(75, 0, 130)),
"ivory" => Some(Color::Rgb(255, 255, 240)),
"khaki" => Some(Color::Rgb(240, 230, 140)),
"lavender" => Some(Color::Rgb(230, 230, 250)),
"lavenderblush" => Some(Color::Rgb(255, 240, 245)),
"lawngreen" => Some(Color::Rgb(124, 252, 0)),
"lemonchiffon" => Some(Color::Rgb(255, 250, 205)),
"lightcoral" => Some(Color::Rgb(240, 128, 128)),
"lightgoldenrodyellow" => Some(Color::Rgb(250, 250, 210)),
"lightgray" | "lightgrey" => Some(Color::Rgb(211, 211, 211)),
"lightpink" => Some(Color::Rgb(255, 182, 193)),
"lightsalmon" => Some(Color::Rgb(255, 160, 122)),
"lightseagreen" => Some(Color::Rgb(32, 178, 170)),
"lightskyblue" => Some(Color::Rgb(135, 206, 250)),
"lightslategray" | "lightslategrey" => Some(Color::Rgb(119, 136, 153)),
"lightsteelblue" => Some(Color::Rgb(176, 196, 222)),
"lime" => Some(Color::Rgb(0, 255, 0)),
"limegreen" => Some(Color::Rgb(50, 205, 50)),
"linen" => Some(Color::Rgb(250, 240, 230)),
"maroon" => Some(Color::Rgb(128, 0, 0)),
"mediumaquamarine" => Some(Color::Rgb(102, 205, 170)),
"mediumblue" => Some(Color::Rgb(0, 0, 205)),
"mediumorchid" => Some(Color::Rgb(186, 85, 211)),
"mediumpurple" => Some(Color::Rgb(147, 112, 219)),
"mediumseagreen" => Some(Color::Rgb(60, 179, 113)),
"mediumslateblue" => Some(Color::Rgb(123, 104, 238)),
"mediumspringgreen" => Some(Color::Rgb(0, 250, 154)),
"mediumturquoise" => Some(Color::Rgb(72, 209, 204)),
"mediumvioletred" => Some(Color::Rgb(199, 21, 133)),
"midnightblue" => Some(Color::Rgb(25, 25, 112)),
"mintcream" => Some(Color::Rgb(245, 255, 250)),
"mistyrose" => Some(Color::Rgb(255, 228, 225)),
"moccasin" => Some(Color::Rgb(255, 228, 181)),
"navajowhite" => Some(Color::Rgb(255, 222, 173)),
"navy" => Some(Color::Rgb(0, 0, 128)),
"oldlace" => Some(Color::Rgb(253, 245, 230)),
"olive" => Some(Color::Rgb(128, 128, 0)),
"olivedrab" => Some(Color::Rgb(107, 142, 35)),
"orange" => Some(Color::Rgb(255, 165, 0)),
"orangered" => Some(Color::Rgb(255, 69, 0)),
"orchid" => Some(Color::Rgb(218, 112, 214)),
"palegoldenrod" => Some(Color::Rgb(238, 232, 170)),
"palegreen" => Some(Color::Rgb(152, 251, 152)),
"paleturquoise" => Some(Color::Rgb(175, 238, 238)),
"palevioletred" => Some(Color::Rgb(219, 112, 147)),
"papayawhip" => Some(Color::Rgb(255, 239, 213)),
"peachpuff" => Some(Color::Rgb(255, 218, 185)),
"peru" => Some(Color::Rgb(205, 133, 63)),
"pink" => Some(Color::Rgb(255, 192, 203)),
"plum" => Some(Color::Rgb(221, 160, 221)),
"powderblue" => Some(Color::Rgb(176, 224, 230)),
"purple" => Some(Color::Rgb(128, 0, 128)),
"rebeccapurple" => Some(Color::Rgb(102, 51, 153)),
"rosybrown" => Some(Color::Rgb(188, 143, 143)),
"royalblue" => Some(Color::Rgb(65, 105, 225)),
"saddlebrown" => Some(Color::Rgb(139, 69, 19)),
"salmon" => Some(Color::Rgb(250, 128, 114)),
"sandybrown" => Some(Color::Rgb(244, 164, 96)),
"seagreen" => Some(Color::Rgb(46, 139, 87)),
"seashell" => Some(Color::Rgb(255, 245, 238)),
"sienna" => Some(Color::Rgb(160, 82, 45)),
"silver" => Some(Color::Rgb(192, 192, 192)),
"skyblue" => Some(Color::Rgb(135, 206, 235)),
"slateblue" => Some(Color::Rgb(106, 90, 205)),
"slategray" | "slategrey" => Some(Color::Rgb(112, 128, 144)),
"snow" => Some(Color::Rgb(255, 250, 250)),
"springgreen" => Some(Color::Rgb(0, 255, 127)),
"steelblue" => Some(Color::Rgb(70, 130, 180)),
"tan" => Some(Color::Rgb(210, 180, 140)),
"teal" => Some(Color::Rgb(0, 128, 128)),
"thistle" => Some(Color::Rgb(216, 191, 216)),
"tomato" => Some(Color::Rgb(255, 99, 71)),
"turquoise" => Some(Color::Rgb(64, 224, 208)),
"violet" => Some(Color::Rgb(238, 130, 238)),
"wheat" => Some(Color::Rgb(245, 222, 179)),
"whitesmoke" => Some(Color::Rgb(245, 245, 245)),
"yellowgreen" => Some(Color::Rgb(154, 205, 50)),
// -- hex and rgb
other => {
// Try as hex
if let Some(color) = parse_hex_color(other) {
Some(color)
} else {
parse_rgb_color(other)
}
}
}
}
/// ### parse_hex_color
///
/// Try to parse a color in hex format, such as:
///
/// - #f0ab05
/// - #AA33BC
fn parse_hex_color(color: &str) -> Option<Color> {
COLOR_HEX_REGEX.captures(color).map(|groups| {
Color::Rgb(
u8::from_str_radix(groups.get(1).unwrap().as_str(), 16)
.ok()
.unwrap(),
u8::from_str_radix(groups.get(2).unwrap().as_str(), 16)
.ok()
.unwrap(),
u8::from_str_radix(groups.get(3).unwrap().as_str(), 16)
.ok()
.unwrap(),
)
})
}
/// ### parse_rgb_color
///
/// Try to parse a color in rgb format, such as:
///
/// - rgb(255, 64, 32)
/// - rgb(255,64,32)
/// - 255, 64, 32
fn parse_rgb_color(color: &str) -> Option<Color> {
COLOR_RGB_REGEX.captures(color).map(|groups| {
Color::Rgb(
u8::from_str(groups.get(2).unwrap().as_str()).ok().unwrap(),
u8::from_str(groups.get(4).unwrap().as_str()).ok().unwrap(),
u8::from_str(groups.get(6).unwrap().as_str()).ok().unwrap(),
)
})
}
#[cfg(test)]
mod tests {
@ -405,4 +651,245 @@ mod tests {
assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
assert!(parse_semver("v1.1").is_none());
}
#[test]
fn test_utils_parse_color_hex() {
assert_eq!(
parse_hex_color("#f0f0f0").unwrap(),
Color::Rgb(240, 240, 240)
);
assert_eq!(
parse_hex_color("#60AAcc").unwrap(),
Color::Rgb(96, 170, 204)
);
assert!(parse_hex_color("#fatboy").is_none());
}
#[test]
fn test_utils_parse_color_rgb() {
assert_eq!(
parse_rgb_color("rgb(255, 64, 32)").unwrap(),
Color::Rgb(255, 64, 32)
);
assert_eq!(
parse_rgb_color("rgb(255,64,32)").unwrap(),
Color::Rgb(255, 64, 32)
);
assert_eq!(
parse_rgb_color("(255,64,32)").unwrap(),
Color::Rgb(255, 64, 32)
);
assert_eq!(
parse_rgb_color("255,64,32").unwrap(),
Color::Rgb(255, 64, 32)
);
assert!(parse_rgb_color("(300, 128, 512)").is_none());
}
#[test]
fn test_utils_parse_color() {
assert_eq!(parse_color("Black").unwrap(), Color::Black);
assert_eq!(parse_color("BLUE").unwrap(), Color::Blue);
assert_eq!(parse_color("Cyan").unwrap(), Color::Cyan);
assert_eq!(parse_color("DarkGray").unwrap(), Color::DarkGray);
assert_eq!(parse_color("Gray").unwrap(), Color::Gray);
assert_eq!(parse_color("Green").unwrap(), Color::Green);
assert_eq!(parse_color("LightBlue").unwrap(), Color::LightBlue);
assert_eq!(parse_color("LightCyan").unwrap(), Color::LightCyan);
assert_eq!(parse_color("LightGreen").unwrap(), Color::LightGreen);
assert_eq!(parse_color("LightMagenta").unwrap(), Color::LightMagenta);
assert_eq!(parse_color("LightRed").unwrap(), Color::LightRed);
assert_eq!(parse_color("LightYellow").unwrap(), Color::LightYellow);
assert_eq!(parse_color("Magenta").unwrap(), Color::Magenta);
assert_eq!(parse_color("Red").unwrap(), Color::Red);
assert_eq!(parse_color("Default").unwrap(), Color::Reset);
assert_eq!(parse_color("White").unwrap(), Color::White);
assert_eq!(parse_color("Yellow").unwrap(), Color::Yellow);
assert_eq!(parse_color("#f0f0f0").unwrap(), Color::Rgb(240, 240, 240));
// -- css colors
assert_eq!(parse_color("aliceblue"), Some(Color::Rgb(240, 248, 255)));
assert_eq!(parse_color("antiquewhite"), Some(Color::Rgb(250, 235, 215)));
assert_eq!(parse_color("aqua"), Some(Color::Rgb(0, 255, 255)));
assert_eq!(parse_color("aquamarine"), Some(Color::Rgb(127, 255, 212)));
assert_eq!(parse_color("azure"), Some(Color::Rgb(240, 255, 255)));
assert_eq!(parse_color("beige"), Some(Color::Rgb(245, 245, 220)));
assert_eq!(parse_color("bisque"), Some(Color::Rgb(255, 228, 196)));
assert_eq!(
parse_color("blanchedalmond"),
Some(Color::Rgb(255, 235, 205))
);
assert_eq!(parse_color("blueviolet"), Some(Color::Rgb(138, 43, 226)));
assert_eq!(parse_color("brown"), Some(Color::Rgb(165, 42, 42)));
assert_eq!(parse_color("burlywood"), Some(Color::Rgb(222, 184, 135)));
assert_eq!(parse_color("cadetblue"), Some(Color::Rgb(95, 158, 160)));
assert_eq!(parse_color("chartreuse"), Some(Color::Rgb(127, 255, 0)));
assert_eq!(parse_color("chocolate"), Some(Color::Rgb(210, 105, 30)));
assert_eq!(parse_color("coral"), Some(Color::Rgb(255, 127, 80)));
assert_eq!(
parse_color("cornflowerblue"),
Some(Color::Rgb(100, 149, 237))
);
assert_eq!(parse_color("cornsilk"), Some(Color::Rgb(255, 248, 220)));
assert_eq!(parse_color("crimson"), Some(Color::Rgb(220, 20, 60)));
assert_eq!(parse_color("darkblue"), Some(Color::Rgb(0, 0, 139)));
assert_eq!(parse_color("darkcyan"), Some(Color::Rgb(0, 139, 139)));
assert_eq!(parse_color("darkgoldenrod"), Some(Color::Rgb(184, 134, 11)));
assert_eq!(parse_color("darkgreen"), Some(Color::Rgb(0, 100, 0)));
assert_eq!(parse_color("darkkhaki"), Some(Color::Rgb(189, 183, 107)));
assert_eq!(parse_color("darkmagenta"), Some(Color::Rgb(139, 0, 139)));
assert_eq!(parse_color("darkolivegreen"), Some(Color::Rgb(85, 107, 47)));
assert_eq!(parse_color("darkorange"), Some(Color::Rgb(255, 140, 0)));
assert_eq!(parse_color("darkorchid"), Some(Color::Rgb(153, 50, 204)));
assert_eq!(parse_color("darkred"), Some(Color::Rgb(139, 0, 0)));
assert_eq!(parse_color("darksalmon"), Some(Color::Rgb(233, 150, 122)));
assert_eq!(parse_color("darkseagreen"), Some(Color::Rgb(143, 188, 143)));
assert_eq!(parse_color("darkslateblue"), Some(Color::Rgb(72, 61, 139)));
assert_eq!(parse_color("darkslategray"), Some(Color::Rgb(47, 79, 79)));
assert_eq!(parse_color("darkslategrey"), Some(Color::Rgb(47, 79, 79)));
assert_eq!(parse_color("darkturquoise"), Some(Color::Rgb(0, 206, 209)));
assert_eq!(parse_color("darkviolet"), Some(Color::Rgb(148, 0, 211)));
assert_eq!(parse_color("deeppink"), Some(Color::Rgb(255, 20, 147)));
assert_eq!(parse_color("deepskyblue"), Some(Color::Rgb(0, 191, 255)));
assert_eq!(parse_color("dimgray"), Some(Color::Rgb(105, 105, 105)));
assert_eq!(parse_color("dimgrey"), Some(Color::Rgb(105, 105, 105)));
assert_eq!(parse_color("dodgerblue"), Some(Color::Rgb(30, 144, 255)));
assert_eq!(parse_color("firebrick"), Some(Color::Rgb(178, 34, 34)));
assert_eq!(parse_color("floralwhite"), Some(Color::Rgb(255, 250, 240)));
assert_eq!(parse_color("forestgreen"), Some(Color::Rgb(34, 139, 34)));
assert_eq!(parse_color("fuchsia"), Some(Color::Rgb(255, 0, 255)));
assert_eq!(parse_color("gainsboro"), Some(Color::Rgb(220, 220, 220)));
assert_eq!(parse_color("ghostwhite"), Some(Color::Rgb(248, 248, 255)));
assert_eq!(parse_color("gold"), Some(Color::Rgb(255, 215, 0)));
assert_eq!(parse_color("goldenrod"), Some(Color::Rgb(218, 165, 32)));
assert_eq!(parse_color("greenyellow"), Some(Color::Rgb(173, 255, 47)));
assert_eq!(parse_color("honeydew"), Some(Color::Rgb(240, 255, 240)));
assert_eq!(parse_color("hotpink"), Some(Color::Rgb(255, 105, 180)));
assert_eq!(parse_color("indianred"), Some(Color::Rgb(205, 92, 92)));
assert_eq!(parse_color("indigo"), Some(Color::Rgb(75, 0, 130)));
assert_eq!(parse_color("ivory"), Some(Color::Rgb(255, 255, 240)));
assert_eq!(parse_color("khaki"), Some(Color::Rgb(240, 230, 140)));
assert_eq!(parse_color("lavender"), Some(Color::Rgb(230, 230, 250)));
assert_eq!(
parse_color("lavenderblush"),
Some(Color::Rgb(255, 240, 245))
);
assert_eq!(parse_color("lawngreen"), Some(Color::Rgb(124, 252, 0)));
assert_eq!(parse_color("lemonchiffon"), Some(Color::Rgb(255, 250, 205)));
assert_eq!(parse_color("lightcoral"), Some(Color::Rgb(240, 128, 128)));
assert_eq!(
parse_color("lightgoldenrodyellow"),
Some(Color::Rgb(250, 250, 210))
);
assert_eq!(parse_color("lightpink"), Some(Color::Rgb(255, 182, 193)));
assert_eq!(parse_color("lightsalmon"), Some(Color::Rgb(255, 160, 122)));
assert_eq!(parse_color("lightseagreen"), Some(Color::Rgb(32, 178, 170)));
assert_eq!(parse_color("lightskyblue"), Some(Color::Rgb(135, 206, 250)));
assert_eq!(
parse_color("lightslategray"),
Some(Color::Rgb(119, 136, 153))
);
assert_eq!(
parse_color("lightslategrey"),
Some(Color::Rgb(119, 136, 153))
);
assert_eq!(
parse_color("lightsteelblue"),
Some(Color::Rgb(176, 196, 222))
);
assert_eq!(parse_color("lime"), Some(Color::Rgb(0, 255, 0)));
assert_eq!(parse_color("limegreen"), Some(Color::Rgb(50, 205, 50)));
assert_eq!(parse_color("linen"), Some(Color::Rgb(250, 240, 230)));
assert_eq!(parse_color("maroon"), Some(Color::Rgb(128, 0, 0)));
assert_eq!(
parse_color("mediumaquamarine"),
Some(Color::Rgb(102, 205, 170))
);
assert_eq!(parse_color("mediumblue"), Some(Color::Rgb(0, 0, 205)));
assert_eq!(parse_color("mediumorchid"), Some(Color::Rgb(186, 85, 211)));
assert_eq!(parse_color("mediumpurple"), Some(Color::Rgb(147, 112, 219)));
assert_eq!(
parse_color("mediumseagreen"),
Some(Color::Rgb(60, 179, 113))
);
assert_eq!(
parse_color("mediumslateblue"),
Some(Color::Rgb(123, 104, 238))
);
assert_eq!(
parse_color("mediumspringgreen"),
Some(Color::Rgb(0, 250, 154))
);
assert_eq!(
parse_color("mediumturquoise"),
Some(Color::Rgb(72, 209, 204))
);
assert_eq!(
parse_color("mediumvioletred"),
Some(Color::Rgb(199, 21, 133))
);
assert_eq!(parse_color("midnightblue"), Some(Color::Rgb(25, 25, 112)));
assert_eq!(parse_color("mintcream"), Some(Color::Rgb(245, 255, 250)));
assert_eq!(parse_color("mistyrose"), Some(Color::Rgb(255, 228, 225)));
assert_eq!(parse_color("moccasin"), Some(Color::Rgb(255, 228, 181)));
assert_eq!(parse_color("navajowhite"), Some(Color::Rgb(255, 222, 173)));
assert_eq!(parse_color("navy"), Some(Color::Rgb(0, 0, 128)));
assert_eq!(parse_color("oldlace"), Some(Color::Rgb(253, 245, 230)));
assert_eq!(parse_color("olive"), Some(Color::Rgb(128, 128, 0)));
assert_eq!(parse_color("olivedrab"), Some(Color::Rgb(107, 142, 35)));
assert_eq!(parse_color("orange"), Some(Color::Rgb(255, 165, 0)));
assert_eq!(parse_color("orangered"), Some(Color::Rgb(255, 69, 0)));
assert_eq!(parse_color("orchid"), Some(Color::Rgb(218, 112, 214)));
assert_eq!(
parse_color("palegoldenrod"),
Some(Color::Rgb(238, 232, 170))
);
assert_eq!(parse_color("palegreen"), Some(Color::Rgb(152, 251, 152)));
assert_eq!(
parse_color("paleturquoise"),
Some(Color::Rgb(175, 238, 238))
);
assert_eq!(
parse_color("palevioletred"),
Some(Color::Rgb(219, 112, 147))
);
assert_eq!(parse_color("papayawhip"), Some(Color::Rgb(255, 239, 213)));
assert_eq!(parse_color("peachpuff"), Some(Color::Rgb(255, 218, 185)));
assert_eq!(parse_color("peru"), Some(Color::Rgb(205, 133, 63)));
assert_eq!(parse_color("pink"), Some(Color::Rgb(255, 192, 203)));
assert_eq!(parse_color("plum"), Some(Color::Rgb(221, 160, 221)));
assert_eq!(parse_color("powderblue"), Some(Color::Rgb(176, 224, 230)));
assert_eq!(parse_color("purple"), Some(Color::Rgb(128, 0, 128)));
assert_eq!(parse_color("rebeccapurple"), Some(Color::Rgb(102, 51, 153)));
assert_eq!(parse_color("rosybrown"), Some(Color::Rgb(188, 143, 143)));
assert_eq!(parse_color("royalblue"), Some(Color::Rgb(65, 105, 225)));
assert_eq!(parse_color("saddlebrown"), Some(Color::Rgb(139, 69, 19)));
assert_eq!(parse_color("salmon"), Some(Color::Rgb(250, 128, 114)));
assert_eq!(parse_color("sandybrown"), Some(Color::Rgb(244, 164, 96)));
assert_eq!(parse_color("seagreen"), Some(Color::Rgb(46, 139, 87)));
assert_eq!(parse_color("seashell"), Some(Color::Rgb(255, 245, 238)));
assert_eq!(parse_color("sienna"), Some(Color::Rgb(160, 82, 45)));
assert_eq!(parse_color("silver"), Some(Color::Rgb(192, 192, 192)));
assert_eq!(parse_color("skyblue"), Some(Color::Rgb(135, 206, 235)));
assert_eq!(parse_color("slateblue"), Some(Color::Rgb(106, 90, 205)));
assert_eq!(parse_color("slategray"), Some(Color::Rgb(112, 128, 144)));
assert_eq!(parse_color("slategrey"), Some(Color::Rgb(112, 128, 144)));
assert_eq!(parse_color("snow"), Some(Color::Rgb(255, 250, 250)));
assert_eq!(parse_color("springgreen"), Some(Color::Rgb(0, 255, 127)));
assert_eq!(parse_color("steelblue"), Some(Color::Rgb(70, 130, 180)));
assert_eq!(parse_color("tan"), Some(Color::Rgb(210, 180, 140)));
assert_eq!(parse_color("teal"), Some(Color::Rgb(0, 128, 128)));
assert_eq!(parse_color("thistle"), Some(Color::Rgb(216, 191, 216)));
assert_eq!(parse_color("tomato"), Some(Color::Rgb(255, 99, 71)));
assert_eq!(parse_color("turquoise"), Some(Color::Rgb(64, 224, 208)));
assert_eq!(parse_color("violet"), Some(Color::Rgb(238, 130, 238)));
assert_eq!(parse_color("wheat"), Some(Color::Rgb(245, 222, 179)));
assert_eq!(parse_color("whitesmoke"), Some(Color::Rgb(245, 245, 245)));
assert_eq!(parse_color("yellowgreen"), Some(Color::Rgb(154, 205, 50)));
// -- hex and rgb
assert_eq!(
parse_color("rgb(255, 64, 32)").unwrap(),
Color::Rgb(255, 64, 32)
);
assert!(parse_color("redd").is_none());
}
}

View file

@ -185,6 +185,13 @@ pub fn make_fsentry(path: PathBuf, is_dir: bool) -> FsEntry {
}
}
/// ### create_file_ioers
///
/// Open a file with two handlers, the first is to read, the second is to write
pub fn create_file_ioers(p: &Path) -> (File, File) {
(File::open(p).ok().unwrap(), File::create(p).ok().unwrap())
}
mod test {
use super::*;
@ -245,4 +252,10 @@ mod test {
assert!(make_dir_at(tmpdir.path(), "docs").is_ok());
assert!(make_dir_at(PathBuf::from("/aaaaa/bbbbb/cccc").as_path(), "docs").is_err());
}
#[test]
fn test_utils_test_helpers_create_file_ioers() {
let (_, tmp) = create_sample_file_entry();
let _ = create_file_ioers(tmp.path());
}
}

25
themes/default.toml Normal file
View file

@ -0,0 +1,25 @@
auth_address = "Yellow"
auth_bookmarks = "LightGreen"
auth_password = "LightBlue"
auth_port = "LightCyan"
auth_protocol = "LightGreen"
auth_recents = "LightBlue"
auth_username = "LightMagenta"
misc_error_dialog = "Red"
misc_input_dialog = "Default"
misc_keys = "Cyan"
misc_quit_dialog = "Yellow"
misc_save_dialog = "LightCyan"
misc_warn_dialog = "LightRed"
transfer_local_explorer_background = "Default"
transfer_local_explorer_foreground = "Default"
transfer_local_explorer_highlighted = "Yellow"
transfer_log_background = "Default"
transfer_log_window = "LightGreen"
transfer_progress_bar = "Green"
transfer_remote_explorer_background = "Default"
transfer_remote_explorer_foreground = "Default"
transfer_remote_explorer_highlighted = "LightBlue"
transfer_status_hidden = "LightBlue"
transfer_status_sorting = "LightYellow"
transfer_status_sync_browsing = "LightGreen"

View file

@ -0,0 +1,25 @@
auth_address = "Yellow"
auth_bookmarks = "skyblue"
auth_password = "#c43bff"
auth_port = "lime"
auth_protocol = "orangered"
auth_recents = "deepskyblue"
auth_username = "aqua"
misc_error_dialog = "crimson"
misc_input_dialog = "turquoise"
misc_keys = "deeppink"
misc_quit_dialog = "lime"
misc_save_dialog = "gold"
misc_warn_dialog = "orangered"
transfer_local_explorer_background = "Default"
transfer_local_explorer_foreground = "Default"
transfer_local_explorer_highlighted = "aquamarine"
transfer_log_background = "Default"
transfer_log_window = "#c43bff"
transfer_progress_bar = "deeppink"
transfer_remote_explorer_background = "Default"
transfer_remote_explorer_foreground = "Default"
transfer_remote_explorer_highlighted = "greenyellow"
transfer_status_hidden = "lime"
transfer_status_sorting = "orangered"
transfer_status_sync_browsing = "darkturquoise"

25
themes/horizon.toml Normal file
View file

@ -0,0 +1,25 @@
auth_address = "salmon"
auth_bookmarks = "cornflowerblue"
auth_password = "crimson"
auth_port = "tomato"
auth_protocol = "coral"
auth_recents = "royalblue"
auth_username = "orangered"
misc_error_dialog = "crimson"
misc_input_dialog = "gold"
misc_keys = "deeppink"
misc_quit_dialog = "coral"
misc_save_dialog = "tomato"
misc_warn_dialog = "orangered"
transfer_local_explorer_background = "Default"
transfer_local_explorer_foreground = "lightcoral"
transfer_local_explorer_highlighted = "coral"
transfer_log_background = "Default"
transfer_log_window = "royalblue"
transfer_progress_bar = "deeppink"
transfer_remote_explorer_background = "Default"
transfer_remote_explorer_foreground = "lightsalmon"
transfer_remote_explorer_highlighted = "salmon"
transfer_status_hidden = "orange"
transfer_status_sorting = "gold"
transfer_status_sync_browsing = "tomato"

25
themes/mono-bright.toml Normal file
View file

@ -0,0 +1,25 @@
auth_address = "black"
auth_bookmarks = "#bbbbbb"
auth_password = "black"
auth_port = "black"
auth_protocol = "black"
auth_recents = "#bbbbbb"
auth_username = "black"
misc_error_dialog = "black"
misc_input_dialog = "black"
misc_keys = "black"
misc_quit_dialog = "black"
misc_save_dialog = "black"
misc_warn_dialog = "black"
transfer_local_explorer_background = "Default"
transfer_local_explorer_foreground = "Default"
transfer_local_explorer_highlighted = "#bbbbbb"
transfer_log_background = "Default"
transfer_log_window = "black"
transfer_progress_bar = "black"
transfer_remote_explorer_background = "Default"
transfer_remote_explorer_foreground = "Default"
transfer_remote_explorer_highlighted = "#bbbbbb"
transfer_status_hidden = "black"
transfer_status_sorting = "black"
transfer_status_sync_browsing = "black"

25
themes/mono-dark.toml Normal file
View file

@ -0,0 +1,25 @@
auth_address = "white"
auth_bookmarks = "white"
auth_password = "white"
auth_port = "white"
auth_protocol = "white"
auth_recents = "white"
auth_username = "white"
misc_error_dialog = "white"
misc_input_dialog = "white"
misc_keys = "white"
misc_quit_dialog = "white"
misc_save_dialog = "white"
misc_warn_dialog = "white"
transfer_local_explorer_background = "Default"
transfer_local_explorer_foreground = "Default"
transfer_local_explorer_highlighted = "white"
transfer_log_background = "Default"
transfer_log_window = "white"
transfer_progress_bar = "white"
transfer_remote_explorer_background = "Default"
transfer_remote_explorer_foreground = "Default"
transfer_remote_explorer_highlighted = "white"
transfer_status_hidden = "white"
transfer_status_sorting = "white"
transfer_status_sync_browsing = "white"

25
themes/sugarplum.toml Normal file
View file

@ -0,0 +1,25 @@
auth_address = "hotpink"
auth_bookmarks = "pink"
auth_password = "violet"
auth_port = "plum"
auth_protocol = "deeppink"
auth_recents = "lightpink"
auth_username = "orchid"
misc_error_dialog = "mediumvioletred"
misc_input_dialog = "plum"
misc_keys = "deeppink"
misc_quit_dialog = "lightcoral"
misc_save_dialog = "violet"
misc_warn_dialog = "hotpink"
transfer_local_explorer_background = "Default"
transfer_local_explorer_foreground = "pink"
transfer_local_explorer_highlighted = "hotpink"
transfer_log_background = "Default"
transfer_log_window = "palevioletred"
transfer_progress_bar = "hotpink"
transfer_remote_explorer_background = "Default"
transfer_remote_explorer_foreground = "plum"
transfer_remote_explorer_highlighted = "violet"
transfer_status_hidden = "violet"
transfer_status_sorting = "plum"
transfer_status_sync_browsing = "orchid"

25
themes/ubuntu.toml Normal file
View file

@ -0,0 +1,25 @@
auth_address = "LightYellow"
auth_bookmarks = "springgreen"
auth_password = "deepskyblue"
auth_port = "LightCyan"
auth_protocol = "LightGreen"
auth_recents = "aquamarine"
auth_username = "hotpink"
misc_error_dialog = "orangered"
misc_input_dialog = "snow"
misc_keys = "LightCyan"
misc_quit_dialog = "LightYellow"
misc_save_dialog = "LightCyan"
misc_warn_dialog = "tomato"
transfer_local_explorer_background = "Default"
transfer_local_explorer_foreground = "Default"
transfer_local_explorer_highlighted = "Yellow"
transfer_log_background = "Default"
transfer_log_window = "lawngreen"
transfer_progress_bar = "lawngreen"
transfer_remote_explorer_background = "Default"
transfer_remote_explorer_foreground = "Default"
transfer_remote_explorer_highlighted = "turquoise"
transfer_status_hidden = "deepskyblue"
transfer_status_sorting = "LightYellow"
transfer_status_sync_browsing = "LightGreen"

25
themes/veeso.toml Normal file
View file

@ -0,0 +1,25 @@
auth_address = "Yellow"
auth_bookmarks = "plum"
auth_password = "LightBlue"
auth_port = "turquoise"
auth_protocol = "greenyellow"
auth_recents = "paleturquoise"
auth_username = "deeppink"
misc_error_dialog = "crimson"
misc_input_dialog = "snow"
misc_keys = "deeppink"
misc_quit_dialog = "tomato"
misc_save_dialog = "gold"
misc_warn_dialog = "orangered"
transfer_local_explorer_background = "Default"
transfer_local_explorer_foreground = "Default"
transfer_local_explorer_highlighted = "orange"
transfer_log_background = "Default"
transfer_log_window = "limegreen"
transfer_progress_bar = "lawngreen"
transfer_remote_explorer_background = "Default"
transfer_remote_explorer_foreground = "Default"
transfer_remote_explorer_highlighted = "turquoise"
transfer_status_hidden = "dodgerblue"
transfer_status_sorting = "LightYellow"
transfer_status_sync_browsing = "palegreen"