Skip to content

Commit

Permalink
Merge pull request #32 from pm100/password
Browse files Browse the repository at this point in the history
Allow masking a text with a specific character
  • Loading branch information
rhysd authored Oct 17, 2023
2 parents a75fcde + 45445e0 commit 455df52
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 24 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
62 changes: 62 additions & 0 deletions examples/password.rs
Original file line number Diff line number Diff line change
@@ -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_mask_char('\u{2022}'); //U+2022 BULLET (•)
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(())
}
63 changes: 40 additions & 23 deletions src/highlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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",
feature = "ratatui-termion",
feature = "ratatui-your-backend",
)))]
use crate::tui::text::Spans;

use crate::util::{num_digits, spaces};
use std::borrow::Cow;
use std::cmp::Ordering;
Expand Down Expand Up @@ -46,27 +48,39 @@ impl Boundary {
}
}

fn replace_tabs(s: &str, tab_len: u8) -> Cow<'_, str> {
let tab = spaces(tab_len);
let mut buf = String::new();
for (i, c) in s.char_indices() {
if buf.is_empty() {
if c == '\t' {
buf.reserve(s.len());
buf.push_str(&s[..i]);
buf.push_str(tab);
fn prepare_line(s: &str, tab_len: u8, mask: Option<char>) -> Cow<'_, str> {
// two tasks performed here
// - mask out chars if mask is Some
// - replace hard tabs by the correct number of spaces
// (soft tabs were done earlier in 'insert_tab')
match mask {
Some(ch) => {
// no tab processing in the mask case
return Cow::Owned(s.chars().map(|_| ch).collect());
}
None => {
let tab = spaces(tab_len);
let mut buf = String::new();
for (i, c) in s.char_indices() {
if buf.is_empty() {
if c == '\t' {
buf.reserve(s.len());
buf.push_str(&s[..i]);
buf.push_str(tab);
}
} else if c == '\t' {
buf.push_str(tab);
} else {
buf.push(c);
}
}
if !buf.is_empty() {
return Cow::Owned(buf);
}
} else if c == '\t' {
buf.push_str(tab);
} else {
buf.push(c);
}
}
if buf.is_empty() {
Cow::Borrowed(s)
} else {
Cow::Owned(buf)
}
};
// drop through in the case of no mask and no tabs
return Cow::Borrowed(s);
}

pub struct LineHighlighter<'a> {
Expand Down Expand Up @@ -119,7 +133,7 @@ impl<'a> LineHighlighter<'a> {
}
}

pub fn into_spans(self) -> Spans<'a> {
pub fn into_spans(self, mask: Option<char>) -> Spans<'a> {
let Self {
line,
mut spans,
Expand All @@ -131,7 +145,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(prepare_line(line, tab_len, mask), style_begin));
if cursor_at_end {
spans.push(Span::styled(" ", cursor_style));
}
Expand All @@ -152,7 +166,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),
prepare_line(&line[start..end], tab_len, mask),
style,
));
}
Expand All @@ -166,7 +180,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(
prepare_line(&line[start..], tab_len, mask),
style,
));
}
if cursor_at_end {
spans.push(Span::styled(" ", cursor_style));
Expand Down
16 changes: 15 additions & 1 deletion src/textarea.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub struct TextArea<'a> {
alignment: Alignment,
pub(crate) placeholder: String,
pub(crate) placeholder_style: Style,
mask: Option<char>,
}

/// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is
Expand Down Expand Up @@ -162,6 +163,7 @@ impl<'a> TextArea<'a> {
alignment: Alignment::Left,
placeholder: String::new(),
placeholder_style: Style::default().fg(Color::DarkGray),
mask: None,
}
}

Expand Down Expand Up @@ -1005,7 +1007,7 @@ impl<'a> TextArea<'a> {
hl.search(matches, self.search.style);
}

hl.into_spans()
hl.into_spans(self.mask)
}

/// Build a tui-rs widget to render the current state of the textarea. The widget instance returned from this
Expand Down Expand Up @@ -1304,6 +1306,18 @@ impl<'a> TextArea<'a> {
}
}

/// Specifies that the text should be masked with the specified character
///
pub fn set_mask_char(&mut self, mask: char) {
self.mask = Some(mask);
}

/// Clear the previously set masking character
///
pub fn clear_mask_char(&mut self) {
self.mask = None;
}

/// 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.
/// ```
Expand Down

0 comments on commit 455df52

Please sign in to comment.