Theme provider and '-t' and '-c' CLI options

This commit is contained in:
veeso
2021-07-04 11:50:32 +02:00
parent a105a42519
commit 0a7e29d92f
50 changed files with 5453 additions and 1835 deletions

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());
}
}