mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Theme provider and '-t' and '-c' CLI options
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
155
src/config/params.rs
Normal 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
574
src/config/serialization.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
260
src/config/themes.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
28
src/main.rs
28
src/main.rs
@@ -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
68
src/support.rs
Normal 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
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -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!"
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
246
src/system/theme_provider.rs
Normal file
246
src/system/theme_provider.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
265
src/ui/activities/setup/view/mod.rs
Normal file
265
src/ui/activities/setup/view/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
414
src/ui/activities/setup/view/setup.rs
Normal file
414
src/ui/activities/setup/view/setup.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
296
src/ui/activities/setup/view/ssh_keys.rs
Normal file
296
src/ui/activities/setup/view/ssh_keys.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
656
src/ui/activities/setup/view/theme.rs
Normal file
656
src/ui/activities/setup/view/theme.rs
Normal 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(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
300
src/ui/components/color_picker.rs
Normal file
300
src/ui/components/color_picker.rs
Normal 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"))))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
*/
|
||||
// exports
|
||||
pub mod bookmark_list;
|
||||
pub mod color_picker;
|
||||
pub mod file_list;
|
||||
pub mod logbox;
|
||||
pub mod msgbox;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
426
src/utils/fmt.rs
426
src/utils/fmt.rs
@@ -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("******"));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user