ratatui/text/
masked.rs

1use std::{borrow::Cow, fmt};
2
3use crate::text::Text;
4
5/// A wrapper around a string that is masked when displayed.
6///
7/// The masked string is displayed as a series of the same character.
8/// This might be used to display a password field or similar secure data.
9///
10/// # Examples
11///
12/// ```rust
13/// use ratatui::{
14///     buffer::Buffer,
15///     layout::Rect,
16///     text::Masked,
17///     widgets::{Paragraph, Widget},
18/// };
19///
20/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
21/// let password = Masked::new("12345", 'x');
22///
23/// Paragraph::new(password).render(buffer.area, &mut buffer);
24/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
25/// ```
26#[derive(Default, Clone, Eq, PartialEq, Hash)]
27pub struct Masked<'a> {
28    inner: Cow<'a, str>,
29    mask_char: char,
30}
31
32impl<'a> Masked<'a> {
33    pub fn new(s: impl Into<Cow<'a, str>>, mask_char: char) -> Self {
34        Self {
35            inner: s.into(),
36            mask_char,
37        }
38    }
39
40    /// The character to use for masking.
41    pub const fn mask_char(&self) -> char {
42        self.mask_char
43    }
44
45    /// The underlying string, with all characters masked.
46    pub fn value(&self) -> Cow<'a, str> {
47        self.inner.chars().map(|_| self.mask_char).collect()
48    }
49}
50
51impl fmt::Debug for Masked<'_> {
52    /// Debug representation of a masked string is the underlying string
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        // note that calling display instead of Debug here is intentional
55        fmt::Display::fmt(&self.inner, f)
56    }
57}
58
59impl fmt::Display for Masked<'_> {
60    /// Display representation of a masked string is the masked string
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        fmt::Display::fmt(&self.value(), f)
63    }
64}
65
66impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
67    fn from(masked: &'a Masked) -> Self {
68        masked.value()
69    }
70}
71
72impl<'a> From<Masked<'a>> for Cow<'a, str> {
73    fn from(masked: Masked<'a>) -> Self {
74        masked.value()
75    }
76}
77
78impl<'a> From<&'a Masked<'_>> for Text<'a> {
79    fn from(masked: &'a Masked) -> Self {
80        Text::raw(masked.value())
81    }
82}
83
84impl<'a> From<Masked<'a>> for Text<'a> {
85    fn from(masked: Masked<'a>) -> Self {
86        Text::raw(masked.value())
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::text::Line;
94
95    #[test]
96    fn new() {
97        let masked = Masked::new("12345", 'x');
98        assert_eq!(masked.inner, "12345");
99        assert_eq!(masked.mask_char, 'x');
100    }
101
102    #[test]
103    fn value() {
104        let masked = Masked::new("12345", 'x');
105        assert_eq!(masked.value(), "xxxxx");
106    }
107
108    #[test]
109    fn mask_char() {
110        let masked = Masked::new("12345", 'x');
111        assert_eq!(masked.mask_char(), 'x');
112    }
113
114    #[test]
115    fn debug() {
116        let masked = Masked::new("12345", 'x');
117        assert_eq!(format!("{masked:?}"), "12345");
118        assert_eq!(format!("{masked:.3?}"), "123", "Debug truncates");
119    }
120
121    #[test]
122    fn display() {
123        let masked = Masked::new("12345", 'x');
124        assert_eq!(format!("{masked}"), "xxxxx");
125        assert_eq!(format!("{masked:.3}"), "xxx", "Display truncates");
126    }
127
128    #[test]
129    fn into_text() {
130        let masked = Masked::new("12345", 'x');
131
132        let text: Text = (&masked).into();
133        assert_eq!(text.lines, [Line::from("xxxxx")]);
134
135        let text: Text = masked.into();
136        assert_eq!(text.lines, [Line::from("xxxxx")]);
137    }
138
139    #[test]
140    fn into_cow() {
141        let masked = Masked::new("12345", 'x');
142        let cow: Cow<str> = (&masked).into();
143        assert_eq!(cow, "xxxxx");
144
145        let cow: Cow<str> = masked.into();
146        assert_eq!(cow, "xxxxx");
147    }
148}