chiark / gitweb /
Add SingleLineEditor feature to mask passwords.
authorSimon Tatham <anakin@pobox.com>
Sat, 20 Jan 2024 10:22:09 +0000 (10:22 +0000)
committerSimon Tatham <anakin@pobox.com>
Sat, 20 Jan 2024 10:59:54 +0000 (10:59 +0000)
The implementor of an EditableMenuLineData trait can now set the trait
constant SECRET to indicate that the editor should display the text
as ******* while editing. However, the trait is responsible for doing
the same masking itself when displaying the data in non-editing
mode.

(This division of labour is necessary so that display() can also apply
other display features, such as complaining that you haven't given a
password at all yet, or that they don't match. display() is the only
thing that can know _which_ of its output needs masking.)

src/editor.rs

index de9fe00e6331cacdce65f51dd880f3eb61b5e6c5..7a2d3ca987c16edc6f4faa9050a9cdd53fb6fe5e 100644 (file)
@@ -17,6 +17,7 @@ struct EditorCore {
     text: String,
     paste_buffer: String,
     point: usize,
+    secret: bool,
 }
 
 impl EditorCore {
@@ -42,6 +43,7 @@ impl EditorCore {
             None => None,
             Some(c) => {
                 let width = UnicodeWidthChar::width(c).unwrap_or(0);
+                let width = if self.secret { min(width, 1) } else { width };
                 let mut end = pos + 1;
                 while !self.is_char_boundary(end) {
                     end += 1;
@@ -51,6 +53,14 @@ impl EditorCore {
         }
     }
 
+    fn char_at(&self, start: usize, end: usize) -> &str {
+        if self.secret {
+            "*"
+        } else {
+            &self.text[start..end]
+        }
+    }
+
     fn is_word_sep(c: char) -> bool {
         c == ' '
     }
@@ -254,6 +264,7 @@ fn test_forward_backward() {
         text: "héllo, wørld".to_owned(),
         point: 0,
         paste_buffer: "".to_owned(),
+        secret: false,
     };
 
     assert!(ec.forward());
@@ -294,6 +305,7 @@ fn test_forward_backward_word() {
         text: "lorem ipsum dolor sit amet".to_owned(),
         point: 0,
         paste_buffer: "".to_owned(),
+        secret: false,
     };
 
     assert!(ec.forward_word());
@@ -327,6 +339,7 @@ fn test_delete() {
         text: "hélło".to_owned(),
         point: 3,
         paste_buffer: "".to_owned(),
+        secret: false,
     };
 
     ec.delete_forward();
@@ -358,6 +371,7 @@ fn test_insert() {
         text: "hélło".to_owned(),
         point: 3,
         paste_buffer: "".to_owned(),
+        secret: false,
     };
 
     ec.insert("PÏNG");
@@ -405,6 +419,7 @@ impl SingleLineEditor {
                 text,
                 point,
                 paste_buffer: "".to_owned(),
+                secret: false,
             },
             width: 0,
             first_visible: 0,
@@ -522,7 +537,7 @@ impl SingleLineEditor {
                         break;
                     } else {
                         s.push_str(ColouredString::plain(
-                            &self.core.text[pos..pos + b],
+                            &self.core.char_at(pos, pos + b),
                         ));
                         pos += b;
                     }
@@ -545,6 +560,7 @@ fn test_single_line_extra_ops() {
             text: "hélło".to_owned(),
             point: 3,
             paste_buffer: "".to_owned(),
+            secret: false,
         },
         width: 0,
         first_visible: 0,
@@ -575,6 +591,7 @@ fn test_single_line_visibility() {
             text: "".to_owned(),
             point: 0,
             paste_buffer: "".to_owned(),
+            secret: false,
         },
         width: 5,
         first_visible: 0,
@@ -723,6 +740,15 @@ impl ActivityState for BottomLineEditorOverlay {
 }
 
 pub trait EditableMenuLineData {
+    // If SECRET, then the implementor of this trait promises that
+    // display() will show the text as ***** when it's not being
+    // edited, and instructs the SingleLineEditor to do the same
+    // during editing.
+    //
+    // display() can use the helper function count_edit_chars to help
+    // it figure out how many * to display.
+    const SECRET: bool = false;
+
     fn display(&self) -> ColouredString;
     fn to_text(&self) -> String;
     fn from_text(text: &str) -> Self;
@@ -773,6 +799,12 @@ pub struct EditableMenuLine<Data: EditableMenuLineData> {
     last_width: Option<usize>,
 }
 
+pub fn count_edit_chars(text: &str) -> usize {
+    text.chars()
+        .map(|c| UnicodeWidthChar::width(c).unwrap_or(0) > 0)
+        .count()
+}
+
 impl<Data: EditableMenuLineData> EditableMenuLine<Data> {
     pub fn new(key: OurKey, description: ColouredString, data: Data) -> Self {
         let menuline = Self::make_menuline(key, &description, &data);
@@ -826,7 +858,9 @@ impl<Data: EditableMenuLineData> EditableMenuLine<Data> {
     // Returns a LogicalAction just to make it more convenient to put
     // in matches on keypresses
     pub fn start_editing(&mut self) -> LogicalAction {
-        self.editor = Some(SingleLineEditor::new(self.data.to_text()));
+        let mut editor = SingleLineEditor::new(self.data.to_text());
+        editor.core.secret = Data::SECRET;
+        self.editor = Some(editor);
         self.refresh_editor_prompt();
         LogicalAction::Nothing
     }
@@ -1029,6 +1063,7 @@ impl Composer {
                 text: post.text,
                 point,
                 paste_buffer: "".to_owned(),
+                secret: false,
             },
             regions: Vec::new(),
             layout: Vec::new(),