Migrated termscp to tui-realm 1.x

This commit is contained in:
veeso
2021-11-21 10:02:03 +01:00
committed by Christian Visintin
parent 30851a78e8
commit 54b5583d1a
54 changed files with 10994 additions and 7691 deletions

View File

@@ -27,13 +27,12 @@
* SOFTWARE.
*/
// Locals
use super::{SetupActivity, ViewLayout};
use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout};
// Ext
use crate::config::themes::Theme;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::env;
use tuirealm::tui::style::Color;
use tuirealm::{Payload, Value};
use tuirealm::{State, StateValue};
impl SetupActivity {
/// ### action_on_esc
@@ -78,7 +77,7 @@ impl SetupActivity {
// Collect input values if in theme form
if self.layout == ViewLayout::Theme {
self.collect_styles()
.map_err(|e| format!("'{}' has an invalid color", e))?;
.map_err(|e| format!("'{:?}' has an invalid color", e))?;
}
// save theme
self.save_theme()
@@ -93,7 +92,7 @@ impl SetupActivity {
ViewLayout::SetupForm => self.collect_input_values(),
ViewLayout::Theme => self
.collect_styles()
.map_err(|e| format!("'{}' has an invalid color", e))?,
.map_err(|e| format!("'{:?}' has an invalid color", e))?,
_ => {}
}
// Update view
@@ -133,8 +132,8 @@ impl SetupActivity {
pub(super) fn action_delete_ssh_key(&mut self) {
// Get key
// get index
let idx: Option<usize> = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) {
Some(Payload::One(Value::Usize(idx))) => Some(idx),
let idx: Option<usize> = match self.app.state(&Id::Ssh(IdSsh::SshKeys)) {
Ok(State::One(StateValue::Usize(idx))) => Some(idx),
_ => None,
};
if let Some(idx) = idx {
@@ -166,29 +165,27 @@ impl SetupActivity {
/// Create a new ssh key
pub(super) fn action_new_ssh_key(&mut self) {
// get parameters
let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) {
Some(Payload::One(Value::Str(host))) => host,
let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) {
Ok(State::One(StateValue::String(host))) => host,
_ => String::new(),
};
let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) {
Some(Payload::One(Value::Str(user))) => user,
let username: String = match self.app.state(&Id::Ssh(IdSsh::SshUsername)) {
Ok(State::One(StateValue::String(user))) => user,
_ => String::new(),
};
// Prepare text editor
env::set_var("EDITOR", self.config().get_text_editor());
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
// Put input mode back to normal
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Could not disable raw mode: {}", err);
}
// Leave alternate mode
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
// Re-enable raw mode
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
if let Err(err) = self.context_mut().terminal().leave_alternate_screen() {
error!("Could not leave alternate screen: {}", err);
}
// Lock ports
assert!(self.app.lock_ports().is_ok());
// Write key to file
match edit::edit(placeholder.as_bytes()) {
Ok(rsa_key) => {
@@ -215,101 +212,246 @@ impl SetupActivity {
}
// Restore terminal
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
if let Err(err) = ctx.terminal().clear_screen() {
error!("Could not clear screen screen: {}", err);
}
// Enter alternate mode
ctx.enter_alternate_screen();
if let Err(err) = ctx.terminal().enter_alternate_screen() {
error!("Could not enter alternate screen: {}", err);
}
// Re-enable raw mode
if let Err(err) = ctx.terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Unlock ports
assert!(self.app.unlock_ports().is_ok());
}
}
/// ### set_color
///
/// Given a component and a color, save the color into the theme
pub(super) fn action_save_color(&mut self, component: &str, color: Color) {
pub(super) fn action_save_color(&mut self, component: IdTheme, color: Color) {
let theme: &mut Theme = self.theme_mut();
match component {
super::COMPONENT_COLOR_AUTH_ADDR => {
IdTheme::AuthAddress => {
theme.auth_address = color;
}
super::COMPONENT_COLOR_AUTH_BOOKMARKS => {
IdTheme::AuthBookmarks => {
theme.auth_bookmarks = color;
}
super::COMPONENT_COLOR_AUTH_PASSWORD => {
IdTheme::AuthPassword => {
theme.auth_password = color;
}
super::COMPONENT_COLOR_AUTH_PORT => {
IdTheme::AuthPort => {
theme.auth_port = color;
}
super::COMPONENT_COLOR_AUTH_PROTOCOL => {
IdTheme::AuthProtocol => {
theme.auth_protocol = color;
}
super::COMPONENT_COLOR_AUTH_RECENTS => {
IdTheme::AuthRecentHosts => {
theme.auth_recents = color;
}
super::COMPONENT_COLOR_AUTH_USERNAME => {
IdTheme::AuthUsername => {
theme.auth_username = color;
}
super::COMPONENT_COLOR_MISC_ERROR => {
IdTheme::MiscError => {
theme.misc_error_dialog = color;
}
super::COMPONENT_COLOR_MISC_INFO => {
IdTheme::MiscInfo => {
theme.misc_info_dialog = color;
}
super::COMPONENT_COLOR_MISC_INPUT => {
IdTheme::MiscInput => {
theme.misc_input_dialog = color;
}
super::COMPONENT_COLOR_MISC_KEYS => {
IdTheme::MiscKeys => {
theme.misc_keys = color;
}
super::COMPONENT_COLOR_MISC_QUIT => {
IdTheme::MiscQuit => {
theme.misc_quit_dialog = color;
}
super::COMPONENT_COLOR_MISC_SAVE => {
IdTheme::MiscSave => {
theme.misc_save_dialog = color;
}
super::COMPONENT_COLOR_MISC_WARN => {
IdTheme::MiscWarn => {
theme.misc_warn_dialog = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG => {
IdTheme::ExplorerLocalBg => {
theme.transfer_local_explorer_background = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG => {
IdTheme::ExplorerLocalFg => {
theme.transfer_local_explorer_foreground = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG => {
IdTheme::ExplorerLocalHg => {
theme.transfer_local_explorer_highlighted = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG => {
IdTheme::ExplorerRemoteBg => {
theme.transfer_remote_explorer_background = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG => {
IdTheme::ExplorerRemoteFg => {
theme.transfer_remote_explorer_foreground = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG => {
IdTheme::ExplorerRemoteHg => {
theme.transfer_remote_explorer_highlighted = color;
}
super::COMPONENT_COLOR_TRANSFER_LOG_BG => {
IdTheme::LogBg => {
theme.transfer_log_background = color;
}
super::COMPONENT_COLOR_TRANSFER_LOG_WIN => {
IdTheme::LogWindow => {
theme.transfer_log_window = color;
}
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL => {
IdTheme::ProgBarFull => {
theme.transfer_progress_bar_full = color;
}
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL => {
IdTheme::ProgBarPartial => {
theme.transfer_progress_bar_partial = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN => {
IdTheme::StatusHidden => {
theme.transfer_status_hidden = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING => {
IdTheme::StatusSorting => {
theme.transfer_status_sorting = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC => {
IdTheme::StatusSync => {
theme.transfer_status_sync_browsing = color;
}
_ => {}
}
}
/// ### collect_styles
///
/// Collect values from input and put them into the theme.
/// If a component has an invalid color, returns Err(component_id)
fn collect_styles(&mut self) -> Result<(), Id> {
// auth
let auth_address = self
.get_color(&Id::Theme(IdTheme::AuthAddress))
.map_err(|_| Id::Theme(IdTheme::AuthAddress))?;
let auth_bookmarks = self
.get_color(&Id::Theme(IdTheme::AuthBookmarks))
.map_err(|_| Id::Theme(IdTheme::AuthBookmarks))?;
let auth_password = self
.get_color(&Id::Theme(IdTheme::AuthPassword))
.map_err(|_| Id::Theme(IdTheme::AuthPassword))?;
let auth_port = self
.get_color(&Id::Theme(IdTheme::AuthPort))
.map_err(|_| Id::Theme(IdTheme::AuthPort))?;
let auth_protocol = self
.get_color(&Id::Theme(IdTheme::AuthProtocol))
.map_err(|_| Id::Theme(IdTheme::AuthProtocol))?;
let auth_recents = self
.get_color(&Id::Theme(IdTheme::AuthRecentHosts))
.map_err(|_| Id::Theme(IdTheme::AuthRecentHosts))?;
let auth_username = self
.get_color(&Id::Theme(IdTheme::AuthUsername))
.map_err(|_| Id::Theme(IdTheme::AuthUsername))?;
// misc
let misc_error_dialog = self
.get_color(&Id::Theme(IdTheme::MiscError))
.map_err(|_| Id::Theme(IdTheme::MiscError))?;
let misc_info_dialog = self
.get_color(&Id::Theme(IdTheme::MiscInfo))
.map_err(|_| Id::Theme(IdTheme::MiscInfo))?;
let misc_input_dialog = self
.get_color(&Id::Theme(IdTheme::MiscInput))
.map_err(|_| Id::Theme(IdTheme::MiscInput))?;
let misc_keys = self
.get_color(&Id::Theme(IdTheme::MiscKeys))
.map_err(|_| Id::Theme(IdTheme::MiscKeys))?;
let misc_quit_dialog = self
.get_color(&Id::Theme(IdTheme::MiscQuit))
.map_err(|_| Id::Theme(IdTheme::MiscQuit))?;
let misc_save_dialog = self
.get_color(&Id::Theme(IdTheme::MiscSave))
.map_err(|_| Id::Theme(IdTheme::MiscSave))?;
let misc_warn_dialog = self
.get_color(&Id::Theme(IdTheme::MiscWarn))
.map_err(|_| Id::Theme(IdTheme::MiscWarn))?;
// transfer
let transfer_local_explorer_background = self
.get_color(&Id::Theme(IdTheme::ExplorerLocalBg))
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalBg))?;
let transfer_local_explorer_foreground = self
.get_color(&Id::Theme(IdTheme::ExplorerLocalFg))
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalFg))?;
let transfer_local_explorer_highlighted = self
.get_color(&Id::Theme(IdTheme::ExplorerLocalHg))
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalHg))?;
let transfer_remote_explorer_background = self
.get_color(&Id::Theme(IdTheme::ExplorerRemoteBg))
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteBg))?;
let transfer_remote_explorer_foreground = self
.get_color(&Id::Theme(IdTheme::ExplorerRemoteFg))
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteFg))?;
let transfer_remote_explorer_highlighted = self
.get_color(&Id::Theme(IdTheme::ExplorerRemoteHg))
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteHg))?;
let transfer_log_background = self
.get_color(&Id::Theme(IdTheme::LogBg))
.map_err(|_| Id::Theme(IdTheme::LogBg))?;
let transfer_log_window = self
.get_color(&Id::Theme(IdTheme::LogWindow))
.map_err(|_| Id::Theme(IdTheme::LogWindow))?;
let transfer_progress_bar_full = self
.get_color(&Id::Theme(IdTheme::ProgBarFull))
.map_err(|_| Id::Theme(IdTheme::ProgBarFull))?;
let transfer_progress_bar_partial = self
.get_color(&Id::Theme(IdTheme::ProgBarPartial))
.map_err(|_| Id::Theme(IdTheme::ProgBarPartial))?;
let transfer_status_hidden = self
.get_color(&Id::Theme(IdTheme::StatusHidden))
.map_err(|_| Id::Theme(IdTheme::StatusHidden))?;
let transfer_status_sorting = self
.get_color(&Id::Theme(IdTheme::StatusSorting))
.map_err(|_| Id::Theme(IdTheme::StatusSorting))?;
let transfer_status_sync_browsing = self
.get_color(&Id::Theme(IdTheme::StatusSync))
.map_err(|_| Id::Theme(IdTheme::StatusSync))?;
// Update theme
let mut theme: &mut Theme = self.theme_mut();
theme.auth_address = auth_address;
theme.auth_bookmarks = auth_bookmarks;
theme.auth_password = auth_password;
theme.auth_port = auth_port;
theme.auth_protocol = auth_protocol;
theme.auth_recents = auth_recents;
theme.auth_username = auth_username;
theme.misc_error_dialog = misc_error_dialog;
theme.misc_info_dialog = misc_info_dialog;
theme.misc_input_dialog = misc_input_dialog;
theme.misc_keys = misc_keys;
theme.misc_quit_dialog = misc_quit_dialog;
theme.misc_save_dialog = misc_save_dialog;
theme.misc_warn_dialog = misc_warn_dialog;
theme.transfer_local_explorer_background = transfer_local_explorer_background;
theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground;
theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted;
theme.transfer_remote_explorer_background = transfer_remote_explorer_background;
theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground;
theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted;
theme.transfer_log_background = transfer_log_background;
theme.transfer_log_window = transfer_log_window;
theme.transfer_progress_bar_full = transfer_progress_bar_full;
theme.transfer_progress_bar_partial = transfer_progress_bar_partial;
theme.transfer_status_hidden = transfer_status_hidden;
theme.transfer_status_sorting = transfer_status_sorting;
theme.transfer_status_sync_browsing = transfer_status_sync_browsing;
Ok(())
}
/// ### get_color
///
/// Get color from component
fn get_color(&self, component: &Id) -> Result<Color, ()> {
match self.app.state(component) {
Ok(State::One(StateValue::String(color))) => {
match crate::utils::parser::parse_color(color.as_str()) {
Some(c) => Ok(c),
None => Err(()),
}
}
_ => Err(()),
}
}
}

View File

@@ -0,0 +1,334 @@
//! ## Config
//!
//! config tab components
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{CommonMsg, Msg, ViewLayout};
use tui_realm_stdlib::{List, Paragraph, Radio, Span};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, TableBuilder, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
#[derive(MockComponent)]
pub struct ErrorPopup {
component: Paragraph,
}
impl ErrorPopup {
pub fn new<S: AsRef<str>>(text: S) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(Color::Red)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Red)
.text(&[TextSpan::from(text.as_ref())])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for ErrorPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::Common(CommonMsg::CloseErrorPopup)),
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct Footer {
component: Span,
}
impl Default for Footer {
fn default() -> Self {
Self {
component: Span::default().spans(&[
TextSpan::new("Press ").bold(),
TextSpan::new("<CTRL+H>").bold().fg(Color::Cyan),
TextSpan::new(" to show keybindings; ").bold(),
TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan),
TextSpan::new(" to save parameters; ").bold(),
TextSpan::new("<TAB>").bold().fg(Color::Cyan),
TextSpan::new(" to change panel").bold(),
]),
}
}
}
impl Component<Msg, NoUserEvent> for Footer {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct Header {
component: Radio,
}
impl Header {
pub fn new(layout: ViewLayout) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Yellow)
.sides(BorderSides::BOTTOM),
)
.choices(&["User interface", "SSH Keys", "Theme"])
.foreground(Color::Yellow)
.value(match layout {
ViewLayout::SetupForm => 0,
ViewLayout::SshKeys => 1,
ViewLayout::Theme => 2,
}),
}
}
}
impl Component<Msg, NoUserEvent> for Header {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct Keybindings {
component: List,
}
impl Default for Keybindings {
fn default() -> Self {
Self {
component: List::default()
.borders(Borders::default().modifiers(BorderType::Rounded))
.title("Keybindings", Alignment::Center)
.scroll(true)
.highlighted_str("? ")
.rows(
TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Exit setup"))
.add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change setup page"))
.add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change cursor"))
.add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change input field"))
.add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Select / Dismiss popup"))
.add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Delete SSH key"))
.add_row()
.add_col(TextSpan::new("<CTRL+N>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" New SSH key"))
.add_row()
.add_col(TextSpan::new("<CTRL+R>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Revert changes"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Save configuration"))
.build(),
),
}
}
}
impl Component<Msg, NoUserEvent> for Keybindings {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::Common(CommonMsg::CloseKeybindingsPopup)),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct QuitPopup {
component: Radio,
}
impl Default for QuitPopup {
fn default() -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Red)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Red)
.title(
"There are unsaved changes! Save changes before leaving?",
Alignment::Center,
)
.rewind(true)
.choices(&["Save", "Don't save", "Cancel"]),
}
}
}
impl Component<Msg, NoUserEvent> for QuitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Common(CommonMsg::CloseQuitPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.perform(Cmd::Submit) {
CmdResult::Submit(State::One(StateValue::Usize(0))) => {
Some(Msg::Common(CommonMsg::SaveAndQuit))
}
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
Some(Msg::Common(CommonMsg::Quit))
}
_ => Some(Msg::Common(CommonMsg::CloseQuitPopup)),
},
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SavePopup {
component: Radio,
}
impl Default for SavePopup {
fn default() -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Yellow)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Yellow)
.title("Save changes?", Alignment::Center)
.rewind(true)
.choices(&["Yes", "No"]),
}
}
}
impl Component<Msg, NoUserEvent> for SavePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Common(CommonMsg::CloseSavePopup))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::Common(CommonMsg::SaveConfig))
} else {
Some(Msg::Common(CommonMsg::CloseSavePopup))
}
}
_ => None,
}
}
}

View File

@@ -0,0 +1,489 @@
//! ## Config
//!
//! config tab components
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{ConfigMsg, Msg};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs as GroupDirsEnum;
use crate::utils::parser::parse_bytesize;
use tui_realm_stdlib::{Input, Radio};
use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
// -- components
#[derive(MockComponent)]
pub struct CheckUpdates {
component: Radio,
}
impl CheckUpdates {
pub fn new(enabled: bool) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightYellow)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::LightYellow)
.rewind(true)
.title("Check for updates?", Alignment::Left)
.value(if enabled { 0 } else { 1 }),
}
}
}
impl Component<Msg, NoUserEvent> for CheckUpdates {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::CheckUpdatesBlurDown),
Msg::Config(ConfigMsg::CheckUpdatesBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct DefaultProtocol {
component: Radio,
}
impl DefaultProtocol {
pub fn new(protocol: FileTransferProtocol) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Cyan)
.modifiers(BorderType::Rounded),
)
.choices(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
.foreground(Color::Cyan)
.rewind(true)
.title("Default protocol", Alignment::Left)
.value(match protocol {
FileTransferProtocol::AwsS3 => 4,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Sftp => 0,
}),
}
}
}
impl Component<Msg, NoUserEvent> for DefaultProtocol {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::DefaultProtocolBlurDown),
Msg::Config(ConfigMsg::DefaultProtocolBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct GroupDirs {
component: Radio,
}
impl GroupDirs {
pub fn new(opt: Option<GroupDirsEnum>) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightMagenta)
.modifiers(BorderType::Rounded),
)
.choices(&["Display first", "Display last", "No"])
.foreground(Color::LightMagenta)
.rewind(true)
.title("Group directories", Alignment::Left)
.value(match opt {
Some(GroupDirsEnum::First) => 0,
Some(GroupDirsEnum::Last) => 1,
None => 2,
}),
}
}
}
impl Component<Msg, NoUserEvent> for GroupDirs {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::GroupDirsBlurDown),
Msg::Config(ConfigMsg::GroupDirsBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct HiddenFiles {
component: Radio,
}
impl HiddenFiles {
pub fn new(enabled: bool) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightRed)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::LightRed)
.rewind(true)
.title("Show hidden files? (by default)", Alignment::Left)
.value(if enabled { 0 } else { 1 }),
}
}
}
impl Component<Msg, NoUserEvent> for HiddenFiles {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::HiddenFilesBlurDown),
Msg::Config(ConfigMsg::HiddenFilesBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct NotificationsEnabled {
component: Radio,
}
impl NotificationsEnabled {
pub fn new(enabled: bool) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightRed)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::LightRed)
.rewind(true)
.title("Enable notifications?", Alignment::Left)
.value(if enabled { 0 } else { 1 }),
}
}
}
impl Component<Msg, NoUserEvent> for NotificationsEnabled {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::NotificationsEnabledBlurDown),
Msg::Config(ConfigMsg::NotificationsEnabledBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct PromptOnFileReplace {
component: Radio,
}
impl PromptOnFileReplace {
pub fn new(enabled: bool) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightBlue)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::LightBlue)
.rewind(true)
.title("Prompt when replacing existing files?", Alignment::Left)
.value(if enabled { 0 } else { 1 }),
}
}
}
impl Component<Msg, NoUserEvent> for PromptOnFileReplace {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::PromptOnFileReplaceBlurDown),
Msg::Config(ConfigMsg::PromptOnFileReplaceBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct LocalFileFmt {
component: Input,
}
impl LocalFileFmt {
pub fn new(value: &str) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::LightGreen)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightGreen)
.input_type(InputType::Text)
.placeholder(
"{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("File formatter syntax (local)", Alignment::Left)
.value(value),
}
}
}
impl Component<Msg, NoUserEvent> for LocalFileFmt {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Config(ConfigMsg::LocalFileFmtBlurDown),
Msg::Config(ConfigMsg::LocalFileFmtBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct NotificationsThreshold {
component: Input,
}
impl NotificationsThreshold {
pub fn new(value: &str) -> Self {
// -- validators
fn validate(bytes: &str) -> bool {
parse_bytesize(bytes).is_some()
}
fn char_valid(_input: &str, incoming: char) -> bool {
incoming.is_digit(10) || ['B', 'K', 'M', 'G', 'T', 'P'].contains(&incoming)
}
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::LightYellow)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightYellow)
.invalid_style(Style::default().fg(Color::Red))
.input_type(InputType::Custom(validate, char_valid))
.placeholder("64 MB", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Notifications: minimum transfer size", Alignment::Left)
.value(value),
}
}
}
impl Component<Msg, NoUserEvent> for NotificationsThreshold {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Config(ConfigMsg::NotificationsThresholdBlurDown),
Msg::Config(ConfigMsg::NotificationsThresholdBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct RemoteFileFmt {
component: Input,
}
impl RemoteFileFmt {
pub fn new(value: &str) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::Cyan)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Cyan)
.input_type(InputType::Text)
.placeholder(
"{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("File formatter syntax (remote)", Alignment::Left)
.value(value),
}
}
}
impl Component<Msg, NoUserEvent> for RemoteFileFmt {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Config(ConfigMsg::RemoteFileFmtBlurDown),
Msg::Config(ConfigMsg::RemoteFileFmtBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct TextEditor {
component: Input,
}
impl TextEditor {
pub fn new(value: &str) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::LightGreen)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightGreen)
.input_type(InputType::Text)
.placeholder("vim", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Text editor", Alignment::Left)
.value(value),
}
}
}
impl Component<Msg, NoUserEvent> for TextEditor {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Config(ConfigMsg::TextEditorBlurDown),
Msg::Config(ConfigMsg::TextEditorBlurUp),
)
}
}
// -- event handler
fn handle_input_ev(
component: &mut dyn Component<Msg, NoUserEvent>,
ev: Event<NoUserEvent>,
on_key_down: Msg,
on_key_up: Msg,
) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
component.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
component.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
component.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
component.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
component.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
component.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
component.perform(Cmd::Type(ch));
Some(Msg::Config(ConfigMsg::ConfigChanged))
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(on_key_down),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up),
_ => None,
}
}
fn handle_radio_ev(
component: &mut dyn Component<Msg, NoUserEvent>,
ev: Event<NoUserEvent>,
on_key_down: Msg,
on_key_up: Msg,
) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
component.perform(Cmd::Move(Direction::Left));
Some(Msg::Config(ConfigMsg::ConfigChanged))
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
component.perform(Cmd::Move(Direction::Right));
Some(Msg::Config(ConfigMsg::ConfigChanged))
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(on_key_down),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up),
_ => None,
}
}

