mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 01:26:04 -08:00
Auth activity input fields
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -442,6 +442,7 @@ dependencies = [
|
|||||||
"ssh2",
|
"ssh2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tui",
|
"tui",
|
||||||
|
"unicode-width",
|
||||||
"users",
|
"users",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user