Auth activity input fields

This commit is contained in:
ChristianVisintin
2020-11-24 15:13:36 +01:00
parent 18db7f0305
commit 1f217f7806
6 changed files with 161 additions and 38 deletions

1
Cargo.lock generated
View File

@@ -442,6 +442,7 @@ dependencies = [
"ssh2", "ssh2",
"tempfile", "tempfile",
"tui", "tui",
"unicode-width",
"users", "users",
"whoami", "whoami",
] ]

View File

@@ -22,6 +22,7 @@ tui = { version = "0.12.0", features = ["crossterm"], default-features = false }
users = "0.11.0" users = "0.11.0"
whoami = "0.9.0" whoami = "0.9.0"
rpassword = "5.0.0" rpassword = "5.0.0"
unicode-width = "0.1.7"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"

View File

@@ -134,7 +134,7 @@ impl ActivityManager {
// Prepare activity // Prepare activity
let mut activity: AuthActivity = AuthActivity::new(); let mut activity: AuthActivity = AuthActivity::new();
// Prepare result // Prepare result
let mut result: Option<NextActivity> = None; let result: Option<NextActivity>;
// Create activity // Create activity
activity.on_create(&mut self.context); activity.on_create(&mut self.context);
loop { loop {
@@ -152,7 +152,7 @@ impl ActivityManager {
break; break;
} }
// Sleep for ticks // Sleep for ticks
//sleep(self.interval); sleep(self.interval);
} }
// Destroy activity // Destroy activity
activity.on_destroy(&mut self.context); activity.on_destroy(&mut self.context);

View File