View File

@@ -0,0 +1,86 @@
//! ## Components
//!
//! setup activity components
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{CommonMsg, ConfigMsg, Msg, SshMsg, ThemeMsg, ViewLayout};
mod commons;
mod config;
mod ssh;
mod theme;
pub(super) use commons::{ErrorPopup, Footer, Header, Keybindings, QuitPopup, SavePopup};
pub(super) use config::{
CheckUpdates, DefaultProtocol, GroupDirs, HiddenFiles, LocalFileFmt, NotificationsEnabled,
NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, TextEditor,
};
pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername};
pub(super) use theme::*;
use tui_realm_stdlib::Phantom;
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, MockComponent};
// -- global listener
#[derive(MockComponent)]
pub struct GlobalListener {
component: Phantom,
}
impl Default for GlobalListener {
fn default() -> Self {
Self {
component: Phantom::default(),
}
}
}
impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Common(CommonMsg::ShowQuitPopup))
}
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Common(CommonMsg::ChangeLayout))
}
Event::Keyboard(KeyEvent {
code: Key::Char('h'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Common(CommonMsg::ShowKeybindings)),
Event::Keyboard(KeyEvent {
code: Key::Char('r'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Common(CommonMsg::RevertChanges)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Common(CommonMsg::ShowSavePopup)),
_ => None,
}
}
}

View File

@@ -0,0 +1,339 @@
//! ## Ssh
//!
//! ssh components
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{Msg, SshMsg};
use tui_realm_stdlib::{Input, List, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{
Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TextSpan,
};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
/* DelSshKeyPopup,
SshHost,
SshKeys,
SshUsername, */
#[derive(MockComponent)]
pub struct DelSshKeyPopup {
component: Radio,
}
impl Default for DelSshKeyPopup {
fn default() -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Red)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::Red)
.rewind(true)
.title("Delete key?", Alignment::Center)
.value(1),
}
}
}
impl Component<Msg, NoUserEvent> for DelSshKeyPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::Ssh(SshMsg::DeleteSshKey))
} else {
Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup))
}
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SshKeys {
component: List,
}
impl SshKeys {
pub fn new(keys: &[String]) -> Self {
Self {
component: List::default()
.borders(
Borders::default()
.color(Color::LightGreen)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightGreen)
.highlighted_color(Color::LightGreen)
.rewind(true)
.rows(keys.iter().map(|x| vec![TextSpan::from(x)]).collect())
.step(4)
.scroll(true)
.title("SSH Keys", Alignment::Left),
}
}
}
impl Component<Msg, NoUserEvent> for SshKeys {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::Usize(choice)) => Some(Msg::Ssh(SshMsg::EditSshKey(choice))),
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => Some(Msg::Ssh(SshMsg::ShowDelSshKeyPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('n'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Ssh(SshMsg::ShowNewSshKeyPopup)),
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SshHost {
component: Input,
}
impl Default for SshHost {
fn default() -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.sides(BorderSides::TOP | BorderSides::RIGHT | BorderSides::LEFT),
)
.input_type(InputType::Text)
.placeholder(
"192.168.1.2",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("Hostname or address", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for SshHost {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Ssh(SshMsg::SaveSshKey)),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::Ssh(SshMsg::SshHostBlur)),
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ssh(SshMsg::CloseNewSshKeyPopup))
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SshUsername {
component: Input,
}
impl Default for SshUsername {
fn default() -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.sides(BorderSides::BOTTOM | BorderSides::RIGHT | BorderSides::LEFT),
)
.input_type(InputType::Text)
.placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Username", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for SshUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Ssh(SshMsg::SaveSshKey)),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
Some(Msg::Ssh(SshMsg::SshUsernameBlur))
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ssh(SshMsg::CloseNewSshKeyPopup))
}
_ => None,
}
}
}

