From: Simon Tatham Date: Sat, 20 Jan 2024 10:22:09 +0000 (+0000) Subject: Add SingleLineEditor feature to mask passwords. X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=8cf2c7b36bbca81057bf80b34c032f41c57c8aac;p=mastodonochrome.git Add SingleLineEditor feature to mask passwords. 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.) --- diff --git a/src/editor.rs b/src/editor.rs index de9fe00..7a2d3ca 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -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 { last_width: Option, } +pub fn count_edit_chars(text: &str) -> usize { + text.chars() + .map(|c| UnicodeWidthChar::width(c).unwrap_or(0) > 0) + .count() +} + impl EditableMenuLine { pub fn new(key: OurKey, description: ColouredString, data: Data) -> Self { let menuline = Self::make_menuline(key, &description, &data); @@ -826,7 +858,9 @@ impl EditableMenuLine { // 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(),