diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f9e3b..69cac89 100644 --- a/CHANGELOG.md +++ b/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). - Enhancements: - Input fields will now support **"input keys"** (such as moving cursor, DEL, END, HOME, ...) -- 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. + - Improved performance regarding configuration I/O (config client is now shared in the activity context) + - Fetch latest version from Github once; cache previous value in the Context Storage. - Bugfix: - 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`) - 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 diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 9e1fe57..6d0ecfb 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -28,6 +28,8 @@ use std::path::PathBuf; // Deps use crate::filetransfer::FileTransferProtocol; use crate::host::{HostError, Localhost}; +use crate::system::config_client::ConfigClient; +use crate::system::environment; use crate::ui::activities::{ auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity, filetransfer_activity::FileTransferParams, setup_activity::SetupActivity, Activity, @@ -66,7 +68,13 @@ impl ActivityManager { Ok(h) => h, Err(e) => return Err(e), }; - let ctx: Context = Context::new(host); + // Initialize configuration client + let (config_client, error): (Option, Option) = + 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 { context: Some(ctx), ftparams: None, @@ -117,7 +125,7 @@ impl ActivityManager { drop(self.context.take()); } - // Loops + // -- Activity Loops /// ### run_authentication /// @@ -251,4 +259,35 @@ impl ActivityManager { // This activity always returns to AuthActivity Some(NextActivity::Authentication) } + + // -- misc + + /// ### init_config_client + /// + /// Initialize configuration client + fn init_config_client() -> Result { + // 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 + )), + } + } } diff --git a/src/ui/activities/auth_activity/mod.rs b/src/ui/activities/auth_activity/mod.rs index 8645ed5..e2db746 100644 --- a/src/ui/activities/auth_activity/mod.rs +++ b/src/ui/activities/auth_activity/mod.rs @@ -37,14 +37,11 @@ extern crate unicode_width; use super::{Activity, Context}; use crate::filetransfer::FileTransferProtocol; use crate::system::bookmarks_client::BookmarksClient; -use crate::system::config_client::ConfigClient; -use crate::system::environment; use crate::ui::layout::view::View; use crate::utils::git; // Includes use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; -use std::path::PathBuf; // -- components 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_RECENTS_LIST: &str = "RECENTS_LIST"; +// Store keys +const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION"; + /// ### AuthActivity /// /// AuthActivity is the data holder for the authentication activity @@ -80,12 +80,9 @@ pub struct AuthActivity { context: Option, view: View, bookmarks_client: Option, - config_client: Option, redraw: bool, // Should ui actually be redrawned? bookmarks_list: Vec, // List of bookmarks recents_list: Vec, // list of recents - // misc - new_version: Option, // Contains new version of termscp } impl Default for AuthActivity { @@ -111,46 +108,9 @@ impl AuthActivity { context: None, view: View::init(), bookmarks_client: None, - config_client: None, redraw: true, // True at startup bookmarks_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 fn check_for_updates(&mut self) { - if let Some(client) = self.config_client.as_ref() { - if client.get_check_for_updates() { - // Send request - match git::check_for_updates(env!("CARGO_PKG_VERSION")) { - Ok(version) => self.new_version = version, - Err(err) => { - // Report error - self.mount_error( - format!("Could not check for new updates: {}", err).as_str(), - ); + // Check version only if unset in the store + let ctx: &Context = self.context.as_ref().unwrap(); + if !ctx.store.isset(STORE_KEY_LATEST_VERSION) { + let mut new_version: Option = match ctx.config_client.as_ref() { + Some(client) => { + if client.get_check_for_updates() { + // Send request + match git::check_for_updates(env!("CARGO_PKG_VERSION")) { + 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() { self.init_bookmarks_client(); } - // init config client - if self.config_client.is_none() { - self.init_config_client(); + // Verify error state from context + if let Some(err) = self.context.as_mut().unwrap().get_error() { + self.mount_error(err.as_str()); } // If check for updates is enabled, check for updates self.check_for_updates(); diff --git a/src/ui/activities/auth_activity/view.rs b/src/ui/activities/auth_activity/view.rs index e9f51b2..51d28ed 100644 --- a/src/ui/activities/auth_activity/view.rs +++ b/src/ui/activities/auth_activity/view.rs @@ -145,7 +145,13 @@ impl AuthActivity { )), ); // 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( super::COMPONENT_TEXT_NEW_VERSION, Box::new(Text::new( diff --git a/src/ui/activities/filetransfer_activity/misc.rs b/src/ui/activities/filetransfer_activity/misc.rs index 736ac22..3c68e80 100644 --- a/src/ui/activities/filetransfer_activity/misc.rs +++ b/src/ui/activities/filetransfer_activity/misc.rs @@ -147,7 +147,7 @@ impl FileTransferActivity { /// /// Set text editor to use 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 env::set_var("EDITOR", config_cli.get_text_editor()); } diff --git a/src/ui/activities/filetransfer_activity/mod.rs b/src/ui/activities/filetransfer_activity/mod.rs index 62ebe4b..66f9a35 100644 --- a/src/ui/activities/filetransfer_activity/mod.rs +++ b/src/ui/activities/filetransfer_activity/mod.rs @@ -229,7 +229,6 @@ pub struct FileTransferActivity { context: Option, // Context holder params: FileTransferParams, // FT connection params client: Box, // File transfer client - config_cli: Option, // Config Client local: FileExplorer, // Local File explorer state remote: FileExplorer, // Remote File explorer state tab: FileExplorerTab, // Current selected tab @@ -267,7 +266,6 @@ impl FileTransferActivity { params, local: Self::build_explorer(config_client.as_ref()), remote: Self::build_explorer(config_client.as_ref()), - config_cli: config_client, tab: FileExplorerTab::Local, log_index: 0, 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(); // Configure 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 diff --git a/src/ui/activities/setup_activity/callbacks.rs b/src/ui/activities/setup_activity/callbacks.rs index 9074124..a766aba 100644 --- a/src/ui/activities/setup_activity/callbacks.rs +++ b/src/ui/activities/setup_activity/callbacks.rs @@ -69,7 +69,7 @@ impl SetupActivity { /// Callback for performing the delete of a ssh key pub(super) fn callback_delete_ssh_key(&mut self) { // 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 = match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) { Some(k) => Some(k.clone()), None => None, @@ -100,7 +100,7 @@ impl SetupActivity { /// /// Create a new ssh key with provided parameters 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 env::set_var("EDITOR", cli.get_text_editor()); let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host); diff --git a/src/ui/activities/setup_activity/config.rs b/src/ui/activities/setup_activity/config.rs index fad56d8..564cf6f 100644 --- a/src/ui/activities/setup_activity/config.rs +++ b/src/ui/activities/setup_activity/config.rs @@ -25,55 +25,17 @@ */ // Locals -use super::{ConfigClient, Popup, SetupActivity}; -use crate::system::environment; +use super::SetupActivity; // Ext use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::env; -use std::path::PathBuf; 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 configuration 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() { Ok(_) => Ok(()), 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 /// since last write action 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() { Ok(_) => Ok(()), Err(err) => Err(format!("Could not restore configuration: {}", err)), @@ -100,7 +62,7 @@ impl SetupActivity { /// /// Delete ssh key from config cli 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) { Ok(_) => Ok(()), Err(err) => Err(format!( @@ -116,58 +78,51 @@ impl SetupActivity { /// /// Edit selected ssh key pub(super) fn edit_ssh_key(&mut self) -> Result<(), String> { - match self.config_cli.as_ref() { - Some(cli) => { - // Set text editor - env::set_var("EDITOR", cli.get_text_editor()); + match self.context.as_mut() { + None => Ok(()), + Some(ctx) => { + // 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 let _ = disable_raw_mode(); // Leave alternate mode - if let Some(ctx) = self.context.as_mut() { - ctx.leave_alternate_screen(); - } - // Check if key exists - match cli.iter_ssh_keys().nth(self.ssh_key_idx) { - Some(key) => { - // Get key path - match cli.get_ssh_key(key) { - Ok(ssh_key) => match ssh_key { - None => Ok(()), - Some((_, _, key_path)) => match edit::edit_file(key_path.as_path()) - { - Ok(_) => { - // Restore terminal - if let Some(ctx) = self.context.as_mut() { - // Clear screen - ctx.clear_screen(); - // Enter alternate mode - ctx.enter_alternate_screen(); + ctx.leave_alternate_screen(); + // Get result + let result: Result<(), String> = match ctx.config_client.as_ref() { + Some(config_cli) => match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) { + Some(key) => { + // Get key path + match config_cli.get_ssh_key(key) { + Ok(ssh_key) => match ssh_key { + None => Ok(()), + Some((_, _, key_path)) => { + match edit::edit_file(key_path.as_path()) { + Ok(_) => Ok(()), + Err(err) => { + Err(format!("Could not edit ssh key: {}", err)) + } } - // 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(()), - } + }; + // 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, rsa_key: &str, ) -> Result<(), String> { - match self.config_cli.as_mut() { + match self.context.as_mut().unwrap().config_client.as_mut() { Some(cli) => { // Add key to client match cli.add_ssh_key(host, username, rsa_key) { diff --git a/src/ui/activities/setup_activity/input.rs b/src/ui/activities/setup_activity/input.rs index 7fee6cc..c52fc4e 100644 --- a/src/ui/activities/setup_activity/input.rs +++ b/src/ui/activities/setup_activity/input.rs @@ -76,7 +76,8 @@ impl SetupActivity { self.tab = SetupTab::UserInterface(UserInterfaceInputField::DefaultProtocol) } // Switch tab to user interface config 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 let ssh_key_size: usize = config_cli.iter_ssh_keys().count(); if self.ssh_key_idx > 0 { @@ -89,7 +90,8 @@ impl SetupActivity { } } 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 let ssh_key_size: usize = config_cli.iter_ssh_keys().count(); 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::Backspace => { // 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 { UserInterfaceInputField::TextEditor => { // Pop from text editor @@ -207,7 +210,8 @@ impl SetupActivity { } KeyCode::Left => { // 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 { UserInterfaceInputField::DefaultProtocol => { // Move left @@ -248,7 +252,8 @@ impl SetupActivity { } KeyCode::Right => { // 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 { UserInterfaceInputField::DefaultProtocol => { // Move left @@ -354,7 +359,9 @@ impl SetupActivity { } } else { // 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 match field { UserInterfaceInputField::TextEditor => { diff --git a/src/ui/activities/setup_activity/layout.rs b/src/ui/activities/setup_activity/layout.rs index 003e917..ab5bc1c 100644 --- a/src/ui/activities/setup_activity/layout.rs +++ b/src/ui/activities/setup_activity/layout.rs @@ -30,6 +30,7 @@ use super::{ }; use crate::filetransfer::FileTransferProtocol; use crate::fs::explorer::GroupDirs; +use crate::system::config_client::ConfigClient; use crate::utils::fmt::align_text_center; // Ext use tui::{ @@ -46,6 +47,7 @@ impl SetupActivity { /// Draw UI pub(super) fn draw(&mut self) { let mut ctx: Context = self.context.take().unwrap(); + let config_client: Option<&ConfigClient> = ctx.config_client.as_ref(); let _ = ctx.terminal.draw(|f| { // Prepare main chunks let chunks = Layout::default() @@ -71,7 +73,7 @@ impl SetupActivity { .direction(Direction::Vertical) .constraints([Constraint::Percentage(100)].as_ref()) .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 let mut ssh_key_state: ListState = ListState::default(); ssh_key_state.select(Some(self.ssh_key_idx)); @@ -97,26 +99,26 @@ impl SetupActivity { ) .split(chunks[1]); // 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]); } - 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]); } - 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]); } - 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]); } - 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]); } - 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]); } // Set cursor - if let Some(cli) = &self.config_cli { + if let Some(cli) = config_client { match form_field { UserInterfaceInputField::TextEditor => { let editor_text: String = @@ -129,8 +131,8 @@ impl SetupActivity { UserInterfaceInputField::FileFmt => { let file_fmt: String = cli.get_file_fmt().unwrap_or_default(); f.set_cursor( - ui_cfg_chunks[4].x + file_fmt.width() as u16 + 1, - ui_cfg_chunks[4].y + 1, + ui_cfg_chunks[5].x + file_fmt.width() as u16 + 1, + ui_cfg_chunks[5].y + 1, ); } _ => { /* Not a text field */ } @@ -247,8 +249,8 @@ impl SetupActivity { /// ### draw_text_editor_input /// /// Draw input text field for text editor parameter - fn draw_text_editor_input(&self) -> Option { - match &self.config_cli { + fn draw_text_editor_input(&self, config_cli: Option<&ConfigClient>) -> Option { + match config_cli.as_ref() { Some(cli) => Some( Paragraph::new(String::from( cli.get_text_editor().as_path().to_string_lossy(), @@ -274,9 +276,9 @@ impl SetupActivity { /// ### draw_default_protocol_tab /// /// Draw default protocol input tab - fn draw_default_protocol_tab(&self) -> Option { + fn draw_default_protocol_tab(&self, config_cli: Option<&ConfigClient>) -> Option { // Check if config client is some - match &self.config_cli { + match config_cli.as_ref() { Some(cli) => { let choices: Vec = vec![ Spans::from("SFTP"), @@ -324,9 +326,9 @@ impl SetupActivity { /// ### draw_hidden_files_tab /// /// Draw default hidden files tab - fn draw_hidden_files_tab(&self) -> Option { + fn draw_hidden_files_tab(&self, config_cli: Option<&ConfigClient>) -> Option { // Check if config client is some - match &self.config_cli { + match config_cli.as_ref() { Some(cli) => { let choices: Vec = vec![Spans::from("Yes"), Spans::from("No")]; 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 - fn draw_check_for_updates_tab(&self) -> Option { + fn draw_check_for_updates_tab(&self, config_cli: Option<&ConfigClient>) -> Option { // Check if config client is some - match &self.config_cli { + match config_cli.as_ref() { Some(cli) => { let choices: Vec = vec![Spans::from("Yes"), Spans::from("No")]; let index: usize = match cli.get_check_for_updates() { @@ -406,9 +408,9 @@ impl SetupActivity { /// ### draw_default_group_dirs_tab /// /// Draw group dirs input tab - fn draw_default_group_dirs_tab(&self) -> Option { + fn draw_default_group_dirs_tab(&self, config_cli: Option<&ConfigClient>) -> Option { // Check if config client is some - match &self.config_cli { + match config_cli.as_ref() { Some(cli) => { let choices: Vec = vec![ Spans::from("Display First"), @@ -454,8 +456,8 @@ impl SetupActivity { /// ### draw_file_fmt_input /// /// Draw input text field for file fmt - fn draw_file_fmt_input(&self) -> Option { - match &self.config_cli { + fn draw_file_fmt_input(&self, config_cli: Option<&ConfigClient>) -> Option { + match config_cli.as_ref() { Some(cli) => Some( Paragraph::new(cli.get_file_fmt().unwrap_or_default()) .style(Style::default().fg(match &self.tab { @@ -479,9 +481,9 @@ impl SetupActivity { /// ### draw_ssh_keys_list /// /// Draw ssh keys list - fn draw_ssh_keys_list(&self) -> Option { + fn draw_ssh_keys_list(&self, config_cli: Option<&ConfigClient>) -> Option { // Check if config client is some - match &self.config_cli { + match config_cli.as_ref() { Some(cli) => { // Iterate over ssh keys let mut ssh_keys: Vec = Vec::with_capacity(cli.iter_ssh_keys().count()); diff --git a/src/ui/activities/setup_activity/mod.rs b/src/ui/activities/setup_activity/mod.rs index 1ac0943..726dedd 100644 --- a/src/ui/activities/setup_activity/mod.rs +++ b/src/ui/activities/setup_activity/mod.rs @@ -37,7 +37,6 @@ extern crate tui; // Locals use super::{Activity, Context}; -use crate::system::config_client::ConfigClient; // Ext use crossterm::event::Event as InputEvent; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; @@ -121,7 +120,6 @@ enum Popup { pub struct SetupActivity { pub quit: bool, // Becomes true when user requests the activity to terminate context: Option, // Context holder - config_cli: Option, // Config client tab: SetupTab, // Current setup tab popup: Option, // Active popup user_input: Vec, // User input holder @@ -142,7 +140,6 @@ impl Default for SetupActivity { SetupActivity { quit: false, context: None, - config_cli: None, tab: SetupTab::UserInterface(UserInterfaceInputField::TextEditor), popup: None, user_input: user_input_buffer, // Max 16 @@ -168,9 +165,9 @@ impl Activity for SetupActivity { self.context.as_mut().unwrap().clear_screen(); // Put raw mode on enabled let _ = enable_raw_mode(); - // Initialize config client - if self.config_cli.is_none() { - self.init_config_client(); + // Verify error state from context + if let Some(err) = self.context.as_mut().unwrap().get_error() { + self.popup = Some(Popup::Fatal(err)); } } diff --git a/src/ui/context.rs b/src/ui/context.rs index 01842bc..b0ee027 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -29,7 +29,9 @@ extern crate tui; // Locals use super::input::InputHandler; +use super::store::Store; use crate::host::Localhost; +use crate::system::config_client::ConfigClient; // Includes use crossterm::event::DisableMouseCapture; @@ -44,25 +46,54 @@ use tui::Terminal; /// Context holds data structures used by the ui pub struct Context { pub local: Localhost, + pub(crate) config_client: Option, + pub(crate) store: Store, pub(crate) input_hnd: InputHandler, pub(crate) terminal: Terminal>, + error: Option, } impl Context { /// ### new /// /// Instantiates a new Context - pub fn new(local: Localhost) -> Context { + pub fn new( + local: Localhost, + config_client: Option, + error: Option, + ) -> Context { // Create terminal let mut stdout = stdout(); assert!(execute!(stdout, EnterAlternateScreen).is_ok()); Context { local, + config_client, + store: Store::init(), input_hnd: InputHandler::new(), 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 { + self.error.take() + } + + /// ### enter_alternate_screen + /// + /// Enter alternate screen (gui window) pub fn enter_alternate_screen(&mut self) { let _ = execute!( 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) { let _ = execute!( self.terminal.backend_mut(), @@ -79,6 +113,9 @@ impl Context { ); } + /// ### clear_screen + /// + /// Clear terminal screen pub fn clear_screen(&mut self) { let _ = self.terminal.clear(); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 32c0f49..e103ba2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -28,3 +28,4 @@ pub mod activities; pub mod context; pub(crate) mod input; pub(crate) mod layout; +pub(crate) mod store; diff --git a/src/ui/store.rs b/src/ui/store.rs new file mode 100644 index 0000000..bad0bc7 --- /dev/null +++ b/src/ui/store.rs @@ -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 . +* +*/ + +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, +} + +#[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 { + 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 { + 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 { + 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 { + 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()); + } +}