Merge branch 'rethink-context' into rethink-activities

This commit is contained in:
veeso
2021-03-15 21:09:02 +01:00
14 changed files with 425 additions and 191 deletions

View File

@@ -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

View File

@@ -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
)),
}
}
} }

View File

@@ -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
let ctx: &Context = self.context.as_ref().unwrap();
if !ctx.store.isset(STORE_KEY_LATEST_VERSION) {
let mut new_version: Option<String> = match ctx.config_client.as_ref() {
Some(client) => {
if client.get_check_for_updates() { if client.get_check_for_updates() {
// Send request // Send request
match git::check_for_updates(env!("CARGO_PKG_VERSION")) { match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
Ok(version) => self.new_version = version, Ok(version) => version,
Err(err) => { Err(err) => {
// Report error // Report error
self.mount_error( self.mount_error(
format!("Could not check for new updates: {}", err).as_str(), 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();

View File

@@ -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(

View File

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

View File

@@ -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

View File

@@ -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);

View File

@@ -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
// Check if key exists let result: Result<(), String> = match ctx.config_client.as_ref() {
match cli.iter_ssh_keys().nth(self.ssh_key_idx) { Some(config_cli) => match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) {
Some(key) => { Some(key) => {
// Get key path // Get key path
match cli.get_ssh_key(key) { match config_cli.get_ssh_key(key) {
Ok(ssh_key) => match ssh_key { Ok(ssh_key) => match ssh_key {
None => Ok(()), None => Ok(()),
Some((_, _, key_path)) => match edit::edit_file(key_path.as_path()) Some((_, _, key_path)) => {
{ match edit::edit_file(key_path.as_path()) {
Ok(_) => { Ok(_) => Ok(()),
// 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();
Ok(())
}
Err(err) => { 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(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(()), 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
}
} }
} }
@@ -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) {

View File

@@ -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 => {

View File

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

View File

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

View File

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

View File

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