From 84985fa04c76517da12742cb366d267804b0d19b Mon Sep 17 00:00:00 2001 From: paul moore Date: Fri, 13 Oct 2023 17:37:37 -0700 Subject: [PATCH] password - second go --- Cargo.toml | 4 +++ examples/password.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++ src/highlight.rs | 24 ++++++++++------- src/textarea.rs | 9 ++++++- 4 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 examples/password.rs diff --git a/Cargo.toml b/Cargo.toml index 490248d..8b23111 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,10 @@ required-features = ["crossterm"] name = "single_line" required-features = ["crossterm"] +[[example]] +name = "password" +required-features = ["crossterm"] + [[example]] name = "variable" required-features = ["crossterm"] diff --git a/examples/password.rs b/examples/password.rs new file mode 100644 index 0000000..8a983dc --- /dev/null +++ b/examples/password.rs @@ -0,0 +1,62 @@ +use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; +use std::io; +use tui::backend::CrosstermBackend; +use tui::layout::{Constraint, Layout}; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders}; +use tui::Terminal; +use tui_textarea::{Input, Key, TextArea}; + +fn main() -> io::Result<()> { + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + enable_raw_mode()?; + crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut term = Terminal::new(backend)?; + + let mut textarea = TextArea::default(); + textarea.set_cursor_line_style(Style::default()); + textarea.set_password_mode(true); + textarea.set_placeholder_text("please enter your password"); + let layout = + Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice()); + textarea.set_style(Style::default().fg(Color::LightGreen)); + textarea.set_block(Block::default().borders(Borders::ALL).title("Password")); + loop { + term.draw(|f| { + let chunks = layout.split(f.size()); + let widget = textarea.widget(); + f.render_widget(widget, chunks[0]); + })?; + + match crossterm::event::read()?.into() { + Input { key: Key::Esc, .. } => break, + Input { + key: Key::Enter, .. + } => break, + + input => { + // TextArea::input returns if the input modified its text + if textarea.input(input) { + // is_valid = validate(&mut textarea); + } + } + } + } + + disable_raw_mode()?; + crossterm::execute!( + term.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + term.show_cursor()?; + + println!("Input: {:?}", textarea.lines()[0]); + Ok(()) +} diff --git a/src/highlight.rs b/src/highlight.rs index 2a257b7..da118b7 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -5,6 +5,7 @@ use crate::tui::style::Style; feature = "ratatui-your-backend", ))] use crate::tui::text::Line as Spans; + use crate::tui::text::Span; #[cfg(not(any( feature = "ratatui-crossterm", @@ -12,6 +13,7 @@ use crate::tui::text::Span; feature = "ratatui-your-backend", )))] use crate::tui::text::Spans; + use crate::util::{num_digits, spaces}; use std::borrow::Cow; use std::cmp::Ordering; @@ -46,7 +48,7 @@ impl Boundary { } } -fn replace_tabs(s: &str, tab_len: u8) -> Cow<'_, str> { +fn replace_tabs(s: &str, tab_len: u8, mask: bool) -> Cow<'_, str> { let tab = spaces(tab_len); let mut buf = String::new(); for (i, c) in s.char_indices() { @@ -62,10 +64,11 @@ fn replace_tabs(s: &str, tab_len: u8) -> Cow<'_, str> { buf.push(c); } } - if buf.is_empty() { - Cow::Borrowed(s) - } else { - Cow::Owned(buf) + match (buf.is_empty(), mask) { + (true, true) => Cow::Owned(s.chars().map(|_| '*').collect()), + (true, false) => Cow::Borrowed(s), + (false, true) => Cow::Owned(buf.chars().map(|_| '*').collect()), + (false, false) => Cow::Owned(buf), } } @@ -119,7 +122,7 @@ impl<'a> LineHighlighter<'a> { } } - pub fn into_spans(self) -> Spans<'a> { + pub fn into_spans(self, mask: bool) -> Spans<'a> { let Self { line, mut spans, @@ -131,7 +134,7 @@ impl<'a> LineHighlighter<'a> { } = self; if boundaries.is_empty() { - spans.push(Span::styled(replace_tabs(line, tab_len), style_begin)); + spans.push(Span::styled(replace_tabs(line, tab_len, mask), style_begin)); if cursor_at_end { spans.push(Span::styled(" ", cursor_style)); } @@ -152,7 +155,7 @@ impl<'a> LineHighlighter<'a> { if let Some((next_boundary, end)) = boundaries.next() { if start < end { spans.push(Span::styled( - replace_tabs(&line[start..end], tab_len), + replace_tabs(&line[start..end], tab_len, mask), style, )); } @@ -166,7 +169,10 @@ impl<'a> LineHighlighter<'a> { start = end; } else { if start != line.len() { - spans.push(Span::styled(replace_tabs(&line[start..], tab_len), style)); + spans.push(Span::styled( + replace_tabs(&line[start..], tab_len, mask), + style, + )); } if cursor_at_end { spans.push(Span::styled(" ", cursor_style)); diff --git a/src/textarea.rs b/src/textarea.rs index 4415834..ca199fb 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -63,6 +63,7 @@ pub struct TextArea<'a> { alignment: Alignment, pub(crate) placeholder: String, pub(crate) placeholder_style: Style, + password_mode: bool, } /// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is @@ -162,6 +163,7 @@ impl<'a> TextArea<'a> { alignment: Alignment::Left, placeholder: String::new(), placeholder_style: Style::default().fg(Color::DarkGray), + password_mode: false, } } @@ -1005,7 +1007,7 @@ impl<'a> TextArea<'a> { hl.search(matches, self.search.style); } - hl.into_spans() + hl.into_spans(self.password_mode) } /// Build a tui-rs widget to render the current state of the textarea. The widget instance returned from this @@ -1304,6 +1306,11 @@ impl<'a> TextArea<'a> { } } + /// Sets a flag indicating if the text should be masked with * characters + /// + pub fn set_password_mode(&mut self, value: bool) { + self.password_mode = value; + } /// Set the style of cursor. By default, a cursor is rendered in the reversed color. Setting the same style as /// cursor line hides a cursor. /// ```