mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-29 08:00:38 +00:00
feat(text)!: add Masked
to display secure data (#168)
Adds a new type Masked that can mask data with a mask character, and can be used anywhere we expect Cow<'a, str> or Text<'a>. E.g. Paragraph, ListItem, Table Cells etc. BREAKING CHANGE: Because Masked implements From for Text<'a>, code that binds Into<Text<'a>> without type annotations may no longer compile (e.g. `Paragraph::new("".as_ref())`) To fix this, annotate or call to_string() / to_owned() / as_str()
This commit is contained in:
parent
c7aca64ba1
commit
2f0d549a50
4 changed files with 132 additions and 3 deletions
|
@ -7,7 +7,7 @@ use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
backend::{Backend, CrosstermBackend},
|
||||||
layout::{Alignment, Constraint, Direction, Layout},
|
layout::{Alignment, Constraint, Direction, Layout},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Span, Spans},
|
text::{Masked, Span, Spans},
|
||||||
widgets::{Block, Borders, Paragraph, Wrap},
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
Frame, Terminal,
|
Frame, Terminal,
|
||||||
};
|
};
|
||||||
|
@ -133,6 +133,13 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||||
.fg(Color::Green)
|
.fg(Color::Green)
|
||||||
.add_modifier(Modifier::ITALIC),
|
.add_modifier(Modifier::ITALIC),
|
||||||
)),
|
)),
|
||||||
|
Spans::from(vec![
|
||||||
|
Span::raw("Masked text: "),
|
||||||
|
Span::styled(
|
||||||
|
Masked::new("password", '*'),
|
||||||
|
Style::default().fg(Color::Red),
|
||||||
|
),
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
let create_block = |title| {
|
let create_block = |title| {
|
||||||
|
|
|
@ -155,7 +155,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||||
let help_message = Paragraph::new(text);
|
let help_message = Paragraph::new(text);
|
||||||
f.render_widget(help_message, chunks[0]);
|
f.render_widget(help_message, chunks[0]);
|
||||||
|
|
||||||
let input = Paragraph::new(app.input.as_ref())
|
let input = Paragraph::new(app.input.as_str())
|
||||||
.style(match app.input_mode {
|
.style(match app.input_mode {
|
||||||
InputMode::Normal => Style::default(),
|
InputMode::Normal => Style::default(),
|
||||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||||
|
|
122
src/text.rs
122
src/text.rs
|
@ -48,6 +48,7 @@
|
||||||
//! ```
|
//! ```
|
||||||
use crate::style::Style;
|
use crate::style::Style;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::fmt::{self, Debug, Display};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
@ -538,3 +539,124 @@ impl<'a> Extend<Spans<'a>> for Text<'a> {
|
||||||
self.lines.extend(iter);
|
self.lines.extend(iter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A wrapper around a string that is masked when displayed.
|
||||||
|
///
|
||||||
|
/// The masked string is displayed as a series of the same character.
|
||||||
|
/// This might be used to display a password field or similar secure data.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use ratatui::{buffer::Buffer, layout::Rect, text::Masked, widgets::{Paragraph, Widget}};
|
||||||
|
///
|
||||||
|
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||||
|
/// let password = Masked::new("12345", 'x');
|
||||||
|
///
|
||||||
|
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||||
|
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Masked<'a> {
|
||||||
|
inner: Cow<'a, str>,
|
||||||
|
mask_char: char,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Masked<'a> {
|
||||||
|
pub fn new(s: impl Into<Cow<'a, str>>, mask_char: char) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: s.into(),
|
||||||
|
mask_char,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The character to use for masking.
|
||||||
|
pub fn mask_char(&self) -> char {
|
||||||
|
self.mask_char
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The underlying string, with all characters masked.
|
||||||
|
pub fn value(&self) -> Cow<'a, str> {
|
||||||
|
self.inner.chars().map(|_| self.mask_char).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Masked<'_> {
|
||||||
|
/// Debug representation of a masked string is the underlying string
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.inner).map_err(|_| fmt::Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Masked<'_> {
|
||||||
|
/// Display representation of a masked string is the masked string
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.value()).map_err(|_| fmt::Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
|
||||||
|
fn from(masked: &'a Masked) -> Cow<'a, str> {
|
||||||
|
masked.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<Masked<'a>> for Cow<'a, str> {
|
||||||
|
fn from(masked: Masked<'a>) -> Cow<'a, str> {
|
||||||
|
masked.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Masked<'_>> for Text<'a> {
|
||||||
|
fn from(masked: &'a Masked) -> Text<'a> {
|
||||||
|
Text::raw(masked.value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<Masked<'a>> for Text<'a> {
|
||||||
|
fn from(masked: Masked<'a>) -> Text<'a> {
|
||||||
|
Text::raw(masked.value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_masked_value() {
|
||||||
|
let masked = Masked::new("12345", 'x');
|
||||||
|
assert_eq!(masked.value(), "xxxxx");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_masked_debug() {
|
||||||
|
let masked = Masked::new("12345", 'x');
|
||||||
|
assert_eq!(format!("{masked:?}"), "12345");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_masked_display() {
|
||||||
|
let masked = Masked::new("12345", 'x');
|
||||||
|
assert_eq!(format!("{masked}"), "xxxxx");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_masked_conversions() {
|
||||||
|
let masked = Masked::new("12345", 'x');
|
||||||
|
|
||||||
|
let text: Text = masked.borrow().into();
|
||||||
|
assert_eq!(text.lines, vec![Spans::from("xxxxx")]);
|
||||||
|
|
||||||
|
let text: Text = masked.to_owned().into();
|
||||||
|
assert_eq!(text.lines, vec![Spans::from("xxxxx")]);
|
||||||
|
|
||||||
|
let cow: Cow<str> = masked.borrow().into();
|
||||||
|
assert_eq!(cow, "xxxxx");
|
||||||
|
|
||||||
|
let cow: Cow<str> = masked.to_owned().into();
|
||||||
|
assert_eq!(cow, "xxxxx");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -169,7 +169,7 @@ pub trait Widget {
|
||||||
/// terminal.draw(|f| {
|
/// terminal.draw(|f| {
|
||||||
/// // The items managed by the application are transformed to something
|
/// // The items managed by the application are transformed to something
|
||||||
/// // that is understood by ratatui.
|
/// // that is understood by ratatui.
|
||||||
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
|
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_str())).collect();
|
||||||
/// // The `List` widget is then built with those items.
|
/// // The `List` widget is then built with those items.
|
||||||
/// let list = List::new(items);
|
/// let list = List::new(items);
|
||||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||||
|
|
Loading…
Reference in a new issue