diff --git a/src/activity_manager.rs b/src/activity_manager.rs index de81411..f8c49d7 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -29,9 +29,8 @@ use std::path::PathBuf; use crate::filetransfer::FileTransferProtocol; use crate::host::Localhost; use crate::ui::activities::{ - auth_activity::AuthActivity, - filetransfer_activity::FileTransferActivity, filetransfer_activity::FileTransferParams, - Activity, + auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity, + filetransfer_activity::FileTransferParams, setup_activity::SetupActivity, Activity, }; use crate::ui::context::Context; @@ -45,6 +44,7 @@ use std::time::Duration; pub enum NextActivity { Authentication, FileTransfer, + SetupActivity, } /// ### ActivityManager @@ -60,10 +60,7 @@ impl ActivityManager { /// ### new /// /// Initializes a new Activity Manager - pub fn new( - local_dir: &PathBuf, - interval: Duration, - ) -> Result { + pub fn new(local_dir: &PathBuf, interval: Duration) -> Result { // Prepare Context let host: Localhost = match Localhost::new(local_dir.clone()) { Ok(h) => h, @@ -109,6 +106,7 @@ impl ActivityManager { Some(activity) => match activity { NextActivity::Authentication => self.run_authentication(), NextActivity::FileTransfer => self.run_filetransfer(), + NextActivity::SetupActivity => self.run_setup(), }, None => break, // Exit } @@ -126,13 +124,13 @@ impl ActivityManager { /// Returns the next activity to run fn run_authentication(&mut self) -> Option { // Prepare activity - let mut activity: AuthActivity = AuthActivity::new(); + let mut activity: AuthActivity = AuthActivity::default(); // Prepare result let result: Option; // Get context let ctx: Context = match self.context.take() { Some(ctx) => ctx, - None => return None + None => return None, }; // Create activity activity.on_create(ctx); @@ -145,6 +143,11 @@ impl ActivityManager { result = None; break; } + if activity.setup { + // User requested activity + result = Some(NextActivity::SetupActivity); + break; + } if activity.submit { // User submitted, set next activity result = Some(NextActivity::FileTransfer); @@ -189,7 +192,7 @@ impl ActivityManager { // Get context let ctx: Context = match self.context.take() { Some(ctx) => ctx, - None => return None + None => return None, }; // Create activity activity.on_create(ctx); @@ -214,4 +217,35 @@ impl ActivityManager { self.context = activity.on_destroy(); result } + + /// ### run_setup + /// + /// `SetupActivity` run loop. + /// Returns when activity terminates. + /// Returns the next activity to run + fn run_setup(&mut self) -> Option { + // Prepare activity + let mut activity: SetupActivity = SetupActivity::default(); + // Get context + let ctx: Context = match self.context.take() { + Some(ctx) => ctx, + None => return None, + }; + // Create activity + activity.on_create(ctx); + loop { + // Draw activity + activity.on_draw(); + // Check if activity has terminated + if activity.quit { + break; + } + // Sleep for ticks + sleep(self.interval); + } + // Destroy activity + self.context = activity.on_destroy(); + // This activity always returns to AuthActivity + Some(NextActivity::Authentication) + } } diff --git a/src/ui/activities/mod.rs b/src/ui/activities/mod.rs index 2ce9ffb..dbc95bb 100644 --- a/src/ui/activities/mod.rs +++ b/src/ui/activities/mod.rs @@ -30,6 +30,7 @@ use super::context::Context; // Activities pub mod auth_activity; pub mod filetransfer_activity; +pub mod setup_activity; // Activity trait diff --git a/src/ui/activities/setup_activity/callbacks.rs b/src/ui/activities/setup_activity/callbacks.rs new file mode 100644 index 0000000..479a046 --- /dev/null +++ b/src/ui/activities/setup_activity/callbacks.rs @@ -0,0 +1,131 @@ +//! ## SetupActivity +//! +//! `setup_activity` is the module which implements the Setup activity, which is the activity to +//! work on termscp configuration + +/* +* +* Copyright (C) 2020 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 . +* +*/ + +// Locals +use super::{Color, Popup, SetupActivity}; +// Ext +use std::env; + +impl SetupActivity { + /// ### callback_nothing_to_do + /// + /// Self titled + pub(super) fn callback_nothing_to_do(&mut self) {} + + /// ### callback_save_config_and_quit + /// + /// Save configuration and quit + pub(super) fn callback_save_config_and_quit(&mut self) { + match self.save_config() { + Ok(_) => self.quit = true, // Quit after successful save + Err(err) => self.popup = Some(Popup::Alert(Color::Red, err)), // Show error and don't quit + } + } + + /// ### callback_save_config + /// + /// Save configuration callback + pub(super) fn callback_save_config(&mut self) { + if let Err(err) = self.save_config() { + self.popup = Some(Popup::Alert(Color::Red, err)); // Show save error + } + } + + /// ### callback_reset_config_changes + /// + /// Reset config changes callback + pub(super) fn callback_reset_config_changes(&mut self) { + if let Err(err) = self.reset_config_changes() { + self.popup = Some(Popup::Alert(Color::Red, err)); // Show reset error + } + } + + /// ### callback_delete_ssh_key + /// + /// 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() { + let key: Option = match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) { + Some(k) => Some(k.clone()), + None => None, + }; + if let Some(key) = key { + match config_cli.get_ssh_key(&key) { + Ok(opt) => { + if let Some((host, username, _)) = opt { + if let Err(err) = self.delete_ssh_key(host.as_str(), username.as_str()) + { + // Report error + self.popup = Some(Popup::Alert(Color::Red, err)); + } + } + } + Err(err) => { + self.popup = Some(Popup::Alert( + Color::Red, + format!("Could not get ssh key \"{}\": {}", key, err), + )) + } // Report error + } + } + } + } + + /// ### callback_new_ssh_key + /// + /// 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() { + // Prepare text editor + env::set_var("EDITOR", cli.get_text_editor()); + let placeholder: String = format!("# Type private SSH key for {}@{}", username, host); + // Write key to file + match edit::edit(placeholder.as_bytes()) { + Ok(rsa_key) => { + // Remove placeholder from `rsa_key` + let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), ""); + // Add key + if let Err(err) = + self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str()) + { + self.popup = Some(Popup::Alert( + Color::Red, + format!("Could not create new private key: {}", err), + )) + } + } + Err(err) => { + // Report error + self.popup = Some(Popup::Alert( + Color::Red, + format!("Could not write private key to file: {}", err), + )) + } + } + } + } +} diff --git a/src/ui/activities/setup_activity/config.rs b/src/ui/activities/setup_activity/config.rs new file mode 100644 index 0000000..84c53da --- /dev/null +++ b/src/ui/activities/setup_activity/config.rs @@ -0,0 +1,165 @@ +//! ## SetupActivity +//! +//! `setup_activity` is the module which implements the Setup activity, which is the activity to +//! work on termscp configuration + +/* +* +* Copyright (C) 2020 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 . +* +*/ + +// Locals +use super::{ConfigClient, Popup, SetupActivity}; +use crate::system::environment; +// Ext +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(format!( + "No configuration directory is available on your system" + ))) + } + }, + 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 { + Some(cli) => match cli.write_config() { + Ok(_) => Ok(()), + Err(err) => Err(format!("Could not save configuration: {}", err)), + }, + None => Ok(()), + } + } + + /// ### reset_config_changes + /// + /// 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() { + Some(cli) => match cli.read_config() { + Ok(_) => Ok(()), + Err(err) => Err(format!("Could not restore configuration: {}", err)), + }, + None => Ok(()), + } + } + + /// ### delete_ssh_key + /// + /// 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() { + Some(cli) => match cli.del_ssh_key(host, username) { + Ok(_) => Ok(()), + Err(err) => Err(format!( + "Could not delete ssh key \"{}@{}\": {}", + host, username, err + )), + }, + None => Ok(()), + } + } + + /// ### edit_ssh_key + /// + /// 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()); + // 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(_) => Ok(()), + Err(err) => Err(format!("Could not edit ssh key: {}", err)), + }, + }, + Err(err) => Err(format!("Could not read ssh key: {}", err)), + } + } + None => Ok(()), + } + } + None => Ok(()), + } + } + + /// ### add_ssh_key + /// + /// Add provided ssh key to config client + pub(super) fn add_ssh_key( + &mut self, + host: &str, + username: &str, + rsa_key: &str, + ) -> Result<(), String> { + match self.config_cli.as_mut() { + Some(cli) => { + // Add key to client + match cli.add_ssh_key(host, username, rsa_key) { + Ok(_) => Ok(()), + Err(err) => Err(format!("Could not add SSH key: {}", err)), + } + } + None => Ok(()), + } + } +} diff --git a/src/ui/activities/setup_activity/input.rs b/src/ui/activities/setup_activity/input.rs new file mode 100644 index 0000000..6d42e41 --- /dev/null +++ b/src/ui/activities/setup_activity/input.rs @@ -0,0 +1,470 @@ +//! ## SetupActivity +//! +//! `setup_activity` is the module which implements the Setup activity, which is the activity to +//! work on termscp configuration + +/* +* +* Copyright (C) 2020 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 . +* +*/ + +// Locals +use super::{ + InputEvent, OnChoiceCallback, Popup, QuitDialogOption, SetupActivity, SetupTab, + UserInterfaceInputField, YesNoDialogOption, +}; +use crate::filetransfer::FileTransferProtocol; +// Ext +use crossterm::event::{KeyCode, KeyModifiers}; +use std::path::PathBuf; +use tui::style::Color; + +impl SetupActivity { + /// ### handle_input_event + /// + /// Handle input event, based on current input mode + pub(super) fn handle_input_event(&mut self, ev: &InputEvent) { + let popup: Option = match &self.popup { + Some(ptype) => Some(ptype.clone()), + None => None, + }; + match &self.popup { + Some(_) => self.handle_input_event_popup(ev, popup.unwrap()), + None => self.handle_input_event_forms(ev), + } + } + + /// ### handle_input_event_forms + /// + /// Handle input event when popup is not visible. + /// InputEvent is handled based on current tab + fn handle_input_event_forms(&mut self, ev: &InputEvent) { + // Match tab + match &self.tab { + SetupTab::SshConfig => self.handle_input_event_forms_ssh_config(ev), + SetupTab::UserInterface(_) => self.handle_input_event_forms_ui(ev), + } + } + + /// ### handle_input_event_forms_ssh_config + /// + /// Handle input event when in ssh config tab + fn handle_input_event_forms_ssh_config(&mut self, ev: &InputEvent) { + // Match input event + if let InputEvent::Key(key) = ev { + // Match key code + match key.code { + KeyCode::Esc => self.popup = Some(Popup::Quit), // Prompt quit + KeyCode::Tab => { + self.tab = SetupTab::UserInterface(UserInterfaceInputField::DefaultProtocol) + } // Switch tab to user interface config + KeyCode::Up => { + if let Some(config_cli) = self.config_cli.as_ref() { + // Move ssh key index up + let ssh_key_size: usize = config_cli.iter_ssh_keys().count(); + if self.ssh_key_idx > 0 { + // Decrement + self.ssh_key_idx -= 1; + } else { + // Set ssh key index to `ssh_key_size -1` + self.ssh_key_idx = ssh_key_size - 1; + } + } + } + KeyCode::Down => { + if let Some(config_cli) = self.config_cli.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 { + // Increment index + self.ssh_key_idx += 1; + } else { + // Wrap to 0 + self.ssh_key_idx = 0; + } + } + } + KeyCode::Delete => { + // Prompt to delete selected key + self.yesno_opt = YesNoDialogOption::No; // Default to no + self.popup = Some(Popup::YesNo( + String::from("Delete key?"), + Self::callback_delete_ssh_key, + Self::callback_nothing_to_do, + )); + } + KeyCode::Enter => { + // Edit selected key + if let Err(err) = self.edit_ssh_key() { + self.popup = Some(Popup::Alert(Color::Red, err)); // Report error + } + } + KeyCode::Char(ch) => { + // Check if is enabled + if key.modifiers.intersects(KeyModifiers::CONTROL) { + // Match char + match ch { + 'h' | 'H' => { + // Show help + self.popup = Some(Popup::Help); + } + 'n' | 'N' => { + // New ssh key + self.popup = Some(Popup::NewSshKey); + } + 'r' | 'R' => { + // Show reset changes dialog + self.popup = Some(Popup::YesNo( + String::from("Reset changes?"), + Self::callback_reset_config_changes, + Self::callback_nothing_to_do, + )); + } + 's' | 'S' => { + // Show save dialog + self.popup = Some(Popup::YesNo( + String::from("Save changes to configuration?"), + Self::callback_save_config, + Self::callback_nothing_to_do, + )); + } + _ => { /* Nothing to do */ } + } + } + } + _ => { /* Nothing to do */ } + } + } + } + + /// ### handle_input_event_forms_ui + /// + /// Handle input event when in UserInterface config tab + fn handle_input_event_forms_ui(&mut self, ev: &InputEvent) { + // Get `UserInterfaceInputField` + let field: UserInterfaceInputField = match &self.tab { + SetupTab::UserInterface(field) => field.clone(), + _ => return, + }; + // Match input event + if let InputEvent::Key(key) = ev { + // Match key code + match key.code { + KeyCode::Esc => self.popup = Some(Popup::Quit), // Prompt quit + 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() { + // NOTE: replace with match if other text fields are added + if matches!(field, UserInterfaceInputField::TextEditor) { + // Pop from text editor + let mut input: String = String::from( + config_cli.get_text_editor().as_path().to_string_lossy(), + ); + input.pop(); + // Update text editor value + config_cli.set_text_editor(PathBuf::from(input.as_str())); + } + } + } + KeyCode::Left => { + // Move left on fields which are tabs + if let Some(config_cli) = self.config_cli.as_mut() { + if matches!(field, UserInterfaceInputField::DefaultProtocol) { + // Move left + config_cli.set_default_protocol( + match config_cli.get_default_protocol() { + FileTransferProtocol::Ftp(secure) => match secure { + true => FileTransferProtocol::Ftp(false), + false => FileTransferProtocol::Scp, + }, + FileTransferProtocol::Scp => FileTransferProtocol::Sftp, + FileTransferProtocol::Sftp => FileTransferProtocol::Ftp(true), // Wrap + }, + ); + } + } + } + KeyCode::Right => { + // Move right on fields which are tabs + if let Some(config_cli) = self.config_cli.as_mut() { + if matches!(field, UserInterfaceInputField::DefaultProtocol) { + // Move left + config_cli.set_default_protocol( + match config_cli.get_default_protocol() { + FileTransferProtocol::Sftp => FileTransferProtocol::Scp, + FileTransferProtocol::Scp => FileTransferProtocol::Ftp(false), + FileTransferProtocol::Ftp(secure) => match secure { + false => FileTransferProtocol::Ftp(true), + true => FileTransferProtocol::Sftp, // Wrap + }, + }, + ); + } + } + } + KeyCode::Up => { + // Change selected field + self.tab = SetupTab::UserInterface(match field { + UserInterfaceInputField::TextEditor => { + UserInterfaceInputField::DefaultProtocol + } + UserInterfaceInputField::DefaultProtocol => { + UserInterfaceInputField::TextEditor + } // Wrap + }); + } + KeyCode::Down => { + // Change selected field + self.tab = SetupTab::UserInterface(match field { + UserInterfaceInputField::DefaultProtocol => { + UserInterfaceInputField::TextEditor + } + UserInterfaceInputField::TextEditor => { + UserInterfaceInputField::DefaultProtocol + } // Wrap + }); + } + KeyCode::Char(ch) => { + // Check if is enabled + if key.modifiers.intersects(KeyModifiers::CONTROL) { + // Match char + match ch { + 'h' | 'H' => { + // Show help + self.popup = Some(Popup::Help); + } + 'r' | 'R' => { + // Show reset changes dialog + self.popup = Some(Popup::YesNo( + String::from("Reset changes?"), + Self::callback_reset_config_changes, + Self::callback_nothing_to_do, + )); + } + 's' | 'S' => { + // Show save dialog + self.popup = Some(Popup::YesNo( + String::from("Save changes to configuration?"), + Self::callback_save_config, + Self::callback_nothing_to_do, + )); + } + _ => { /* Nothing to do */ } + } + } else { + // Push character to input field + if let Some(config_cli) = self.config_cli.as_mut() { + // NOTE: change to match if other fields are added + if matches!(field, UserInterfaceInputField::TextEditor) { + // Get current text editor and push character + let mut input: String = String::from( + config_cli.get_text_editor().as_path().to_string_lossy(), + ); + input.push(ch); + // Update text editor value + config_cli.set_text_editor(PathBuf::from(input.as_str())); + } + } + } + } + _ => { /* Nothing to do */ } + } + } + } + + /// ### handle_input_event_popup + /// + /// Handler for input event when popup is visible + fn handle_input_event_popup(&mut self, ev: &InputEvent, ptype: Popup) { + match ptype { + Popup::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev), + Popup::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev), + Popup::Help => self.handle_input_event_mode_popup_help(ev), + Popup::NewSshKey => self.handle_input_event_mode_popup_newsshkey(ev), + Popup::Quit => self.handle_input_event_mode_popup_quit(ev), + Popup::YesNo(_, yes_cb, no_cb) => { + self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb) + } + } + } + + /// ### handle_input_event_mode_popup_alert + /// + /// Handle input event when the input mode is popup, and popup type is alert + fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) { + // Only enter should be allowed here + if let InputEvent::Key(key) = ev { + if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { + self.popup = None; // Hide popup + } + } + } + + /// ### handle_input_event_mode_popup_fatal + /// + /// Handle input event when the input mode is popup, and popup type is fatal + fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) { + // Only enter should be allowed here + if let InputEvent::Key(key) = ev { + if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { + // Quit after acknowelding fatal error + self.quit = true; + } + } + } + + /// ### handle_input_event_mode_popup_help + /// + /// Input event handler for popup help + fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) { + // If enter, close popup + if let InputEvent::Key(key) = ev { + if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { + self.popup = None; // Hide popup + } + } + } + + /// ### handle_input_event_mode_popup_newsshkey + /// + /// Handle input events for `Popup::NewSshKey` + fn handle_input_event_mode_popup_newsshkey(&mut self, ev: &InputEvent) { + // If enter, close popup, otherwise push chars to input + if let InputEvent::Key(key) = ev { + match key.code { + KeyCode::Esc => { + // Abort input + // Clear buffer + self.clear_user_input(); + // Hide popup + self.popup = None; + } + KeyCode::Enter => { + // Submit + let address: String = self.user_input.get(0).unwrap().to_string(); + let username: String = self.user_input.get(1).unwrap().to_string(); + // Clear buffer + self.clear_user_input(); + // Close popup BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh? + self.popup = None; + // Reset user ptr + self.user_input_ptr = 0; + // Call cb + self.callback_new_ssh_key(address, username); + } + KeyCode::Up => { + // Move ptr up, or to maximum index (1) + self.user_input_ptr = match self.user_input_ptr { + 1 => 0, + _ => 1, // Wrap + }; + } + KeyCode::Down => { + // Move ptr down, or to minimum index (0) + self.user_input_ptr = match self.user_input_ptr { + 0 => 1, + _ => 0, // Wrap + } + } + KeyCode::Char(ch) => { + // Get current input + let input: &mut String = + self.user_input.get_mut(self.user_input_ptr).unwrap(); + input.push(ch); + } + KeyCode::Backspace => { + let input: &mut String = + self.user_input.get_mut(self.user_input_ptr).unwrap(); + input.pop(); + } + _ => { /* Nothing to do */ } + } + } + } + + /// ### handle_input_event_mode_popup_quit + /// + /// Handle input events for `Popup::Quit` + fn handle_input_event_mode_popup_quit(&mut self, ev: &InputEvent) { + if let InputEvent::Key(key) = ev { + match key.code { + KeyCode::Esc => { + // Hide popup + self.popup = None; + } + KeyCode::Enter => { + // Perform enter, based on current choice + match self.quit_opt { + QuitDialogOption::Cancel => self.popup = None, // Hide popup + QuitDialogOption::DontSave => self.quit = true, // Just quit + QuitDialogOption::Save => self.callback_save_config_and_quit(), // Save and quit + } + } + KeyCode::Right => { + // Change option + self.quit_opt = match self.quit_opt { + QuitDialogOption::Save => QuitDialogOption::DontSave, + QuitDialogOption::DontSave => QuitDialogOption::Cancel, + QuitDialogOption::Cancel => QuitDialogOption::Save, // Wrap + } + } + KeyCode::Left => { + // Change option + self.quit_opt = match self.quit_opt { + QuitDialogOption::Cancel => QuitDialogOption::DontSave, + QuitDialogOption::DontSave => QuitDialogOption::Save, + QuitDialogOption::Save => QuitDialogOption::Cancel, // Wrap + } + } + _ => { /* Nothing to do */ } + } + } + } + + /// ### handle_input_event_mode_popup_yesno + /// + /// Input event handler for popup alert + pub(super) fn handle_input_event_mode_popup_yesno( + &mut self, + ev: &InputEvent, + yes_cb: OnChoiceCallback, + no_cb: OnChoiceCallback, + ) { + // If enter, close popup, otherwise move dialog option + if let InputEvent::Key(key) = ev { + match key.code { + KeyCode::Enter => { + // Hide popup BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh? + self.popup = None; + // Check if user selected yes or not + match self.yesno_opt { + YesNoDialogOption::No => no_cb(self), + YesNoDialogOption::Yes => yes_cb(self), + } + // Reset choice option to yes + self.yesno_opt = YesNoDialogOption::Yes; + } + KeyCode::Right => self.yesno_opt = YesNoDialogOption::No, // Set to NO + KeyCode::Left => self.yesno_opt = YesNoDialogOption::Yes, // Set to YES + _ => { /* Nothing to do */ } + } + } + } +} diff --git a/src/ui/activities/setup_activity/layout.rs b/src/ui/activities/setup_activity/layout.rs new file mode 100644 index 0000000..3349179 --- /dev/null +++ b/src/ui/activities/setup_activity/layout.rs @@ -0,0 +1,49 @@ +//! ## SetupActivity +//! +//! `setup_activity` is the module which implements the Setup activity, which is the activity to +//! work on termscp configuration + +/* +* +* Copyright (C) 2020 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 super::{Context, Popup, QuitDialogOption, SetupActivity, SetupTab}; +use crate::utils::fmt::align_text_center; + +use tui::{ + layout::{Constraint, Corner, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs}, +}; +use unicode_width::UnicodeWidthStr; + +impl SetupActivity { + /// ### draw + /// + /// Draw UI + pub(super) fn draw(&mut self) { + let mut ctx: Context = self.context.take().unwrap(); + let _ = ctx.terminal.draw(|f| { + // TODO: prepare layout + }); + self.context = Some(ctx); + } +} diff --git a/src/ui/activities/setup_activity/misc.rs b/src/ui/activities/setup_activity/misc.rs new file mode 100644 index 0000000..4903c60 --- /dev/null +++ b/src/ui/activities/setup_activity/misc.rs @@ -0,0 +1,38 @@ +//! ## SetupActivity +//! +//! `setup_activity` is the module which implements the Setup activity, which is the activity to +//! work on termscp configuration + +/* +* +* Copyright (C) 2020 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 super::SetupActivity; + +impl SetupActivity { + /// ### clear_user_input + /// + /// Clear user input buffers + pub(super) fn clear_user_input(&mut self) { + for s in self.user_input.iter_mut() { + s.clear(); + } + } +} diff --git a/src/ui/activities/setup_activity/mod.rs b/src/ui/activities/setup_activity/mod.rs new file mode 100644 index 0000000..5e9401a --- /dev/null +++ b/src/ui/activities/setup_activity/mod.rs @@ -0,0 +1,200 @@ +//! ## SetupActivity +//! +//! `setup_activity` is the module which implements the Setup activity, which is the activity to +//! work on termscp configuration + +/* +* +* Copyright (C) 2020 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 . +* +*/ + +// Submodules +mod callbacks; +mod config; +mod input; +mod layout; +mod misc; + +// Deps +extern crate crossterm; +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}; +use tui::style::Color; + +// Types +type OnChoiceCallback = fn(&mut SetupActivity); + +/// ### UserInterfaceInputField +/// +/// Input field selected in user interface +#[derive(std::cmp::PartialEq, Clone)] +enum UserInterfaceInputField { + DefaultProtocol, + TextEditor, +} + +/// ### SetupTab +/// +/// Selected setup tab +#[derive(std::cmp::PartialEq)] +enum SetupTab { + UserInterface(UserInterfaceInputField), + SshConfig, +} + +/// ### QuitDialogOption +/// +/// Quit dialog options +#[derive(std::cmp::PartialEq, Clone)] +enum QuitDialogOption { + Save, + DontSave, + Cancel, +} + +/// ### YesNoDialogOption +/// +/// YesNo dialog options +#[derive(std::cmp::PartialEq, Clone)] +enum YesNoDialogOption { + Yes, + No, +} + +/// ## Popup +/// +/// Popup describes the type of popup +#[derive(Clone)] +enum Popup { + Alert(Color, String), // Block color; Block text + Fatal(String), // Must quit after being hidden + Help, // Show Help + NewSshKey, // + Quit, // Quit dialog + YesNo(String, OnChoiceCallback, OnChoiceCallback), // Yes/No Dialog +} + +/// ## SetupActivity +/// +/// Setup activity states holder +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 + user_input_ptr: usize, // Selected user input + quit_opt: QuitDialogOption, // Popup::Quit selected option + yesno_opt: YesNoDialogOption, // Popup::YesNo selected option + ssh_key_idx: usize, // Index of selected ssh key in list +} + +impl Default for SetupActivity { + fn default() -> Self { + // Initialize user input + let mut user_input_buffer: Vec = Vec::with_capacity(16); + for _ in 0..16 { + user_input_buffer.push(String::new()); + } + SetupActivity { + quit: false, + context: None, + config_cli: None, + tab: SetupTab::UserInterface(UserInterfaceInputField::TextEditor), + popup: None, + user_input: user_input_buffer, // Max 16 + user_input_ptr: 0, + quit_opt: QuitDialogOption::Save, + yesno_opt: YesNoDialogOption::Yes, + ssh_key_idx: 0, + } + } +} + +impl Activity for SetupActivity { + /// ### on_create + /// + /// `on_create` is the function which must be called to initialize the activity. + /// `on_create` must initialize all the data structures used by the activity + /// Context is taken from activity manager and will be released only when activity is destroyed + fn on_create(&mut self, context: Context) { + // Set context + self.context = Some(context); + // Clear terminal + 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(); + } + } + + /// ### on_draw + /// + /// `on_draw` is the function which draws the graphical interface. + /// This function must be called at each tick to refresh the interface + fn on_draw(&mut self) { + // Context must be something + if self.context.is_none() { + return; + } + let mut redraw: bool = false; + // Read one event + if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() { + if let Some(event) = event { + // Set redraw to true + redraw = true; + // Handle event + self.handle_input_event(&event); + } + } + // Redraw if necessary + if redraw { + // Draw + self.draw(); + } + } + + /// ### on_destroy + /// + /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity. + /// This function must be called once before terminating the activity. + /// This function finally releases the context + fn on_destroy(&mut self) -> Option { + // Disable raw mode + let _ = disable_raw_mode(); + self.context.as_ref()?; + // Clear terminal and return + match self.context.take() { + Some(mut ctx) => { + ctx.clear_screen(); + Some(ctx) + } + None => None, + } + } +}