diff --git a/src/command.rs b/src/command.rs index 652e9f1c0..e564ebc41 100644 --- a/src/command.rs +++ b/src/command.rs @@ -233,5 +233,11 @@ pub fn execute( // Ignore the character typed. } } + + if let Some(validator) = s.helper { + if validator.validate_while_typing() { + s.validate()?; + } + } Ok(Proceed) } diff --git a/src/edit.rs b/src/edit.rs index b7e29e5b2..332eed76b 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -128,7 +128,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } if self.highlight_char() { let prompt_size = self.prompt_size; - self.refresh(self.prompt, prompt_size, true, Info::NoHint)?; + self.refresh(self.prompt, prompt_size, true, Info::NoHint, false)?; } else { self.out.move_cursor(self.layout.cursor, cursor)?; self.layout.prompt_size = self.prompt_size; @@ -158,6 +158,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { prompt_size: Position, default_prompt: bool, info: Info<'_>, + submit: bool, ) -> Result<()> { let info = match info { Info::NoHint => None, @@ -183,6 +184,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { &self.layout, &new_layout, highlighter, + submit, )?; self.layout = new_layout; @@ -258,25 +260,32 @@ impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { } impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { + fn submit_line(&mut self) -> Result<()> { + let prompt_size = self.prompt_size; + self.hint(); + self.highlight_char(); + self.refresh(self.prompt, prompt_size, true, Info::Hint, true) + } + fn refresh_line(&mut self) -> Result<()> { let prompt_size = self.prompt_size; self.hint(); self.highlight_char(); - self.refresh(self.prompt, prompt_size, true, Info::Hint) + self.refresh(self.prompt, prompt_size, true, Info::Hint, false) } fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()> { let prompt_size = self.prompt_size; self.hint = None; self.highlight_char(); - self.refresh(self.prompt, prompt_size, true, Info::Msg(msg)) + self.refresh(self.prompt, prompt_size, true, Info::Msg(msg), false) } fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { let prompt_size = self.out.calculate_position(prompt, Position::default()); self.hint(); self.highlight_char(); - self.refresh(prompt, prompt_size, false, Info::Hint) + self.refresh(prompt, prompt_size, false, Info::Hint, false) } fn doing_insert(&mut self) { @@ -366,7 +375,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { let bits = ch.encode_utf8(&mut self.byte_buffer); self.out.write_and_flush(bits) } else { - self.refresh(self.prompt, prompt_size, true, Info::Hint) + self.refresh(self.prompt, prompt_size, true, Info::Hint, false) } } else { self.refresh_line() diff --git a/src/examples/example.rs b/src/examples/example.rs new file mode 100644 index 000000000..b98e9463f --- /dev/null +++ b/src/examples/example.rs @@ -0,0 +1,98 @@ +use std::borrow::Cow::{self, Borrowed, Owned}; + +use rustyline::completion::FilenameCompleter; +use rustyline::error::ReadlineError; +use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; +use rustyline::hint::HistoryHinter; +use rustyline::validate::MatchingBracketValidator; +use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, KeyEvent}; +use rustyline_derive::{Completer, Helper, Hinter, Validator}; + +#[derive(Helper, Completer, Hinter, Validator)] +struct MyHelper { + #[rustyline(Completer)] + completer: FilenameCompleter, + highlighter: MatchingBracketHighlighter, + #[rustyline(Validator)] + validator: MatchingBracketValidator, + #[rustyline(Hinter)] + hinter: HistoryHinter, + colored_prompt: String, +} + +impl Highlighter for MyHelper { + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( + &'s self, + prompt: &'p str, + default: bool, + ) -> Cow<'b, str> { + if default { + Borrowed(&self.colored_prompt) + } else { + Borrowed(prompt) + } + } + + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + Owned("\x1b[1m".to_owned() + hint + "\x1b[m") + } + + fn highlight<'l>(&self, line: &'l str, pos: usize, submit: bool) -> Cow<'l, str> { + self.highlighter.highlight(line, pos, submit) + } + + fn highlight_char(&self, line: &str, pos: usize) -> bool { + self.highlighter.highlight_char(line, pos) + } +} + +// To debug rustyline: +// RUST_LOG=rustyline=debug cargo run --example example 2> debug.log +fn main() -> rustyline::Result<()> { + env_logger::init(); + let config = Config::builder() + .history_ignore_space(true) + .completion_type(CompletionType::List) + .edit_mode(EditMode::Emacs) + .build(); + let h = MyHelper { + completer: FilenameCompleter::new(), + highlighter: MatchingBracketHighlighter::new(), + hinter: HistoryHinter {}, + colored_prompt: "".to_owned(), + validator: MatchingBracketValidator::new(), + }; + let mut rl = Editor::with_config(config)?; + rl.set_helper(Some(h)); + rl.bind_sequence(KeyEvent::alt('n'), Cmd::HistorySearchForward); + rl.bind_sequence(KeyEvent::alt('p'), Cmd::HistorySearchBackward); + if rl.load_history("history.txt").is_err() { + println!("No previous history."); + } + let mut count = 1; + loop { + let p = format!("{}> ", count); + rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p); + let readline = rl.readline(&p); + match readline { + Ok(line) => { + rl.add_history_entry(line.as_str()); + println!("Line: {}", line); + } + Err(ReadlineError::Interrupted) => { + println!("Interrupted"); + break; + } + Err(ReadlineError::Eof) => { + println!("Encountered Eof"); + break; + } + Err(err) => { + println!("Error: {:?}", err); + break; + } + } + count += 1; + } + rl.append_history("history.txt") +} diff --git a/src/examples/read_password.rs b/src/examples/read_password.rs new file mode 100644 index 000000000..dbd26c365 --- /dev/null +++ b/src/examples/read_password.rs @@ -0,0 +1,43 @@ +use std::borrow::Cow::{self, Borrowed, Owned}; + +use rustyline::config::Configurer; +use rustyline::highlight::Highlighter; +use rustyline::{ColorMode, Editor, Result}; +use rustyline_derive::{Completer, Helper, Hinter, Validator}; + +#[derive(Completer, Helper, Hinter, Validator)] +struct MaskingHighlighter { + masking: bool, +} + +impl Highlighter for MaskingHighlighter { + fn highlight<'l>(&self, line: &'l str, _pos: usize, _submit: bool) -> Cow<'l, str> { + use unicode_width::UnicodeWidthStr; + if self.masking { + Owned("*".repeat(line.width())) + } else { + Borrowed(line) + } + } + + fn highlight_char(&self, _line: &str, _pos: usize) -> bool { + self.masking + } +} + +fn main() -> Result<()> { + println!("This is just a hack. Reading passwords securely requires more than that."); + let h = MaskingHighlighter { masking: false }; + let mut rl = Editor::new()?; + rl.set_helper(Some(h)); + + let username = rl.readline("Username:")?; + println!("Username: {}", username); + + rl.helper_mut().expect("No helper").masking = true; + rl.set_color_mode(ColorMode::Forced); // force masking + rl.set_auto_add_history(false); // make sure password is not added to history + let passwd = rl.readline("Password:")?; + println!("Secret: {}", passwd); + Ok(()) +} diff --git a/src/highlight.rs b/src/highlight.rs index f1b3a57de..9582698f3 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -17,8 +17,8 @@ pub trait Highlighter { /// /// For example, you can implement /// [blink-matching-paren](https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html). - fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { - let _ = pos; + fn highlight<'l>(&self, line: &'l str, pos: usize, submit: bool) -> Cow<'l, str> { + let _ = (pos, submit); Borrowed(line) } /// Takes the `prompt` and @@ -62,8 +62,8 @@ pub trait Highlighter { impl Highlighter for () {} impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H { - fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { - (**self).highlight(line, pos) + fn highlight<'l>(&self, line: &'l str, pos: usize, submit: bool) -> Cow<'l, str> { + (**self).highlight(line, pos, submit) } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( @@ -113,7 +113,7 @@ impl MatchingBracketHighlighter { } impl Highlighter for MatchingBracketHighlighter { - fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { + fn highlight<'l>(&self, line: &'l str, _pos: usize, _submit: bool) -> Cow<'l, str> { if line.len() <= 1 { return Borrowed(line); } diff --git a/src/keymap.rs b/src/keymap.rs index 025ea97a7..f7ef7eae5 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -369,6 +369,7 @@ impl Invoke for &str { } pub trait Refresher { + fn submit_line(&mut self) -> Result<()>; /// Rewrite the currently edited line accordingly to the buffer content, /// cursor position, and number of columns of the terminal. fn refresh_line(&mut self) -> Result<()>; diff --git a/src/lib.rs b/src/lib.rs index 3ec5901b0..60e50fe98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -644,6 +644,7 @@ impl Editor { let (original_mode, term_key_map) = self.term.enable_raw_mode()?; let guard = Guard(&original_mode); let user_input = self.readline_edit(prompt, initial, &original_mode, term_key_map); + if self.config.auto_add_history() { if let Ok(ref line) = user_input { self.add_history_entry(line.as_str()); @@ -770,6 +771,8 @@ impl Editor { // next thing application prints goes after the input s.edit_move_buffer_end()?; + s.submit_line()?; + if cfg!(windows) { let _ = original_mode; // silent warning } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 24f04f751..a7d224211 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -52,6 +52,7 @@ pub trait Renderer { old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, + submit: bool, ) -> Result<()>; /// Compute layout for rendering prompt + line + some info (either hint, @@ -131,8 +132,9 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, + submit: bool, ) -> Result<()> { - (**self).refresh_line(prompt, line, hint, old_layout, new_layout, highlighter) + (**self).refresh_line(prompt, line, hint, old_layout, new_layout, highlighter, submit) } fn calculate_position(&self, s: &str, orig: Position) -> Position { diff --git a/src/tty/test.rs b/src/tty/test.rs index 88e22a6e0..22a8dcf18 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -96,6 +96,7 @@ impl Renderer for Sink { _old_layout: &Layout, _new_layout: &Layout, _highlighter: Option<&dyn Highlighter>, + _submit: bool, ) -> Result<()> { Ok(()) } diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 34e695e9f..2015627d6 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -953,6 +953,7 @@ impl Renderer for PosixRenderer { old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, + submit: bool, ) -> Result<()> { use std::fmt::Write; self.buffer.clear(); @@ -969,7 +970,7 @@ impl Renderer for PosixRenderer { .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); // display the input line self.buffer - .push_str(&highlighter.highlight(line, line.pos())); + .push_str(&highlighter.highlight(line, line.pos(), submit)); } else { // display the prompt self.buffer.push_str(prompt); @@ -1542,7 +1543,7 @@ mod test { let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor); assert_eq!(new_layout.cursor, new_layout.end); - out.refresh_line(prompt, &line, None, &old_layout, &new_layout, None) + out.refresh_line(prompt, &line, None, &old_layout, &new_layout, None, true) .unwrap(); #[rustfmt::skip] assert_eq!(