View File

@@ -0,0 +1,910 @@
//! ## Theme
//!
//! theme tab components
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{Msg, ThemeMsg};
use crate::ui::activities::setup::IdTheme;
use tui_realm_stdlib::{Input, Label};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style, TextModifiers};
use tuirealm::{
AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent, State, StateValue,
};
// -- components
#[derive(MockComponent)]
pub struct AuthTitle {
component: Label,
}
impl Default for AuthTitle {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD)
.text("Authentication styles"),
}
}
}
impl Component<Msg, NoUserEvent> for AuthTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct MiscTitle {
component: Label,
}
impl Default for MiscTitle {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD)
.text("Misc styles"),
}
}
}
impl Component<Msg, NoUserEvent> for MiscTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct TransferTitle {
component: Label,
}
impl Default for TransferTitle {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD)
.text("Transfer styles"),
}
}
}
impl Component<Msg, NoUserEvent> for TransferTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct TransferTitle2 {
component: Label,
}
impl Default for TransferTitle2 {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD)
.text("Transfer styles (2)"),
}
}
}
impl Component<Msg, NoUserEvent> for TransferTitle2 {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct AuthAddress {
component: InputColor,
}
impl AuthAddress {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Ip Address",
IdTheme::AuthAddress,
value,
Msg::Theme(ThemeMsg::AuthAddressBlurDown),
Msg::Theme(ThemeMsg::AuthAddressBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthAddress {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthBookmarks {
component: InputColor,
}
impl AuthBookmarks {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Bookmarks",
IdTheme::AuthBookmarks,
value,
Msg::Theme(ThemeMsg::AuthBookmarksBlurDown),
Msg::Theme(ThemeMsg::AuthBookmarksBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthBookmarks {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthPassword {
component: InputColor,
}
impl AuthPassword {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Password",
IdTheme::AuthPassword,
value,
Msg::Theme(ThemeMsg::AuthPasswordBlurDown),
Msg::Theme(ThemeMsg::AuthPasswordBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthPassword {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthPort {
component: InputColor,
}
impl AuthPort {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Port",
IdTheme::AuthPort,
value,
Msg::Theme(ThemeMsg::AuthPortBlurDown),
Msg::Theme(ThemeMsg::AuthPortBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthPort {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthProtocol {
component: InputColor,
}
impl AuthProtocol {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Protocol",
IdTheme::AuthProtocol,
value,
Msg::Theme(ThemeMsg::AuthProtocolBlurDown),
Msg::Theme(ThemeMsg::AuthProtocolBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthProtocol {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthRecentHosts {
component: InputColor,
}
impl AuthRecentHosts {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Recent connections",
IdTheme::AuthRecentHosts,
value,
Msg::Theme(ThemeMsg::AuthRecentHostsBlurDown),
Msg::Theme(ThemeMsg::AuthRecentHostsBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthRecentHosts {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthUsername {
component: InputColor,
}
impl AuthUsername {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Username",
IdTheme::AuthUsername,
value,
Msg::Theme(ThemeMsg::AuthUsernameBlurDown),
Msg::Theme(ThemeMsg::AuthUsernameBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerLocalBg {
component: InputColor,
}
impl ExplorerLocalBg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Local explorer background",
IdTheme::ExplorerLocalBg,
value,
Msg::Theme(ThemeMsg::ExplorerLocalBgBlurDown),
Msg::Theme(ThemeMsg::ExplorerLocalBgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerLocalBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerLocalFg {
component: InputColor,
}
impl ExplorerLocalFg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Local explorer foreground",
IdTheme::ExplorerLocalFg,
value,
Msg::Theme(ThemeMsg::ExplorerLocalFgBlurDown),
Msg::Theme(ThemeMsg::ExplorerLocalFgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerLocalFg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerLocalHg {
component: InputColor,
}
impl ExplorerLocalHg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Local explorer highlighted",
IdTheme::ExplorerLocalHg,
value,
Msg::Theme(ThemeMsg::ExplorerLocalHgBlurDown),
Msg::Theme(ThemeMsg::ExplorerLocalHgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerLocalHg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerRemoteBg {
component: InputColor,
}
impl ExplorerRemoteBg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Remote explorer background",
IdTheme::ExplorerRemoteBg,
value,
Msg::Theme(ThemeMsg::ExplorerRemoteBgBlurDown),
Msg::Theme(ThemeMsg::ExplorerRemoteBgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerRemoteBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerRemoteFg {
component: InputColor,
}
impl ExplorerRemoteFg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Remote explorer foreground",
IdTheme::ExplorerRemoteFg,
value,
Msg::Theme(ThemeMsg::ExplorerRemoteFgBlurDown),
Msg::Theme(ThemeMsg::ExplorerRemoteFgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerRemoteFg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerRemoteHg {
component: InputColor,
}
impl ExplorerRemoteHg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Remote explorer highlighted",
IdTheme::ExplorerRemoteHg,
value,
Msg::Theme(ThemeMsg::ExplorerRemoteHgBlurDown),
Msg::Theme(ThemeMsg::ExplorerRemoteHgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerRemoteHg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct LogBg {
component: InputColor,
}
impl LogBg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Log window background",
IdTheme::LogBg,
value,
Msg::Theme(ThemeMsg::LogBgBlurDown),
Msg::Theme(ThemeMsg::LogBgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for LogBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct LogWindow {
component: InputColor,
}
impl LogWindow {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Log window",
IdTheme::LogWindow,
value,
Msg::Theme(ThemeMsg::LogWindowBlurDown),
Msg::Theme(ThemeMsg::LogWindowBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for LogWindow {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscError {
component: InputColor,
}
impl MiscError {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Error",
IdTheme::MiscError,
value,
Msg::Theme(ThemeMsg::MiscErrorBlurDown),
Msg::Theme(ThemeMsg::MiscErrorBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscError {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscInfo {
component: InputColor,
}
impl MiscInfo {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Info",
IdTheme::MiscInfo,
value,
Msg::Theme(ThemeMsg::MiscInfoBlurDown),
Msg::Theme(ThemeMsg::MiscInfoBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscInfo {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscInput {
component: InputColor,
}
impl MiscInput {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Input",
IdTheme::MiscInput,
value,
Msg::Theme(ThemeMsg::MiscInputBlurDown),
Msg::Theme(ThemeMsg::MiscInputBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscInput {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscKeys {
component: InputColor,
}
impl MiscKeys {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Key strokes",
IdTheme::MiscKeys,
value,
Msg::Theme(ThemeMsg::MiscKeysBlurDown),
Msg::Theme(ThemeMsg::MiscKeysBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscKeys {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscQuit {
component: InputColor,
}
impl MiscQuit {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Quit dialogs",
IdTheme::MiscQuit,
value,
Msg::Theme(ThemeMsg::MiscQuitBlurDown),
Msg::Theme(ThemeMsg::MiscQuitBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscQuit {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscSave {
component: InputColor,
}
impl MiscSave {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Save confirmations",
IdTheme::MiscSave,
value,
Msg::Theme(ThemeMsg::MiscSaveBlurDown),
Msg::Theme(ThemeMsg::MiscSaveBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscSave {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscWarn {
component: InputColor,
}
impl MiscWarn {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Warnings",
IdTheme::MiscWarn,
value,
Msg::Theme(ThemeMsg::MiscWarnBlurDown),
Msg::Theme(ThemeMsg::MiscWarnBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscWarn {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ProgBarFull {
component: InputColor,
}
impl ProgBarFull {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"'Full transfer' Progress bar",
IdTheme::ProgBarFull,
value,
Msg::Theme(ThemeMsg::ProgBarFullBlurDown),
Msg::Theme(ThemeMsg::ProgBarFullBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ProgBarFull {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ProgBarPartial {
component: InputColor,
}
impl ProgBarPartial {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"'Partial transfer' Progress bar",
IdTheme::ProgBarPartial,
value,
Msg::Theme(ThemeMsg::ProgBarPartialBlurDown),
Msg::Theme(ThemeMsg::ProgBarPartialBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ProgBarPartial {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct StatusHidden {
component: InputColor,
}
impl StatusHidden {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Hidden files",
IdTheme::StatusHidden,
value,
Msg::Theme(ThemeMsg::StatusHiddenBlurDown),
Msg::Theme(ThemeMsg::StatusHiddenBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for StatusHidden {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct StatusSorting {
component: InputColor,
}
impl StatusSorting {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"File sorting",
IdTheme::StatusSorting,
value,
Msg::Theme(ThemeMsg::StatusSortingBlurDown),
Msg::Theme(ThemeMsg::StatusSortingBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for StatusSorting {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct StatusSync {
component: InputColor,
}
impl StatusSync {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Synchronized browsing",
IdTheme::StatusSync,
value,
Msg::Theme(ThemeMsg::StatusSyncBlurDown),
Msg::Theme(ThemeMsg::StatusSyncBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for StatusSync {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
// -- input color
#[derive(MockComponent)]
struct InputColor {
component: Input,
id: IdTheme,
on_key_down: Msg,
on_key_up: Msg,
}
impl InputColor {
pub fn new(name: &str, id: IdTheme, color: Color, on_key_down: Msg, on_key_up: Msg) -> Self {
let value = crate::utils::fmt::fmt_color(&color);
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.input_type(InputType::Color)
.placeholder("#aa33ee", Style::default().fg(Color::Rgb(128, 128, 128)))
.title(name, Alignment::Left)
.value(value),
id,
on_key_down,
on_key_up,
}
}
fn update_color(&mut self, result: CmdResult) -> Option<Msg> {
if let CmdResult::Changed(State::One(StateValue::String(color))) = result {
let color = tuirealm::utils::parser::parse_color(&color).unwrap();
self.attr(Attribute::Foreground, AttrValue::Color(color));
self.attr(
Attribute::Borders,
AttrValue::Borders(
Borders::default()
.modifiers(BorderType::Rounded)
.color(color),
),
);
Some(Msg::Theme(ThemeMsg::ColorChanged(self.id.clone(), color)))
} else {
self.attr(Attribute::Foreground, AttrValue::Color(Color::Red));
self.attr(
Attribute::Borders,
AttrValue::Borders(
Borders::default()
.modifiers(BorderType::Rounded)
.color(Color::Red),
),
);
Some(Msg::None)
}
}
}
impl Component<Msg, NoUserEvent> for InputColor {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
let result = self.perform(Cmd::Cancel);
self.update_color(result)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
let result = self.perform(Cmd::Delete);
self.update_color(result)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
let result = self.perform(Cmd::Type(ch));
self.update_color(result)
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(self.on_key_down.clone()),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(self.on_key_up.clone()),
_ => None,
}
}
}

View File

@@ -29,7 +29,6 @@
// Locals
use super::SetupActivity;
// Ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::env;
impl SetupActivity {
@@ -94,11 +93,15 @@ impl SetupActivity {
// Set editor if config client exists
env::set_var("EDITOR", ctx.config().get_text_editor());
// Prepare terminal
if let Err(err) = disable_raw_mode() {
if let Err(err) = ctx.terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
ctx.leave_alternate_screen();
if let Err(err) = ctx.terminal().leave_alternate_screen() {
error!("Could not leave alternate screen: {}", err);
}
// Lock ports
assert!(self.app.lock_ports().is_ok());
// Get result
let result: Result<(), String> = match ctx.config().iter_ssh_keys().nth(idx) {
Some(key) => {
@@ -120,13 +123,19 @@ impl SetupActivity {
};
// Restore terminal
// Clear screen
ctx.clear_screen();
if let Err(err) = ctx.terminal().clear_screen() {
error!("Could not clear screen screen: {}", err);
}
// Enter alternate mode
ctx.enter_alternate_screen();
if let Err(err) = ctx.terminal().enter_alternate_screen() {
error!("Could not enter alternate screen: {}", err);
}
// Re-enable raw mode
if let Err(err) = enable_raw_mode() {
if let Err(err) = ctx.terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Unlock ports
assert!(self.app.unlock_ports().is_ok());
// Return result
result
}

View File

@@ -28,6 +28,7 @@
*/
// Submodules
mod actions;
mod components;
mod config;
mod update;
mod view;
@@ -38,71 +39,209 @@ use crate::config::themes::Theme;
use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider;
// Ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::{Update, View};
use std::time::Duration;
use tuirealm::listener::EventListenerCfg;
use tuirealm::props::Color;
use tuirealm::{application::PollStrategy, Application, NoUserEvent, Update};
// -- components
// -- common
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_RADIO_TAB: &str = "RADIO_TAB";
// -- config
const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR";
const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL";
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLACE";
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
const COMPONENT_RADIO_NOTIFICATIONS_ENABLED: &str = "RADIO_NOTIFICATIONS_ENABLED";
const COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD: &str = "INPUT_NOTIFICATIONS_THRESHOLD";
// -- ssh keys
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";
// -- theme
const COMPONENT_COLOR_AUTH_TITLE: &str = "COMPONENT_COLOR_AUTH_TITLE";
const COMPONENT_COLOR_MISC_TITLE: &str = "COMPONENT_COLOR_MISC_TITLE";
const COMPONENT_COLOR_TRANSFER_TITLE: &str = "COMPONENT_COLOR_TRANSFER_TITLE";
const COMPONENT_COLOR_TRANSFER_TITLE_2: &str = "COMPONENT_COLOR_TRANSFER_TITLE_2";
const COMPONENT_COLOR_AUTH_ADDR: &str = "COMPONENT_COLOR_AUTH_ADDR";
const COMPONENT_COLOR_AUTH_BOOKMARKS: &str = "COMPONENT_COLOR_AUTH_BOOKMARKS";
const COMPONENT_COLOR_AUTH_PASSWORD: &str = "COMPONENT_COLOR_AUTH_PASSWORD";
const COMPONENT_COLOR_AUTH_PORT: &str = "COMPONENT_COLOR_AUTH_PORT";
const COMPONENT_COLOR_AUTH_PROTOCOL: &str = "COMPONENT_COLOR_AUTH_PROTOCOL";
const COMPONENT_COLOR_AUTH_RECENTS: &str = "COMPONENT_COLOR_AUTH_RECENTS";
const COMPONENT_COLOR_AUTH_USERNAME: &str = "COMPONENT_COLOR_AUTH_USERNAME";
const COMPONENT_COLOR_MISC_ERROR: &str = "COMPONENT_COLOR_MISC_ERROR";
const COMPONENT_COLOR_MISC_INFO: &str = "COMPONENT_COLOR_MISC_INFO";
const COMPONENT_COLOR_MISC_INPUT: &str = "COMPONENT_COLOR_MISC_INPUT";
const COMPONENT_COLOR_MISC_KEYS: &str = "COMPONENT_COLOR_MISC_KEYS";
const COMPONENT_COLOR_MISC_QUIT: &str = "COMPONENT_COLOR_MISC_QUIT";
const COMPONENT_COLOR_MISC_SAVE: &str = "COMPONENT_COLOR_MISC_SAVE";
const COMPONENT_COLOR_MISC_WARN: &str = "COMPONENT_COLOR_MISC_WARN";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG";
const COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL";
const COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL";
const COMPONENT_COLOR_TRANSFER_LOG_BG: &str = "COMPONENT_COLOR_TRANSFER_LOG_BG";
const COMPONENT_COLOR_TRANSFER_LOG_WIN: &str = "COMPONENT_COLOR_TRANSFER_LOG_WIN";
const COMPONENT_COLOR_TRANSFER_STATUS_SORTING: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SORTING";
const COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN: &str = "COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN";
const COMPONENT_COLOR_TRANSFER_STATUS_SYNC: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SYNC";
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum Id {
Common(IdCommon),
Config(IdConfig),
Ssh(IdSsh),
Theme(IdTheme),
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum IdCommon {
ErrorPopup,
Footer,
GlobalListener,
Header,
Keybindings,
QuitPopup,
SavePopup,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum IdConfig {
CheckUpdates,
DefaultProtocol,
GroupDirs,
HiddenFiles,
LocalFileFmt,
NotificationsEnabled,
NotificationsThreshold,
PromptOnFileReplace,
RemoteFileFmt,
TextEditor,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum IdSsh {
DelSshKeyPopup,
SshHost,
SshKeys,
SshUsername,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum IdTheme {
AuthAddress,
AuthBookmarks,
AuthPassword,
AuthPort,
AuthProtocol,
AuthRecentHosts,
AuthTitle,
AuthUsername,
ExplorerLocalBg,
ExplorerLocalFg,
ExplorerLocalHg,
ExplorerRemoteBg,
ExplorerRemoteFg,
ExplorerRemoteHg,
LogBg,
LogWindow,
MiscError,
MiscInfo,
MiscInput,
MiscKeys,
MiscQuit,
MiscSave,
MiscTitle,
MiscWarn,
ProgBarFull,
ProgBarPartial,
StatusHidden,
StatusSorting,
StatusSync,
TransferTitle,
TransferTitle2,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Msg {
Common(CommonMsg),
Config(ConfigMsg),
Ssh(SshMsg),
Theme(ThemeMsg),
None,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CommonMsg {
ChangeLayout,
CloseErrorPopup,
CloseKeybindingsPopup,
CloseQuitPopup,
CloseSavePopup,
Quit,
RevertChanges,
SaveAndQuit,
SaveConfig,
ShowKeybindings,
ShowQuitPopup,
ShowSavePopup,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigMsg {
CheckUpdatesBlurDown,
CheckUpdatesBlurUp,
ConfigChanged,
DefaultProtocolBlurDown,
DefaultProtocolBlurUp,
GroupDirsBlurDown,
GroupDirsBlurUp,
HiddenFilesBlurDown,
HiddenFilesBlurUp,
LocalFileFmtBlurDown,
LocalFileFmtBlurUp,
NotificationsEnabledBlurDown,
NotificationsEnabledBlurUp,
NotificationsThresholdBlurDown,
NotificationsThresholdBlurUp,
PromptOnFileReplaceBlurDown,
PromptOnFileReplaceBlurUp,
RemoteFileFmtBlurDown,
RemoteFileFmtBlurUp,
TextEditorBlurDown,
TextEditorBlurUp,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SshMsg {
CloseDelSshKeyPopup,
CloseNewSshKeyPopup,
DeleteSshKey,
EditSshKey(usize),
SaveSshKey,
ShowDelSshKeyPopup,
ShowNewSshKeyPopup,
SshHostBlur,
SshUsernameBlur,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ThemeMsg {
AuthAddressBlurDown,
AuthAddressBlurUp,
AuthBookmarksBlurDown,
AuthBookmarksBlurUp,
AuthPasswordBlurDown,
AuthPasswordBlurUp,
AuthPortBlurDown,
AuthPortBlurUp,
AuthProtocolBlurDown,
AuthProtocolBlurUp,
AuthRecentHostsBlurDown,
AuthRecentHostsBlurUp,
AuthUsernameBlurDown,
AuthUsernameBlurUp,
ColorChanged(IdTheme, Color),
ExplorerLocalBgBlurDown,
ExplorerLocalBgBlurUp,
ExplorerLocalFgBlurDown,
ExplorerLocalFgBlurUp,
ExplorerLocalHgBlurDown,
ExplorerLocalHgBlurUp,
ExplorerRemoteBgBlurDown,
ExplorerRemoteBgBlurUp,
ExplorerRemoteFgBlurDown,
ExplorerRemoteFgBlurUp,
ExplorerRemoteHgBlurDown,
ExplorerRemoteHgBlurUp,
LogBgBlurDown,
LogBgBlurUp,
LogWindowBlurDown,
LogWindowBlurUp,
MiscErrorBlurDown,
MiscErrorBlurUp,
MiscInfoBlurDown,
MiscInfoBlurUp,
MiscInputBlurDown,
MiscInputBlurUp,
MiscKeysBlurDown,
MiscKeysBlurUp,
MiscQuitBlurDown,
MiscQuitBlurUp,
MiscSaveBlurDown,
MiscSaveBlurUp,
MiscWarnBlurDown,
MiscWarnBlurUp,
ProgBarFullBlurDown,
ProgBarFullBlurUp,
ProgBarPartialBlurDown,
ProgBarPartialBlurUp,
StatusHiddenBlurDown,
StatusHiddenBlurUp,
StatusSortingBlurDown,
StatusSortingBlurUp,
StatusSyncBlurDown,
StatusSyncBlurUp,
}
// -- store
const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED";
@@ -110,8 +249,8 @@ const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED";
/// ### ViewLayout
///
/// Current view layout
#[derive(std::cmp::PartialEq)]
enum ViewLayout {
#[derive(PartialEq)]
pub enum ViewLayout {
SetupForm,
SshKeys,
Theme,
@@ -121,26 +260,28 @@ enum ViewLayout {
///
/// Setup activity states holder
pub struct SetupActivity {
app: Application<Id, Msg, NoUserEvent>,
exit_reason: Option<ExitReason>,
context: Option<Context>, // Context holder
view: View, // View
layout: ViewLayout, // View layout
redraw: bool,
}
impl Default for SetupActivity {
fn default() -> Self {
SetupActivity {
impl SetupActivity {
pub fn new(ticks: Duration) -> Self {
Self {
app: Application::init(
EventListenerCfg::default()
.default_input_listener(ticks)
.poll_timeout(ticks),
),
exit_reason: None,
context: None,
view: View::init(),
layout: ViewLayout::SetupForm,
redraw: true, // Draw at first `on_draw`
}
}
}
impl SetupActivity {
/// ### context
///
/// Returns a reference to context
@@ -205,11 +346,13 @@ impl Activity for SetupActivity {
// Set context
self.context = Some(context);
// Clear terminal
self.context.as_mut().unwrap().clear_screen();
if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
// Set config changed to false
self.set_config_changed(false);
// Put raw mode on enabled
if let Err(err) = enable_raw_mode() {
if let Err(err) = self.context_mut().terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Init view
@@ -229,20 +372,25 @@ impl Activity for SetupActivity {
if self.context.is_none() {
return;
}
// Read one event
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
// Set redraw to true
self.redraw = true;
// Handle event
let msg = self.view.on(event);
self.update(msg);
match self.app.tick(PollStrategy::UpTo(3)) {
Ok(messages) => {
if !messages.is_empty() {
self.redraw = true;
}
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = self.update(msg);
}
}
}
Err(err) => {
self.mount_error(format!("Application error: {}", err));
}
}
// Redraw if necessary
// View
if self.redraw {
// View
self.view();
// Redraw back to false
self.redraw = false;
}
}
@@ -262,17 +410,12 @@ impl Activity for SetupActivity {
/// This function finally releases the context
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
if let Err(err) = disable_raw_mode() {
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
self.context.as_ref()?;
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
ctx.clear_screen();
Some(ctx)
}
None => None,
if let Err(err) = self.context_mut().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
self.context.take()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,18 +31,15 @@ pub mod ssh_keys;
pub mod theme;
use super::*;
use crate::utils::ui::draw_area_in;
pub use setup::*;
pub use ssh_keys::*;
pub use theme::*;
// Ext
use tui_realm_stdlib::{
Input, InputPropsBuilder, List, ListPropsBuilder, Paragraph, ParagraphPropsBuilder, Radio,
RadioPropsBuilder, Span, SpanPropsBuilder,
};
use tuirealm::props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan};
use tuirealm::tui::{
style::Color,
widgets::{BorderType, Borders},
use tuirealm::tui::widgets::Clear;
use tuirealm::{
event::{Key, KeyEvent, KeyModifiers},
Frame, Sub, SubClause, SubEventClause,
};
impl SetupActivity {
@@ -61,6 +58,7 @@ impl SetupActivity {
///
/// View gui
pub(super) fn view(&mut self) {
self.redraw = false;
match self.layout {
ViewLayout::SetupForm => self.view_setup(),
ViewLayout::SshKeys => self.view_ssh_keys(),
@@ -73,238 +71,229 @@ impl SetupActivity {
/// ### mount_error
///
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, Color::Red);
pub(super) fn mount_error<S: AsRef<str>>(&mut self, text: S) {
assert!(self
.app
.remount(
Id::Common(IdCommon::ErrorPopup),
Box::new(components::ErrorPopup::new(text)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::Common(IdCommon::ErrorPopup)).is_ok());
}
/// ### umount_error
///
/// Umount error message
pub(super) fn umount_error(&mut self) {
self.view.umount(super::COMPONENT_TEXT_ERROR);
let _ = self.app.umount(&Id::Common(IdCommon::ErrorPopup));
}
/// ### mount_quit
///
/// Mount quit popup
pub(super) fn mount_quit(&mut self) {
self.mount_radio_dialog(
super::COMPONENT_RADIO_QUIT,
"There are unsaved changes! Save changes before leaving?",
&["Save", "Don't save", "Cancel"],
0,
Color::LightRed,
);
assert!(self
.app
.remount(
Id::Common(IdCommon::QuitPopup),
Box::new(components::QuitPopup::default()),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::Common(IdCommon::QuitPopup)).is_ok());
}
/// ### umount_quit
///
/// Umount quit
pub(super) fn umount_quit(&mut self) {
self.view.umount(super::COMPONENT_RADIO_QUIT);
let _ = self.app.umount(&Id::Common(IdCommon::QuitPopup));
}
/// ### mount_save_popup
///
/// Mount save popup
pub(super) fn mount_save_popup(&mut self) {
self.mount_radio_dialog(
super::COMPONENT_RADIO_SAVE,
"Save changes?",
&["Yes", "No"],
0,
Color::LightYellow,
);
assert!(self
.app
.remount(
Id::Common(IdCommon::SavePopup),
Box::new(components::SavePopup::default()),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::Common(IdCommon::SavePopup)).is_ok());
}
/// ### umount_quit
///
/// Umount quit
pub(super) fn umount_save_popup(&mut self) {
self.view.umount(super::COMPONENT_RADIO_SAVE);
}
pub(self) fn mount_header_tab(&mut self, idx: usize) {
self.view.mount(
super::COMPONENT_RADIO_TAB,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::White)
.with_options(&[
String::from("User Interface"),
String::from("SSH Keys"),
String::from("Theme"),
])
.with_value(idx)
.rewind(true)
.build(),
)),
);
}
pub(self) fn mount_footer(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpan::new("Press ").bold(),
TextSpan::new("<CTRL+H>").bold().fg(Color::Cyan),
TextSpan::new(" to show keybindings").bold(),
])
.build(),
)),
);
let _ = self.app.umount(&Id::Common(IdCommon::SavePopup));
}
/// ### mount_help
///
/// Mount help
pub(super) fn mount_help(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(List::new(
ListPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_highlighted_str(Some("?"))
.with_max_scroll_step(8)
.bold()
.with_title("Help", Alignment::Center)
.scrollable(true)
.with_rows(
TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Exit setup"))
.add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change setup page"))
.add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change cursor"))
.add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change input field"))
.add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Select / Dismiss popup"))
.add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Delete SSH key"))
.add_row()
.add_col(TextSpan::new("<CTRL+N>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" New SSH key"))
.add_row()
.add_col(TextSpan::new("<CTRL+R>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Revert changes"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Save configuration"))
.build(),
)
.build(),
)),
);
// Active help
self.view.active(super::COMPONENT_TEXT_HELP);
assert!(self
.app
.remount(
Id::Common(IdCommon::Keybindings),
Box::new(components::Keybindings::default()),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::Common(IdCommon::Keybindings)).is_ok());
}
/// ### umount_help
///
/// Umount help
pub(super) fn umount_help(&mut self) {
self.view.umount(super::COMPONENT_TEXT_HELP);
let _ = self.app.umount(&Id::Common(IdCommon::Keybindings));
}
// -- mount helpers
fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) {
// Mount
self.view.mount(
id,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Thick, color)
.with_foreground(color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(id);
}
fn mount_radio_dialog(
&mut self,
id: &str,
text: &str,
opts: &[&str],
default: usize,
color: Color,
) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text, Alignment::Center)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
// Active
self.view.active(id);
}
fn mount_radio(&mut self, id: &str, text: &str, opts: &[&str], default: usize, color: Color) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text, Alignment::Left)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
}
fn mount_input(&mut self, id: &str, label: &str, fg: Color, typ: InputType) {
self.mount_input_ex(id, label, fg, typ, None, None);
}
fn mount_input_ex(
&mut self,
id: &str,
label: &str,
fg: Color,
typ: InputType,
len: Option<usize>,
value: Option<String>,
) {
let mut props = InputPropsBuilder::default();
props
.with_foreground(fg)
.with_borders(Borders::ALL, BorderType::Rounded, fg)
.with_label(label, Alignment::Left)
.with_input(typ);
if let Some(len) = len {
props.with_input_len(len);
pub(super) fn view_popups(&mut self, f: &mut Frame) {
if self.app.mounted(&Id::Common(IdCommon::ErrorPopup)) {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::Common(IdCommon::ErrorPopup), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::QuitPopup)) {
// make popup
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::QuitPopup), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::Keybindings)) {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::Keybindings), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::SavePopup)) {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::SavePopup), f, popup);
}
if let Some(value) = value {
props.with_value(value);
}
self.view.mount(id, Box::new(Input::new(props.build())));
}
/// ### new_app
///
/// Clean app up and remount common components and global listener
fn new_app(&mut self, layout: ViewLayout) {
self.app.umount_all();
self.mount_global_listener();
self.mount_commons(layout);
}
/// ### mount_commons
///
/// Mount common components
fn mount_commons(&mut self, layout: ViewLayout) {
// Radio tab
assert!(self
.app
.remount(
Id::Common(IdCommon::Header),
Box::new(components::Header::new(layout)),
vec![],
)
.is_ok());
// Footer
assert!(self
.app
.remount(
Id::Common(IdCommon::Footer),
Box::new(components::Footer::default()),
vec![],
)
.is_ok());
}
/// ### mount_global_listener
///
/// Mount global listener
fn mount_global_listener(&mut self) {
assert!(self
.app
.mount(
Id::Common(IdCommon::GlobalListener),
Box::new(components::GlobalListener::default()),
vec![
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Esc,
modifiers: KeyModifiers::NONE,
}),
Self::no_popup_mounted_clause(),
),
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}),
Self::no_popup_mounted_clause(),
),
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Char('h'),
modifiers: KeyModifiers::CONTROL,
}),
Self::no_popup_mounted_clause(),
),
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Char('r'),
modifiers: KeyModifiers::CONTROL,
}),
Self::no_popup_mounted_clause(),
),
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}),
Self::no_popup_mounted_clause(),
),
]
)
.is_ok());
}
/// ### no_popup_mounted_clause
///
/// Returns a sub clause which requires that no popup is mounted in order to be satisfied
fn no_popup_mounted_clause() -> SubClause<Id> {
SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::ErrorPopup,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::Keybindings,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::QuitPopup,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::SavePopup,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh(
IdSsh::DelSshKeyPopup,
))))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh(
IdSsh::SshHost,
))))),
)),
)),
)),
)),
)
}
}

View File

@@ -27,23 +27,15 @@
* SOFTWARE.
*/
// Locals
use super::{Context, InputType, SetupActivity};
use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
use crate::ui::components::bytes::{Bytes, BytesPropsBuilder};
use crate::utils::ui::draw_area_in;
use crate::utils::fmt::fmt_bytes;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{Alignment, PropsBuilder},
Payload, Value, View,
};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::{State, StateValue};
impl SetupActivity {
// -- view
@@ -52,92 +44,17 @@ impl SetupActivity {
///
/// Initialize setup view
pub(super) fn init_setup(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.mount_header_tab(0);
// Footer
self.mount_footer();
// Input fields
self.mount_input(
super::COMPONENT_INPUT_TEXT_EDITOR,
"Text editor",
Color::LightGreen,
InputType::Text,
);
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
self.mount_radio(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
"Default protocol",
&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"],
0,
Color::LightCyan,
);
self.mount_radio(
super::COMPONENT_RADIO_HIDDEN_FILES,
"Show hidden files (by default)?",
&["Yes", "No"],
0,
Color::LightRed,
);
self.mount_radio(
super::COMPONENT_RADIO_UPDATES,
"Check for updates?",
&["Yes", "No"],
0,
Color::LightYellow,
);
self.mount_radio(
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
"Prompt when replacing existing files?",
&["Yes", "No"],
0,
Color::LightCyan,
);
self.mount_radio(
super::COMPONENT_RADIO_GROUP_DIRS,
"Group directories",
&["Display first", "Display last", "No"],
0,
Color::LightMagenta,
);
self.mount_input(
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
"File formatter syntax (local)",
Color::LightGreen,
InputType::Text,
);
self.mount_input(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
"File formatter syntax (remote)",
Color::LightCyan,
InputType::Text,
);
self.mount_radio(
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
"Enable notifications?",
&["Yes", "No"],
0,
Color::LightRed,
);
self.view.mount(
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
Box::new(Bytes::new(
BytesPropsBuilder::default()
.with_foreground(Color::LightYellow)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_label("Notifications: minimum transfer size", Alignment::Left)
.build(),
)),
);
// Init view (and mount commons)
self.new_app(ViewLayout::SetupForm);
// Load values
self.load_input_values();
// Active text editor
assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok());
}
pub(super) fn view_setup(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal().draw(|f| {
let _ = ctx.terminal().raw_mut().draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -152,8 +69,8 @@ impl SetupActivity {
)
.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]);
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
// Make chunks (two columns)
let ui_cfg_chunks = Layout::default()
.direction(Direction::Horizontal)
@@ -174,27 +91,27 @@ impl SetupActivity {
.as_ref(),
)
.split(ui_cfg_chunks[0]);
self.view
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks_col1[0]);
self.view.render(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
self.app
.view(&Id::Config(IdConfig::TextEditor), f, ui_cfg_chunks_col1[0]);
self.app.view(
&Id::Config(IdConfig::DefaultProtocol),
f,
ui_cfg_chunks_col1[1],
);
self.view.render(
super::COMPONENT_RADIO_HIDDEN_FILES,
self.app
.view(&Id::Config(IdConfig::HiddenFiles), f, ui_cfg_chunks_col1[2]);
self.app.view(
&Id::Config(IdConfig::CheckUpdates),
f,
ui_cfg_chunks_col1[2],
ui_cfg_chunks_col1[3],
);
self.view
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks_col1[3]);
self.view.render(
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
self.app.view(
&Id::Config(IdConfig::PromptOnFileReplace),
f,
ui_cfg_chunks_col1[4],
);
self.view
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks_col1[5]);
self.app
.view(&Id::Config(IdConfig::GroupDirs), f, ui_cfg_chunks_col1[5]);
// Column 2
let ui_cfg_chunks_col2 = Layout::default()
.direction(Direction::Vertical)
@@ -209,59 +126,28 @@ impl SetupActivity {
.as_ref(),
)
.split(ui_cfg_chunks[1]);
self.view.render(
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
self.app.view(
&Id::Config(IdConfig::LocalFileFmt),
f,
ui_cfg_chunks_col2[0],
);
self.view.render(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
self.app.view(
&Id::Config(IdConfig::RemoteFileFmt),
f,
ui_cfg_chunks_col2[1],
);
self.view.render(
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
self.app.view(
&Id::Config(IdConfig::NotificationsEnabled),
f,
ui_cfg_chunks_col2[2],
);
self.view.render(
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
self.app.view(
&Id::Config(IdConfig::NotificationsThreshold),
f,
ui_cfg_chunks_col2[3],
);
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.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(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.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);
}
}
self.view_popups(f);
});
// Put context back to context
self.context = Some(ctx);
@@ -272,125 +158,127 @@ impl SetupActivity {
/// Load values from configuration into input fields
pub(crate) fn load_input_values(&mut self) {
// Text editor
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
let text_editor: String =
String::from(self.config().get_text_editor().as_path().to_string_lossy());
let props = InputPropsBuilder::from(props)
.with_value(text_editor)
.build();
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
}
let text_editor: String =
String::from(self.config().get_text_editor().as_path().to_string_lossy());
assert!(self
.app
.remount(
Id::Config(IdConfig::TextEditor),
Box::new(components::TextEditor::new(text_editor.as_str())),
vec![]
)
.is_ok());
// Protocol
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
let protocol: usize = match self.config().get_default_protocol() {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
};
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::DefaultProtocol),
Box::new(components::DefaultProtocol::new(
self.config().get_default_protocol()
)),
vec![]
)
.is_ok());
// Hidden files
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
let hidden: usize = match self.config().get_show_hidden_files() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::HiddenFiles),
Box::new(components::HiddenFiles::new(
self.config().get_show_hidden_files()
)),
vec![]
)
.is_ok());
// Updates
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
let updates: usize = match self.config().get_check_for_updates() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::CheckUpdates),
Box::new(components::CheckUpdates::new(
self.config().get_check_for_updates()
)),
vec![]
)
.is_ok());
// File replace
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
{
let updates: usize = match self.config().get_prompt_on_file_replace() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::PromptOnFileReplace),
Box::new(components::PromptOnFileReplace::new(
self.config().get_prompt_on_file_replace()
)),
vec![]
)
.is_ok());
// Group dirs
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
let dirs: usize = match self.config().get_group_dirs() {
Some(GroupDirs::First) => 0,
Some(GroupDirs::Last) => 1,
None => 2,
};
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::GroupDirs),
Box::new(components::GroupDirs::new(self.config().get_group_dirs())),
vec![]
)
.is_ok());
// Local File Fmt
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) {
let file_fmt: String = self.config().get_local_file_fmt().unwrap_or_default();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::LocalFileFmt),
Box::new(components::LocalFileFmt::new(
&self.config().get_local_file_fmt().unwrap_or_default()
)),
vec![]
)
.is_ok());
// Remote File Fmt
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) {
let file_fmt: String = self.config().get_remote_file_fmt().unwrap_or_default();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::RemoteFileFmt),
Box::new(components::RemoteFileFmt::new(
&self.config().get_remote_file_fmt().unwrap_or_default()
)),
vec![]
)
.is_ok());
// Notifications enabled
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
{
let enabled: usize = match self.config().get_notifications() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(enabled).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::NotificationsEnabled),
Box::new(components::NotificationsEnabled::new(
self.config().get_notifications()
)),
vec![]
)
.is_ok());
// Notifications threshold
if let Some(props) = self
.view
.get_props(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
{
let value: u64 = self.config().get_notification_threshold();
let props = BytesPropsBuilder::from(props).with_value(value).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::NotificationsThreshold),
Box::new(components::NotificationsThreshold::new(&fmt_bytes(
self.config().get_notification_threshold()
))),
vec![]
)
.is_ok());
}
/// ### collect_input_values
///
/// Collect values from input and put them into the configuration
pub(crate) fn collect_input_values(&mut self) {
if let Some(Payload::One(Value::Str(editor))) =
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
if let Ok(State::One(StateValue::String(editor))) =
self.app.state(&Id::Config(IdConfig::TextEditor))
{
self.config_mut()
.set_text_editor(PathBuf::from(editor.as_str()));
}
if let Some(Payload::One(Value::Usize(protocol))) =
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
if let Ok(State::One(StateValue::Usize(protocol))) =
self.app.state(&Id::Config(IdConfig::DefaultProtocol))
{
let protocol: FileTransferProtocol = match protocol {
1 => FileTransferProtocol::Scp,
@@ -401,37 +289,36 @@ impl SetupActivity {
};
self.config_mut().set_default_protocol(protocol);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::HiddenFiles))
{
let show: bool = matches!(opt, 0);
self.config_mut().set_show_hidden_files(show);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::CheckUpdates))
{
let check: bool = matches!(opt, 0);
self.config_mut().set_check_for_updates(check);
}
if let Some(Payload::One(Value::Usize(opt))) = self
.view
.get_state(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::PromptOnFileReplace))
{
let check: bool = matches!(opt, 0);
self.config_mut().set_prompt_on_file_replace(check);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
if let Ok(State::One(StateValue::String(fmt))) =
self.app.state(&Id::Config(IdConfig::LocalFileFmt))
{
self.config_mut().set_local_file_fmt(fmt);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT)
if let Ok(State::One(StateValue::String(fmt))) =
self.app.state(&Id::Config(IdConfig::RemoteFileFmt))
{
self.config_mut().set_remote_file_fmt(fmt);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::GroupDirs))
{
let dirs: Option<GroupDirs> = match opt {
0 => Some(GroupDirs::First),
@@ -440,15 +327,14 @@ impl SetupActivity {
};
self.config_mut().set_group_dirs(dirs);
}
if let Some(Payload::One(Value::Usize(opt))) = self
.view
.get_state(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::NotificationsEnabled))
{
self.config_mut().set_notifications(opt == 0);
}
if let Some(Payload::One(Value::U64(bytes))) = self
.view
.get_state(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
if let Ok(State::One(StateValue::U64(bytes))) = self
.app
.state(&Id::Config(IdConfig::NotificationsThreshold))
{
self.config_mut().set_notification_threshold(bytes);
}

View File

@@ -27,20 +27,12 @@
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity};
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use super::{components, Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout};
use crate::utils::ui::draw_area_in;
// Ext
use tui_realm_stdlib::{Input, InputPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{Alignment, PropsBuilder},
View,
};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear;
impl SetupActivity {
// -- view
@@ -49,34 +41,17 @@ impl SetupActivity {
///
/// Initialize ssh keys view
pub(super) fn init_ssh_keys(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
// Radio tab
self.mount_header_tab(1);
// Footer
self.mount_footer();
self.view.mount(
super::COMPONENT_LIST_SSH_KEYS,
Box::new(BookmarkList::new(
BookmarkListPropsBuilder::default()
.with_title("SSH keys", Alignment::Left)
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.with_background(Color::LightGreen)
.with_foreground(Color::Black)
.build(),
)),
);
// Give focus
self.view.active(super::COMPONENT_LIST_SSH_KEYS);
// Init view (and mount commons)
self.new_app(ViewLayout::SshKeys);
// Load keys
self.reload_ssh_keys();
// Give focus
assert!(self.app.active(&Id::Ssh(IdSsh::SshKeys)).is_ok());
}
pub(crate) fn view_ssh_keys(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal().draw(|f| {
let _ = ctx.terminal().raw_mut().draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -91,72 +66,31 @@ impl SetupActivity {
)
.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]);
self.view
.render(super::COMPONENT_LIST_SSH_KEYS, f, chunks[1]);
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
self.app.view(&Id::Ssh(IdSsh::SshKeys), f, chunks[1]);
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.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(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
if props.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(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
if props.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]);
}
self.view_popups(f);
if self.app.mounted(&Id::Ssh(IdSsh::DelSshKeyPopup)) {
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.app.view(&Id::Ssh(IdSsh::DelSshKeyPopup), f, popup);
} else if self.app.mounted(&Id::Ssh(IdSsh::SshHost)) {
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.app.view(&Id::Ssh(IdSsh::SshHost), f, popup_chunks[0]);
self.app
.view(&Id::Ssh(IdSsh::SshUsername), f, popup_chunks[1]);
}
});
// Put context back to context
@@ -169,82 +103,74 @@ impl SetupActivity {
///
/// Mount delete ssh key component
pub(crate) fn mount_del_ssh_key(&mut self) {
self.mount_radio_dialog(
super::COMPONENT_RADIO_DEL_SSH_KEY,
"Delete key?",
&["Yes", "No"],
1,
Color::LightRed,
);
assert!(self
.app
.remount(
Id::Ssh(IdSsh::DelSshKeyPopup),
Box::new(components::DelSshKeyPopup::default()),
vec![]
)
.is_ok());
assert!(self.app.active(&Id::Ssh(IdSsh::DelSshKeyPopup)).is_ok());
}
/// ### umount_del_ssh_key
///
/// Umount delete ssh key
pub(crate) fn umount_del_ssh_key(&mut self) {
self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY);
let _ = self.app.umount(&Id::Ssh(IdSsh::DelSshKeyPopup));
}
/// ### mount_new_ssh_key
///
/// Mount new ssh key prompt
pub(crate) fn mount_new_ssh_key(&mut self) {
self.view.mount(
super::COMPONENT_INPUT_SSH_HOST,
Box::new(Input::new(
InputPropsBuilder::default()
.with_label("Hostname or address", Alignment::Center)
.with_borders(
Borders::TOP | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_SSH_USERNAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_label("Username", Alignment::Center)
.with_borders(
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
self.view.active(super::COMPONENT_INPUT_SSH_HOST);
assert!(self
.app
.remount(
Id::Ssh(IdSsh::SshHost),
Box::new(components::SshHost::default()),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Ssh(IdSsh::SshUsername),
Box::new(components::SshUsername::default()),
vec![]
)
.is_ok());
assert!(self.app.active(&Id::Ssh(IdSsh::SshHost)).is_ok());
}
/// ### umount_new_ssh_key
///
/// Umount new ssh key prompt
pub(crate) fn umount_new_ssh_key(&mut self) {
self.view.umount(super::COMPONENT_INPUT_SSH_HOST);
self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME);
let _ = self.app.umount(&Id::Ssh(IdSsh::SshUsername));
let _ = self.app.umount(&Id::Ssh(IdSsh::SshHost));
}
/// ### reload_ssh_keys
///
/// Reload ssh keys
pub(crate) fn reload_ssh_keys(&mut self) {
// get props
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
// Create texts
let keys: Vec<String> = self
.config()
.iter_ssh_keys()
.map(|x| {
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap();
format!("{} at {}", addr, username)
})
.collect();
let props = BookmarkListPropsBuilder::from(props)
.with_bookmarks(keys)
.build();
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
}
let keys: Vec<String> = self
.config()
.iter_ssh_keys()
.map(|x| {
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap();
format!("{} at {}", addr, username)
})
.collect();
assert!(self
.app
.remount(
Id::Ssh(IdSsh::SshKeys),
Box::new(components::SshKeys::new(&keys)),
vec![]
)
.is_ok());
}
}

View File

@@ -27,22 +27,10 @@
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity};
use crate::config::themes::Theme;
use crate::ui::components::color_picker::{ColorPicker, ColorPickerPropsBuilder};
use crate::utils::parser::parse_color;
use crate::utils::ui::draw_area_in;
use super::{components, Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout};
// Ext
use tui_realm_stdlib::{Label, LabelPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{Alignment, PropsBuilder},
Payload, Value, View,
};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
impl SetupActivity {
// -- view
@@ -51,96 +39,19 @@ impl SetupActivity {
///
/// Initialize thene view
pub(super) fn init_theme(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.mount_header_tab(2);
// Footer
self.mount_footer();
// auth colors
self.mount_title(super::COMPONENT_COLOR_AUTH_TITLE, "Authentication styles");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PROTOCOL, "Protocol");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_ADDR, "Ip address");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PORT, "Port");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_USERNAME, "Username");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PASSWORD, "Password");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_BOOKMARKS, "Bookmarks");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_RECENTS, "Recent connections");
// Misc
self.mount_title(super::COMPONENT_COLOR_MISC_TITLE, "Misc styles");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_ERROR, "Error");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INFO, "Info dialogs");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INPUT, "Input fields");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_KEYS, "Key strokes");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_QUIT, "Quit dialogs");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_SAVE, "Save confirmations");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_WARN, "Warnings");
// Transfer (1)
self.mount_title(super::COMPONENT_COLOR_TRANSFER_TITLE, "Transfer styles");
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
"Local explorer background",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
"Local explorer foreground",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
"Local explorer highlighted",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
"Remote explorer background",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
"Remote explorer foreground",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
"Remote explorer highlighted",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
"'Full transfer' Progress bar",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
"'Partial transfer' Progress bar",
);
// Transfer (2)
self.mount_title(
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
"Transfer styles (2)",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
"Log window background",
);
self.mount_color_picker(super::COMPONENT_COLOR_TRANSFER_LOG_WIN, "Log window");
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
"File sorting",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
"Hidden files",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
"Synchronized browsing",
);
// Init view (and mount commons)
self.new_app(ViewLayout::Theme);
// Mount titles
self.load_titles();
// Load styles
self.load_styles();
// Active first field
self.view.active(super::COMPONENT_COLOR_AUTH_PROTOCOL);
assert!(self.app.active(&Id::Theme(IdTheme::AuthProtocol)).is_ok());
}
pub(super) fn view_theme(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal().draw(|f| {
let _ = ctx.terminal().raw_mut().draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -155,8 +66,8 @@ impl SetupActivity {
)
.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]);
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
// Make chunks
let colors_layout = Layout::default()
.direction(Direction::Horizontal)
@@ -186,34 +97,22 @@ impl SetupActivity {
.as_ref(),
)
.split(colors_layout[0]);
self.view
.render(super::COMPONENT_COLOR_AUTH_TITLE, f, auth_colors_layout[0]);
self.view.render(
super::COMPONENT_COLOR_AUTH_PROTOCOL,
f,
auth_colors_layout[1],
);
self.view
.render(super::COMPONENT_COLOR_AUTH_ADDR, f, auth_colors_layout[2]);
self.view
.render(super::COMPONENT_COLOR_AUTH_PORT, f, auth_colors_layout[3]);
self.view.render(
super::COMPONENT_COLOR_AUTH_USERNAME,
f,
auth_colors_layout[4],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_PASSWORD,
f,
auth_colors_layout[5],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_BOOKMARKS,
f,
auth_colors_layout[6],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_RECENTS,
self.app
.view(&Id::Theme(IdTheme::AuthTitle), f, auth_colors_layout[0]);
self.app
.view(&Id::Theme(IdTheme::AuthProtocol), f, auth_colors_layout[1]);
self.app
.view(&Id::Theme(IdTheme::AuthAddress), f, auth_colors_layout[2]);
self.app
.view(&Id::Theme(IdTheme::AuthPort), f, auth_colors_layout[3]);
self.app
.view(&Id::Theme(IdTheme::AuthUsername), f, auth_colors_layout[4]);
self.app
.view(&Id::Theme(IdTheme::AuthPassword), f, auth_colors_layout[5]);
self.app
.view(&Id::Theme(IdTheme::AuthBookmarks), f, auth_colors_layout[6]);
self.app.view(
&Id::Theme(IdTheme::AuthRecentHosts),
f,
auth_colors_layout[7],
);
@@ -233,22 +132,22 @@ impl SetupActivity {
.as_ref(),
)
.split(colors_layout[1]);
self.view
.render(super::COMPONENT_COLOR_MISC_TITLE, f, misc_colors_layout[0]);
self.view
.render(super::COMPONENT_COLOR_MISC_ERROR, f, misc_colors_layout[1]);
self.view
.render(super::COMPONENT_COLOR_MISC_INFO, f, misc_colors_layout[2]);
self.view
.render(super::COMPONENT_COLOR_MISC_INPUT, f, misc_colors_layout[3]);
self.view
.render(super::COMPONENT_COLOR_MISC_KEYS, f, misc_colors_layout[4]);
self.view
.render(super::COMPONENT_COLOR_MISC_QUIT, f, misc_colors_layout[5]);
self.view
.render(super::COMPONENT_COLOR_MISC_SAVE, f, misc_colors_layout[6]);
self.view
.render(super::COMPONENT_COLOR_MISC_WARN, f, misc_colors_layout[7]);
self.app
.view(&Id::Theme(IdTheme::MiscTitle), f, misc_colors_layout[0]);
self.app
.view(&Id::Theme(IdTheme::MiscError), f, misc_colors_layout[1]);
self.app
.view(&Id::Theme(IdTheme::MiscInfo), f, misc_colors_layout[2]);
self.app
.view(&Id::Theme(IdTheme::MiscInput), f, misc_colors_layout[3]);
self.app
.view(&Id::Theme(IdTheme::MiscKeys), f, misc_colors_layout[4]);
self.app
.view(&Id::Theme(IdTheme::MiscQuit), f, misc_colors_layout[5]);
self.app
.view(&Id::Theme(IdTheme::MiscSave), f, misc_colors_layout[6]);
self.app
.view(&Id::Theme(IdTheme::MiscWarn), f, misc_colors_layout[7]);
let transfer_colors_layout_col1 = Layout::default()
.direction(Direction::Vertical)
@@ -266,38 +165,38 @@ impl SetupActivity {
.as_ref(),
)
.split(colors_layout[2]);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_TITLE,
self.app.view(
&Id::Theme(IdTheme::TransferTitle),
f,
transfer_colors_layout_col1[0],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
self.app.view(
&Id::Theme(IdTheme::ExplorerLocalBg),
f,
transfer_colors_layout_col1[1],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
self.app.view(
&Id::Theme(IdTheme::ExplorerLocalFg),
f,
transfer_colors_layout_col1[2],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
self.app.view(
&Id::Theme(IdTheme::ExplorerLocalHg),
f,
transfer_colors_layout_col1[3],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
self.app.view(
&Id::Theme(IdTheme::ExplorerRemoteBg),
f,
transfer_colors_layout_col1[4],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
self.app.view(
&Id::Theme(IdTheme::ExplorerRemoteFg),
f,
transfer_colors_layout_col1[5],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
self.app.view(
&Id::Theme(IdTheme::ExplorerRemoteHg),
f,
transfer_colors_layout_col1[6],
);
@@ -317,332 +216,328 @@ impl SetupActivity {
.as_ref(),
)
.split(colors_layout[3]);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
self.app.view(
&Id::Theme(IdTheme::TransferTitle2),
f,
transfer_colors_layout_col2[0],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
self.app.view(
&Id::Theme(IdTheme::ProgBarFull),
f,
transfer_colors_layout_col2[1],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
self.app.view(
&Id::Theme(IdTheme::ProgBarPartial),
f,
transfer_colors_layout_col2[2],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
self.app.view(
&Id::Theme(IdTheme::LogBg),
f,
transfer_colors_layout_col2[3],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
self.app.view(
&Id::Theme(IdTheme::LogWindow),
f,
transfer_colors_layout_col2[4],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
self.app.view(
&Id::Theme(IdTheme::StatusSorting),
f,
transfer_colors_layout_col2[5],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
self.app.view(
&Id::Theme(IdTheme::StatusHidden),
f,
transfer_colors_layout_col2[6],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
self.app.view(
&Id::Theme(IdTheme::StatusSync),
f,
transfer_colors_layout_col2[7],
);
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.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(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.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(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.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);
}
}
self.view_popups(f);
});
// Put context back to context
self.context = Some(ctx);
}
fn load_titles(&mut self) {
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthTitle),
Box::new(components::AuthTitle::default()),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscTitle),
Box::new(components::MiscTitle::default()),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::TransferTitle),
Box::new(components::TransferTitle::default()),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::TransferTitle2),
Box::new(components::TransferTitle2::default()),
vec![]
)
.is_ok());
}
/// ### load_styles
///
/// Load values from theme into input fields
pub(crate) fn load_styles(&mut self) {
let theme: Theme = self.theme().clone();
self.update_color(super::COMPONENT_COLOR_AUTH_ADDR, theme.auth_address);
self.update_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS, theme.auth_bookmarks);
self.update_color(super::COMPONENT_COLOR_AUTH_PASSWORD, theme.auth_password);
self.update_color(super::COMPONENT_COLOR_AUTH_PORT, theme.auth_port);
self.update_color(super::COMPONENT_COLOR_AUTH_PROTOCOL, theme.auth_protocol);
self.update_color(super::COMPONENT_COLOR_AUTH_RECENTS, theme.auth_recents);
self.update_color(super::COMPONENT_COLOR_AUTH_USERNAME, theme.auth_username);
self.update_color(super::COMPONENT_COLOR_MISC_ERROR, theme.misc_error_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_INFO, theme.misc_info_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_INPUT, theme.misc_input_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_KEYS, theme.misc_keys);
self.update_color(super::COMPONENT_COLOR_MISC_QUIT, theme.misc_quit_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_SAVE, theme.misc_save_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_WARN, theme.misc_warn_dialog);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
theme.transfer_local_explorer_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
theme.transfer_local_explorer_foreground,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
theme.transfer_local_explorer_highlighted,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
theme.transfer_remote_explorer_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
theme.transfer_remote_explorer_foreground,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
theme.transfer_remote_explorer_highlighted,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
theme.transfer_progress_bar_full,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
theme.transfer_progress_bar_partial,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
theme.transfer_log_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
theme.transfer_log_window,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
theme.transfer_status_sorting,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
theme.transfer_status_hidden,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
theme.transfer_status_sync_browsing,
);
}
/// ### collect_styles
///
/// Collect values from input and put them into the theme.
/// If a component has an invalid color, returns Err(component_id)
pub(crate) fn collect_styles(&mut self) -> Result<(), &'static str> {
// auth
let auth_address: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_ADDR)
.map_err(|_| super::COMPONENT_COLOR_AUTH_ADDR)?;
let auth_bookmarks: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS)
.map_err(|_| super::COMPONENT_COLOR_AUTH_BOOKMARKS)?;
let auth_password: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PASSWORD)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PASSWORD)?;
let auth_port: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PORT)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PORT)?;
let auth_protocol: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PROTOCOL)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PROTOCOL)?;
let auth_recents: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_RECENTS)
.map_err(|_| super::COMPONENT_COLOR_AUTH_RECENTS)?;
let auth_username: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_USERNAME)
.map_err(|_| super::COMPONENT_COLOR_AUTH_USERNAME)?;
// misc
let misc_error_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_ERROR)
.map_err(|_| super::COMPONENT_COLOR_MISC_ERROR)?;
let misc_info_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_INFO)
.map_err(|_| super::COMPONENT_COLOR_MISC_INFO)?;
let misc_input_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_INPUT)
.map_err(|_| super::COMPONENT_COLOR_MISC_INPUT)?;
let misc_keys: Color = self
.get_color(super::COMPONENT_COLOR_MISC_KEYS)
.map_err(|_| super::COMPONENT_COLOR_MISC_KEYS)?;
let misc_quit_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_QUIT)
.map_err(|_| super::COMPONENT_COLOR_MISC_QUIT)?;
let misc_save_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_SAVE)
.map_err(|_| super::COMPONENT_COLOR_MISC_SAVE)?;
let misc_warn_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_WARN)
.map_err(|_| super::COMPONENT_COLOR_MISC_WARN)?;
// transfer
let transfer_local_explorer_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)?;
let transfer_local_explorer_foreground: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)?;
let transfer_local_explorer_highlighted: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)?;
let transfer_remote_explorer_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)?;
let transfer_remote_explorer_foreground: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)?;
let transfer_remote_explorer_highlighted: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)?;
let transfer_log_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_BG)?;
let transfer_log_window: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_WIN)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_WIN)?;
let transfer_progress_bar_full: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)?;
let transfer_progress_bar_partial: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)?;
let transfer_status_hidden: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)?;
let transfer_status_sorting: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)?;
let transfer_status_sync_browsing: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)?;
// Update theme
let mut theme: &mut Theme = self.theme_mut();
theme.auth_address = auth_address;
theme.auth_bookmarks = auth_bookmarks;
theme.auth_password = auth_password;
theme.auth_port = auth_port;
theme.auth_protocol = auth_protocol;
theme.auth_recents = auth_recents;
theme.auth_username = auth_username;
theme.misc_error_dialog = misc_error_dialog;
theme.misc_info_dialog = misc_info_dialog;
theme.misc_input_dialog = misc_input_dialog;
theme.misc_keys = misc_keys;
theme.misc_quit_dialog = misc_quit_dialog;
theme.misc_save_dialog = misc_save_dialog;
theme.misc_warn_dialog = misc_warn_dialog;
theme.transfer_local_explorer_background = transfer_local_explorer_background;
theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground;
theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted;
theme.transfer_remote_explorer_background = transfer_remote_explorer_background;
theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground;
theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted;
theme.transfer_log_background = transfer_log_background;
theme.transfer_log_window = transfer_log_window;
theme.transfer_progress_bar_full = transfer_progress_bar_full;
theme.transfer_progress_bar_partial = transfer_progress_bar_partial;
theme.transfer_status_hidden = transfer_status_hidden;
theme.transfer_status_sorting = transfer_status_sorting;
theme.transfer_status_sync_browsing = transfer_status_sync_browsing;
Ok(())
}
/// ### update_color
///
/// Update color for provided component
fn update_color(&mut self, component: &str, color: Color) {
if let Some(props) = self.view.get_props(component) {
self.view.update(
component,
ColorPickerPropsBuilder::from(props)
.with_color(&color)
.build(),
);
}
}
/// ### get_color
///
/// Get color from component
fn get_color(&self, component: &str) -> Result<Color, ()> {
match self.view.get_state(component) {
Some(Payload::One(Value::Str(color))) => match parse_color(color.as_str()) {
Some(c) => Ok(c),
None => Err(()),
},
_ => Err(()),
}
}
/// ### mount_color_picker
///
/// Mount color picker with provided data
fn mount_color_picker(&mut self, id: &str, label: &str) {
self.view.mount(
id,
Box::new(ColorPicker::new(
ColorPickerPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::Reset)
.with_label(label.to_string(), Alignment::Left)
.build(),
)),
);
}
/// ### mount_title
///
/// Mount title
fn mount_title(&mut self, id: &str, text: &str) {
self.view.mount(
id,
Box::new(Label::new(
LabelPropsBuilder::default()
.bold()
.with_text(text.to_string())
.build(),
)),
);
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthAddress),
Box::new(components::AuthAddress::new(theme.auth_address)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthBookmarks),
Box::new(components::AuthBookmarks::new(theme.auth_bookmarks)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthPassword),
Box::new(components::AuthPassword::new(theme.auth_password)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthPort),
Box::new(components::AuthPort::new(theme.auth_port)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthProtocol),
Box::new(components::AuthProtocol::new(theme.auth_protocol)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthRecentHosts),
Box::new(components::AuthRecentHosts::new(theme.auth_recents)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthUsername),
Box::new(components::AuthUsername::new(theme.auth_username)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscError),
Box::new(components::MiscError::new(theme.misc_error_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscInfo),
Box::new(components::MiscInfo::new(theme.misc_info_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscInput),
Box::new(components::MiscInput::new(theme.misc_input_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscKeys),
Box::new(components::MiscKeys::new(theme.misc_keys)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscQuit),
Box::new(components::MiscQuit::new(theme.misc_quit_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscSave),
Box::new(components::MiscSave::new(theme.misc_save_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscWarn),
Box::new(components::MiscWarn::new(theme.misc_warn_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerLocalBg),
Box::new(components::ExplorerLocalBg::new(
theme.transfer_local_explorer_background
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerLocalFg),
Box::new(components::ExplorerLocalFg::new(
theme.transfer_local_explorer_foreground
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerLocalHg),
Box::new(components::ExplorerLocalHg::new(
theme.transfer_local_explorer_highlighted
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerRemoteBg),
Box::new(components::ExplorerRemoteBg::new(
theme.transfer_remote_explorer_background
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerRemoteFg),
Box::new(components::ExplorerRemoteFg::new(
theme.transfer_remote_explorer_foreground
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerRemoteHg),
Box::new(components::ExplorerRemoteHg::new(
theme.transfer_remote_explorer_highlighted
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ProgBarFull),
Box::new(components::ProgBarFull::new(
theme.transfer_progress_bar_full
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ProgBarPartial),
Box::new(components::ProgBarPartial::new(
theme.transfer_progress_bar_partial
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::LogBg),
Box::new(components::LogBg::new(theme.transfer_log_background)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::LogWindow),
Box::new(components::LogWindow::new(theme.transfer_log_window)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::StatusSorting),
Box::new(components::StatusSorting::new(
theme.transfer_status_sorting
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::StatusHidden),
Box::new(components::StatusHidden::new(theme.transfer_status_hidden)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::StatusSync),
Box::new(components::StatusSync::new(
theme.transfer_status_sync_browsing
)),
vec![]
)
.is_ok());
}
}