From 1f217f7806cd0c415a150c791f71c0329a22448d Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Tue, 24 Nov 2020 15:13:36 +0100 Subject: [PATCH] Auth activity input fields --- Cargo.lock | 1 + Cargo.toml | 1 + src/activity_manager.rs | 4 +- src/main.rs | 2 +- src/ui/activities/auth_activity.rs | 189 +++++++++++++++++++++++------ src/ui/input.rs | 2 +- 6 files changed, 161 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94fdf3a..7ba7516 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,6 +442,7 @@ dependencies = [ "ssh2", "tempfile", "tui", + "unicode-width", "users", "whoami", ] diff --git a/Cargo.toml b/Cargo.toml index 905958b..a971f9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ tui = { version = "0.12.0", features = ["crossterm"], default-features = false } users = "0.11.0" whoami = "0.9.0" rpassword = "5.0.0" +unicode-width = "0.1.7" [dev-dependencies] tempfile = "3" diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 292717a..bc26d78 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -134,7 +134,7 @@ impl ActivityManager { // Prepare activity let mut activity: AuthActivity = AuthActivity::new(); // Prepare result - let mut result: Option = None; + let result: Option; // Create activity activity.on_create(&mut self.context); loop { @@ -152,7 +152,7 @@ impl ActivityManager { break; } // Sleep for ticks - //sleep(self.interval); + sleep(self.interval); } // Destroy activity activity.on_destroy(&mut self.context); diff --git a/src/main.rs b/src/main.rs index 25672d6..4366d96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,7 +60,7 @@ fn main() { let mut address: Option = None; // None let mut port: u16 = 22; // Default port let mut username: Option = None; // Default username - let mut password: Option = None; // Default password + let password: Option; // Default password let mut protocol: ScpProtocol = ScpProtocol::Sftp; // Default protocol let mut ticks: Duration = Duration::from_micros(50); //Process options diff --git a/src/ui/activities/auth_activity.rs b/src/ui/activities/auth_activity.rs index e5eb961..10a4e3a 100644 --- a/src/ui/activities/auth_activity.rs +++ b/src/ui/activities/auth_activity.rs @@ -26,22 +26,22 @@ // Dependencies extern crate crossterm; extern crate tui; +extern crate unicode_width; // locals use super::{Activity, Context}; // Includes use crossterm::event::Event as InputEvent; -use crossterm::event::{KeyCode, KeyEvent}; -use crossterm::terminal::{enable_raw_mode, disable_raw_mode}; +use crossterm::event::KeyCode; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use tui::{ - backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans, Text}, - widgets::{Block, Borders, List, ListItem, Paragraph}, - Terminal, + widgets::{Block, Borders, List, ListItem, Paragraph, Tabs}, }; +use unicode_width::UnicodeWidthStr; /// ### InputField /// @@ -74,8 +74,10 @@ pub struct AuthActivity { pub username: String, pub password: String, pub submit: bool, // becomes true after user has submitted fields - pub quit: bool, // Becomes true if user has pressed esc + pub quit: bool, // Becomes true if user has pressed esc selected_field: InputField, + popup_message: Option, + password_placeholder: String, } impl AuthActivity { @@ -92,8 +94,109 @@ impl AuthActivity { submit: false, quit: false, selected_field: InputField::Address, + popup_message: None, + password_placeholder: String::new(), } } + + /// ### draw_remote_address + /// + /// Draw remote address block + fn draw_remote_address(&self) -> Paragraph { + Paragraph::new(self.address.as_ref()) + .style(match self.selected_field { + InputField::Address => Style::default().fg(Color::Yellow), + _ => Style::default(), + }) + .block( + Block::default() + .borders(Borders::ALL) + .title("Remote address"), + ) + } + + /// ### draw_remote_port + /// + /// Draw remote port block + fn draw_remote_port(&self) -> Paragraph { + Paragraph::new(self.port.as_ref()) + .style(match self.selected_field { + InputField::Port => Style::default().fg(Color::Cyan), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("Remote port")) + } + + /// ### draw_protocol_select + /// + /// Draw protocol select + fn draw_protocol_select(&self) -> Tabs { + let protocols: Vec = vec![Spans::from("SFTP"), Spans::from("FTP")]; + let index: usize = match self.protocol { + ScpProtocol::Sftp => 0, + ScpProtocol::Ftp => 1, + }; + Tabs::new(protocols) + .block(Block::default().borders(Borders::ALL).title("Protocol")) + .select(index) + .style(match self.selected_field { + InputField::Protocol => Style::default().fg(Color::Green), + _ => Style::default(), + }) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Green), + ) + } + + /// ### draw_protocol_username + /// + /// Draw username block + fn draw_protocol_username(&self) -> Paragraph { + Paragraph::new(self.username.as_ref()) + .style(match self.selected_field { + InputField::Username => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("Username")) + } + + /// ### draw_protocol_password + /// + /// Draw password block + fn draw_protocol_password(&mut self) -> Paragraph { + // Create password secret + self.password_placeholder = (0..self.password.width()).map(|_| "*").collect::(); + Paragraph::new(self.password_placeholder.as_ref()) + .style(match self.selected_field { + InputField::Password => Style::default().fg(Color::LightBlue), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("Password")) + } + + /// ### draw_footer + /// + /// Draw authentication page footer + fn draw_footer(&self) -> Paragraph { + // Write header + let (footer, h_style) = ( + vec![ + Span::raw("Press "), + Span::styled("", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to change input field,"), + Span::styled("", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to submit form"), + ], + Style::default().add_modifier(Modifier::RAPID_BLINK), + ); + let mut footer_text = Text::from(Spans::from(footer)); + footer_text.patch_style(h_style); + Paragraph::new(footer_text) + } } impl Activity for AuthActivity { @@ -104,6 +207,8 @@ impl Activity for AuthActivity { fn on_create(&mut self, context: &mut Context) { // Put raw mode on enabled let _ = enable_raw_mode(); + // Clear terminal + let _ = context.terminal.clear(); } /// ### on_draw @@ -112,7 +217,6 @@ impl Activity for AuthActivity { /// This function must be called at each tick to refresh the interface fn on_draw(&mut self, context: &mut Context) { // Start catching Input Events - let mut popup: Option = None; if let Ok(input_events) = context.input_hnd.fetch_events() { // Iterate over input events for event in input_events.iter() { @@ -128,7 +232,7 @@ impl Activity for AuthActivity { // Check form // Check address if self.address.len() == 0 { - popup = Some(String::from("Invalid address")); + self.popup_message = Some(String::from("Invalid address")); break; } // Check port @@ -136,26 +240,29 @@ impl Activity for AuthActivity { match self.port.parse::() { Ok(val) => { if val > 65535 { - popup = Some(String::from( + self.popup_message = Some(String::from( "Specified port must be in range 0-65535", )); break; } } Err(_) => { - popup = + self.popup_message = Some(String::from("Specified port is not a number")); break; } } // Check username if self.username.len() == 0 { - popup = Some(String::from("Invalid username")); + self.popup_message = Some(String::from("Invalid username")); break; } // Everything OK, set enter self.submit = true; - popup = Some(format!("Connecting to {}:{}...", self.address, self.port)); + self.popup_message = Some(format!( + "Connecting to {}:{}...", + self.address, self.port + )); } KeyCode::Backspace => { // Pop last char @@ -238,35 +345,47 @@ impl Activity for AuthActivity { let _ = context.terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) - .margin(2) + .margin(5) .constraints( [ - Constraint::Length(1), Constraint::Length(3), - Constraint::Min(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), ] .as_ref(), ) .split(f.size()); - // Write header - let (header, h_style) = ( - vec![ - Span::raw("Press "), - Span::styled("", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to exit, "), - Span::styled("", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to change input field,"), - Span::styled("", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to submit form"), - ], - Style::default().add_modifier(Modifier::RAPID_BLINK), - ); - let mut header_text = Text::from(Spans::from(header)); - header_text.patch_style(h_style); - let header_message = Paragraph::new(header_text); - f.render_widget(header_message, chunks[0]); - // Create input fields - // TODO: + // Draw input fields + f.render_widget(self.draw_remote_address(), chunks[0]); + f.render_widget(self.draw_remote_port(), chunks[1]); + f.render_widget(self.draw_protocol_select(), chunks[2]); + f.render_widget(self.draw_protocol_username(), chunks[3]); + f.render_widget(self.draw_protocol_password(), chunks[4]); + // Draw footer + f.render_widget(self.draw_footer(), chunks[5]); + // TODO: popup + // Set cursor + match self.selected_field { + InputField::Address => f.set_cursor( + chunks[0].x + self.address.width() as u16 + 1, + chunks[0].y + 1, + ), + InputField::Port => { + f.set_cursor(chunks[1].x + self.port.width() as u16 + 1, chunks[1].y + 1) + } + InputField::Username => f.set_cursor( + chunks[3].x + self.username.width() as u16 + 1, + chunks[3].y + 1, + ), + InputField::Password => f.set_cursor( + chunks[4].x + self.password_placeholder.width() as u16 + 1, + chunks[4].y + 1, + ), + _ => {} + } }); } @@ -277,5 +396,7 @@ impl Activity for AuthActivity { fn on_destroy(&mut self, context: &mut Context) { // Disable raw mode let _ = disable_raw_mode(); + // Clear terminal + let _ = context.terminal.clear(); } } diff --git a/src/ui/input.rs b/src/ui/input.rs index 2801c46..32d5315 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -75,7 +75,7 @@ mod tests { #[test] fn test_ui_input_new() { - let input_hnd: InputHandler = InputHandler::new(); + let _: InputHandler = InputHandler::new(); } /* ERRORS ON GITHUB ACTIONS