diff --git a/src/ui/activities/setup_activity/callbacks.rs b/src/ui/activities/setup_activity/actions.rs similarity index 53% rename from src/ui/activities/setup_activity/callbacks.rs rename to src/ui/activities/setup_activity/actions.rs index a766aba..4e887ca 100644 --- a/src/ui/activities/setup_activity/callbacks.rs +++ b/src/ui/activities/setup_activity/actions.rs @@ -25,82 +25,89 @@ */ // Locals -use super::{Color, Popup, SetupActivity}; +use super::SetupActivity; +use crate::ui::layout::Payload; // Ext use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::env; impl SetupActivity { - /// ### callback_nothing_to_do + /// ### action_save_config /// - /// Self titled - pub(super) fn callback_nothing_to_do(&mut self) {} + /// Save configuration + pub(super) fn action_save_config(&mut self) -> Result<(), String> { + // Collect input values + self.collect_input_values(); + self.save_config() + } - /// ### callback_save_config_and_quit + /// ### action_reset_config /// - /// 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 + /// Reset configuration input fields + pub(super) fn action_reset_config(&mut self) -> Result<(), String> { + match self.reset_config_changes() { + Err(err) => Err(err), + Ok(_) => { + self.load_input_values(); + Ok(()) + } } } - /// ### callback_save_config + /// ### action_delete_ssh_key /// - /// 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) { + /// delete of a ssh key + pub(super) fn action_delete_ssh_key(&mut self) { // Get key 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, + // get index + let idx: Option = match self.view.get_value(super::COMPONENT_LIST_SSH_KEYS) { + Some(Payload::Unsigned(idx)) => Some(idx), + _ => 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)); + if let Some(idx) = idx { + let key: Option = match config_cli.iter_ssh_keys().nth(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.mount_error(err.as_str()); + } } } + Err(err) => { + // Report error + self.mount_error( + format!("Could not get ssh key \"{}\": {}", key, err).as_str(), + ); + } } - Err(err) => { - self.popup = Some(Popup::Alert( - Color::Red, - format!("Could not get ssh key \"{}\": {}", key, err), - )) - } // Report error } } } } - /// ### callback_new_ssh_key + /// ### action_new_ssh_key /// - /// Create a new ssh key with provided parameters - pub(super) fn callback_new_ssh_key(&mut self, host: String, username: String) { + /// Create a new ssh key + pub(super) fn action_new_ssh_key(&mut self) { if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() { + // get parameters + let host: String = match self.view.get_value(super::COMPONENT_INPUT_SSH_HOST) { + Some(Payload::Text(host)) => host, + _ => String::new(), + }; + let username: String = match self.view.get_value(super::COMPONENT_INPUT_SSH_USERNAME) { + Some(Payload::Text(user)) => user, + _ => String::new(), + }; // Prepare text editor env::set_var("EDITOR", cli.get_text_editor()); let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host); @@ -119,25 +126,23 @@ impl SetupActivity { let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), ""); if rsa_key.is_empty() { // Report error: empty key - self.popup = Some(Popup::Alert(Color::Red, "SSH Key is empty".to_string())); + self.mount_error("SSH key is empty!"); } else { // 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), - )) + self.mount_error( + format!("Could not create new private key: {}", err).as_str(), + ); } } } Err(err) => { // Report error - self.popup = Some(Popup::Alert( - Color::Red, - format!("Could not write private key to file: {}", err), - )) + self.mount_error( + format!("Could not write private key to file: {}", err).as_str(), + ); } } // Restore terminal diff --git a/src/ui/activities/setup_activity/config.rs b/src/ui/activities/setup_activity/config.rs index 564cf6f..a9c78ab 100644 --- a/src/ui/activities/setup_activity/config.rs +++ b/src/ui/activities/setup_activity/config.rs @@ -77,7 +77,7 @@ impl SetupActivity { /// ### edit_ssh_key /// /// Edit selected ssh key - pub(super) fn edit_ssh_key(&mut self) -> Result<(), String> { + pub(super) fn edit_ssh_key(&mut self, idx: usize) -> Result<(), String> { match self.context.as_mut() { None => Ok(()), Some(ctx) => { @@ -91,7 +91,7 @@ impl SetupActivity { 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(config_cli) => match config_cli.iter_ssh_keys().nth(idx) { Some(key) => { // Get key path match config_cli.get_ssh_key(key) { diff --git a/src/ui/activities/setup_activity/input.rs b/src/ui/activities/setup_activity/input.rs deleted file mode 100644 index c52fc4e..0000000 --- a/src/ui/activities/setup_activity/input.rs +++ /dev/null @@ -1,572 +0,0 @@ -//! ## SetupActivity -//! -//! `setup_activity` is the module which implements the Setup activity, which is the activity to -//! work on termscp configuration - -/* -* -* 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 . -* -*/ - -// Locals -use super::{ - InputEvent, OnChoiceCallback, Popup, QuitDialogOption, SetupActivity, SetupTab, - UserInterfaceInputField, YesNoDialogOption, -}; -use crate::filetransfer::FileTransferProtocol; -use crate::fs::explorer::GroupDirs; -// 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.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 { - // 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.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 { - // 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 { - 'e' | 'E' => { - // 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, - )); - } - '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.context.as_mut().unwrap().config_client.as_mut() - { - match 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())); - } - UserInterfaceInputField::FileFmt => { - // Push char to current file fmt - let mut file_fmt = config_cli.get_file_fmt().unwrap_or_default(); - // Pop from file fmt - file_fmt.pop(); - // If len is 0, will become None - config_cli.set_file_fmt(file_fmt); - } - _ => { /* Not a text field */ } - } - // NOTE: replace with match if other text fields are added - if matches!(field, UserInterfaceInputField::TextEditor) {} - } - } - KeyCode::Left => { - // Move left on fields which are tabs - if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut() - { - match 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 - }, - ); - } - UserInterfaceInputField::GroupDirs => { - // Move left - config_cli.set_group_dirs(match config_cli.get_group_dirs() { - None => Some(GroupDirs::Last), - Some(val) => match val { - GroupDirs::Last => Some(GroupDirs::First), - GroupDirs::First => None, - }, - }); - } - UserInterfaceInputField::ShowHiddenFiles => { - // Move left - config_cli.set_show_hidden_files(true); - } - UserInterfaceInputField::CheckForUpdates => { - // move left - config_cli.set_check_for_updates(true); - } - _ => { /* Not a tab field */ } - } - } - } - KeyCode::Right => { - // Move right on fields which are tabs - if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut() - { - match 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 - }, - }, - ); - } - UserInterfaceInputField::GroupDirs => { - // Move right - config_cli.set_group_dirs(match config_cli.get_group_dirs() { - Some(val) => match val { - GroupDirs::First => Some(GroupDirs::Last), - GroupDirs::Last => None, - }, - None => Some(GroupDirs::First), - }); - } - UserInterfaceInputField::ShowHiddenFiles => { - // Move right - config_cli.set_show_hidden_files(false); - } - UserInterfaceInputField::CheckForUpdates => { - // move right - config_cli.set_check_for_updates(false); - } - _ => { /* Not a tab field */ } - } - } - } - KeyCode::Up => { - // Change selected field - self.tab = SetupTab::UserInterface(match field { - UserInterfaceInputField::FileFmt => UserInterfaceInputField::GroupDirs, - UserInterfaceInputField::GroupDirs => { - UserInterfaceInputField::CheckForUpdates - } - UserInterfaceInputField::CheckForUpdates => { - UserInterfaceInputField::ShowHiddenFiles - } - UserInterfaceInputField::ShowHiddenFiles => { - UserInterfaceInputField::DefaultProtocol - } - UserInterfaceInputField::DefaultProtocol => { - UserInterfaceInputField::TextEditor - } - UserInterfaceInputField::TextEditor => UserInterfaceInputField::FileFmt, // Wrap - }); - } - KeyCode::Down => { - // Change selected field - self.tab = SetupTab::UserInterface(match field { - UserInterfaceInputField::TextEditor => { - UserInterfaceInputField::DefaultProtocol - } - UserInterfaceInputField::DefaultProtocol => { - UserInterfaceInputField::ShowHiddenFiles - } - UserInterfaceInputField::ShowHiddenFiles => { - UserInterfaceInputField::CheckForUpdates - } - UserInterfaceInputField::CheckForUpdates => { - UserInterfaceInputField::GroupDirs - } - UserInterfaceInputField::GroupDirs => UserInterfaceInputField::FileFmt, - UserInterfaceInputField::FileFmt => UserInterfaceInputField::TextEditor, // 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.context.as_mut().unwrap().config_client.as_mut() - { - // NOTE: change to match if other fields are added - match 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())); - } - UserInterfaceInputField::FileFmt => { - // Push char to current file fmt - let mut file_fmt = - config_cli.get_file_fmt().unwrap_or_default(); - file_fmt.push(ch); - // update value - config_cli.set_file_fmt(file_fmt); - } - _ => { /* Not a text field */ } - } - } - } - } - _ => { /* 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 - } - // Reset choice - self.quit_opt = QuitDialogOption::Save; - } - 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 - 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 deleted file mode 100644 index ab5bc1c..0000000 --- a/src/ui/activities/setup_activity/layout.rs +++ /dev/null @@ -1,788 +0,0 @@ -//! ## SetupActivity -//! -//! `setup_activity` is the module which implements the Setup activity, which is the activity to -//! work on termscp configuration - -/* -* -* 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 super::{ - Context, Popup, QuitDialogOption, SetupActivity, SetupTab, UserInterfaceInputField, - YesNoDialogOption, -}; -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::{ - 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 config_client: Option<&ConfigClient> = ctx.config_client.as_ref(); - let _ = ctx.terminal.draw(|f| { - // Prepare main chunks - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints( - [ - Constraint::Length(3), // Current tab - Constraint::Percentage(90), // Main body - Constraint::Length(3), // Help footer - ] - .as_ref(), - ) - .split(f.size()); - // Prepare selected tab - f.render_widget(self.draw_selected_tab(), chunks[0]); - // Draw main layout - match &self.tab { - SetupTab::SshConfig => { - // Draw ssh config - // Create explorer chunks - let sshcfg_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(100)].as_ref()) - .split(chunks[1]); - 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)); - // Render ssh keys - f.render_stateful_widget(ssh_key_tab, sshcfg_chunks[0], &mut ssh_key_state); - } - } - SetupTab::UserInterface(form_field) => { - // Create chunks - let ui_cfg_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(1), - ] - .as_ref(), - ) - .split(chunks[1]); - // Render input forms - 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(config_client) { - f.render_widget(tab, ui_cfg_chunks[1]); - } - 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(config_client) { - f.render_widget(tab, ui_cfg_chunks[3]); - } - 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(config_client) { - f.render_widget(tab, ui_cfg_chunks[5]); - } - // Set cursor - if let Some(cli) = config_client { - match form_field { - UserInterfaceInputField::TextEditor => { - let editor_text: String = - String::from(cli.get_text_editor().as_path().to_string_lossy()); - f.set_cursor( - ui_cfg_chunks[0].x + editor_text.width() as u16 + 1, - ui_cfg_chunks[0].y + 1, - ); - } - UserInterfaceInputField::FileFmt => { - let file_fmt: String = cli.get_file_fmt().unwrap_or_default(); - f.set_cursor( - ui_cfg_chunks[5].x + file_fmt.width() as u16 + 1, - ui_cfg_chunks[5].y + 1, - ); - } - _ => { /* Not a text field */ } - } - } - } - } - // Draw footer - f.render_widget(self.draw_footer(), chunks[2]); - // Draw popup - if let Some(popup) = &self.popup { - // Calculate popup size - let (width, height): (u16, u16) = match popup { - Popup::Alert(_, _) | Popup::Fatal(_) => (50, 10), - Popup::Help => (50, 70), - Popup::NewSshKey => (50, 20), - Popup::Quit => (40, 10), - Popup::YesNo(_, _, _) => (30, 10), - }; - let popup_area: Rect = self.draw_popup_area(f.size(), width, height); - f.render_widget(Clear, popup_area); //this clears out the background - match popup { - Popup::Alert(color, txt) => f.render_widget( - self.draw_popup_alert(*color, txt.clone(), popup_area.width), - popup_area, - ), - Popup::Fatal(txt) => f.render_widget( - self.draw_popup_fatal(txt.clone(), popup_area.width), - popup_area, - ), - Popup::Help => f.render_widget(self.draw_popup_help(), popup_area), - Popup::NewSshKey => { - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(3), // Address form - Constraint::Length(3), // Username form - ] - .as_ref(), - ) - .split(popup_area); - let (address_form, username_form): (Paragraph, Paragraph) = - self.draw_popup_new_ssh_key(); - // Render parts - f.render_widget(address_form, popup_chunks[0]); - f.render_widget(username_form, popup_chunks[1]); - // Set cursor to popup form - if self.user_input_ptr < 2 { - if let Some(selected_text) = self.user_input.get(self.user_input_ptr) { - // Set cursor - f.set_cursor( - popup_chunks[self.user_input_ptr].x - + selected_text.width() as u16 - + 1, - popup_chunks[self.user_input_ptr].y + 1, - ) - } - } - } - Popup::Quit => f.render_widget(self.draw_popup_quit(), popup_area), - Popup::YesNo(txt, _, _) => { - f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area) - } - } - } - }); - self.context = Some(ctx); - } - - /// ### draw_selecte_tab - /// - /// Draw selected tab tab - fn draw_selected_tab(&self) -> Tabs { - let choices: Vec = vec![Spans::from("User Interface"), Spans::from("SSH Keys")]; - let index: usize = match self.tab { - SetupTab::UserInterface(_) => 0, - SetupTab::SshConfig => 1, - }; - Tabs::new(choices) - .block(Block::default().borders(Borders::BOTTOM).title("Setup")) - .select(index) - .style(Style::default()) - .highlight_style( - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Yellow), - ) - } - - /// ### draw_footer - /// - /// Draw authentication page footer - fn draw_footer(&self) -> Paragraph { - // Write header - let (footer, h_style) = ( - vec![ - Span::raw("Press "), - Span::styled( - "", - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Cyan), - ), - Span::raw(" to show keybindings"), - ], - Style::default().add_modifier(Modifier::BOLD), - ); - let mut footer_text = Text::from(Spans::from(footer)); - footer_text.patch_style(h_style); - Paragraph::new(footer_text) - } - - /// ### draw_text_editor_input - /// - /// Draw input text field for text editor parameter - 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(), - )) - .style(Style::default().fg(match &self.tab { - SetupTab::SshConfig => Color::White, - SetupTab::UserInterface(field) => match field { - UserInterfaceInputField::TextEditor => Color::LightGreen, - _ => Color::White, - }, - })) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title("Text Editor"), - ), - ), - None => None, - } - } - - /// ### draw_default_protocol_tab - /// - /// Draw default protocol input tab - fn draw_default_protocol_tab(&self, config_cli: Option<&ConfigClient>) -> Option { - // Check if config client is some - match config_cli.as_ref() { - Some(cli) => { - let choices: Vec = vec![ - Spans::from("SFTP"), - Spans::from("SCP"), - Spans::from("FTP"), - Spans::from("FTPS"), - ]; - let index: usize = match cli.get_default_protocol() { - FileTransferProtocol::Sftp => 0, - FileTransferProtocol::Scp => 1, - FileTransferProtocol::Ftp(secure) => match secure { - false => 2, - true => 3, - }, - }; - let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab { - SetupTab::UserInterface(field) => match field { - UserInterfaceInputField::DefaultProtocol => { - (Color::Cyan, Color::Black, Color::Cyan) - } - _ => (Color::Reset, Color::Cyan, Color::Reset), - }, - _ => (Color::Reset, Color::Reset, Color::Reset), - }; - Some( - Tabs::new(choices) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .style(Style::default().fg(block_fg)) - .title("Default File Transfer Protocol"), - ) - .select(index) - .style(Style::default()) - .highlight_style( - Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg), - ), - ) - } - None => None, - } - } - - /// ### draw_hidden_files_tab - /// - /// Draw default hidden files tab - fn draw_hidden_files_tab(&self, config_cli: Option<&ConfigClient>) -> Option { - // Check if config client is some - 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() { - true => 0, - false => 1, - }; - let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab { - SetupTab::UserInterface(field) => match field { - UserInterfaceInputField::ShowHiddenFiles => { - (Color::LightRed, Color::Black, Color::LightRed) - } - _ => (Color::Reset, Color::LightRed, Color::Reset), - }, - _ => (Color::Reset, Color::Reset, Color::Reset), - }; - Some( - Tabs::new(choices) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .style(Style::default().fg(block_fg)) - .title("Show hidden files (by default)"), - ) - .select(index) - .style(Style::default()) - .highlight_style( - Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg), - ), - ) - } - None => None, - } - } - - /// ### draw_check_for_updates_tab - /// - /// Draw check for updates tab - fn draw_check_for_updates_tab(&self, config_cli: Option<&ConfigClient>) -> Option { - // Check if config client is some - 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() { - true => 0, - false => 1, - }; - let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab { - SetupTab::UserInterface(field) => match field { - UserInterfaceInputField::CheckForUpdates => { - (Color::LightYellow, Color::Black, Color::LightYellow) - } - _ => (Color::Reset, Color::LightYellow, Color::Reset), - }, - _ => (Color::Reset, Color::Reset, Color::Reset), - }; - Some( - Tabs::new(choices) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .style(Style::default().fg(block_fg)) - .title("Check for updates?"), - ) - .select(index) - .style(Style::default()) - .highlight_style( - Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg), - ), - ) - } - None => None, - } - } - - /// ### draw_default_group_dirs_tab - /// - /// Draw group dirs input tab - fn draw_default_group_dirs_tab(&self, config_cli: Option<&ConfigClient>) -> Option { - // Check if config client is some - match config_cli.as_ref() { - Some(cli) => { - let choices: Vec = vec![ - Spans::from("Display First"), - Spans::from("Display Last"), - Spans::from("No"), - ]; - let index: usize = match cli.get_group_dirs() { - None => 2, - Some(val) => match val { - GroupDirs::First => 0, - GroupDirs::Last => 1, - }, - }; - let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab { - SetupTab::UserInterface(field) => match field { - UserInterfaceInputField::GroupDirs => { - (Color::LightMagenta, Color::Black, Color::LightMagenta) - } - _ => (Color::Reset, Color::LightMagenta, Color::Reset), - }, - _ => (Color::Reset, Color::Reset, Color::Reset), - }; - Some( - Tabs::new(choices) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .style(Style::default().fg(block_fg)) - .title("Group directories"), - ) - .select(index) - .style(Style::default()) - .highlight_style( - Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg), - ), - ) - } - None => None, - } - } - - /// ### draw_file_fmt_input - /// - /// Draw input text field for file fmt - 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 { - SetupTab::SshConfig => Color::White, - SetupTab::UserInterface(field) => match field { - UserInterfaceInputField::FileFmt => Color::LightCyan, - _ => Color::White, - }, - })) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title("File formatter syntax"), - ), - ), - None => None, - } - } - - /// ### draw_ssh_keys_list - /// - /// Draw ssh keys list - fn draw_ssh_keys_list(&self, config_cli: Option<&ConfigClient>) -> Option { - // Check if config client is some - match config_cli.as_ref() { - Some(cli) => { - // Iterate over ssh keys - let mut ssh_keys: Vec = Vec::with_capacity(cli.iter_ssh_keys().count()); - for key in cli.iter_ssh_keys() { - if let Ok(Some((addr, username, _))) = cli.get_ssh_key(key) { - ssh_keys.push(ListItem::new(Span::from(format!( - "{} at {}", - username, addr, - )))); - } else { - continue; - } - } - // Return list - Some( - List::new(ssh_keys) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::LightGreen)) - .title("SSH Keys"), - ) - .start_corner(Corner::TopLeft) - .highlight_style( - Style::default() - .fg(Color::Black) - .bg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ), - ) - } - None => None, - } - } - - /// ### draw_popup_area - /// - /// Draw popup area - fn draw_popup_area(&self, area: Rect, width: u16, height: u16) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage((100 - height) / 2), - Constraint::Percentage(height), - Constraint::Percentage((100 - height) / 2), - ] - .as_ref(), - ) - .split(area); - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage((100 - width) / 2), - Constraint::Percentage(width), - Constraint::Percentage((100 - width) / 2), - ] - .as_ref(), - ) - .split(popup_layout[1])[1] - } - - /// ### draw_popup_alert - /// - /// Draw alert popup - fn draw_popup_alert(&self, color: Color, text: String, width: u16) -> List { - // Wraps texts - let message_rows = textwrap::wrap(text.as_str(), width as usize); - let mut lines: Vec = Vec::new(); - for msg in message_rows.iter() { - lines.push(ListItem::new(Spans::from(align_text_center(msg, width)))); - } - List::new(lines) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(color)) - .border_type(BorderType::Rounded) - .title("Alert"), - ) - .start_corner(Corner::TopLeft) - .style(Style::default().fg(color)) - } - - /// ### draw_popup_fatal - /// - /// Draw fatal error popup - fn draw_popup_fatal(&self, text: String, width: u16) -> List { - self.draw_popup_alert(Color::Red, text, width) - } - - /// ### draw_popup_new_ssh_key - /// - /// Draw new ssh key form popup - fn draw_popup_new_ssh_key(&self) -> (Paragraph, Paragraph) { - let address: Paragraph = Paragraph::new(self.user_input.get(0).unwrap().as_str()) - .style(Style::default().fg(match self.user_input_ptr { - 0 => Color::LightCyan, - _ => Color::White, - })) - .block( - Block::default() - .borders(Borders::TOP | Borders::RIGHT | Borders::LEFT) - .border_type(BorderType::Rounded) - .style(Style::default().fg(Color::White)) - .title("Host name or address"), - ); - let username: Paragraph = Paragraph::new(self.user_input.get(1).unwrap().as_str()) - .style(Style::default().fg(match self.user_input_ptr { - 1 => Color::LightMagenta, - _ => Color::White, - })) - .block( - Block::default() - .borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT) - .border_type(BorderType::Rounded) - .style(Style::default().fg(Color::White)) - .title("Username"), - ); - (address, username) - } - - /// ### draw_popup_quit - /// - /// Draw quit select popup - fn draw_popup_quit(&self) -> Tabs { - let choices: Vec = vec![ - Spans::from("Save"), - Spans::from("Don't save"), - Spans::from("Cancel"), - ]; - let index: usize = match self.quit_opt { - QuitDialogOption::Save => 0, - QuitDialogOption::DontSave => 1, - QuitDialogOption::Cancel => 2, - }; - Tabs::new(choices) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title("Exit setup?"), - ) - .select(index) - .style(Style::default()) - .highlight_style(Style::default().add_modifier(Modifier::BOLD).fg(Color::Red)) - } - - /// ### draw_popup_yesno - /// - /// Draw yes/no select popup - fn draw_popup_yesno(&self, text: String) -> Tabs { - let choices: Vec = vec![Spans::from("Yes"), Spans::from("No")]; - let index: usize = match self.yesno_opt { - YesNoDialogOption::Yes => 0, - YesNoDialogOption::No => 1, - }; - Tabs::new(choices) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title(text), - ) - .select(index) - .style(Style::default()) - .highlight_style( - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Yellow), - ) - } - - /// ### draw_popup_help - /// - /// Draw authentication page help popup - fn draw_popup_help(&self) -> List { - // Write header - let cmds: Vec = vec![ - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Exit setup"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Change setup page"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Change selected element in tab"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Change input field"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Submit / Dismiss popup"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Delete entry"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Delete entry"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Show help"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("New SSH key"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Revert changes"), - ])), - ListItem::new(Spans::from(vec![ - Span::styled( - "", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::raw("Save configuration"), - ])), - ]; - List::new(cmds) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default()) - .border_type(BorderType::Rounded) - .title("Help"), - ) - .start_corner(Corner::TopLeft) - } -} diff --git a/src/ui/activities/setup_activity/misc.rs b/src/ui/activities/setup_activity/misc.rs deleted file mode 100644 index 025cc5c..0000000 --- a/src/ui/activities/setup_activity/misc.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! ## SetupActivity -//! -//! `setup_activity` is the module which implements the Setup activity, which is the activity to -//! work on termscp configuration - -/* -* -* 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 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 index 726dedd..432a1f2 100644 --- a/src/ui/activities/setup_activity/mod.rs +++ b/src/ui/activities/setup_activity/mod.rs @@ -25,11 +25,10 @@ */ // Submodules -mod callbacks; // TOREM: this +mod actions; mod config; -mod input; // TOREM: this -mod layout; // TOREM: this -mod misc; +mod update; +mod view; // Deps extern crate crossterm; @@ -37,14 +36,14 @@ extern crate tui; // Locals use super::{Activity, Context}; +use crate::ui::layout::view::View; // Ext -use crossterm::event::Event as InputEvent; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; -use tui::style::Color; // -- components const COMPONENT_TEXT_HELP: &str = "TEXT_HELP"; const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER"; +const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR"; const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT"; const COMPONENT_RADIO_SAVE: &str = "RADIO_SAVE"; const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR"; @@ -55,63 +54,17 @@ const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS"; const COMPONENT_INPUT_FILE_FMT: &str = "INPUT_FILE_FMT"; const COMPONENT_RADIO_TAB: &str = "RADIO_TAB"; const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS"; +const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST"; +const COMPONENT_INPUT_SSH_USERNAME: &str = "INPUT_SSH_USERNAME"; const COMPONENT_RADIO_DEL_SSH_KEY: &str = "RADIO_DEL_SSH_KEY"; -// Types -type OnChoiceCallback = fn(&mut SetupActivity); - -/// ### UserInterfaceInputField +/// ### ViewLayout /// -/// Input field selected in user interface -#[derive(std::cmp::PartialEq, Clone)] -enum UserInterfaceInputField { - DefaultProtocol, - TextEditor, - ShowHiddenFiles, - CheckForUpdates, - GroupDirs, - FileFmt, -} - -/// ### SetupTab -/// -/// Selected setup tab +/// Current view layout #[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 +enum ViewLayout { + SetupForm, + SshKeys, } /// ## SetupActivity @@ -120,14 +73,9 @@ enum Popup { pub struct SetupActivity { pub quit: bool, // Becomes true when user requests the activity to terminate context: Option, // Context holder - 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 - redraw: bool, // Redraw ui? + view: View, // View + layout: ViewLayout, // View layout + redraw: bool, } impl Default for SetupActivity { @@ -140,13 +88,8 @@ impl Default for SetupActivity { SetupActivity { quit: false, context: 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, + view: View::init(), + layout: ViewLayout::SetupForm, redraw: true, // Draw at first `on_draw` } } @@ -165,9 +108,11 @@ impl Activity for SetupActivity { self.context.as_mut().unwrap().clear_screen(); // Put raw mode on enabled let _ = enable_raw_mode(); + // Init view + self.init_setup(); // Verify error state from context if let Some(err) = self.context.as_mut().unwrap().get_error() { - self.popup = Some(Popup::Fatal(err)); + self.mount_error(err.as_str()); } } @@ -185,12 +130,13 @@ impl Activity for SetupActivity { // Set redraw to true self.redraw = true; // Handle event - self.handle_input_event(&event); + let msg = self.view.on(event); + self.update(msg); } // Redraw if necessary if self.redraw { - // Draw - self.draw(); + // View + self.view(); // Redraw back to false self.redraw = false; } diff --git a/src/ui/activities/setup_activity/update.rs b/src/ui/activities/setup_activity/update.rs index f1efeb4..ee17155 100644 --- a/src/ui/activities/setup_activity/update.rs +++ b/src/ui/activities/setup_activity/update.rs @@ -26,8 +26,11 @@ // locals use super::{ - AuthActivity, COMPONENT_INPUT_FILE_FMT, COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_GROUP_DIRS, - COMPONENT_RADIO_HIDDEN_FILES, COMPONENT_RADIO_TAB, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_FOOTER, COMPONENT_TEXT_HELP, + SetupActivity, COMPONENT_INPUT_FILE_FMT, COMPONENT_INPUT_SSH_HOST, + COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, + COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS, + COMPONENT_RADIO_HIDDEN_FILES, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, + COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP, }; use crate::ui::layout::{Msg, Payload}; // ext @@ -50,14 +53,6 @@ const MSG_KEY_DOWN: Msg = Msg::OnKey(KeyEvent { code: KeyCode::Down, modifiers: KeyModifiers::NONE, }); -const MSG_KEY_LEFT: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::NONE, -}); -const MSG_KEY_RIGHT: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Right, - modifiers: KeyModifiers::NONE, -}); const MSG_KEY_UP: Msg = Msg::OnKey(KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::NONE, @@ -66,11 +61,7 @@ const MSG_KEY_DEL: Msg = Msg::OnKey(KeyEvent { code: KeyCode::Delete, modifiers: KeyModifiers::NONE, }); -const MSG_KEY_CHAR_E: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::NONE, -}); -const MSG_KEY_CTRL_C: Msg = Msg::OnKey(KeyEvent { +const MSG_KEY_CTRL_E: Msg = Msg::OnKey(KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, }); @@ -78,6 +69,14 @@ const MSG_KEY_CTRL_H: Msg = Msg::OnKey(KeyEvent { code: KeyCode::Char('h'), modifiers: KeyModifiers::CONTROL, }); +const MSG_KEY_CTRL_N: Msg = Msg::OnKey(KeyEvent { + code: KeyCode::Char('n'), + modifiers: KeyModifiers::CONTROL, +}); +const MSG_KEY_CTRL_R: Msg = Msg::OnKey(KeyEvent { + code: KeyCode::Char('r'), + modifiers: KeyModifiers::CONTROL, +}); const MSG_KEY_CTRL_S: Msg = Msg::OnKey(KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, @@ -85,7 +84,7 @@ const MSG_KEY_CTRL_S: Msg = Msg::OnKey(KeyEvent { // -- update -impl AuthActivity { +impl SetupActivity { /// ### update /// /// Update auth activity model based on msg @@ -97,6 +96,215 @@ impl AuthActivity { }; // Match msg match ref_msg { + None => None, + Some(msg) => match msg { + // Input field + (COMPONENT_INPUT_TEXT_EDITOR, &MSG_KEY_DOWN) => { + self.view.active(COMPONENT_RADIO_DEFAULT_PROTOCOL); + None + } + (COMPONENT_RADIO_DEFAULT_PROTOCOL, &MSG_KEY_DOWN) => { + self.view.active(COMPONENT_RADIO_HIDDEN_FILES); + None + } + (COMPONENT_RADIO_HIDDEN_FILES, &MSG_KEY_DOWN) => { + self.view.active(COMPONENT_RADIO_UPDATES); + None + } + (COMPONENT_RADIO_UPDATES, &MSG_KEY_DOWN) => { + self.view.active(COMPONENT_RADIO_GROUP_DIRS); + None + } + (COMPONENT_RADIO_GROUP_DIRS, &MSG_KEY_DOWN) => { + self.view.active(COMPONENT_INPUT_FILE_FMT); + None + } + (COMPONENT_INPUT_FILE_FMT, &MSG_KEY_DOWN) => { + self.view.active(COMPONENT_INPUT_TEXT_EDITOR); + None + } + // Input field + (COMPONENT_INPUT_FILE_FMT, &MSG_KEY_UP) => { + self.view.active(COMPONENT_RADIO_GROUP_DIRS); + None + } + (COMPONENT_RADIO_GROUP_DIRS, &MSG_KEY_UP) => { + self.view.active(COMPONENT_RADIO_UPDATES); + None + } + (COMPONENT_RADIO_UPDATES, &MSG_KEY_UP) => { + self.view.active(COMPONENT_RADIO_HIDDEN_FILES); + None + } + (COMPONENT_RADIO_HIDDEN_FILES, &MSG_KEY_UP) => { + self.view.active(COMPONENT_RADIO_DEFAULT_PROTOCOL); + None + } + (COMPONENT_RADIO_DEFAULT_PROTOCOL, &MSG_KEY_UP) => { + self.view.active(COMPONENT_INPUT_TEXT_EDITOR); + None + } + (COMPONENT_INPUT_TEXT_EDITOR, &MSG_KEY_UP) => { + self.view.active(COMPONENT_INPUT_FILE_FMT); + None + } + // Error or + (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => { + // Umount text error + self.umount_error(); + None + } + // Exit + (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(0))) => { + // Save changes + if let Err(err) = self.action_save_config() { + self.mount_error(err.as_str()); + } + // Exit + self.quit = true; + None + } + (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(1))) => { + // Quit + self.quit = true; + self.umount_quit(); + None + } + (COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => { + // Umount popup + self.umount_quit(); + None + } + // Close help + (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => { + // Umount help + self.umount_help(); + None + } + // Delete key + (COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(Payload::Unsigned(0))) => { + // Delete key + self.action_delete_ssh_key(); + // Reload ssh keys + self.reload_ssh_keys(); + // Delete popup + self.umount_del_ssh_key(); + None + } + (COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(_)) => { + // Umount + self.umount_del_ssh_key(); + None + } + // Save popup + (COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::Unsigned(0))) => { + // Save config + if let Err(err) = self.action_save_config() { + self.mount_error(err.as_str()); + } + self.umount_save_popup(); + None + } + (COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => { + // Umount radio save + self.umount_save_popup(); + None + } + // Edit SSH Key + // Change view + (COMPONENT_LIST_SSH_KEYS, &MSG_KEY_TAB) => { + // Change view + self.init_setup(); + None + } + // Show help + (_, &MSG_KEY_CTRL_H) => { + // Show help + self.mount_help(); + None + } + // New key + (COMPONENT_INPUT_SSH_HOST, &MSG_KEY_DOWN) => { + self.view.active(COMPONENT_INPUT_SSH_USERNAME); + None + } + (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_DOWN) => { + self.view.active(COMPONENT_INPUT_SSH_HOST); + None + } + // New key + (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_UP) | (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_TAB) => { + self.view.active(COMPONENT_INPUT_SSH_HOST); + None + } + (COMPONENT_INPUT_SSH_HOST, &MSG_KEY_UP) | (COMPONENT_INPUT_SSH_HOST, &MSG_KEY_TAB) => { + self.view.active(COMPONENT_INPUT_SSH_USERNAME); + None + } + // New key + (COMPONENT_INPUT_SSH_HOST, Msg::OnSubmit(_)) + | (COMPONENT_INPUT_SSH_USERNAME, Msg::OnSubmit(_)) => { + // Save ssh key + self.action_new_ssh_key(); + self.umount_new_ssh_key(); + self.reload_ssh_keys(); + None + } + // New key + (COMPONENT_INPUT_SSH_HOST, &MSG_KEY_ESC) + | (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_ESC) => { + // Umount new ssh key + self.umount_new_ssh_key(); + None + } + // New key + (COMPONENT_LIST_SSH_KEYS, &MSG_KEY_CTRL_N) => { + // Show new key popup + self.mount_new_ssh_key(); + None + } + // Edit key + (COMPONENT_LIST_SSH_KEYS, Msg::OnSubmit(Payload::Unsigned(idx))) => { + // Edit ssh key + if let Err(err) = self.edit_ssh_key(*idx) { + self.mount_error(err.as_str()); + } + None + } + // Show delete + (COMPONENT_LIST_SSH_KEYS, &MSG_KEY_CTRL_E) + | (COMPONENT_LIST_SSH_KEYS, &MSG_KEY_DEL) => { + // Show delete key + self.mount_del_ssh_key(); + None + } + (_, &MSG_KEY_TAB) => { + // Change view + self.init_ssh_keys(); + None + } + // Revert changes + (_, &MSG_KEY_CTRL_R) => { + // Revert changes + if let Err(err) = self.action_reset_config() { + self.mount_error(err.as_str()); + } + None + } + // Save + (_, &MSG_KEY_CTRL_S) => { + // Show save + self.mount_save_popup(); + None + } + // + (_, &MSG_KEY_ESC) => { + // Mount quit prompt + self.mount_quit(); + None + } + (_, _) => None, // Nothing to do + }, } } } diff --git a/src/ui/activities/setup_activity/view.rs b/src/ui/activities/setup_activity/view.rs new file mode 100644 index 0000000..90b3af1 --- /dev/null +++ b/src/ui/activities/setup_activity/view.rs @@ -0,0 +1,790 @@ +//! ## SetupActivity +//! +//! `setup_activity` is the module which implements the Setup activity, which is the activity to +//! work on termscp configuration + +/* +* +* 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 . +* +*/ + +// Locals +use super::{Context, SetupActivity, ViewLayout}; +use crate::filetransfer::FileTransferProtocol; +use crate::fs::explorer::GroupDirs; +use crate::ui::layout::components::{ + bookmark_list::BookmarkList, ctext::CText, input::Input, radio_group::RadioGroup, table::Table, + text::Text, +}; +use crate::ui::layout::props::{ + PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder, +}; +use crate::ui::layout::utils::draw_area_in; +use crate::ui::layout::view::View; +use crate::ui::layout::Payload; +// Ext +use std::path::PathBuf; +use tui::{ + layout::{Constraint, Direction, Layout}, + style::Color, + widgets::{Borders, Clear}, +}; + +impl SetupActivity { + // -- view + + /// ### init_setup + /// + /// Initialize setup view + pub(super) fn init_setup(&mut self) { + // Init view + self.view = View::init(); + // Common stuff + // Radio tab + self.view.mount( + super::COMPONENT_RADIO_TAB, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightYellow) + .with_background(Color::Black) + .with_borders(Borders::BOTTOM) + .with_texts(TextParts::new( + None, + Some(vec![ + TextSpan::from("User Interface"), + TextSpan::from("SSH Keys"), + ]), + )) + .with_value(PropValue::Unsigned(0)) + .build(), + )), + ); + // Footer + self.view.mount( + super::COMPONENT_TEXT_FOOTER, + Box::new(Text::new( + PropsBuilder::default() + .with_texts(TextParts::new( + None, + Some(vec![ + TextSpanBuilder::new("Press ").bold().build(), + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + TextSpanBuilder::new(" to show keybindings").bold().build(), + ]), + )) + .build(), + )), + ); + // Input fields + self.view.mount( + super::COMPONENT_INPUT_TEXT_EDITOR, + Box::new(Input::new( + PropsBuilder::default() + .with_foreground(Color::LightGreen) + .with_texts(TextParts::new(Some(String::from("Text editor")), None)) + .build(), + )), + ); + self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus + self.view.mount( + super::COMPONENT_RADIO_DEFAULT_PROTOCOL, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightCyan) + .with_background(Color::Black) + .with_texts(TextParts::new( + Some(String::from("Default file transfer protocol")), + Some(vec![ + TextSpan::from("SFTP"), + TextSpan::from("SCP"), + TextSpan::from("FTP"), + TextSpan::from("FTPS"), + ]), + )) + .build(), + )), + ); + self.view.mount( + super::COMPONENT_RADIO_HIDDEN_FILES, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightRed) + .with_background(Color::Black) + .with_texts(TextParts::new( + Some(String::from("Show hidden files (by default)")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .build(), + )), + ); + self.view.mount( + super::COMPONENT_RADIO_UPDATES, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightYellow) + .with_background(Color::Black) + .with_texts(TextParts::new( + Some(String::from("Check for updates?")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .build(), + )), + ); + self.view.mount( + super::COMPONENT_RADIO_GROUP_DIRS, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightMagenta) + .with_background(Color::Black) + .with_texts(TextParts::new( + Some(String::from("Group directories")), + Some(vec![ + TextSpan::from("Display first"), + TextSpan::from("Display Last"), + TextSpan::from("No"), + ]), + )) + .build(), + )), + ); + self.view.mount( + super::COMPONENT_INPUT_FILE_FMT, + Box::new(Input::new( + PropsBuilder::default() + .with_foreground(Color::LightBlue) + .with_texts(TextParts::new( + Some(String::from("File formatter syntax")), + None, + )) + .build(), + )), + ); + // Load values + self.load_input_values(); + // Set view + self.layout = ViewLayout::SetupForm; + } + + /// ### init_ssh_keys + /// + /// Initialize ssh keys view + pub(super) fn init_ssh_keys(&mut self) { + // Init view + self.view = View::init(); + // Common stuff + // Radio tab + self.view.mount( + super::COMPONENT_RADIO_TAB, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightYellow) + .with_background(Color::Black) + .with_borders(Borders::BOTTOM) + .with_texts(TextParts::new( + None, + Some(vec![ + TextSpan::from("User Interface"), + TextSpan::from("SSH Keys"), + ]), + )) + .with_value(PropValue::Unsigned(1)) + .build(), + )), + ); + // Footer + self.view.mount( + super::COMPONENT_TEXT_FOOTER, + Box::new(Text::new( + PropsBuilder::default() + .with_texts(TextParts::new( + None, + Some(vec![ + TextSpanBuilder::new("Press ").bold().build(), + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + TextSpanBuilder::new(" to show keybindings").bold().build(), + ]), + )) + .build(), + )), + ); + self.view.mount( + super::COMPONENT_LIST_SSH_KEYS, + Box::new(BookmarkList::new( + PropsBuilder::default() + .with_texts(TextParts::new(Some(String::from("SSH Keys")), Some(vec![]))) + .with_background(Color::LightGreen) + .with_foreground(Color::Black) + .build(), + )), + ); + // Give focus + self.view.active(super::COMPONENT_LIST_SSH_KEYS); + // Load keys + self.reload_ssh_keys(); + // Set view + self.layout = ViewLayout::SshKeys; + } + + /// ### view + /// + /// View gui + pub(super) fn view(&mut self) { + let mut ctx: Context = self.context.take().unwrap(); + let _ = ctx.terminal.draw(|f| { + // Prepare main chunks + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Length(3), // Current tab + Constraint::Percentage(90), // Main body + Constraint::Length(3), // Help footer + ] + .as_ref(), + ) + .split(f.size()); + // Render common widget + self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]); + self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]); + match self.layout { + ViewLayout::SetupForm => { + // Make chunks + let ui_cfg_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), // Text editor + Constraint::Length(3), // Protocol tab + Constraint::Length(3), // Hidden files + Constraint::Length(3), // Updates tab + Constraint::Length(3), // Group dirs + Constraint::Length(3), // Format input + Constraint::Length(1), // Empty ? + ] + .as_ref(), + ) + .split(chunks[1]); + self.view + .render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks[0]); + self.view + .render(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, f, ui_cfg_chunks[1]); + self.view + .render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]); + self.view + .render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]); + self.view + .render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]); + self.view + .render(super::COMPONENT_INPUT_FILE_FMT, f, ui_cfg_chunks[5]); + } + ViewLayout::SshKeys => { + let sshcfg_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(100)].as_ref()) + .split(chunks[1]); + self.view + .render(super::COMPONENT_LIST_SSH_KEYS, f, sshcfg_chunks[0]); + } + } + // Popups + if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { + if props.build().visible { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_TEXT_ERROR, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) { + if props.build().visible { + // make popup + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + self.view.render(super::COMPONENT_RADIO_QUIT, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) { + if props.build().visible { + // make popup + let popup = draw_area_in(f.size(), 50, 70); + f.render_widget(Clear, popup); + self.view.render(super::COMPONENT_TEXT_HELP, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) { + if props.build().visible { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.view.render(super::COMPONENT_RADIO_SAVE, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) { + if props.build().visible { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.view + .render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup); + } + } + if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) { + if props.build().visible { + // make popup + let popup = draw_area_in(f.size(), 50, 20); + f.render_widget(Clear, popup); + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), // Host + Constraint::Length(3), // Username + ] + .as_ref(), + ) + .split(popup); + self.view + .render(super::COMPONENT_INPUT_SSH_HOST, f, popup_chunks[0]); + self.view + .render(super::COMPONENT_INPUT_SSH_USERNAME, f, popup_chunks[1]); + } + } + }); + // Put context back to context + self.context = Some(ctx); + } + + // -- mount + + /// ### mount_error + /// + /// Mount error box + pub(super) fn mount_error(&mut self, text: &str) { + // Mount + self.view.mount( + super::COMPONENT_TEXT_ERROR, + Box::new(CText::new( + PropsBuilder::default() + .with_foreground(Color::Red) + .bold() + .with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)]))) + .build(), + )), + ); + // Give focus to error + self.view.active(super::COMPONENT_TEXT_ERROR); + } + + /// ### umount_error + /// + /// Umount error message + pub(super) fn umount_error(&mut self) { + self.view.umount(super::COMPONENT_TEXT_ERROR); + } + + /// ### mount_del_ssh_key + /// + /// Mount delete ssh key component + pub(super) fn mount_del_ssh_key(&mut self) { + self.view.mount( + super::COMPONENT_RADIO_DEL_SSH_KEY, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightRed) + .bold() + .with_texts(TextParts::new( + Some(String::from("Delete key?")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .with_value(PropValue::Unsigned(1)) // Default: No + .build(), + )), + ); + // Active + self.view.active(super::COMPONENT_RADIO_DEL_SSH_KEY); + } + + /// ### umount_del_ssh_key + /// + /// Umount delete ssh key + pub(super) fn umount_del_ssh_key(&mut self) { + self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY); + } + + /// ### mount_new_ssh_key + /// + /// Mount new ssh key prompt + pub(super) fn mount_new_ssh_key(&mut self) { + self.view.mount( + super::COMPONENT_INPUT_SSH_HOST, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new( + Some(String::from("Hostname or address")), + None, + )) + .with_borders(Borders::TOP | Borders::RIGHT | Borders::LEFT) + .build(), + )), + ); + self.view.mount( + super::COMPONENT_INPUT_SSH_USERNAME, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new(Some(String::from("Username")), None)) + .with_borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT) + .build(), + )), + ); + self.view.active(super::COMPONENT_INPUT_SSH_HOST); + } + + /// ### umount_new_ssh_key + /// + /// Umount new ssh key prompt + pub(super) fn umount_new_ssh_key(&mut self) { + self.view.umount(super::COMPONENT_INPUT_SSH_HOST); + self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME); + } + + /// ### mount_quit + /// + /// Mount quit popup + pub(super) fn mount_quit(&mut self) { + self.view.mount( + super::COMPONENT_RADIO_QUIT, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightRed) + .bold() + .with_texts(TextParts::new( + Some(String::from("Exit setup?")), + Some(vec![ + TextSpan::from("Save"), + TextSpan::from("Don't save"), + TextSpan::from("Cancel"), + ]), + )) + .build(), + )), + ); + // Active + self.view.active(super::COMPONENT_RADIO_QUIT); + } + + /// ### umount_quit + /// + /// Umount quit + pub(super) fn umount_quit(&mut self) { + self.view.umount(super::COMPONENT_RADIO_QUIT); + } + + /// ### mount_save_popup + /// + /// Mount save popup + pub(super) fn mount_save_popup(&mut self) { + self.view.mount( + super::COMPONENT_RADIO_SAVE, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightYellow) + .bold() + .with_texts(TextParts::new( + Some(String::from("Save changes?")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .build(), + )), + ); + // Active + self.view.active(super::COMPONENT_RADIO_SAVE); + } + + /// ### umount_quit + /// + /// Umount quit + pub(super) fn umount_save_popup(&mut self) { + self.view.umount(super::COMPONENT_RADIO_SAVE); + } + + /// ### mount_help + /// + /// Mount help + pub(super) fn mount_help(&mut self) { + self.view.mount( + super::COMPONENT_TEXT_HELP, + Box::new(Table::new( + PropsBuilder::default() + .with_texts(TextParts::table( + Some(String::from("Help")), + TableBuilder::default() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Exit setup")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Change setup page")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Change cursor")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Change input field")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Select / Dismiss popup")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Delete SSH key")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" New SSH key")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Revert changes")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Save configuration")) + .build(), + )) + .build(), + )), + ); + // Active help + self.view.active(super::COMPONENT_TEXT_HELP); + } + + /// ### umount_help + /// + /// Umount help + pub(super) fn umount_help(&mut self) { + self.view.umount(super::COMPONENT_TEXT_HELP); + } + + /// ### load_input_values + /// + /// Load values from configuration into input fields + pub(super) fn load_input_values(&mut self) { + if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() { + // Text editor + if let Some(props) = self + .view + .get_props(super::COMPONENT_INPUT_TEXT_EDITOR) + .as_mut() + { + let text_editor: String = + String::from(cli.get_text_editor().as_path().to_string_lossy()); + let props = props.with_value(PropValue::Str(text_editor)).build(); + let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props); + } + // Protocol + if let Some(props) = self + .view + .get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) + .as_mut() + { + let protocol: usize = match cli.get_default_protocol() { + FileTransferProtocol::Sftp => 0, + FileTransferProtocol::Scp => 1, + FileTransferProtocol::Ftp(false) => 2, + FileTransferProtocol::Ftp(true) => 3, + }; + let props = props.with_value(PropValue::Unsigned(protocol)).build(); + let _ = self + .view + .update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props); + } + // Hidden files + if let Some(props) = self + .view + .get_props(super::COMPONENT_RADIO_HIDDEN_FILES) + .as_mut() + { + let hidden: usize = match cli.get_show_hidden_files() { + true => 0, + false => 1, + }; + let props = props.with_value(PropValue::Unsigned(hidden)).build(); + let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props); + } + // Updates + if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES).as_mut() { + let updates: usize = match cli.get_check_for_updates() { + true => 0, + false => 1, + }; + let props = props.with_value(PropValue::Unsigned(updates)).build(); + let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props); + } + // Group dirs + if let Some(props) = self + .view + .get_props(super::COMPONENT_RADIO_GROUP_DIRS) + .as_mut() + { + let dirs: usize = match cli.get_group_dirs() { + Some(GroupDirs::First) => 0, + Some(GroupDirs::Last) => 1, + None => 2, + }; + let props = props.with_value(PropValue::Unsigned(dirs)).build(); + let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props); + } + // File Fmt + if let Some(props) = self + .view + .get_props(super::COMPONENT_INPUT_FILE_FMT) + .as_mut() + { + let file_fmt: String = cli.get_file_fmt().unwrap_or(String::new()); + let props = props.with_value(PropValue::Str(file_fmt)).build(); + let _ = self.view.update(super::COMPONENT_INPUT_FILE_FMT, props); + } + } + } + + /// ### collect_input_values + /// + /// Collect values from input and put them into the configuration + pub(super) fn collect_input_values(&mut self) { + if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() { + if let Some(Payload::Text(editor)) = + self.view.get_value(super::COMPONENT_INPUT_TEXT_EDITOR) + { + cli.set_text_editor(PathBuf::from(editor.as_str())); + } + if let Some(Payload::Unsigned(protocol)) = + self.view.get_value(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) + { + let protocol: FileTransferProtocol = match protocol { + 1 => FileTransferProtocol::Scp, + 2 => FileTransferProtocol::Ftp(false), + 3 => FileTransferProtocol::Ftp(true), + _ => FileTransferProtocol::Sftp, + }; + cli.set_default_protocol(protocol); + } + if let Some(Payload::Unsigned(opt)) = + self.view.get_value(super::COMPONENT_RADIO_HIDDEN_FILES) + { + let show: bool = match opt { + 0 => true, + _ => false, + }; + cli.set_show_hidden_files(show); + } + if let Some(Payload::Unsigned(opt)) = + self.view.get_value(super::COMPONENT_RADIO_UPDATES) + { + let check: bool = match opt { + 0 => true, + _ => false, + }; + cli.set_check_for_updates(check); + } + if let Some(Payload::Text(fmt)) = self.view.get_value(super::COMPONENT_INPUT_FILE_FMT) { + cli.set_file_fmt(fmt); + } + if let Some(Payload::Unsigned(opt)) = + self.view.get_value(super::COMPONENT_RADIO_GROUP_DIRS) + { + let dirs: Option = match opt { + 0 => Some(GroupDirs::First), + 1 => Some(GroupDirs::Last), + _ => None, + }; + cli.set_group_dirs(dirs); + } + } + } + + /// ### reload_ssh_keys + /// + /// Reload ssh keys + pub(super) fn reload_ssh_keys(&mut self) { + if let Some(cli) = self.context.as_ref().unwrap().config_client.as_ref() { + // get props + if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS).as_mut() { + // Create texts + let keys: Vec = cli + .iter_ssh_keys() + .map(|x| { + let (addr, username, _) = cli.get_ssh_key(x).ok().unwrap().unwrap(); + TextSpan::from(format!("{} at {}", addr, username).as_str()) + }) + .collect(); + let props = props + .with_texts(TextParts::new(Some(String::from("SSH Keys")), Some(keys))) + .build(); + self.view.update(super::COMPONENT_LIST_SSH_KEYS, props); + } + } + } +}