mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-25 14:10:31 +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},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
text::{Masked, Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
@ -133,6 +133,13 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
|||
.fg(Color::Green)
|
||||
.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| {
|
||||
|
|
|
@ -155,7 +155,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
|||
let help_message = Paragraph::new(text);
|
||||
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 {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
|
|
122
src/text.rs
122
src/text.rs
|
@ -48,6 +48,7 @@
|
|||
//! ```
|
||||
use crate::style::Style;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Debug, Display};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
|
@ -538,3 +539,124 @@ impl<'a> Extend<Spans<'a>> for Text<'a> {
|
|||
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| {
|
||||
/// // The items managed by the application are transformed to something
|
||||
/// // 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.
|
||||
/// let list = List::new(items);
|
||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||
|
|
Loading…
Reference in a new issue