@@ -60,7 +60,7 @@ fn main() {
let mut address: Option<String> = None; // None let mut address: Option<String> = None; // None
let mut port: u16 = 22; // Default port let mut port: u16 = 22; // Default port
let mut username: Option<String> = None; // Default username let mut username: Option<String> = None; // Default username
let mut password: Option<String> = None; // Default password let password: Option<String>; // Default password
let mut protocol: ScpProtocol = ScpProtocol::Sftp; // Default protocol let mut protocol: ScpProtocol = ScpProtocol::Sftp; // Default protocol
let mut ticks: Duration = Duration::from_micros(50); let mut ticks: Duration = Duration::from_micros(50);
//Process options //Process options

View File

@@ -26,22 +26,22 @@
// Dependencies // Dependencies
extern crate crossterm; extern crate crossterm;
extern crate tui; extern crate tui;
extern crate unicode_width;
// locals // locals
use super::{Activity, Context}; use super::{Activity, Context};
// Includes // Includes
use crossterm::event::Event as InputEvent; use crossterm::event::Event as InputEvent;
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::KeyCode;
use crossterm::terminal::{enable_raw_mode, disable_raw_mode}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tui::{ use tui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans, Text}, text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, Paragraph}, widgets::{Block, Borders, List, ListItem, Paragraph, Tabs},
Terminal,
}; };
use unicode_width::UnicodeWidthStr;
/// ### InputField /// ### InputField
/// ///
@@ -74,8 +74,10 @@ pub struct AuthActivity {
pub username: String, pub username: String,
pub password: String, pub password: String,
pub submit: bool, // becomes true after user has submitted fields 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, selected_field: InputField,
popup_message: Option<String>,
password_placeholder: String,
} }
impl AuthActivity { impl AuthActivity {
@@ -92,8 +94,109 @@ impl AuthActivity {
submit: false, submit: false,
quit: false, quit: false,
selected_field: InputField::Address, 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<Spans> = 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::<String>();
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("<ESC>", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("<UP,DOWN>", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to change input field,"),
Span::styled("<ENTER>", 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 { impl Activity for AuthActivity {
@@ -104,6 +207,8 @@ impl Activity for AuthActivity {
fn on_create(&mut self, context: &mut Context) { fn on_create(&mut self, context: &mut Context) {
// Put raw mode on enabled // Put raw mode on enabled
let _ = enable_raw_mode(); let _ = enable_raw_mode();
// Clear terminal
let _ = context.terminal.clear();
} }
/// ### on_draw /// ### on_draw
@@ -112,7 +217,6 @@ impl Activity for AuthActivity {
/// This function must be called at each tick to refresh the interface /// This function must be called at each tick to refresh the interface
fn on_draw(&mut self, context: &mut Context) { fn on_draw(&mut self, context: &mut Context) {
// Start catching Input Events // Start catching Input Events
let mut popup: Option<String> = None;
if let Ok(input_events) = context.input_hnd.fetch_events() { if let Ok(input_events) = context.input_hnd.fetch_events() {
// Iterate over input events // Iterate over input events
for event in input_events.iter() { for event in input_events.iter() {
@@ -128,7 +232,7 @@ impl Activity for AuthActivity {
// Check form // Check form
// Check address // Check address
if self.address.len() == 0 { if self.address.len() == 0 {
popup = Some(String::from("Invalid address")); self.popup_message = Some(String::from("Invalid address"));
break; break;
} }
// Check port // Check port
@@ -136,26 +240,29 @@ impl Activity for AuthActivity {
match self.port.parse::<usize>() { match self.port.parse::<usize>() {
Ok(val) => { Ok(val) => {
if val > 65535 { if val > 65535 {
popup = Some(String::from( self.popup_message = Some(String::from(
"Specified port must be in range 0-65535", "Specified port must be in range 0-65535",
)); ));
break; break;
} }
} }
Err(_) => { Err(_) => {
popup = self.popup_message =
Some(String::from("Specified port is not a number")); Some(String::from("Specified port is not a number"));
break; break;
} }
} }
// Check username // Check username
if self.username.len() == 0 { if self.username.len() == 0 {
popup = Some(String::from("Invalid username")); self.popup_message = Some(String::from("Invalid username"));
break; break;
} }
// Everything OK, set enter // Everything OK, set enter
self.submit = true; 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 => { KeyCode::Backspace => {
// Pop last char // Pop last char
@@ -238,35 +345,47 @@ impl Activity for AuthActivity {
let _ = context.terminal.draw(|f| { let _ = context.terminal.draw(|f| {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(2) .margin(5)
.constraints( .constraints(
[ [
Constraint::Length(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Min(1), Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
] ]
.as_ref(), .as_ref(),
) )
.split(f.size()); .split(f.size());
// Write header // Draw input fields
let (header, h_style) = ( f.render_widget(self.draw_remote_address(), chunks[0]);
vec![ f.render_widget(self.draw_remote_port(), chunks[1]);
Span::raw("Press "), f.render_widget(self.draw_protocol_select(), chunks[2]);
Span::styled("<ESC>", Style::default().add_modifier(Modifier::BOLD)), f.render_widget(self.draw_protocol_username(), chunks[3]);
Span::raw(" to exit, "), f.render_widget(self.draw_protocol_password(), chunks[4]);
Span::styled("<UP,DOWN>", Style::default().add_modifier(Modifier::BOLD)), // Draw footer
Span::raw(" to change input field,"), f.render_widget(self.draw_footer(), chunks[5]);
Span::styled("<ENTER>", Style::default().add_modifier(Modifier::BOLD)), // TODO: popup
Span::raw(" to submit form"), // Set cursor
], match self.selected_field {
Style::default().add_modifier(Modifier::RAPID_BLINK), InputField::Address => f.set_cursor(
); chunks[0].x + self.address.width() as u16 + 1,
let mut header_text = Text::from(Spans::from(header)); chunks[0].y + 1,
header_text.patch_style(h_style); ),
let header_message = Paragraph::new(header_text); InputField::Port => {
f.render_widget(header_message, chunks[0]); f.set_cursor(chunks[1].x + self.port.width() as u16 + 1, chunks[1].y + 1)
// Create input fields }
// TODO: 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) { fn on_destroy(&mut self, context: &mut Context) {
// Disable raw mode // Disable raw mode
let _ = disable_raw_mode(); let _ = disable_raw_mode();
// Clear terminal
let _ = context.terminal.clear();
} }
} }

View File

@@ -75,7 +75,7 @@ mod tests {
#[test] #[test]
fn test_ui_input_new() { fn test_ui_input_new() {
let input_hnd: InputHandler = InputHandler::new(); let _: InputHandler = InputHandler::new();
} }
/* ERRORS ON GITHUB ACTIONS /* ERRORS ON GITHUB ACTIONS