mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Merge branch 'rethink-context' into rethink-activities
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -22,14 +22,17 @@ Released on FIXME: date
|
|||||||
- **Execute** a command pressing `X`. This feature is supported on both local and remote hosts (only SFTP/SCP protocols support this feature).
|
- **Execute** a command pressing `X`. This feature is supported on both local and remote hosts (only SFTP/SCP protocols support this feature).
|
||||||
- Enhancements:
|
- Enhancements:
|
||||||
- Input fields will now support **"input keys"** (such as moving cursor, DEL, END, HOME, ...)
|
- Input fields will now support **"input keys"** (such as moving cursor, DEL, END, HOME, ...)
|
||||||
- For developers:
|
- Improved performance regarding configuration I/O (config client is now shared in the activity context)
|
||||||
- Activity refactoring
|
- Fetch latest version from Github once; cache previous value in the Context Storage.
|
||||||
- Developed an internal library used to create components, components are then nested inside a View
|
|
||||||
- The new engine works through properties and states, then returns Messages. I was inspired by both React and Elm.
|
|
||||||
- Bugfix:
|
- Bugfix:
|
||||||
- Prevent resetting explorer index on remote tab after performing certain actions (list dir, exec, ...)
|
- Prevent resetting explorer index on remote tab after performing certain actions (list dir, exec, ...)
|
||||||
- SCP file transfer: prevent infinite loops while performing `stat` on symbolic links pointing to themselves (e.g. `mylink -> mylink`)
|
- SCP file transfer: prevent infinite loops while performing `stat` on symbolic links pointing to themselves (e.g. `mylink -> mylink`)
|
||||||
- Fixed a bug causing termscp to crash if removing a bookmark
|
- Fixed a bug causing termscp to crash if removing a bookmark
|
||||||
|
- Fixed file format cursor position in the GUI
|
||||||
|
- For developers:
|
||||||
|
- Activity refactoring
|
||||||
|
- Developed an internal library used to create components, components are then nested inside a View
|
||||||
|
- The new engine works through properties and states, then returns Messages. I was inspired by both React and Elm.
|
||||||
|
|
||||||
## 0.3.3
|
## 0.3.3
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ use std::path::PathBuf;
|
|||||||
// Deps
|
// Deps
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::host::{HostError, Localhost};
|
use crate::host::{HostError, Localhost};
|
||||||
|
use crate::system::config_client::ConfigClient;
|
||||||
|
use crate::system::environment;
|
||||||
use crate::ui::activities::{
|
use crate::ui::activities::{
|
||||||
auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity,
|
auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity,
|
||||||
filetransfer_activity::FileTransferParams, setup_activity::SetupActivity, Activity,
|
filetransfer_activity::FileTransferParams, setup_activity::SetupActivity, Activity,
|
||||||
@@ -66,7 +68,13 @@ impl ActivityManager {
|
|||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
};
|
};
|
||||||
let ctx: Context = Context::new(host);
|
// Initialize configuration client
|
||||||
|
let (config_client, error): (Option<ConfigClient>, Option<String>) =
|
||||||
|
match Self::init_config_client() {
|
||||||
|
Ok(cli) => (Some(cli), None),
|
||||||
|
Err(err) => (None, Some(err)),
|
||||||
|
};
|
||||||
|
let ctx: Context = Context::new(host, config_client, error);
|
||||||
Ok(ActivityManager {
|
Ok(ActivityManager {
|
||||||
context: Some(ctx),
|
context: Some(ctx),
|
||||||
ftparams: None,
|
ftparams: None,
|
||||||
@@ -117,7 +125,7 @@ impl ActivityManager {
|
|||||||
drop(self.context.take());
|
drop(self.context.take());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loops
|
// -- Activity Loops
|
||||||
|
|
||||||
/// ### run_authentication
|
/// ### run_authentication
|
||||||
///
|
///
|
||||||
@@ -251,4 +259,35 @@ impl ActivityManager {
|
|||||||
// This activity always returns to AuthActivity
|
// This activity always returns to AuthActivity
|
||||||
Some(NextActivity::Authentication)
|
Some(NextActivity::Authentication)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- misc
|
||||||
|
|
||||||
|
/// ### init_config_client
|
||||||
|
///
|
||||||
|
/// Initialize configuration client
|
||||||
|
fn init_config_client() -> Result<ConfigClient, String> {
|
||||||
|
// Get config dir
|
||||||
|
match environment::init_config_dir() {
|
||||||
|
Ok(config_dir) => {
|
||||||
|
match config_dir {
|
||||||
|
Some(config_dir) => {
|
||||||
|
// Get config client paths
|
||||||
|
let (config_path, ssh_dir): (PathBuf, PathBuf) =
|
||||||
|
environment::get_config_paths(config_dir.as_path());
|
||||||
|
match ConfigClient::new(config_path.as_path(), ssh_dir.as_path()) {
|
||||||
|
Ok(cli) => Ok(cli),
|
||||||
|
Err(err) => Err(format!("Could not read configuration: {}", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(String::from(
|
||||||
|
"Your system doesn't support configuration paths",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(format!(
|
||||||
|
"Could not initialize configuration directory: {}",
|
||||||
|
err
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,14 +37,11 @@ extern crate unicode_width;
|
|||||||
use super::{Activity, Context};
|
use super::{Activity, Context};
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::system::bookmarks_client::BookmarksClient;
|
use crate::system::bookmarks_client::BookmarksClient;
|
||||||
use crate::system::config_client::ConfigClient;
|
|
||||||
use crate::system::environment;
|
|
||||||
use crate::ui::layout::view::View;
|
use crate::ui::layout::view::View;
|
||||||
use crate::utils::git;
|
use crate::utils::git;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
// -- components
|
// -- components
|
||||||
const COMPONENT_TEXT_HEADER: &str = "TEXT_HEADER";
|
const COMPONENT_TEXT_HEADER: &str = "TEXT_HEADER";
|
||||||
@@ -65,6 +62,9 @@ const COMPONENT_RADIO_BOOKMARK_SAVE_PWD: &str = "RADIO_SAVE_PASSWORD";
|
|||||||
const COMPONENT_BOOKMARKS_LIST: &str = "BOOKMARKS_LIST";
|
const COMPONENT_BOOKMARKS_LIST: &str = "BOOKMARKS_LIST";
|
||||||
const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST";
|
const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST";
|
||||||
|
|
||||||
|
// Store keys
|
||||||
|
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||||
|
|
||||||
/// ### AuthActivity
|
/// ### AuthActivity
|
||||||
///
|
///
|
||||||
/// AuthActivity is the data holder for the authentication activity
|
/// AuthActivity is the data holder for the authentication activity
|
||||||
@@ -80,12 +80,9 @@ pub struct AuthActivity {
|
|||||||
context: Option<Context>,
|
context: Option<Context>,
|
||||||
view: View,
|
view: View,
|
||||||
bookmarks_client: Option<BookmarksClient>,
|
bookmarks_client: Option<BookmarksClient>,
|
||||||
config_client: Option<ConfigClient>,
|
|
||||||
redraw: bool, // Should ui actually be redrawned?
|
redraw: bool, // Should ui actually be redrawned?
|
||||||
bookmarks_list: Vec<String>, // List of bookmarks
|
bookmarks_list: Vec<String>, // List of bookmarks
|
||||||
recents_list: Vec<String>, // list of recents
|
recents_list: Vec<String>, // list of recents
|
||||||
// misc
|
|
||||||
new_version: Option<String>, // Contains new version of termscp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AuthActivity {
|
impl Default for AuthActivity {
|
||||||
@@ -111,46 +108,9 @@ impl AuthActivity {
|
|||||||
context: None,
|
context: None,
|
||||||
view: View::init(),
|
view: View::init(),
|
||||||
bookmarks_client: None,
|
bookmarks_client: None,
|
||||||
config_client: None,
|
|
||||||
redraw: true, // True at startup
|
redraw: true, // True at startup
|
||||||
bookmarks_list: Vec::new(),
|
bookmarks_list: Vec::new(),
|
||||||
recents_list: Vec::new(),
|
recents_list: Vec::new(),
|
||||||
new_version: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### init_config_client
|
|
||||||
///
|
|
||||||
/// Initialize config client
|
|
||||||
fn init_config_client(&mut self) {
|
|
||||||
// Get config dir
|
|
||||||
match environment::init_config_dir() {
|
|
||||||
Ok(config_dir) => {
|
|
||||||
if let Some(config_dir) = config_dir {
|
|
||||||
// Get config client paths
|
|
||||||
let (config_path, ssh_dir): (PathBuf, PathBuf) =
|
|
||||||
environment::get_config_paths(config_dir.as_path());
|
|
||||||
match ConfigClient::new(config_path.as_path(), ssh_dir.as_path()) {
|
|
||||||
Ok(cli) => {
|
|
||||||
// Set default protocol
|
|
||||||
self.protocol = cli.get_default_protocol();
|
|
||||||
// Set client
|
|
||||||
self.config_client = Some(cli);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.mount_error(
|
|
||||||
format!("Could not initialize user configuration: {}", err)
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.mount_error(
|
|
||||||
format!("Could not initialize configuration directory: {}", err).as_str(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,18 +118,35 @@ impl AuthActivity {
|
|||||||
///
|
///
|
||||||
/// If enabled in configuration, check for updates from Github
|
/// If enabled in configuration, check for updates from Github
|
||||||
fn check_for_updates(&mut self) {
|
fn check_for_updates(&mut self) {
|
||||||
if let Some(client) = self.config_client.as_ref() {
|
// Check version only if unset in the store
|
||||||
if client.get_check_for_updates() {
|
let ctx: &Context = self.context.as_ref().unwrap();
|
||||||
// Send request
|
if !ctx.store.isset(STORE_KEY_LATEST_VERSION) {
|
||||||
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
let mut new_version: Option<String> = match ctx.config_client.as_ref() {
|
||||||
Ok(version) => self.new_version = version,
|
Some(client) => {
|
||||||
Err(err) => {
|
if client.get_check_for_updates() {
|
||||||
// Report error
|
// Send request
|
||||||
self.mount_error(
|
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
||||||
format!("Could not check for new updates: {}", err).as_str(),
|
Ok(version) => version,
|
||||||
);
|
Err(err) => {
|
||||||
|
// Report error
|
||||||
|
self.mount_error(
|
||||||
|
format!("Could not check for new updates: {}", err).as_str(),
|
||||||
|
);
|
||||||
|
// None
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let ctx: &mut Context = self.context.as_mut().unwrap();
|
||||||
|
// Set version into the store (or just a flag)
|
||||||
|
match new_version.take() {
|
||||||
|
Some(new_version) => ctx.store.set_string(STORE_KEY_LATEST_VERSION, new_version), // If Some, set String
|
||||||
|
None => ctx.store.set(STORE_KEY_LATEST_VERSION), // If None, just set flag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,9 +169,9 @@ impl Activity for AuthActivity {
|
|||||||
if self.bookmarks_client.is_none() {
|
if self.bookmarks_client.is_none() {
|
||||||
self.init_bookmarks_client();
|
self.init_bookmarks_client();
|
||||||
}
|
}
|
||||||
// init config client
|
// Verify error state from context
|
||||||
if self.config_client.is_none() {
|
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||||
self.init_config_client();
|
self.mount_error(err.as_str());
|
||||||
}
|
}
|
||||||
// If check for updates is enabled, check for updates
|
// If check for updates is enabled, check for updates
|
||||||
self.check_for_updates();
|
self.check_for_updates();
|
||||||
|
|||||||
@@ -145,7 +145,13 @@ impl AuthActivity {
|
|||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
// Version notice
|
// Version notice
|
||||||
if let Some(version) = self.new_version.as_ref() {
|
if let Some(version) = self
|
||||||
|
.context
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.store
|
||||||
|
.get_string(super::STORE_KEY_LATEST_VERSION)
|
||||||
|
{
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_NEW_VERSION,
|
super::COMPONENT_TEXT_NEW_VERSION,
|
||||||
Box::new(Text::new(
|
Box::new(Text::new(
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Set text editor to use
|
/// Set text editor to use
|
||||||
pub(super) fn setup_text_editor(&self) {
|
pub(super) fn setup_text_editor(&self) {
|
||||||
if let Some(config_cli) = &self.config_cli {
|
if let Some(config_cli) = self.context.as_ref().unwrap().config_client.as_ref() {
|
||||||
// Set text editor
|
// Set text editor
|
||||||
env::set_var("EDITOR", config_cli.get_text_editor());
|
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ pub struct FileTransferActivity {
|
|||||||
context: Option<Context>, // Context holder
|
context: Option<Context>, // Context holder
|
||||||
params: FileTransferParams, // FT connection params
|
params: FileTransferParams, // FT connection params
|
||||||
client: Box<dyn FileTransfer>, // File transfer client
|
client: Box<dyn FileTransfer>, // File transfer client
|
||||||
config_cli: Option<ConfigClient>, // Config Client
|
|
||||||
local: FileExplorer, // Local File explorer state
|
local: FileExplorer, // Local File explorer state
|
||||||
remote: FileExplorer, // Remote File explorer state
|
remote: FileExplorer, // Remote File explorer state
|
||||||
tab: FileExplorerTab, // Current selected tab
|
tab: FileExplorerTab, // Current selected tab
|
||||||
@@ -267,7 +266,6 @@ impl FileTransferActivity {
|
|||||||
params,
|
params,
|
||||||
local: Self::build_explorer(config_client.as_ref()),
|
local: Self::build_explorer(config_client.as_ref()),
|
||||||
remote: Self::build_explorer(config_client.as_ref()),
|
remote: Self::build_explorer(config_client.as_ref()),
|
||||||
config_cli: config_client,
|
|
||||||
tab: FileExplorerTab::Local,
|
tab: FileExplorerTab::Local,
|
||||||
log_index: 0,
|
log_index: 0,
|
||||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||||
@@ -308,6 +306,10 @@ impl Activity for FileTransferActivity {
|
|||||||
self.local.index_at_first();
|
self.local.index_at_first();
|
||||||
// Configure text editor
|
// Configure text editor
|
||||||
self.setup_text_editor();
|
self.setup_text_editor();
|
||||||
|
// Verify error state from context
|
||||||
|
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||||
|
self.popup = Some(Popup::Fatal(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on_draw
|
/// ### on_draw
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ impl SetupActivity {
|
|||||||
/// Callback for performing the delete of a ssh key
|
/// Callback for performing the delete of a ssh key
|
||||||
pub(super) fn callback_delete_ssh_key(&mut self) {
|
pub(super) fn callback_delete_ssh_key(&mut self) {
|
||||||
// Get key
|
// Get key
|
||||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
let key: Option<String> = match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) {
|
let key: Option<String> = match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) {
|
||||||
Some(k) => Some(k.clone()),
|
Some(k) => Some(k.clone()),
|
||||||
None => None,
|
None => None,
|
||||||
@@ -100,7 +100,7 @@ impl SetupActivity {
|
|||||||
///
|
///
|
||||||
/// Create a new ssh key with provided parameters
|
/// Create a new ssh key with provided parameters
|
||||||
pub(super) fn callback_new_ssh_key(&mut self, host: String, username: String) {
|
pub(super) fn callback_new_ssh_key(&mut self, host: String, username: String) {
|
||||||
if let Some(cli) = self.config_cli.as_ref() {
|
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
// Prepare text editor
|
// Prepare text editor
|
||||||
env::set_var("EDITOR", cli.get_text_editor());
|
env::set_var("EDITOR", cli.get_text_editor());
|
||||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||||
|
|||||||
@@ -25,55 +25,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
use super::{ConfigClient, Popup, SetupActivity};
|
use super::SetupActivity;
|
||||||
use crate::system::environment;
|
|
||||||
// Ext
|
// Ext
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
impl SetupActivity {
|
impl SetupActivity {
|
||||||
/// ### init_config_dir
|
|
||||||
///
|
|
||||||
/// Initialize configuration directory
|
|
||||||
pub(super) fn init_config_client(&mut self) {
|
|
||||||
match environment::init_config_dir() {
|
|
||||||
Ok(config_dir) => match config_dir {
|
|
||||||
Some(config_dir) => {
|
|
||||||
// Get paths
|
|
||||||
let (config_file, ssh_dir): (PathBuf, PathBuf) =
|
|
||||||
environment::get_config_paths(config_dir.as_path());
|
|
||||||
// Create config client
|
|
||||||
match ConfigClient::new(config_file.as_path(), ssh_dir.as_path()) {
|
|
||||||
Ok(cli) => self.config_cli = Some(cli),
|
|
||||||
Err(err) => {
|
|
||||||
self.popup = Some(Popup::Fatal(format!(
|
|
||||||
"Could not initialize configuration client: {}",
|
|
||||||
err
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.popup = Some(Popup::Fatal(
|
|
||||||
"No configuration directory is available on your system".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
self.popup = Some(Popup::Fatal(format!(
|
|
||||||
"Could not initialize configuration directory: {}",
|
|
||||||
err
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### save_config
|
/// ### save_config
|
||||||
///
|
///
|
||||||
/// Save configuration
|
/// Save configuration
|
||||||
pub(super) fn save_config(&mut self) -> Result<(), String> {
|
pub(super) fn save_config(&mut self) -> Result<(), String> {
|
||||||
match &self.config_cli {
|
match self.context.as_ref().unwrap().config_client.as_ref() {
|
||||||
Some(cli) => match cli.write_config() {
|
Some(cli) => match cli.write_config() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => Err(format!("Could not save configuration: {}", err)),
|
Err(err) => Err(format!("Could not save configuration: {}", err)),
|
||||||
@@ -87,7 +49,7 @@ impl SetupActivity {
|
|||||||
/// Reset configuration changes; pratically read config from file, overwriting any change made
|
/// Reset configuration changes; pratically read config from file, overwriting any change made
|
||||||
/// since last write action
|
/// since last write action
|
||||||
pub(super) fn reset_config_changes(&mut self) -> Result<(), String> {
|
pub(super) fn reset_config_changes(&mut self) -> Result<(), String> {
|
||||||
match self.config_cli.as_mut() {
|
match self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
Some(cli) => match cli.read_config() {
|
Some(cli) => match cli.read_config() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => Err(format!("Could not restore configuration: {}", err)),
|
Err(err) => Err(format!("Could not restore configuration: {}", err)),
|
||||||
@@ -100,7 +62,7 @@ impl SetupActivity {
|
|||||||
///
|
///
|
||||||
/// Delete ssh key from config cli
|
/// Delete ssh key from config cli
|
||||||
pub(super) fn delete_ssh_key(&mut self, host: &str, username: &str) -> Result<(), String> {
|
pub(super) fn delete_ssh_key(&mut self, host: &str, username: &str) -> Result<(), String> {
|
||||||
match self.config_cli.as_mut() {
|
match self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
Some(cli) => match cli.del_ssh_key(host, username) {
|
Some(cli) => match cli.del_ssh_key(host, username) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => Err(format!(
|
Err(err) => Err(format!(
|
||||||
@@ -116,58 +78,51 @@ impl SetupActivity {
|
|||||||
///
|
///
|
||||||
/// Edit selected ssh key
|
/// Edit selected ssh key
|
||||||
pub(super) fn edit_ssh_key(&mut self) -> Result<(), String> {
|
pub(super) fn edit_ssh_key(&mut self) -> Result<(), String> {
|
||||||
match self.config_cli.as_ref() {
|
match self.context.as_mut() {
|
||||||
Some(cli) => {
|
None => Ok(()),
|
||||||
// Set text editor
|
Some(ctx) => {
|
||||||
env::set_var("EDITOR", cli.get_text_editor());
|
// Set editor if config client exists
|
||||||
|
if let Some(config_cli) = ctx.config_client.as_ref() {
|
||||||
|
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||||
|
}
|
||||||
// Prepare terminal
|
// Prepare terminal
|
||||||
let _ = disable_raw_mode();
|
let _ = disable_raw_mode();
|
||||||
// Leave alternate mode
|
// Leave alternate mode
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
ctx.leave_alternate_screen();
|
||||||
ctx.leave_alternate_screen();
|
// Get result
|
||||||
}
|
let result: Result<(), String> = match ctx.config_client.as_ref() {
|
||||||
// Check if key exists
|
Some(config_cli) => match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) {
|
||||||
match cli.iter_ssh_keys().nth(self.ssh_key_idx) {
|
Some(key) => {
|
||||||
Some(key) => {
|
// Get key path
|
||||||
// Get key path
|
match config_cli.get_ssh_key(key) {
|
||||||
match cli.get_ssh_key(key) {
|
Ok(ssh_key) => match ssh_key {
|
||||||
Ok(ssh_key) => match ssh_key {
|
None => Ok(()),
|
||||||
None => Ok(()),
|
Some((_, _, key_path)) => {
|
||||||
Some((_, _, key_path)) => match edit::edit_file(key_path.as_path())
|
match edit::edit_file(key_path.as_path()) {
|
||||||
{
|
Ok(_) => Ok(()),
|
||||||
Ok(_) => {
|
Err(err) => {
|
||||||
// Restore terminal
|
Err(format!("Could not edit ssh key: {}", err))
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
}
|
||||||
// Clear screen
|
|
||||||
ctx.clear_screen();
|
|
||||||
// Enter alternate mode
|
|
||||||
ctx.enter_alternate_screen();
|
|
||||||
}
|
}
|
||||||
// Re-enable raw mode
|
|
||||||
let _ = enable_raw_mode();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// Restore terminal
|
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
|
||||||
// Clear screen
|
|
||||||
ctx.clear_screen();
|
|
||||||
// Enter alternate mode
|
|
||||||
ctx.enter_alternate_screen();
|
|
||||||
}
|
|
||||||
// Re-enable raw mode
|
|
||||||
let _ = enable_raw_mode();
|
|
||||||
Err(format!("Could not edit ssh key: {}", err))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
Err(err) => Err(format!("Could not read ssh key: {}", err)),
|
||||||
Err(err) => Err(format!("Could not read ssh key: {}", err)),
|
}
|
||||||
}
|
}
|
||||||
}
|
None => Ok(()),
|
||||||
|
},
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
}
|
};
|
||||||
|
// Restore terminal
|
||||||
|
// Clear screen
|
||||||
|
ctx.clear_screen();
|
||||||
|
// Enter alternate mode
|
||||||
|
ctx.enter_alternate_screen();
|
||||||
|
// Re-enable raw mode
|
||||||
|
let _ = enable_raw_mode();
|
||||||
|
// Return result
|
||||||
|
result
|
||||||
}
|
}
|
||||||
None => Ok(()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +135,7 @@ impl SetupActivity {
|
|||||||
username: &str,
|
username: &str,
|
||||||
rsa_key: &str,
|
rsa_key: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
match self.config_cli.as_mut() {
|
match self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
Some(cli) => {
|
Some(cli) => {
|
||||||
// Add key to client
|
// Add key to client
|
||||||
match cli.add_ssh_key(host, username, rsa_key) {
|
match cli.add_ssh_key(host, username, rsa_key) {
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ impl SetupActivity {
|
|||||||
self.tab = SetupTab::UserInterface(UserInterfaceInputField::DefaultProtocol)
|
self.tab = SetupTab::UserInterface(UserInterfaceInputField::DefaultProtocol)
|
||||||
} // Switch tab to user interface config
|
} // Switch tab to user interface config
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
if let Some(config_cli) = self.config_cli.as_ref() {
|
if let Some(config_cli) = self.context.as_ref().unwrap().config_client.as_ref()
|
||||||
|
{
|
||||||
// Move ssh key index up
|
// Move ssh key index up
|
||||||
let ssh_key_size: usize = config_cli.iter_ssh_keys().count();
|
let ssh_key_size: usize = config_cli.iter_ssh_keys().count();
|
||||||
if self.ssh_key_idx > 0 {
|
if self.ssh_key_idx > 0 {
|
||||||
@@ -89,7 +90,8 @@ impl SetupActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
if let Some(config_cli) = self.config_cli.as_ref() {
|
if let Some(config_cli) = self.context.as_ref().unwrap().config_client.as_ref()
|
||||||
|
{
|
||||||
// Move ssh key index down
|
// Move ssh key index down
|
||||||
let ssh_key_size: usize = config_cli.iter_ssh_keys().count();
|
let ssh_key_size: usize = config_cli.iter_ssh_keys().count();
|
||||||
if self.ssh_key_idx + 1 < ssh_key_size {
|
if self.ssh_key_idx + 1 < ssh_key_size {
|
||||||
@@ -180,7 +182,8 @@ impl SetupActivity {
|
|||||||
KeyCode::Tab => self.tab = SetupTab::SshConfig, // Switch tab to ssh config
|
KeyCode::Tab => self.tab = SetupTab::SshConfig, // Switch tab to ssh config
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
// Pop character from selected input
|
// Pop character from selected input
|
||||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut()
|
||||||
|
{
|
||||||
match field {
|
match field {
|
||||||
UserInterfaceInputField::TextEditor => {
|
UserInterfaceInputField::TextEditor => {
|
||||||
// Pop from text editor
|
// Pop from text editor
|
||||||
@@ -207,7 +210,8 @@ impl SetupActivity {
|
|||||||
}
|
}
|
||||||
KeyCode::Left => {
|
KeyCode::Left => {
|
||||||
// Move left on fields which are tabs
|
// Move left on fields which are tabs
|
||||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut()
|
||||||
|
{
|
||||||
match field {
|
match field {
|
||||||
UserInterfaceInputField::DefaultProtocol => {
|
UserInterfaceInputField::DefaultProtocol => {
|
||||||
// Move left
|
// Move left
|
||||||
@@ -248,7 +252,8 @@ impl SetupActivity {
|
|||||||
}
|
}
|
||||||
KeyCode::Right => {
|
KeyCode::Right => {
|
||||||
// Move right on fields which are tabs
|
// Move right on fields which are tabs
|
||||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut()
|
||||||
|
{
|
||||||
match field {
|
match field {
|
||||||
UserInterfaceInputField::DefaultProtocol => {
|
UserInterfaceInputField::DefaultProtocol => {
|
||||||
// Move left
|
// Move left
|
||||||
@@ -354,7 +359,9 @@ impl SetupActivity {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Push character to input field
|
// Push character to input field
|
||||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
if let Some(config_cli) =
|
||||||
|
self.context.as_mut().unwrap().config_client.as_mut()
|
||||||
|
{
|
||||||
// NOTE: change to match if other fields are added
|
// NOTE: change to match if other fields are added
|
||||||
match field {
|
match field {
|
||||||
UserInterfaceInputField::TextEditor => {
|
UserInterfaceInputField::TextEditor => {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::fs::explorer::GroupDirs;
|
use crate::fs::explorer::GroupDirs;
|
||||||
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::utils::fmt::align_text_center;
|
use crate::utils::fmt::align_text_center;
|
||||||
// Ext
|
// Ext
|
||||||
use tui::{
|
use tui::{
|
||||||
@@ -46,6 +47,7 @@ impl SetupActivity {
|
|||||||
/// Draw UI
|
/// Draw UI
|
||||||
pub(super) fn draw(&mut self) {
|
pub(super) fn draw(&mut self) {
|
||||||
let mut ctx: Context = self.context.take().unwrap();
|
let mut ctx: Context = self.context.take().unwrap();
|
||||||
|
let config_client: Option<&ConfigClient> = ctx.config_client.as_ref();
|
||||||
let _ = ctx.terminal.draw(|f| {
|
let _ = ctx.terminal.draw(|f| {
|
||||||
// Prepare main chunks
|
// Prepare main chunks
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
@@ -71,7 +73,7 @@ impl SetupActivity {
|
|||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Percentage(100)].as_ref())
|
.constraints([Constraint::Percentage(100)].as_ref())
|
||||||
.split(chunks[1]);
|
.split(chunks[1]);
|
||||||
if let Some(ssh_key_tab) = self.draw_ssh_keys_list() {
|
if let Some(ssh_key_tab) = self.draw_ssh_keys_list(config_client) {
|
||||||
// Create ssh list state
|
// Create ssh list state
|
||||||
let mut ssh_key_state: ListState = ListState::default();
|
let mut ssh_key_state: ListState = ListState::default();
|
||||||
ssh_key_state.select(Some(self.ssh_key_idx));
|
ssh_key_state.select(Some(self.ssh_key_idx));
|
||||||
@@ -97,26 +99,26 @@ impl SetupActivity {
|
|||||||
)
|
)
|
||||||
.split(chunks[1]);
|
.split(chunks[1]);
|
||||||
// Render input forms
|
// Render input forms
|
||||||
if let Some(field) = self.draw_text_editor_input() {
|
if let Some(field) = self.draw_text_editor_input(config_client) {
|
||||||
f.render_widget(field, ui_cfg_chunks[0]);
|
f.render_widget(field, ui_cfg_chunks[0]);
|
||||||
}
|
}
|
||||||
if let Some(tab) = self.draw_default_protocol_tab() {
|
if let Some(tab) = self.draw_default_protocol_tab(config_client) {
|
||||||
f.render_widget(tab, ui_cfg_chunks[1]);
|
f.render_widget(tab, ui_cfg_chunks[1]);
|
||||||
}
|
}
|
||||||
if let Some(tab) = self.draw_hidden_files_tab() {
|
if let Some(tab) = self.draw_hidden_files_tab(config_client) {
|
||||||
f.render_widget(tab, ui_cfg_chunks[2]);
|
f.render_widget(tab, ui_cfg_chunks[2]);
|
||||||
}
|
}
|
||||||
if let Some(tab) = self.draw_check_for_updates_tab() {
|
if let Some(tab) = self.draw_check_for_updates_tab(config_client) {
|
||||||
f.render_widget(tab, ui_cfg_chunks[3]);
|
f.render_widget(tab, ui_cfg_chunks[3]);
|
||||||
}
|
}
|
||||||
if let Some(tab) = self.draw_default_group_dirs_tab() {
|
if let Some(tab) = self.draw_default_group_dirs_tab(config_client) {
|
||||||
f.render_widget(tab, ui_cfg_chunks[4]);
|
f.render_widget(tab, ui_cfg_chunks[4]);
|
||||||
}
|
}
|
||||||
if let Some(tab) = self.draw_file_fmt_input() {
|
if let Some(tab) = self.draw_file_fmt_input(config_client) {
|
||||||
f.render_widget(tab, ui_cfg_chunks[5]);
|
f.render_widget(tab, ui_cfg_chunks[5]);
|
||||||
}
|
}
|
||||||
// Set cursor
|
// Set cursor
|
||||||
if let Some(cli) = &self.config_cli {
|
if let Some(cli) = config_client {
|
||||||
match form_field {
|
match form_field {
|
||||||
UserInterfaceInputField::TextEditor => {
|
UserInterfaceInputField::TextEditor => {
|
||||||
let editor_text: String =
|
let editor_text: String =
|
||||||
@@ -129,8 +131,8 @@ impl SetupActivity {
|
|||||||
UserInterfaceInputField::FileFmt => {
|
UserInterfaceInputField::FileFmt => {
|
||||||
let file_fmt: String = cli.get_file_fmt().unwrap_or_default();
|
let file_fmt: String = cli.get_file_fmt().unwrap_or_default();
|
||||||
f.set_cursor(
|
f.set_cursor(
|
||||||
ui_cfg_chunks[4].x + file_fmt.width() as u16 + 1,
|
ui_cfg_chunks[5].x + file_fmt.width() as u16 + 1,
|
||||||
ui_cfg_chunks[4].y + 1,
|
ui_cfg_chunks[5].y + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => { /* Not a text field */ }
|
_ => { /* Not a text field */ }
|
||||||
@@ -247,8 +249,8 @@ impl SetupActivity {
|
|||||||
/// ### draw_text_editor_input
|
/// ### draw_text_editor_input
|
||||||
///
|
///
|
||||||
/// Draw input text field for text editor parameter
|
/// Draw input text field for text editor parameter
|
||||||
fn draw_text_editor_input(&self) -> Option<Paragraph> {
|
fn draw_text_editor_input(&self, config_cli: Option<&ConfigClient>) -> Option<Paragraph> {
|
||||||
match &self.config_cli {
|
match config_cli.as_ref() {
|
||||||
Some(cli) => Some(
|
Some(cli) => Some(
|
||||||
Paragraph::new(String::from(
|
Paragraph::new(String::from(
|
||||||
cli.get_text_editor().as_path().to_string_lossy(),
|
cli.get_text_editor().as_path().to_string_lossy(),
|
||||||
@@ -274,9 +276,9 @@ impl SetupActivity {
|
|||||||
/// ### draw_default_protocol_tab
|
/// ### draw_default_protocol_tab
|
||||||
///
|
///
|
||||||
/// Draw default protocol input tab
|
/// Draw default protocol input tab
|
||||||
fn draw_default_protocol_tab(&self) -> Option<Tabs> {
|
fn draw_default_protocol_tab(&self, config_cli: Option<&ConfigClient>) -> Option<Tabs> {
|
||||||
// Check if config client is some
|
// Check if config client is some
|
||||||
match &self.config_cli {
|
match config_cli.as_ref() {
|
||||||
Some(cli) => {
|
Some(cli) => {
|
||||||
let choices: Vec<Spans> = vec![
|
let choices: Vec<Spans> = vec![
|
||||||
Spans::from("SFTP"),
|
Spans::from("SFTP"),
|
||||||
@@ -324,9 +326,9 @@ impl SetupActivity {
|
|||||||
/// ### draw_hidden_files_tab
|
/// ### draw_hidden_files_tab
|
||||||
///
|
///
|
||||||
/// Draw default hidden files tab
|
/// Draw default hidden files tab
|
||||||
fn draw_hidden_files_tab(&self) -> Option<Tabs> {
|
fn draw_hidden_files_tab(&self, config_cli: Option<&ConfigClient>) -> Option<Tabs> {
|
||||||
// Check if config client is some
|
// Check if config client is some
|
||||||
match &self.config_cli {
|
match config_cli.as_ref() {
|
||||||
Some(cli) => {
|
Some(cli) => {
|
||||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||||
let index: usize = match cli.get_show_hidden_files() {
|
let index: usize = match cli.get_show_hidden_files() {
|
||||||
@@ -365,9 +367,9 @@ impl SetupActivity {
|
|||||||
/// ### draw_check_for_updates_tab
|
/// ### draw_check_for_updates_tab
|
||||||
///
|
///
|
||||||
/// Draw check for updates tab
|
/// Draw check for updates tab
|
||||||
fn draw_check_for_updates_tab(&self) -> Option<Tabs> {
|
fn draw_check_for_updates_tab(&self, config_cli: Option<&ConfigClient>) -> Option<Tabs> {
|
||||||
// Check if config client is some
|
// Check if config client is some
|
||||||
match &self.config_cli {
|
match config_cli.as_ref() {
|
||||||
Some(cli) => {
|
Some(cli) => {
|
||||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||||
let index: usize = match cli.get_check_for_updates() {
|
let index: usize = match cli.get_check_for_updates() {
|
||||||
@@ -406,9 +408,9 @@ impl SetupActivity {
|
|||||||
/// ### draw_default_group_dirs_tab
|
/// ### draw_default_group_dirs_tab
|
||||||
///
|
///
|
||||||
/// Draw group dirs input tab
|
/// Draw group dirs input tab
|
||||||
fn draw_default_group_dirs_tab(&self) -> Option<Tabs> {
|
fn draw_default_group_dirs_tab(&self, config_cli: Option<&ConfigClient>) -> Option<Tabs> {
|
||||||
// Check if config client is some
|
// Check if config client is some
|
||||||
match &self.config_cli {
|
match config_cli.as_ref() {
|
||||||
Some(cli) => {
|
Some(cli) => {
|
||||||
let choices: Vec<Spans> = vec![
|
let choices: Vec<Spans> = vec![
|
||||||
Spans::from("Display First"),
|
Spans::from("Display First"),
|
||||||
@@ -454,8 +456,8 @@ impl SetupActivity {
|
|||||||
/// ### draw_file_fmt_input
|
/// ### draw_file_fmt_input
|
||||||
///
|
///
|
||||||
/// Draw input text field for file fmt
|
/// Draw input text field for file fmt
|
||||||
fn draw_file_fmt_input(&self) -> Option<Paragraph> {
|
fn draw_file_fmt_input(&self, config_cli: Option<&ConfigClient>) -> Option<Paragraph> {
|
||||||
match &self.config_cli {
|
match config_cli.as_ref() {
|
||||||
Some(cli) => Some(
|
Some(cli) => Some(
|
||||||
Paragraph::new(cli.get_file_fmt().unwrap_or_default())
|
Paragraph::new(cli.get_file_fmt().unwrap_or_default())
|
||||||
.style(Style::default().fg(match &self.tab {
|
.style(Style::default().fg(match &self.tab {
|
||||||
@@ -479,9 +481,9 @@ impl SetupActivity {
|
|||||||
/// ### draw_ssh_keys_list
|
/// ### draw_ssh_keys_list
|
||||||
///
|
///
|
||||||
/// Draw ssh keys list
|
/// Draw ssh keys list
|
||||||
fn draw_ssh_keys_list(&self) -> Option<List> {
|
fn draw_ssh_keys_list(&self, config_cli: Option<&ConfigClient>) -> Option<List> {
|
||||||
// Check if config client is some
|
// Check if config client is some
|
||||||
match &self.config_cli {
|
match config_cli.as_ref() {
|
||||||
Some(cli) => {
|
Some(cli) => {
|
||||||
// Iterate over ssh keys
|
// Iterate over ssh keys
|
||||||
let mut ssh_keys: Vec<ListItem> = Vec::with_capacity(cli.iter_ssh_keys().count());
|
let mut ssh_keys: Vec<ListItem> = Vec::with_capacity(cli.iter_ssh_keys().count());
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ extern crate tui;
|
|||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
use super::{Activity, Context};
|
use super::{Activity, Context};
|
||||||
use crate::system::config_client::ConfigClient;
|
|
||||||
// Ext
|
// Ext
|
||||||
use crossterm::event::Event as InputEvent;
|
use crossterm::event::Event as InputEvent;
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
@@ -121,7 +120,6 @@ enum Popup {
|
|||||||
pub struct SetupActivity {
|
pub struct SetupActivity {
|
||||||
pub quit: bool, // Becomes true when user requests the activity to terminate
|
pub quit: bool, // Becomes true when user requests the activity to terminate
|
||||||
context: Option<Context>, // Context holder
|
context: Option<Context>, // Context holder
|
||||||
config_cli: Option<ConfigClient>, // Config client
|
|
||||||
tab: SetupTab, // Current setup tab
|
tab: SetupTab, // Current setup tab
|
||||||
popup: Option<Popup>, // Active popup
|
popup: Option<Popup>, // Active popup
|
||||||
user_input: Vec<String>, // User input holder
|
user_input: Vec<String>, // User input holder
|
||||||
@@ -142,7 +140,6 @@ impl Default for SetupActivity {
|
|||||||
SetupActivity {
|
SetupActivity {
|
||||||
quit: false,
|
quit: false,
|
||||||
context: None,
|
context: None,
|
||||||
config_cli: None,
|
|
||||||
tab: SetupTab::UserInterface(UserInterfaceInputField::TextEditor),
|
tab: SetupTab::UserInterface(UserInterfaceInputField::TextEditor),
|
||||||
popup: None,
|
popup: None,
|
||||||
user_input: user_input_buffer, // Max 16
|
user_input: user_input_buffer, // Max 16
|
||||||
@@ -168,9 +165,9 @@ impl Activity for SetupActivity {
|
|||||||
self.context.as_mut().unwrap().clear_screen();
|
self.context.as_mut().unwrap().clear_screen();
|
||||||
// Put raw mode on enabled
|
// Put raw mode on enabled
|
||||||
let _ = enable_raw_mode();
|
let _ = enable_raw_mode();
|
||||||
// Initialize config client
|
// Verify error state from context
|
||||||
if self.config_cli.is_none() {
|
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||||
self.init_config_client();
|
self.popup = Some(Popup::Fatal(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ extern crate tui;
|
|||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
use super::input::InputHandler;
|
use super::input::InputHandler;
|
||||||
|
use super::store::Store;
|
||||||
use crate::host::Localhost;
|
use crate::host::Localhost;
|
||||||
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use crossterm::event::DisableMouseCapture;
|
use crossterm::event::DisableMouseCapture;
|
||||||
@@ -44,25 +46,54 @@ use tui::Terminal;
|
|||||||
/// Context holds data structures used by the ui
|
/// Context holds data structures used by the ui
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
pub local: Localhost,
|
pub local: Localhost,
|
||||||
|
pub(crate) config_client: Option<ConfigClient>,
|
||||||
|
pub(crate) store: Store,
|
||||||
pub(crate) input_hnd: InputHandler,
|
pub(crate) input_hnd: InputHandler,
|
||||||
pub(crate) terminal: Terminal<CrosstermBackend<Stdout>>,
|
pub(crate) terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// ### new
|
/// ### new
|
||||||
///
|
///
|
||||||
/// Instantiates a new Context
|
/// Instantiates a new Context
|
||||||
pub fn new(local: Localhost) -> Context {
|
pub fn new(
|
||||||
|
local: Localhost,
|
||||||
|
config_client: Option<ConfigClient>,
|
||||||
|
error: Option<String>,
|
||||||
|
) -> Context {
|
||||||
// Create terminal
|
// Create terminal
|
||||||
let mut stdout = stdout();
|
let mut stdout = stdout();
|
||||||
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
|
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
|
||||||
Context {
|
Context {
|
||||||
local,
|
local,
|
||||||
|
config_client,
|
||||||
|
store: Store::init(),
|
||||||
input_hnd: InputHandler::new(),
|
input_hnd: InputHandler::new(),
|
||||||
terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(),
|
terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(),
|
||||||
|
error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* NOTE: in case is necessary
|
||||||
|
/// ### set_error
|
||||||
|
///
|
||||||
|
/// Set context error
|
||||||
|
pub fn set_error(&mut self, err: String) {
|
||||||
|
self.error = Some(err);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// ### get_error
|
||||||
|
///
|
||||||
|
/// Get error message and remove it from the context
|
||||||
|
pub fn get_error(&mut self) -> Option<String> {
|
||||||
|
self.error.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### enter_alternate_screen
|
||||||
|
///
|
||||||
|
/// Enter alternate screen (gui window)
|
||||||
pub fn enter_alternate_screen(&mut self) {
|
pub fn enter_alternate_screen(&mut self) {
|
||||||
let _ = execute!(
|
let _ = execute!(
|
||||||
self.terminal.backend_mut(),
|
self.terminal.backend_mut(),
|
||||||
@@ -71,6 +102,9 @@ impl Context {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### leave_alternate_screen
|
||||||
|
///
|
||||||
|
/// Go back to normal screen (gui window)
|
||||||
pub fn leave_alternate_screen(&mut self) {
|
pub fn leave_alternate_screen(&mut self) {
|
||||||
let _ = execute!(
|
let _ = execute!(
|
||||||
self.terminal.backend_mut(),
|
self.terminal.backend_mut(),
|
||||||
@@ -79,6 +113,9 @@ impl Context {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### clear_screen
|
||||||
|
///
|
||||||
|
/// Clear terminal screen
|
||||||
pub fn clear_screen(&mut self) {
|
pub fn clear_screen(&mut self) {
|
||||||
let _ = self.terminal.clear();
|
let _ = self.terminal.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ pub mod activities;
|
|||||||
pub mod context;
|
pub mod context;
|
||||||
pub(crate) mod input;
|
pub(crate) mod input;
|
||||||
pub(crate) mod layout;
|
pub(crate) mod layout;
|
||||||
|
pub(crate) mod store;
|
||||||
|
|||||||
208
src/ui/store.rs
Normal file
208
src/ui/store.rs
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
//! ## Store
|
||||||
|
//!
|
||||||
|
//! `Store` is the module which provides the Context Storage.
|
||||||
|
//! The context storage is a storage indeed which is shared between the activities thanks to the context
|
||||||
|
//! The storage can be used to store any values which should be cached or shared between activities
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||||
|
*
|
||||||
|
* This file is part of "TermSCP"
|
||||||
|
*
|
||||||
|
* TermSCP is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* TermSCP is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
// -- store state
|
||||||
|
|
||||||
|
/// ## StoreState
|
||||||
|
///
|
||||||
|
/// Store state describes a value in the store
|
||||||
|
#[allow(dead_code)]
|
||||||
|
enum StoreState {
|
||||||
|
Str(String), // String
|
||||||
|
Signed(isize), // Signed number
|
||||||
|
Unsigned(usize), // Unsigned number
|
||||||
|
Float(f64), // Floating point number
|
||||||
|
Boolean(bool), // Boolean value
|
||||||
|
Flag, // Empty value; used to work as a Flag (set unset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- store
|
||||||
|
|
||||||
|
/// ## Store
|
||||||
|
///
|
||||||
|
/// Store represent the context store
|
||||||
|
/// The store is a key-value hash map. Each key must be unique
|
||||||
|
/// To each key a `StoreState` is assigned
|
||||||
|
pub(crate) struct Store {
|
||||||
|
store: HashMap<String, StoreState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl Store {
|
||||||
|
/// ### init
|
||||||
|
///
|
||||||
|
/// Initialize a new Store
|
||||||
|
pub fn init() -> Self {
|
||||||
|
Store {
|
||||||
|
store: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- getters
|
||||||
|
/// ### get_string
|
||||||
|
///
|
||||||
|
/// Get string from store
|
||||||
|
pub fn get_string(&self, key: &str) -> Option<&str> {
|
||||||
|
match self.store.get(key) {
|
||||||
|
Some(StoreState::Str(s)) => Some(s.as_str()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_signed
|
||||||
|
///
|
||||||
|
/// Get signed from store
|
||||||
|
pub fn get_signed(&self, key: &str) -> Option<isize> {
|
||||||
|
match self.store.get(key) {
|
||||||
|
Some(StoreState::Signed(i)) => Some(*i),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_unsigned
|
||||||
|
///
|
||||||
|
/// Get unsigned from store
|
||||||
|
pub fn get_unsigned(&self, key: &str) -> Option<usize> {
|
||||||
|
match self.store.get(key) {
|
||||||
|
Some(StoreState::Unsigned(u)) => Some(*u),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_float
|
||||||
|
///
|
||||||
|
/// get float from store
|
||||||
|
pub fn get_float(&self, key: &str) -> Option<f64> {
|
||||||
|
match self.store.get(key) {
|
||||||
|
Some(StoreState::Float(f)) => Some(*f),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_boolean
|
||||||
|
///
|
||||||
|
/// get boolean from store
|
||||||
|
pub fn get_boolean(&self, key: &str) -> Option<bool> {
|
||||||
|
match self.store.get(key) {
|
||||||
|
Some(StoreState::Boolean(b)) => Some(*b),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### isset
|
||||||
|
///
|
||||||
|
/// Check if a state is set in the store
|
||||||
|
pub fn isset(&self, key: &str) -> bool {
|
||||||
|
self.store.get(key).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- setters
|
||||||
|
|
||||||
|
/// ### set_string
|
||||||
|
///
|
||||||
|
/// Set string into the store
|
||||||
|
pub fn set_string(&mut self, key: &str, val: String) {
|
||||||
|
self.store.insert(key.to_string(), StoreState::Str(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### set_signed
|
||||||
|
///
|
||||||
|
/// Set signed number
|
||||||
|
pub fn set_signed(&mut self, key: &str, val: isize) {
|
||||||
|
self.store.insert(key.to_string(), StoreState::Signed(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### set_signed
|
||||||
|
///
|
||||||
|
/// Set unsigned number
|
||||||
|
pub fn set_unsigned(&mut self, key: &str, val: usize) {
|
||||||
|
self.store
|
||||||
|
.insert(key.to_string(), StoreState::Unsigned(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### set_float
|
||||||
|
///
|
||||||
|
/// Set floating point number
|
||||||
|
pub fn set_float(&mut self, key: &str, val: f64) {
|
||||||
|
self.store.insert(key.to_string(), StoreState::Float(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### set_boolean
|
||||||
|
///
|
||||||
|
/// Set boolean
|
||||||
|
pub fn set_boolean(&mut self, key: &str, val: bool) {
|
||||||
|
self.store.insert(key.to_string(), StoreState::Boolean(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### set
|
||||||
|
///
|
||||||
|
/// Set a key as a flag; has no value
|
||||||
|
pub fn set(&mut self, key: &str) {
|
||||||
|
self.store.insert(key.to_string(), StoreState::Flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ui_store() {
|
||||||
|
// Create store
|
||||||
|
let mut store: Store = Store::init();
|
||||||
|
// Test string
|
||||||
|
store.set_string("test", String::from("hello"));
|
||||||
|
assert_eq!(*store.get_string("test").as_ref().unwrap(), "hello");
|
||||||
|
// Test isize
|
||||||
|
store.set_signed("number", 3005);
|
||||||
|
assert_eq!(store.get_signed("number").unwrap(), 3005);
|
||||||
|
store.set_signed("number", -123);
|
||||||
|
assert_eq!(store.get_signed("number").unwrap(), -123);
|
||||||
|
// Test usize
|
||||||
|
store.set_unsigned("unumber", 1024);
|
||||||
|
assert_eq!(store.get_unsigned("unumber").unwrap(), 1024);
|
||||||
|
// Test float
|
||||||
|
store.set_float("float", 3.33);
|
||||||
|
assert_eq!(store.get_float("float").unwrap(), 3.33);
|
||||||
|
// Test boolean
|
||||||
|
store.set_boolean("bool", true);
|
||||||
|
assert_eq!(store.get_boolean("bool").unwrap(), true);
|
||||||
|
// Test flag
|
||||||
|
store.set("myflag");
|
||||||
|
assert_eq!(store.isset("myflag"), true);
|
||||||
|
// Test unexisting
|
||||||
|
assert!(store.get_boolean("unexisting-key").is_none());
|
||||||
|
assert!(store.get_float("unexisting-key").is_none());
|
||||||
|
assert!(store.get_signed("unexisting-key").is_none());
|
||||||
|
assert!(store.get_signed("unexisting-key").is_none());
|
||||||
|
assert!(store.get_string("unexisting-key").is_none());
|
||||||
|
assert!(store.get_unsigned("unexisting-key").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user