diff --git a/src/ui/activities/auth_activity/mod.rs b/src/ui/activities/auth_activity/mod.rs index de3461f..a544a87 100644 --- a/src/ui/activities/auth_activity/mod.rs +++ b/src/ui/activities/auth_activity/mod.rs @@ -54,6 +54,7 @@ type DialogCallback = fn(&mut AuthActivity); // -- components const COMPONENT_TEXT_HEADER: &str = "TEXT_HEADER"; +const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION"; const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER"; const COMPONENT_TEXT_HELP: &str = "TEXT_HELP"; const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR"; diff --git a/src/ui/activities/auth_activity/update.rs b/src/ui/activities/auth_activity/update.rs index 5182177..8524f3b 100644 --- a/src/ui/activities/auth_activity/update.rs +++ b/src/ui/activities/auth_activity/update.rs @@ -25,7 +25,7 @@ // locals use super::{ - AuthActivity, FileTransferProtocol, InputEvent, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR, + AuthActivity, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR, COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT, COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD, @@ -91,7 +91,6 @@ const MSG_KEY_CTRL_S: Msg = Msg::OnKey(KeyEvent { // -- update impl AuthActivity { - /// ### update /// /// Update auth activity model based on msg @@ -182,39 +181,17 @@ impl AuthActivity { None } // - (COMPONENT_BOOKMARKS_LIST, &MSG_KEY_DEL) | (COMPONENT_BOOKMARKS_LIST, &MSG_KEY_CHAR_E) => { + (COMPONENT_BOOKMARKS_LIST, &MSG_KEY_DEL) + | (COMPONENT_BOOKMARKS_LIST, &MSG_KEY_CHAR_E) => { // Show delete popup - match self - .view - .get_props(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK) - .as_mut() - { - None => None, - Some(props) => { - let msg = self.view.update( - COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, - props.visible().build(), - ); - self.update(msg) - } - } + self.mount_bookmark_del_dialog(); + None } - (COMPONENT_RECENTS_LIST, &MSG_KEY_DEL) | (COMPONENT_RECENTS_LIST, &MSG_KEY_CHAR_E) => { + (COMPONENT_RECENTS_LIST, &MSG_KEY_DEL) + | (COMPONENT_RECENTS_LIST, &MSG_KEY_CHAR_E) => { // Show delete popup - match self - .view - .get_props(COMPONENT_RADIO_BOOKMARK_DEL_RECENT) - .as_mut() - { - None => None, - Some(props) => { - let msg = self.view.update( - COMPONENT_RADIO_BOOKMARK_DEL_RECENT, - props.visible().build(), - ); - self.update(msg) - } - } + self.mount_recent_del_dialog(); + None } // Bookmark radio // Del bookmarks @@ -222,52 +199,65 @@ impl AuthActivity { COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, Msg::OnSubmit(Payload::Unsigned(index)), ) => { - // Delete bookmark - self.del_bookmark(*index); - // TODO: view bookmarks - // Hide bookmark del - if let Some(props) = self - .view - .get_props(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK) - .as_mut() - { - let msg = self.view.update( - COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, - props.hidden().build(), - ); - let _ = self.update(msg); - } - // Update bookmarks - match self.view.get_props(COMPONENT_BOOKMARKS_LIST).as_mut() { - None => None, - Some(props) => { - let msg = self.view.update(COMPONENT_BOOKMARKS_LIST, props.build()); // TODO: set rows - self.update(msg) + // hide bookmark delete + self.umount_bookmark_del_dialog(); + // Index must be 0 => YES + match *index { + 0 => { + // Get selected bookmark + match self.view.get_value(COMPONENT_BOOKMARKS_LIST) { + Some(Payload::Unsigned(index)) => { + // Delete bookmark + self.del_bookmark(index); + // TODO: view bookmarks + // Update bookmarks + match self.view.get_props(COMPONENT_BOOKMARKS_LIST).as_mut() { + None => None, + Some(props) => { + let msg = self + .view + .update(COMPONENT_BOOKMARKS_LIST, props.with_texts( + TextParts::new(Some(String::from("Bookmarks")), Some(self.view_bookmarks())) + ).build()); // TODO: set rows + self.update(msg) + } + } + } + _ => None, + } } + _ => None, } } (COMPONENT_RADIO_BOOKMARK_DEL_RECENT, Msg::OnSubmit(Payload::Unsigned(index))) => { - // Delete recent - self.del_recent(*index); - // TODO: view bookmarks - // Hide bookmark del - if let Some(props) = self - .view - .get_props(COMPONENT_RADIO_BOOKMARK_DEL_RECENT) - .as_mut() - { - let msg = self - .view - .update(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, props.hidden().build()); - let _ = self.update(msg); - } - // Update bookmarks - match self.view.get_props(COMPONENT_RECENTS_LIST).as_mut() { - None => None, - Some(props) => { - let msg = self.view.update(COMPONENT_RECENTS_LIST, props.build()); // TODO: set rows - self.update(msg) + // hide bookmark delete + self.umount_recent_del_dialog(); + // Index must be 0 => YES + match *index { + 0 => { + // Get selected bookmark + match self.view.get_value(COMPONENT_RECENTS_LIST) { + Some(Payload::Unsigned(index)) => { + // Delete recent + self.del_recent(index); + // TODO: view recents + // Update bookmarks + match self.view.get_props(COMPONENT_RECENTS_LIST).as_mut() { + None => None, + Some(props) => { + let msg = self + .view + .update(COMPONENT_RECENTS_LIST, props.with_texts( + TextParts::new(Some(String::from("Recent connections")), Some(self.view_recent_connections())) + ).build()); // TODO: set rows + self.update(msg) + } + } + } + _ => None, + } } + _ => None, } } // hide tab @@ -306,27 +296,13 @@ impl AuthActivity { // Help (_, &MSG_KEY_CTRL_H) => { // Show help - match self.view.get_props(COMPONENT_TEXT_HELP).as_mut() { - Some(props) => { - let msg = self - .view - .update(COMPONENT_TEXT_HELP, props.visible().build()); - self.update(msg) - } - None => None, - } + self.mount_help(); + None } (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => { // Hide text help - match self.view.get_props(COMPONENT_TEXT_HELP).as_mut() { - None => None, - Some(props) => { - let msg = self - .view - .update(COMPONENT_TEXT_HELP, props.hidden().build()); - self.update(msg) - } - } + self.umount_help(); + None } // Enter setup (_, &MSG_KEY_CTRL_C) => { @@ -336,23 +312,7 @@ impl AuthActivity { // Save bookmark; show popup (_, &MSG_KEY_CTRL_S) => { // Show popup - if let Some(props) = self - .view - .get_props(COMPONENT_RADIO_BOOKMARK_SAVE_PWD) - .as_mut() - { - let msg = self - .view - .update(COMPONENT_RADIO_BOOKMARK_SAVE_PWD, props.visible().build()); - let _ = self.update(msg); - } - if let Some(props) = self.view.get_props(COMPONENT_INPUT_BOOKMARK_NAME).as_mut() - { - let msg = self - .view - .update(COMPONENT_INPUT_BOOKMARK_NAME, props.visible().build()); - let _ = self.update(msg); - } + self.mount_bookmark_save_dialog(); // Give focus to bookmark name self.view.active(COMPONENT_INPUT_BOOKMARK_NAME); None @@ -385,47 +345,21 @@ impl AuthActivity { _ => false, }; // TODO: save bookmark - // Hide popup - if let Some(props) = self - .view - .get_props(COMPONENT_RADIO_BOOKMARK_SAVE_PWD) - .as_mut() - { - let msg = self - .view - .update(COMPONENT_RADIO_BOOKMARK_SAVE_PWD, props.hidden().build()); - let _ = self.update(msg); - } - if let Some(props) = self.view.get_props(COMPONENT_INPUT_BOOKMARK_NAME).as_mut() - { - let msg = self - .view - .update(COMPONENT_INPUT_BOOKMARK_NAME, props.hidden().build()); - let _ = self.update(msg); - } + // Umount popup + self.umount_bookmark_save_dialog(); None } // Hide save bookmark (COMPONENT_INPUT_BOOKMARK_NAME, &MSG_KEY_ESC) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, &MSG_KEY_ESC) => { - // Hide popup - if let Some(props) = self - .view - .get_props(COMPONENT_RADIO_BOOKMARK_SAVE_PWD) - .as_mut() - { - let msg = self - .view - .update(COMPONENT_RADIO_BOOKMARK_SAVE_PWD, props.hidden().build()); - let _ = self.update(msg); - } - if let Some(props) = self.view.get_props(COMPONENT_INPUT_BOOKMARK_NAME).as_mut() - { - let msg = self - .view - .update(COMPONENT_INPUT_BOOKMARK_NAME, props.hidden().build()); - let _ = self.update(msg); - } + // Umount popup + self.umount_bookmark_save_dialog(); + None + } + // Error message + (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) => { + // Umount text error + self.umount_error(); None } // On submit on any unhandled (connect) @@ -436,21 +370,7 @@ impl AuthActivity { Some(Payload::Text(addr)) => addr, _ => { // Show error - if let Some(props) = self.view.get_props(COMPONENT_TEXT_ERROR).as_mut() - { - let msg = self.view.update( - COMPONENT_TEXT_ERROR, - props - .visible() - .with_texts(TextParts::new( - None, - Some(vec![TextSpan::from("Invalid address!")]), - )) - .build(), - ); - return self.update(msg); - } - // Return None + self.mount_error("Invalid address!"); return None; } }; @@ -458,20 +378,7 @@ impl AuthActivity { Some(Payload::Unsigned(p)) => p as u16, _ => { // Show error - if let Some(props) = self.view.get_props(COMPONENT_TEXT_ERROR).as_mut() - { - let msg = self.view.update( - COMPONENT_TEXT_ERROR, - props - .visible() - .with_texts(TextParts::new( - None, - Some(vec![TextSpan::from("Invalid port number!")]), - )) - .build(), - ); - return self.update(msg); - } + self.mount_error("Invalid port number!"); // Return None return None; } diff --git a/src/ui/activities/auth_activity/view.rs b/src/ui/activities/auth_activity/view.rs index 10a23f1..e286abf 100644 --- a/src/ui/activities/auth_activity/view.rs +++ b/src/ui/activities/auth_activity/view.rs @@ -29,7 +29,7 @@ use crate::ui::layout::components::{ bookmark_list::BookmarkList, input::Input, radio_group::RadioGroup, table::Table, text::Text, }; use crate::ui::layout::props::{ - PropValue, Props, PropsBuilder, TextParts, TextSpan, TextSpanBuilder, + InputType, PropValue, Props, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder, }; use crate::utils::fmt::align_text_center; // Ext @@ -37,7 +37,7 @@ use std::string::ToString; use tui::{ layout::{Constraint, Corner, Direction, Layout, Rect}, style::{Color, Modifier, Style}, - widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs}, + widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs, Widget}, }; use unicode_width::UnicodeWidthStr; @@ -45,7 +45,7 @@ impl AuthActivity { /// ### init /// /// Initialize view, mounting all startup components inside the view - pub fn init(&mut self) { + pub(super) fn init(&mut self) { // Header self.view.mount(super::COMPONENT_TEXT_HEADER, Box::new( Text::new( @@ -55,26 +55,394 @@ impl AuthActivity { ) )); // Footer - self.view.mount(super::COMPONENT_TEXT_FOOTER, Box::new( - Text::new( - PropsBuilder::default().with_foreground(Color::White).with_texts( - TextParts::new(None, Some(vec![ - TextSpanBuilder::new("Press ").bold().build(), - TextSpanBuilder::new("").bold().with_foreground(Color::Cyan).build(), - TextSpanBuilder::new(" to show keybindings; ").bold().build(), - TextSpanBuilder::new("").bold().with_foreground(Color::Cyan).build(), - TextSpanBuilder::new(" to enter setup").bold().build(), - ])) - ).build() - ) - )); + self.view.mount( + super::COMPONENT_TEXT_FOOTER, + Box::new(Text::new( + PropsBuilder::default() + .with_foreground(Color::White) + .with_texts(TextParts::new( + None, + Some(vec![ + TextSpanBuilder::new("Press ").bold().build(), + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + TextSpanBuilder::new(" to show keybindings; ") + .bold() + .build(), + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + TextSpanBuilder::new(" to enter setup").bold().build(), + ]), + )) + .build(), + )), + ); + // Address + self.view.mount( + super::COMPONENT_INPUT_ADDR, + Box::new(Input::new( + PropsBuilder::default() + .with_foreground(Color::Yellow) + .with_texts(TextParts::new(Some(String::from("Remote address")), None)) + .build(), + )), + ); + // Port + self.view.mount( + super::COMPONENT_INPUT_PORT, + Box::new(Input::new( + PropsBuilder::default() + .with_foreground(Color::LightCyan) + .with_texts(TextParts::new(Some(String::from("Port number")), None)) + .with_input(InputType::Number) + .with_input_len(5) + .with_value(PropValue::Unsigned(22)) + .build(), + )), + ); + // Protocol + self.view.mount( + super::COMPONENT_RADIO_PROTOCOL, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::LightGreen) + .with_texts(TextParts::new( + Some(String::from("Protocol")), + Some(vec![ + TextSpan::from("SFTP"), + TextSpan::from("SCP"), + TextSpan::from("FTP"), + TextSpan::from("FTPS"), + ]), + )) + .build(), + )), + ); + // Username + self.view.mount( + super::COMPONENT_INPUT_USERNAME, + Box::new(Input::new( + PropsBuilder::default() + .with_foreground(Color::LightMagenta) + .with_texts(TextParts::new(Some(String::from("Username")), None)) + .build(), + )), + ); + // Password + self.view.mount( + super::COMPONENT_INPUT_PASSWORD, + Box::new(Input::new( + PropsBuilder::default() + .with_foreground(Color::LightBlue) + .with_texts(TextParts::new(Some(String::from("Password")), None)) + .with_input(InputType::Password) + .build(), + )), + ); + // Version notice + if let Some(version) = self.new_version.as_ref() { + self.view.mount( + super::COMPONENT_TEXT_NEW_VERSION, + Box::new(Text::new( + PropsBuilder::default() + .with_foreground(Color::Yellow) + .with_texts(TextParts::new(None, Some(vec![format!("TermSCP {} is now available! Download it from ", version)]))) + .bold() + .build() + )) + ); + } } /// ### view - /// + /// /// Display view on canvas - pub fn view(&mut self) { - + pub(super) fn view(&mut self) { + let mut ctx: Context = self.context.take().unwrap(); + let _ = ctx.terminal.draw(|f| { + // Prepare chunks + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(70), // Auth Form + Constraint::Percentage(30), // Bookmarks + ] + .as_ref(), + ) + .split(f.size()); + // Create explorer chunks + let auth_chunks = Layout::default() + .constraints( + [ + Constraint::Length(5), + Constraint::Length(1), // Version + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ] + .as_ref(), + ) + .direction(Direction::Vertical) + .split(chunks[0]); + // Create bookmark chunks + let bookmark_chunks = Layout::default() + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .direction(Direction::Horizontal) + .split(chunks[1]); + // Get focus holder + let focus: Option = self.view.who_has_focus(); + // Render + // Header + if let Some(render) = self.view.render(super::COMPONENT_TEXT_HEADER).as_ref() { + f.render_widget(render.widget, auth_chunks[0]); + } + }); } + // -- partials + + /// ### view_bookmarks + /// + /// Make text span from bookmarks + pub(super) fn view_bookmarks(&self) -> Vec { + self.bookmarks_list + .iter() + .map(|x| TextSpan::from(x.as_str())) + .collect() + } + + /// ### view_recent_connections + /// + /// View recent connections + pub(super) fn view_recent_connections(&self) -> Vec { + self.recents_list + .iter() + .map(|x| TextSpan::from(x.as_str())) + .collect() + } + + // -- mount + + /// ### mount_error + /// + /// Mount error box + pub(super) fn mount_error(&mut self, text: &str) { + // Mount + self.view.mount( + super::COMPONENT_TEXT_ERROR, + Box::new(Text::new( + PropsBuilder::default() + .with_foreground(Color::Red) + .bold() + .with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)]))) + .build(), + )), + ); + // Give focus to error + self.view.active(super::COMPONENT_TEXT_ERROR); + } + + /// ### umount_error + /// + /// Umount error message + pub(super) fn umount_error(&mut self) { + self.view.umount(super::COMPONENT_TEXT_ERROR); + } + + /// ### mount_bookmark_del_dialog + /// + /// Mount bookmark delete dialog + pub(super) fn mount_bookmark_del_dialog(&mut self) { + self.view.mount( + super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::Yellow) + .with_texts(TextParts::new( + Some(String::from("Delete bookmark?")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .with_value(PropValue::Unsigned(1)) + .build(), + )), + ); + // Active + self.view + .active(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK); + } + + /// ### umount_bookmark_del_dialog + /// + /// umount delete bookmark dialog + pub(super) fn umount_bookmark_del_dialog(&mut self) { + self.view + .umount(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK); + } + + /// ### mount_bookmark_del_dialog + /// + /// Mount recent delete dialog + pub(super) fn mount_recent_del_dialog(&mut self) { + self.view.mount( + super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::Yellow) + .with_texts(TextParts::new( + Some(String::from("Delete bookmark?")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .with_value(PropValue::Unsigned(1)) + .build(), + )), + ); + // Active + self.view.active(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT); + } + + /// ### umount_recent_del_dialog + /// + /// umount delete recent dialog + pub(super) fn umount_recent_del_dialog(&mut self) { + self.view.umount(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT); + } + + /// ### mount_bookmark_save_dialog + /// + /// Mount bookmark save dialog + pub(super) fn mount_bookmark_save_dialog(&mut self) { + self.view.mount( + super::COMPONENT_INPUT_BOOKMARK_NAME, + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new( + Some(String::from("Save bookmark as...")), + None, + )) + .build(), + )), + ); + self.view.mount( + super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD, + Box::new(RadioGroup::new( + PropsBuilder::default() + .with_foreground(Color::Red) + .with_texts(TextParts::new( + Some(String::from("Save password?")), + Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]), + )) + .with_value(PropValue::Unsigned(1)) + .build(), + )), + ); + // Give focus to input bookmark name + self.view.active(super::COMPONENT_INPUT_BOOKMARK_NAME); + } + + /// ### umount_bookmark_save_dialog + /// + /// Umount bookmark save dialog + pub(super) fn umount_bookmark_save_dialog(&mut self) { + self.view.umount(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD); + self.view.umount(super::COMPONENT_INPUT_BOOKMARK_NAME); + } + + /// ### mount_help + /// + /// Mount help + pub(super) fn mount_help(&mut self) { + self.view.mount( + super::COMPONENT_TEXT_HELP, + Box::new(Table::new( + PropsBuilder::default() + .with_texts(TextParts::table( + Some(String::from("Help")), + TableBuilder::default() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Quit TermSCP")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Switch from form and bookmarks")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Switch bookmark tab")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Move up/down in current tab")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Connect/Load bookmark")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Delete selected bookmark")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Enter setup")) + .add_row() + .add_col( + TextSpanBuilder::new("") + .bold() + .with_foreground(Color::Cyan) + .build(), + ) + .add_col(TextSpan::from(" Save bookmark")) + .build(), + )) + .build(), + )), + ); + // Active help + self.view.active(super::COMPONENT_TEXT_HELP); + } + + /// ### umount_help + /// + /// Umount help + pub(super) fn umount_help(&mut self) { + self.view.umount(super::COMPONENT_TEXT_HELP); + } }