From 56d705e253f17e44cbb3ab489afee640e184aad6 Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Mon, 8 Mar 2021 12:01:40 +0100 Subject: [PATCH 1/4] Config client shared in the context --- CHANGELOG.md | 5 +- src/activity_manager.rs | 43 +++++- src/ui/activities/auth_activity/mod.rs | 49 +------ .../activities/filetransfer_activity/misc.rs | 2 +- .../activities/filetransfer_activity/mod.rs | 6 +- src/ui/activities/setup_activity/callbacks.rs | 4 +- src/ui/activities/setup_activity/config.rs | 127 ++++++------------ src/ui/activities/setup_activity/input.rs | 19 ++- src/ui/activities/setup_activity/layout.rs | 50 +++---- src/ui/activities/setup_activity/mod.rs | 9 +- src/ui/context.rs | 36 ++++- 11 files changed, 174 insertions(+), 176 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 504f975..b7ad0aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,10 @@ Released on FIXME: date -- REPL +- Enhancements: + - Improved performance regarding configuration I/O (config client is now shared in the activity context) +- Bugfix: + - Fixed file format cursor position in the GUI ## 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 ffe55d3..b9d701b 100644 --- a/src/ui/activities/auth_activity/mod.rs +++ b/src/ui/activities/auth_activity/mod.rs @@ -38,14 +38,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::utils::git; // Includes use crossterm::event::Event as InputEvent; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; -use std::path::PathBuf; use tui::style::Color; // Types @@ -107,7 +104,6 @@ pub struct AuthActivity { pub setup: bool, // Becomes true if user has requested setup context: Option, bookmarks_client: Option, - config_client: Option, selected_field: InputField, // Selected field in AuthCredentials Form popup: Option, input_form: InputForm, @@ -145,7 +141,6 @@ impl AuthActivity { setup: false, context: None, bookmarks_client: None, - config_client: None, selected_field: InputField::Address, popup: None, input_form: InputForm::AuthCredentials, @@ -161,47 +156,11 @@ impl AuthActivity { } } - /// ### 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.popup = Some(Popup::Alert( - Color::Red, - format!("Could not initialize user configuration: {}", err), - )) - } - } - } - } - Err(err) => { - self.popup = Some(Popup::Alert( - Color::Red, - format!("Could not initialize configuration directory: {}", err), - )) - } - } - } - /// ### on_create /// /// 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 let Some(client) = self.context.as_ref().unwrap().config_client.as_ref() { if client.get_check_for_updates() { // Send request match git::check_for_updates(env!("CARGO_PKG_VERSION")) { @@ -237,9 +196,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.popup = Some(Popup::Alert(Color::Red, err)); } // If check for updates is enabled, check for updates self.check_for_updates(); 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 6e7bc9b..f09f036 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}; @@ -106,7 +105,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 @@ -127,7 +125,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 @@ -153,9 +150,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..9ffda58 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -30,6 +30,7 @@ extern crate tui; // Locals use super::input::InputHandler; use crate::host::Localhost; +use crate::system::config_client::ConfigClient; // Includes use crossterm::event::DisableMouseCapture; @@ -44,25 +45,52 @@ use tui::Terminal; /// Context holds data structures used by the ui pub struct Context { pub local: Localhost, + pub(crate) config_client: Option, 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, 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 +99,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 +110,9 @@ impl Context { ); } + /// ### clear_screen + /// + /// Clear terminal screen pub fn clear_screen(&mut self) { let _ = self.terminal.clear(); } From a4544e35f6629bf4a5739020bca1f2108913e462 Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Mon, 8 Mar 2021 13:57:16 +0100 Subject: [PATCH 2/4] Store as part of the Context --- src/ui/context.rs | 3 + src/ui/mod.rs | 1 + src/ui/store.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 src/ui/store.rs diff --git a/src/ui/context.rs b/src/ui/context.rs index 9ffda58..b0ee027 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -29,6 +29,7 @@ extern crate tui; // Locals use super::input::InputHandler; +use super::store::Store; use crate::host::Localhost; use crate::system::config_client::ConfigClient; @@ -46,6 +47,7 @@ use tui::Terminal; 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, @@ -66,6 +68,7 @@ impl Context { Context { local, config_client, + store: Store::init(), input_hnd: InputHandler::new(), terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(), error, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0e8d9c8..456e9e0 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -27,3 +27,4 @@ pub mod activities; pub mod context; pub(crate) mod input; +pub(crate) mod store; diff --git a/src/ui/store.rs b/src/ui/store.rs new file mode 100644 index 0000000..6999064 --- /dev/null +++ b/src/ui/store.rs @@ -0,0 +1,221 @@ +//! ## 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 +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, +} + +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) { + None => None, + Some(val) => match val { + 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) { + None => None, + Some(val) => match val { + 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) { + None => None, + Some(val) => match val { + 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) { + None => None, + Some(val) => match val { + 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) { + None => None, + Some(val) => match val { + 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()); + } +} From f75dd5d4e3031dda5ad407d29341f773c34bbcc0 Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Mon, 8 Mar 2021 14:20:13 +0100 Subject: [PATCH 3/4] Cache version fetched from Github --- CHANGELOG.md | 1 + src/ui/activities/auth_activity/layout.rs | 8 ++-- src/ui/activities/auth_activity/mod.rs | 45 ++++++++++++++++------- src/ui/store.rs | 37 ++++++------------- 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ad0aa..97d70d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Released on FIXME: date - Enhancements: - 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: - Fixed file format cursor position in the GUI diff --git a/src/ui/activities/auth_activity/layout.rs b/src/ui/activities/auth_activity/layout.rs index 351ed4f..d04037c 100644 --- a/src/ui/activities/auth_activity/layout.rs +++ b/src/ui/activities/auth_activity/layout.rs @@ -27,6 +27,7 @@ use super::{ AuthActivity, Context, DialogYesNoOption, FileTransferProtocol, InputField, InputForm, Popup, }; +use crate::ui::store::Store; use crate::utils::fmt::align_text_center; // Ext use std::string::ToString; @@ -44,6 +45,7 @@ impl AuthActivity { /// Draw UI pub(super) fn draw(&mut self) { let mut ctx: Context = self.context.take().unwrap(); + let store: &Store = &ctx.store; let _ = ctx.terminal.draw(|f| { // Prepare chunks let chunks = Layout::default() @@ -81,7 +83,7 @@ impl AuthActivity { .split(chunks[1]); // Draw header f.render_widget(self.draw_header(), auth_chunks[0]); - f.render_widget(self.draw_new_version(), auth_chunks[1]); + f.render_widget(self.draw_new_version(store), auth_chunks[1]); // Draw input fields f.render_widget(self.draw_remote_address(), auth_chunks[2]); f.render_widget(self.draw_remote_port(), auth_chunks[3]); @@ -289,8 +291,8 @@ impl AuthActivity { /// ### draw_new_version /// /// Draw new version disclaimer - fn draw_new_version(&self) -> Paragraph { - let content: String = match self.new_version.as_ref() { + fn draw_new_version(&self, store: &Store) -> Paragraph { + let content: String = match store.get_string(super::STORE_KEY_LATEST_VERSION) { Some(ver) => format!("TermSCP {} is now available! Download it from ", ver), None => String::new(), }; diff --git a/src/ui/activities/auth_activity/mod.rs b/src/ui/activities/auth_activity/mod.rs index b9d701b..4eee6c7 100644 --- a/src/ui/activities/auth_activity/mod.rs +++ b/src/ui/activities/auth_activity/mod.rs @@ -48,6 +48,9 @@ use tui::style::Color; // Types type DialogCallback = fn(&mut AuthActivity); +// Store keys +const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION"; + /// ### InputField /// /// InputField describes the current input field to edit @@ -115,8 +118,6 @@ pub struct AuthActivity { bookmarks_list: Vec, // List of bookmarks recents_idx: usize, // Index of selected recent recents_list: Vec, // list of recents - // misc - new_version: Option, // Contains new version of termscp } impl Default for AuthActivity { @@ -152,7 +153,6 @@ impl AuthActivity { bookmarks_list: Vec::new(), recents_idx: 0, recents_list: Vec::new(), - new_version: None, } } @@ -160,19 +160,36 @@ impl AuthActivity { /// /// If enabled in configuration, check for updates from Github fn check_for_updates(&mut self) { - if let Some(client) = self.context.as_ref().unwrap().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.popup = Some(Popup::Alert( - Color::Red, - format!("Could not check for new updates: {}", err), - )) + // 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.popup = Some(Popup::Alert( + Color::Red, + format!("Could not check for new updates: {}", err), + )); + // 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 } } } diff --git a/src/ui/store.rs b/src/ui/store.rs index 6999064..bad0bc7 100644 --- a/src/ui/store.rs +++ b/src/ui/store.rs @@ -32,6 +32,7 @@ use std::collections::HashMap; /// ## StoreState /// /// Store state describes a value in the store +#[allow(dead_code)] enum StoreState { Str(String), // String Signed(isize), // Signed number @@ -52,6 +53,7 @@ pub(crate) struct Store { store: HashMap, } +#[allow(dead_code)] impl Store { /// ### init /// @@ -68,11 +70,8 @@ impl Store { /// Get string from store pub fn get_string(&self, key: &str) -> Option<&str> { match self.store.get(key) { - None => None, - Some(val) => match val { - StoreState::Str(s) => Some(s.as_str()), - _ => None, - }, + Some(StoreState::Str(s)) => Some(s.as_str()), + _ => None, } } @@ -81,11 +80,8 @@ impl Store { /// Get signed from store pub fn get_signed(&self, key: &str) -> Option { match self.store.get(key) { - None => None, - Some(val) => match val { - StoreState::Signed(i) => Some(*i), - _ => None, - }, + Some(StoreState::Signed(i)) => Some(*i), + _ => None, } } @@ -94,11 +90,8 @@ impl Store { /// Get unsigned from store pub fn get_unsigned(&self, key: &str) -> Option { match self.store.get(key) { - None => None, - Some(val) => match val { - StoreState::Unsigned(u) => Some(*u), - _ => None, - }, + Some(StoreState::Unsigned(u)) => Some(*u), + _ => None, } } @@ -107,11 +100,8 @@ impl Store { /// get float from store pub fn get_float(&self, key: &str) -> Option { match self.store.get(key) { - None => None, - Some(val) => match val { - StoreState::Float(f) => Some(*f), - _ => None, - }, + Some(StoreState::Float(f)) => Some(*f), + _ => None, } } @@ -120,11 +110,8 @@ impl Store { /// get boolean from store pub fn get_boolean(&self, key: &str) -> Option { match self.store.get(key) { - None => None, - Some(val) => match val { - StoreState::Boolean(b) => Some(*b), - _ => None, - }, + Some(StoreState::Boolean(b)) => Some(*b), + _ => None, } } From c4bc0af58a7006a83a3683815450563dd5b4cd90 Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Mon, 8 Mar 2021 14:23:42 +0100 Subject: [PATCH 4/4] Allow `store` in codecov --- codecov.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index 1852ca8..fa13f4c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,6 +2,8 @@ ignore: - src/main.rs - src/lib.rs - src/activity_manager.rs - - src/ui/ + - src/ui/activities/ + - src/ui/context.rs + - src/ui/input.rs fixes: - "/::"