Migrated setup activity to new activity lifecycle

This commit is contained in:
veeso
2021-03-17 21:00:26 +01:00
parent 5156928bdc
commit 3b99a5401f
8 changed files with 1105 additions and 1554 deletions

View File

@@ -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<String> = match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) {
Some(k) => Some(k.clone()),
None => None,
// get index
let idx: Option<usize> = 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<String> = 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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<Popup> = 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 <CTRL> 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 <CTRL> 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 */ }
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Spans> = 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(
"<CTRL+H>",
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<Paragraph> {
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<Tabs> {
// Check if config client is some
match config_cli.as_ref() {
Some(cli) => {
let choices: Vec<Spans> = 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<Tabs> {
// Check if config client is some
match config_cli.as_ref() {
Some(cli) => {
let choices: Vec<Spans> = 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<Tabs> {
// Check if config client is some
match config_cli.as_ref() {
Some(cli) => {
let choices: Vec<Spans> = 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<Tabs> {
// Check if config client is some
match config_cli.as_ref() {
Some(cli) => {
let choices: Vec<Spans> = 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<Paragraph> {
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<List> {
// Check if config client is some
match config_cli.as_ref() {
Some(cli) => {
// Iterate over ssh keys
let mut ssh_keys: Vec<ListItem> = 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<ListItem> = 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<Spans> = 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<Spans> = 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<ListItem> = vec![
ListItem::new(Spans::from(vec![
Span::styled(
"<ESC>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Exit setup"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<TAB>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Change setup page"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<RIGHT/LEFT>",
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(
"<UP/DOWN>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Change input field"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<ENTER>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Submit / Dismiss popup"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<DEL>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Delete entry"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+E>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Delete entry"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+H>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Show help"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+N>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("New SSH key"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+R>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Revert changes"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+S>",
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)
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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();
}
}
}

View File

@@ -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>, // Context holder
tab: SetupTab, // Current setup tab
popup: Option<Popup>, // Active popup
user_input: Vec<String>, // 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;
}

View File

@@ -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 <DOWN>
(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 <UP>
(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 <ENTER> or <ESC>
(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
// <TAB> Change view
(COMPONENT_LIST_SSH_KEYS, &MSG_KEY_TAB) => {
// Change view
self.init_setup();
None
}
// <CTRL+H> Show help
(_, &MSG_KEY_CTRL_H) => {
// Show help
self.mount_help();
None
}
// New key <DOWN>
(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 <UP>
(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 <ENTER>
(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 <ESC>
(COMPONENT_INPUT_SSH_HOST, &MSG_KEY_ESC)
| (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_ESC) => {
// Umount new ssh key
self.umount_new_ssh_key();
None
}
// <CTRL+N> New key
(COMPONENT_LIST_SSH_KEYS, &MSG_KEY_CTRL_N) => {
// Show new key popup
self.mount_new_ssh_key();
None
}
// <ENTER> 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
}
// <DEL | CTRL+E> 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
}
// <CTRL+R> Revert changes
(_, &MSG_KEY_CTRL_R) => {
// Revert changes
if let Err(err) = self.action_reset_config() {
self.mount_error(err.as_str());
}
None
}
// <CTRL+S> Save
(_, &MSG_KEY_CTRL_S) => {
// Show save
self.mount_save_popup();
None
}
// <ESC>
(_, &MSG_KEY_ESC) => {
// Mount quit prompt
self.mount_quit();
None
}
(_, _) => None, // Nothing to do
},
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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("<CTRL+H>")
.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("<CTRL+H>")
.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("<ESC>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Exit setup"))
.add_row()
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change setup page"))
.add_row()
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change cursor"))
.add_row()
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Change input field"))
.add_row()
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Select / Dismiss popup"))
.add_row()
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Delete SSH key"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+N>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" New SSH key"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+R>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Revert changes"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+S>")
.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<GroupDirs> = 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<TextSpan> = 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);
}
}
}
}