//! ## Config //! //! config tab components use tui_realm_stdlib::components::{Input, Radio}; use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::component::{AppComponent, Component}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; use tuirealm::props::{ BorderType, Borders, Color, HorizontalAlignment, InputType, Style, TextModifiers, Title, }; use super::{ConfigMsg, Msg}; use crate::explorer::GroupDirs as GroupDirsEnum; use crate::filetransfer::FileTransferProtocol; use crate::ui::activities::setup::{ RADIO_PROTOCOL_FTP, RADIO_PROTOCOL_FTPS, RADIO_PROTOCOL_GCS, RADIO_PROTOCOL_KUBE, RADIO_PROTOCOL_S3, RADIO_PROTOCOL_SCP, RADIO_PROTOCOL_SFTP, RADIO_PROTOCOL_SMB, RADIO_PROTOCOL_WEBDAV, }; use crate::utils::parser::parse_bytesize; // -- components #[derive(Component)] pub struct CheckUpdates { component: Radio, } impl CheckUpdates { pub fn new(enabled: bool) -> Self { Self { component: Radio::default() .highlight_style( Style::default() .fg(Color::LightYellow) .add_modifier(TextModifiers::REVERSED), ) .borders( Borders::default() .color(Color::LightYellow) .modifiers(BorderType::Rounded), ) .choices(["Yes", "No"]) .rewind(true) .title(Title::from("Check for updates?").alignment(HorizontalAlignment::Left)) .value(usize::from(!enabled)), } } } impl AppComponent for CheckUpdates { fn on(&mut self, ev: &Event) -> Option { handle_radio_ev( self, ev, Msg::Config(ConfigMsg::CheckUpdatesBlurDown), Msg::Config(ConfigMsg::CheckUpdatesBlurUp), ) } } #[derive(Component)] 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", "Kube", "S3", "GCS", "SMB", "WebDAV", ]) .foreground(Color::Cyan) .rewind(true) .title(Title::from("Default protocol").alignment(HorizontalAlignment::Left)) .value(match protocol { FileTransferProtocol::Sftp => RADIO_PROTOCOL_SFTP, FileTransferProtocol::Scp => RADIO_PROTOCOL_SCP, FileTransferProtocol::Ftp(false) => RADIO_PROTOCOL_FTP, FileTransferProtocol::Ftp(true) => RADIO_PROTOCOL_FTPS, FileTransferProtocol::Kube => RADIO_PROTOCOL_KUBE, FileTransferProtocol::AwsS3 => RADIO_PROTOCOL_S3, FileTransferProtocol::GoogleCloudStorage => RADIO_PROTOCOL_GCS, FileTransferProtocol::Smb => RADIO_PROTOCOL_SMB, FileTransferProtocol::WebDAV => RADIO_PROTOCOL_WEBDAV, }), } } } impl AppComponent for DefaultProtocol { fn on(&mut self, ev: &Event) -> Option { handle_radio_ev( self, ev, Msg::Config(ConfigMsg::DefaultProtocolBlurDown), Msg::Config(ConfigMsg::DefaultProtocolBlurUp), ) } } #[derive(Component)] pub struct GroupDirs { component: Radio, } impl GroupDirs { pub fn new(opt: Option) -> Self { Self { component: Radio::default() .highlight_style( Style::default() .fg(Color::LightMagenta) .add_modifier(TextModifiers::REVERSED), ) .borders( Borders::default() .color(Color::LightMagenta) .modifiers(BorderType::Rounded), ) .choices(["Display first", "Display last", "No"]) .rewind(true) .title(Title::from("Group directories").alignment(HorizontalAlignment::Left)) .value(match opt { Some(GroupDirsEnum::First) => 0, Some(GroupDirsEnum::Last) => 1, None => 2, }), } } } impl AppComponent for GroupDirs { fn on(&mut self, ev: &Event) -> Option { handle_radio_ev( self, ev, Msg::Config(ConfigMsg::GroupDirsBlurDown), Msg::Config(ConfigMsg::GroupDirsBlurUp), ) } } #[derive(Component)] 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( Title::from("Show hidden files? (by default)") .alignment(HorizontalAlignment::Left), ) .value(usize::from(!enabled)), } } } impl AppComponent for HiddenFiles { fn on(&mut self, ev: &Event) -> Option { handle_radio_ev( self, ev, Msg::Config(ConfigMsg::HiddenFilesBlurDown), Msg::Config(ConfigMsg::HiddenFilesBlurUp), ) } } #[derive(Component)] pub struct NotificationsEnabled { component: Radio, } impl NotificationsEnabled { pub fn new(enabled: bool) -> Self { Self { component: Radio::default() .highlight_style( Style::default() .fg(Color::LightRed) .add_modifier(TextModifiers::REVERSED), ) .borders( Borders::default() .color(Color::LightRed) .modifiers(BorderType::Rounded), ) .choices(["Yes", "No"]) .rewind(true) .title(Title::from("Enable notifications?").alignment(HorizontalAlignment::Left)) .value(usize::from(!enabled)), } } } impl AppComponent for NotificationsEnabled { fn on(&mut self, ev: &Event) -> Option { handle_radio_ev( self, ev, Msg::Config(ConfigMsg::NotificationsEnabledBlurDown), Msg::Config(ConfigMsg::NotificationsEnabledBlurUp), ) } } #[derive(Component)] 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( Title::from("Prompt when replacing existing files?") .alignment(HorizontalAlignment::Left), ) .value(usize::from(!enabled)), } } } impl AppComponent for PromptOnFileReplace { fn on(&mut self, ev: &Event) -> Option { handle_radio_ev( self, ev, Msg::Config(ConfigMsg::PromptOnFileReplaceBlurDown), Msg::Config(ConfigMsg::PromptOnFileReplaceBlurUp), ) } } #[derive(Component)] 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(tuirealm::props::SpanStatic::styled( "{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}", Style::default().fg(Color::Rgb(128, 128, 128)), )) .title( Title::from("File formatter syntax (local)") .alignment(HorizontalAlignment::Left), ) .value(value), } } } impl AppComponent for LocalFileFmt { fn on(&mut self, ev: &Event) -> Option { handle_input_ev( self, ev, Msg::Config(ConfigMsg::LocalFileFmtBlurDown), Msg::Config(ConfigMsg::LocalFileFmtBlurUp), ) } } #[derive(Component)] 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_ascii_digit() || ['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(tuirealm::props::SpanStatic::styled( "64 MB", Style::default().fg(Color::Rgb(128, 128, 128)), )) .title( Title::from("Notifications: minimum transfer size") .alignment(HorizontalAlignment::Left), ) .value(value), } } } impl AppComponent for NotificationsThreshold { fn on(&mut self, ev: &Event) -> Option { handle_input_ev( self, ev, Msg::Config(ConfigMsg::NotificationsThresholdBlurDown), Msg::Config(ConfigMsg::NotificationsThresholdBlurUp), ) } } #[derive(Component)] 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(tuirealm::props::SpanStatic::styled( "{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}", Style::default().fg(Color::Rgb(128, 128, 128)), )) .title( Title::from("File formatter syntax (remote)") .alignment(HorizontalAlignment::Left), ) .value(value), } } } impl AppComponent for RemoteFileFmt { fn on(&mut self, ev: &Event) -> Option { handle_input_ev( self, ev, Msg::Config(ConfigMsg::RemoteFileFmtBlurDown), Msg::Config(ConfigMsg::RemoteFileFmtBlurUp), ) } } #[derive(Component)] pub struct SshConfig { component: Input, } impl SshConfig { pub fn new(value: &str) -> Self { Self { component: Input::default() .borders( Borders::default() .color(Color::LightBlue) .modifiers(BorderType::Rounded), ) .foreground(Color::LightBlue) .input_type(InputType::Text) .placeholder(tuirealm::props::SpanStatic::styled( "~/.ssh/config", Style::default().fg(Color::Rgb(128, 128, 128)), )) .title(Title::from("SSH configuration path").alignment(HorizontalAlignment::Left)) .value(value), } } } impl AppComponent for SshConfig { fn on(&mut self, ev: &Event) -> Option { handle_input_ev( self, ev, Msg::Config(ConfigMsg::SshConfigBlurDown), Msg::Config(ConfigMsg::SshConfigBlurUp), ) } } #[derive(Component)] 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(tuirealm::props::SpanStatic::styled( "vim", Style::default().fg(Color::Rgb(128, 128, 128)), )) .title(Title::from("Text editor").alignment(HorizontalAlignment::Left)) .value(value), } } } impl AppComponent for TextEditor { fn on(&mut self, ev: &Event) -> Option { handle_input_ev( self, ev, Msg::Config(ConfigMsg::TextEditorBlurDown), Msg::Config(ConfigMsg::TextEditorBlurUp), ) } } // -- event handler fn handle_input_ev( component: &mut dyn AppComponent, ev: &Event, on_key_down: Msg, on_key_up: Msg, ) -> Option { 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 { // NOTE: escaped control sequence code: Key::Char('h' | 'r' | 's'), modifiers: KeyModifiers::CONTROL, }) => Some(Msg::None), Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { 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 AppComponent, ev: &Event, on_key_down: Msg, on_key_up: Msg, ) -> Option { 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, } }