use crate::console::{Console, Key, LineBuffer};
use std::borrow::Cow;
use std::io;
const SECURE_CHAR: &str = "*";
enum DispatchResult {
Continue,
Submit,
}
struct Readline<'a, 'h> {
console: &'a mut dyn Console,
history: Option<&'h mut Vec<String>>,
echo: bool,
input_width: usize,
line: LineBuffer,
pos: usize,
view_start: usize,
rendered: String,
rendered_len: usize,
history_pos: usize,
}
impl<'a, 'h> Readline<'a, 'h> {
fn adjust_view(&self, pos: usize, view_start: usize) -> usize {
if self.input_width == 0 {
0
} else if pos < view_start {
pos
} else if pos > view_start + self.input_width {
pos - self.input_width
} else {
view_start
}
}
fn current_pos(&self) -> usize {
self.pos - self.view_start
}
fn render_line(&self, view_start: usize) -> String {
debug_assert!(view_start <= self.line.len());
if !self.echo {
SECURE_CHAR.repeat((self.line.len() - view_start).min(self.input_width))
} else {
self.line.range(view_start, view_start + self.input_width)
}
}
fn sync_rendered(&mut self) {
self.rendered = self.render_line(self.view_start);
self.rendered_len = self.rendered.chars().count();
}
fn reset_view_to_end(&mut self) {
self.view_start =
if self.input_width == 0 { 0 } else { self.pos.saturating_sub(self.input_width) };
self.sync_rendered();
}
fn redraw_line(&mut self, old_pos: usize, clear_len: usize, new_pos: usize) -> io::Result<()> {
self.console.hide_cursor()?;
if old_pos > 0 {
self.console.move_within_line(-(old_pos as i16))?;
}
if !self.rendered.is_empty() {
self.console.write(&self.rendered)?;
}
let written_len = self.rendered_len.max(clear_len);
if self.rendered_len < clear_len {
let diff = clear_len - self.rendered_len;
self.console.write(&" ".repeat(diff))?;
}
debug_assert!(new_pos <= written_len);
let reset = written_len - new_pos;
if reset > 0 {
self.console.move_within_line(-(reset as i16))?;
}
self.console.show_cursor()
}
fn erase_at(&mut self, cursor_pos: usize, remove_pos: usize) -> io::Result<()> {
debug_assert!(remove_pos < self.line.len());
debug_assert!(remove_pos <= cursor_pos);
self.console.hide_cursor()?;
let delta = cursor_pos - remove_pos;
if delta > 0 {
self.console.move_within_line(-(delta as i16))?;
}
let tail_len = self.line.len() - remove_pos - 1;
if self.echo {
self.console.write(&self.line.end(remove_pos + 1))?;
} else {
self.console.write(&SECURE_CHAR.repeat(tail_len))?;
}
self.console.write(" ")?;
self.console.move_within_line(-((tail_len + 1) as i16))?;
self.console.show_cursor()?;
self.line.remove(remove_pos);
Ok(())
}
fn do_backspace(&mut self) -> io::Result<DispatchResult> {
if self.pos == 0 {
return Ok(DispatchResult::Continue);
}
if self.view_start == 0 && self.line.len() <= self.input_width {
self.erase_at(self.pos, self.pos - 1)?;
self.pos -= 1;
self.sync_rendered();
} else {
let old_pos = self.current_pos();
let clear_len = self.rendered_len;
self.line.remove(self.pos - 1);
self.pos -= 1;
self.view_start = self.adjust_view(self.pos, self.view_start);
self.sync_rendered();
self.redraw_line(old_pos, clear_len, self.current_pos())?;
}
Ok(DispatchResult::Continue)
}
fn do_carriage_return(&mut self) -> io::Result<DispatchResult> {
if cfg!(not(target_os = "windows")) {
self.console.print("")?;
Ok(DispatchResult::Submit)
} else {
Ok(DispatchResult::Continue)
}
}
fn do_char(&mut self, ch: char) -> io::Result<DispatchResult> {
if self.input_width == 0 {
return Ok(DispatchResult::Continue);
}
let line_len = self.line.len();
let new_view_start = self.adjust_view(self.pos + 1, self.view_start);
if new_view_start == self.view_start && line_len < self.input_width {
if self.pos < line_len {
self.console.hide_cursor()?;
if self.echo {
let mut buf = [0u8; 4];
self.console.write(ch.encode_utf8(&mut buf))?;
self.console.write(&self.line.end(self.pos))?;
} else {
self.console.write(&SECURE_CHAR.repeat(line_len - self.pos + 1))?;
}
self.console.move_within_line(-((line_len - self.pos) as i16))?;
self.console.show_cursor()?;
self.line.insert(self.pos, ch);
} else {
if self.echo {
let mut buf = [0u8; 4];
self.console.write(ch.encode_utf8(&mut buf))?;
} else {
self.console.write(SECURE_CHAR)?;
}
self.line.insert(line_len, ch);
}
self.pos += 1;
self.sync_rendered();
} else {
let old_pos = self.current_pos();
let clear_len = self.rendered_len;
self.line.insert(self.pos, ch);
self.pos += 1;
self.view_start = new_view_start;
self.sync_rendered();
self.redraw_line(old_pos, clear_len, self.current_pos())?;
}
Ok(DispatchResult::Continue)
}
fn do_delete(&mut self) -> io::Result<DispatchResult> {
if self.pos >= self.line.len() {
return Ok(DispatchResult::Continue);
}
if self.view_start == 0 && self.line.len() <= self.input_width {
self.erase_at(self.pos, self.pos)?;
self.sync_rendered();
} else {
let old_pos = self.current_pos();
let clear_len = self.rendered_len;
self.line.remove(self.pos);
self.view_start = self.adjust_view(self.pos, self.view_start);
self.sync_rendered();
self.redraw_line(old_pos, clear_len, self.current_pos())?;
}
Ok(DispatchResult::Continue)
}
fn do_end(&mut self) -> io::Result<DispatchResult> {
self.do_move_to(self.line.len())
}
fn do_eof_or_delete(&mut self) -> io::Result<DispatchResult> {
if self.line.is_empty() {
Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF"))
} else {
self.do_delete()
}
}
fn do_home(&mut self) -> io::Result<DispatchResult> {
self.do_move_to(0)
}
fn do_ignore(&mut self) -> io::Result<DispatchResult> {
Ok(DispatchResult::Continue)
}
fn do_interrupt(&mut self) -> io::Result<DispatchResult> {
Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C"))
}
fn do_move_to(&mut self, new_pos: usize) -> io::Result<DispatchResult> {
if new_pos == self.pos {
return Ok(DispatchResult::Continue);
}
let old_pos = self.current_pos();
let new_view_start = self.adjust_view(new_pos, self.view_start);
if new_view_start == self.view_start {
let delta = new_pos as i16 - self.pos as i16;
self.console.move_within_line(delta)?;
self.pos = new_pos;
} else {
let clear_len = self.rendered_len;
self.pos = new_pos;
self.view_start = new_view_start;
self.sync_rendered();
self.redraw_line(old_pos, clear_len, self.current_pos())?;
}
Ok(DispatchResult::Continue)
}
fn do_newline(&mut self) -> io::Result<DispatchResult> {
self.console.print("")?;
Ok(DispatchResult::Submit)
}
fn do_up_down(&mut self, delta: isize) -> io::Result<DispatchResult> {
let (new_history_pos, new_line) = {
let history = match self.history.as_deref_mut() {
Some(history) => history,
None => return Ok(DispatchResult::Continue),
};
let new_history_pos = if delta < 0 {
if self.history_pos == 0 {
return Ok(DispatchResult::Continue);
}
self.history_pos - 1
} else {
if self.history_pos == history.len() - 1 {
return Ok(DispatchResult::Continue);
}
self.history_pos + 1
};
history[self.history_pos] = self.line.to_string();
(new_history_pos, history[new_history_pos].clone())
};
let old_pos = self.current_pos();
let clear_len = self.rendered_len;
self.history_pos = new_history_pos;
self.line = LineBuffer::from(&new_line);
self.pos = self.line.len();
self.reset_view_to_end();
self.redraw_line(old_pos, clear_len, self.current_pos())?;
Ok(DispatchResult::Continue)
}
fn finish(mut self) -> String {
if let Some(history) = self.history.as_mut() {
if self.line.is_empty() {
history.pop();
} else {
let last = history.len() - 1;
history[last] = self.line.to_string();
}
}
self.line.into_inner()
}
fn new(
console: &'a mut dyn Console,
prompt: &str,
previous: &str,
mut history: Option<&'h mut Vec<String>>,
echo: bool,
) -> io::Result<Self> {
let console_width = {
let console_size = console.size_chars()?;
usize::from(console_size.x)
};
let mut prompt = Cow::from(prompt);
let mut prompt_len = prompt.len();
if prompt_len >= console_width {
if console_width >= 5 {
prompt = Cow::from(format!("{}...", &prompt[0..console_width - 5]));
} else {
prompt = Cow::from("");
}
prompt_len = prompt.len();
}
let input_width = {
let width = console_width - prompt_len;
width.saturating_sub(1)
};
let line = LineBuffer::from(previous);
let pos = line.len();
let view_start = if input_width == 0 { 0 } else { pos.saturating_sub(input_width) };
let history_pos = match history.as_mut() {
Some(history) => {
history.push(line.to_string());
history.len() - 1
}
None => 0,
};
let mut readline = Self {
console,
history,
echo,
input_width,
line,
pos,
view_start,
rendered: String::new(),
rendered_len: 0,
history_pos,
};
readline.view_start = readline.adjust_view(readline.pos, readline.view_start);
readline.sync_rendered();
if !prompt.is_empty() || !readline.rendered.is_empty() {
readline.console.write(&format!("{}{}", prompt, readline.rendered))?;
readline.console.sync_now()?;
}
Ok(readline)
}
async fn run(mut self) -> io::Result<String> {
loop {
let result = match self.console.read_key().await? {
Key::ArrowDown => self.do_up_down(1),
Key::ArrowLeft => self.do_move_to(self.pos.saturating_sub(1)),
Key::ArrowRight => self.do_move_to((self.pos + 1).min(self.line.len())),
Key::ArrowUp => self.do_up_down(-1),
Key::Backspace => self.do_backspace(),
Key::CarriageReturn => self.do_carriage_return(),
Key::Char(ch) => self.do_char(ch),
Key::Delete => self.do_delete(),
Key::End => self.do_end(),
Key::EofOrDelete => self.do_eof_or_delete(),
Key::Escape => self.do_ignore(),
Key::Home => self.do_home(),
Key::Interrupt => self.do_interrupt(),
Key::NewLine => self.do_newline(),
Key::PageDown | Key::PageUp | Key::Tab | Key::Unknown => self.do_ignore(),
}?;
if let DispatchResult::Submit = result {
break;
}
}
Ok(self.finish())
}
}
async fn read_line_interactive(
console: &mut dyn Console,
prompt: &str,
previous: &str,
mut history: Option<&mut Vec<String>>,
echo: bool,
) -> io::Result<String> {
Readline::new(console, prompt, previous, history.take(), echo)?.run().await
}
async fn read_line_raw(console: &mut dyn Console) -> io::Result<String> {
let mut line = String::new();
loop {
match console.read_key().await? {
Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight => (),
Key::Backspace => {
if !line.is_empty() {
line.pop();
}
}
Key::CarriageReturn => {
if cfg!(not(target_os = "windows")) {
break;
}
}
Key::Char(ch) => line.push(ch),
Key::Delete => (),
Key::End | Key::Home => (),
Key::Escape => (),
Key::EofOrDelete => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
Key::NewLine => break,
Key::PageDown | Key::PageUp => (),
Key::Tab => (),
Key::Unknown => line.push('?'),
}
}
Ok(line)
}
pub async fn read_line(
console: &mut dyn Console,
prompt: &str,
previous: &str,
history: Option<&mut Vec<String>>,
) -> io::Result<String> {
if console.is_interactive() {
read_line_interactive(console, prompt, previous, history, true).await
} else {
read_line_raw(console).await
}
}
pub async fn read_line_secure(console: &mut dyn Console, prompt: &str) -> io::Result<String> {
if !console.is_interactive() {
return Err(io::Error::other("Cannot read secure strings from a raw console".to_owned()));
}
read_line_interactive(console, prompt, "", None, false).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::CharsXY;
use crate::testutils::*;
use futures_lite::future::block_on;
#[must_use]
struct ReadLineInteractiveTest {
size_chars: CharsXY,
keys: Vec<Key>,
prompt: &'static str,
previous: &'static str,
history: Option<Vec<String>>,
echo: bool,
exp_line: &'static str,
exp_output: Vec<CapturedOut>,
exp_history: Option<Vec<String>>,
}
impl Default for ReadLineInteractiveTest {
fn default() -> Self {
Self {
size_chars: CharsXY::new(15, 5),
keys: vec![],
prompt: "",
previous: "",
history: None,
echo: true,
exp_line: "",
exp_output: vec![],
exp_history: None,
}
}
}
impl ReadLineInteractiveTest {
fn add_key(mut self, key: Key) -> Self {
self.keys.push(key);
self
}
fn add_key_chars(mut self, chars: &'static str) -> Self {
for ch in chars.chars() {
self.keys.push(Key::Char(ch));
}
self
}
fn add_output(mut self, output: CapturedOut) -> Self {
self.exp_output.push(output);
self
}
fn add_output_bytes(mut self, bytes: &'static str) -> Self {
if bytes.is_empty() {
self.exp_output.push(CapturedOut::Write("".to_string()))
} else {
for b in bytes.chars() {
let mut buf = [0u8; 4];
self.exp_output.push(CapturedOut::Write(b.encode_utf8(&mut buf).to_string()));
}
}
self
}
fn set_size_chars(mut self, size: CharsXY) -> Self {
self.size_chars = size;
self
}
fn set_line(mut self, line: &'static str) -> Self {
self.exp_line = line;
self
}
fn set_prompt(mut self, prompt: &'static str) -> Self {
self.prompt = prompt;
self
}
fn set_previous(mut self, previous: &'static str) -> Self {
self.previous = previous;
self
}
fn set_history(mut self, history: Vec<String>, exp_history: Vec<String>) -> Self {
self.history = Some(history);
self.exp_history = Some(exp_history);
self
}
fn set_echo(mut self, echo: bool) -> Self {
self.echo = echo;
self
}
fn accept(mut self) {
self.keys.push(Key::NewLine);
self.exp_output.push(CapturedOut::Print("".to_owned()));
let mut console = MockConsole::default();
console.add_input_keys(&self.keys);
console.set_size_chars(self.size_chars);
let line = block_on(read_line_interactive(
&mut console,
self.prompt,
self.previous,
self.history.as_mut(),
self.echo,
))
.unwrap();
assert_eq!(self.exp_line, &line);
assert_eq!(self.exp_output.as_slice(), console.captured_out());
assert_eq!(self.exp_history, self.history);
}
fn expect_err(mut self, exp_kind: io::ErrorKind) {
let mut console = MockConsole::default();
console.add_input_keys(&self.keys);
console.set_size_chars(self.size_chars);
let err = block_on(read_line_interactive(
&mut console,
self.prompt,
self.previous,
self.history.as_mut(),
self.echo,
))
.expect_err("read_line_interactive should fail");
assert_eq!(exp_kind, err.kind());
assert_eq!(self.exp_output.as_slice(), console.captured_out());
assert_eq!(self.exp_history, self.history);
}
}
#[test]
fn test_read_line_interactive_empty() {
ReadLineInteractiveTest::default().accept();
ReadLineInteractiveTest::default().add_key(Key::Backspace).accept();
ReadLineInteractiveTest::default().add_key(Key::Delete).accept();
ReadLineInteractiveTest::default()
.add_key(Key::EofOrDelete)
.expect_err(io::ErrorKind::UnexpectedEof);
ReadLineInteractiveTest::default().add_key(Key::ArrowLeft).accept();
ReadLineInteractiveTest::default().add_key(Key::ArrowRight).accept();
}
#[test]
fn test_read_line_with_prompt() {
ReadLineInteractiveTest::default()
.set_prompt("Ready> ")
.add_output(CapturedOut::Write("Ready> ".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key_chars("hello")
.add_output_bytes("hello")
.set_line("hello")
.accept();
ReadLineInteractiveTest::default()
.set_prompt("Cannot delete")
.add_output(CapturedOut::Write("Cannot delete".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key(Key::Backspace)
.accept();
}
#[test]
fn test_read_line_with_prompt_larger_than_screen() {
ReadLineInteractiveTest::default()
.set_size_chars(CharsXY::new(15, 5))
.set_prompt("This is larger than the screen> ")
.add_output(CapturedOut::Write("This is la...".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key_chars("hello")
.add_output_bytes("h")
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("e".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("l".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("l".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("o".to_string()))
.add_output(CapturedOut::ShowCursor)
.set_line("hello")
.accept();
}
#[test]
fn test_read_line_with_prompt_equal_to_screen() {
ReadLineInteractiveTest::default()
.set_size_chars(CharsXY::new(10, 5))
.set_prompt("0123456789")
.add_output(CapturedOut::Write("01234...".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key_chars("hello")
.add_output_bytes("h")
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("e".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("l".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("l".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("o".to_string()))
.add_output(CapturedOut::ShowCursor)
.set_line("hello")
.accept();
}
#[test]
fn test_read_line_with_prompt_larger_than_tiny_screen() {
ReadLineInteractiveTest::default()
.set_size_chars(CharsXY::new(3, 5))
.set_prompt("This is larger than the screen> ")
.add_key_chars("hello")
.add_output_bytes("he")
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::Write("el".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::Write("ll".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::Write("lo".to_string()))
.add_output(CapturedOut::ShowCursor)
.set_line("hello")
.accept();
}
#[test]
fn test_read_line_with_prompt_shorter_than_tiny_screen() {
ReadLineInteractiveTest::default()
.set_size_chars(CharsXY::new(3, 5))
.set_prompt("?")
.add_output(CapturedOut::Write("?".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key_chars("hello")
.add_output_bytes("h")
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("e".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("l".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("l".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("o".to_string()))
.add_output(CapturedOut::ShowCursor)
.set_line("hello")
.accept();
}
#[test]
fn test_read_line_interactive_trailing_input() {
ReadLineInteractiveTest::default()
.add_key_chars("hello")
.add_output_bytes("hello")
.set_line("hello")
.accept();
ReadLineInteractiveTest::default()
.set_previous("123")
.add_output(CapturedOut::Write("123".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key_chars("hello")
.add_output_bytes("hello")
.set_line("123hello")
.accept();
}
#[test]
fn test_read_line_interactive_middle_input() {
ReadLineInteractiveTest::default()
.add_key_chars("some text")
.add_output_bytes("some text")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key_chars(" ")
.add_output(CapturedOut::HideCursor)
.add_output_bytes(" ")
.add_output(CapturedOut::Write("xt".to_string()))
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.add_key_chars(".")
.add_output(CapturedOut::HideCursor)
.add_output_bytes(".")
.add_output(CapturedOut::Write("xt".to_string()))
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.set_line("some te .xt")
.accept();
}
#[test]
fn test_read_line_interactive_utf8_basic() {
ReadLineInteractiveTest::default()
.add_key_chars("é")
.add_output(CapturedOut::Write("é".to_string()))
.set_line("é")
.accept();
}
#[test]
fn test_read_line_interactive_utf8_remove_2byte_char() {
ReadLineInteractiveTest::default()
.add_key_chars("é")
.add_output(CapturedOut::Write("é".to_string()))
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output_bytes("")
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.set_line("")
.accept();
}
#[test]
fn test_read_line_interactive_utf8_add_and_remove_last() {
ReadLineInteractiveTest::default()
.add_key_chars("à é")
.add_output(CapturedOut::Write("Ã ".to_string()))
.add_output(CapturedOut::Write("é".to_string()))
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output_bytes("")
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.set_line("Ã ")
.accept();
}
#[test]
fn test_read_line_interactive_utf8_navigate_2byte_chars() {
ReadLineInteractiveTest::default()
.add_key_chars("à é")
.add_output(CapturedOut::Write("Ã ".to_string()))
.add_output(CapturedOut::Write("é".to_string()))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("é".to_string()))
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.set_line("é")
.accept();
}
#[test]
fn test_read_line_interactive_trailing_backspace() {
ReadLineInteractiveTest::default()
.add_key_chars("bar")
.add_output_bytes("bar")
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output_bytes("")
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.add_key_chars("zar")
.add_output_bytes("zar")
.set_line("bazar")
.accept();
}
#[test]
fn test_read_line_interactive_middle_backspace() {
ReadLineInteractiveTest::default()
.add_key_chars("has a tYpo")
.add_output_bytes("has a tYpo")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("po".to_string()))
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-3))
.add_output(CapturedOut::ShowCursor)
.add_key_chars("y")
.add_output(CapturedOut::HideCursor)
.add_output_bytes("y")
.add_output(CapturedOut::Write("po".to_string()))
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.set_line("has a typo")
.accept();
}
#[test]
fn test_read_line_interactive_middle_delete() {
ReadLineInteractiveTest::default()
.add_key_chars("has a typo")
.add_output_bytes("has a typo")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::Delete)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::Write("o".to_string()))
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.add_key_chars("e")
.add_output(CapturedOut::HideCursor)
.add_output_bytes("e")
.add_output(CapturedOut::Write("o".to_string()))
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.set_line("has a tyeo")
.accept();
}
#[test]
fn test_read_line_interactive_middle_ctrl_d_deletes() {
ReadLineInteractiveTest::default()
.add_key_chars("has a typo")
.add_output_bytes("has a typo")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::EofOrDelete)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::Write("o".to_string()))
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.add_key_chars("e")
.add_output(CapturedOut::HideCursor)
.add_output_bytes("e")
.add_output(CapturedOut::Write("o".to_string()))
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.set_line("has a tyeo")
.accept();
}
#[test]
fn test_read_line_interactive_utf8_delete_2byte_char() {
ReadLineInteractiveTest::default()
.add_key_chars("à é")
.add_output(CapturedOut::Write("Ã ".to_string()))
.add_output(CapturedOut::Write("é".to_string()))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::Delete)
.add_output(CapturedOut::HideCursor)
.add_output_bytes("")
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.set_line("Ã ")
.accept();
}
#[test]
fn test_read_line_interactive_delete_at_end_is_ignored() {
ReadLineInteractiveTest::default()
.set_previous("sample")
.add_output(CapturedOut::Write("sample".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key(Key::Delete)
.set_line("sample")
.accept();
}
#[test]
fn test_read_line_interactive_ctrl_d_at_end_is_ignored() {
ReadLineInteractiveTest::default()
.set_previous("sample")
.add_output(CapturedOut::Write("sample".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key(Key::EofOrDelete)
.set_line("sample")
.accept();
}
#[test]
fn test_read_line_interactive_test_move_bounds() {
ReadLineInteractiveTest::default()
.set_previous("12")
.add_output(CapturedOut::Write("12".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_key(Key::ArrowRight)
.add_key_chars("3")
.add_output_bytes("3")
.set_line("123")
.accept();
}
#[test]
fn test_read_line_interactive_test_home_end() {
ReadLineInteractiveTest::default()
.set_previous("sample text")
.add_output(CapturedOut::Write("sample text".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key(Key::End)
.add_key(Key::Home)
.add_output(CapturedOut::MoveWithinLine(-11))
.add_key(Key::Home)
.add_key(Key::Char('>'))
.add_output(CapturedOut::HideCursor)
.add_output_bytes(">")
.add_output(CapturedOut::Write("sample text".to_string()))
.add_output(CapturedOut::MoveWithinLine(-11))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::End)
.add_output(CapturedOut::MoveWithinLine(11))
.add_key(Key::Char('<'))
.add_output_bytes("<")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("text<".to_string()))
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-6))
.add_output(CapturedOut::ShowCursor)
.set_line(">sampletext<")
.accept();
}
#[test]
fn test_read_line_interactive_horizontal_scrolling_while_appending() {
ReadLineInteractiveTest::default()
.add_key_chars("12345678901234")
.add_output_bytes("12345678901234")
.add_key_chars("5")
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-14))
.add_output(CapturedOut::Write("23456789012345".to_string()))
.add_output(CapturedOut::ShowCursor)
.set_line("123456789012345")
.accept();
}
#[test]
fn test_read_line_interactive_horizontal_scrolling_while_navigating() {
ReadLineInteractiveTest::default()
.set_size_chars(CharsXY::new(6, 5))
.set_previous("abcdef")
.add_output(CapturedOut::Write("bcdef".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::Write("abcde".to_string()))
.add_output(CapturedOut::MoveWithinLine(-5))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-5))
.add_output(CapturedOut::Write("bcdef".to_string()))
.add_output(CapturedOut::ShowCursor)
.set_line("abcdef")
.accept();
}
#[test]
fn test_read_line_interactive_horizontal_scrolling_with_prompt_and_previous() {
ReadLineInteractiveTest::default()
.set_prompt("12345")
.set_previous("67890")
.add_output(CapturedOut::Write("1234567890".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key_chars("1234567890")
.add_output_bytes("1234")
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-9))
.add_output(CapturedOut::Write("789012345".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-9))
.add_output(CapturedOut::Write("890123456".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-9))
.add_output(CapturedOut::Write("901234567".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-9))
.add_output(CapturedOut::Write("012345678".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-9))
.add_output(CapturedOut::Write("123456789".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-9))
.add_output(CapturedOut::Write("234567890".to_string()))
.add_output(CapturedOut::ShowCursor)
.set_line("678901234567890")
.accept();
}
#[test]
fn test_read_line_interactive_horizontal_scrolling_secure() {
ReadLineInteractiveTest::default()
.set_size_chars(CharsXY::new(6, 5))
.set_echo(false)
.add_key_chars("abcdef")
.add_output_bytes("*****")
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-5))
.add_output(CapturedOut::Write("*****".to_string()))
.add_output(CapturedOut::ShowCursor)
.set_line("abcdef")
.accept();
}
#[test]
fn test_read_line_interactive_horizontal_scrolling_delete() {
ReadLineInteractiveTest::default()
.set_size_chars(CharsXY::new(6, 5))
.set_previous("abcdef")
.add_output(CapturedOut::Write("bcdef".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::Delete)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-4))
.add_output(CapturedOut::Write("bcde".to_string()))
.add_output(CapturedOut::Write(" ".to_string()))
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.set_line("abcde")
.accept();
}
#[test]
fn test_read_line_interactive_history_not_enabled_by_default() {
ReadLineInteractiveTest::default().add_key(Key::ArrowUp).accept();
ReadLineInteractiveTest::default().add_key(Key::ArrowDown).accept();
}
#[test]
fn test_read_line_interactive_history_empty() {
ReadLineInteractiveTest::default()
.set_history(vec![], vec!["foobarbaz".to_owned()])
.add_key_chars("foo")
.add_output_bytes("foo")
.add_key(Key::ArrowUp)
.add_key_chars("bar")
.add_output_bytes("bar")
.add_key(Key::ArrowDown)
.add_key_chars("baz")
.add_output_bytes("baz")
.set_line("foobarbaz")
.accept();
}
#[test]
fn test_read_line_interactive_skips_empty_lines() {
ReadLineInteractiveTest::default()
.set_history(vec!["first".to_owned()], vec!["first".to_owned()])
.add_key_chars("x")
.add_output(CapturedOut::Write("x".to_string()))
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output_bytes("")
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.accept();
}
#[test]
fn test_read_line_interactive_history_navigate_up_down_end_of_line() {
ReadLineInteractiveTest::default()
.set_prompt("? ")
.add_output(CapturedOut::Write("? ".to_string()))
.add_output(CapturedOut::SyncNow)
.set_history(
vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()],
vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()],
)
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::Write("last".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("last".len() as i16)))
.add_output(CapturedOut::Write(" second line".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-(" second line".len() as i16)))
.add_output(CapturedOut::Write("first".to_string()))
.add_output(CapturedOut::Write(" ".to_string()))
.add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowUp)
.add_key(Key::ArrowDown)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("first".len() as i16)))
.add_output(CapturedOut::Write(" second line".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowDown)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-(" second line".len() as i16)))
.add_output(CapturedOut::Write("last".to_string()))
.add_output(CapturedOut::Write(" ".to_string()))
.add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowDown)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("last".len() as i16)))
.add_output(CapturedOut::Write(" ".to_string()))
.add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowDown)
.accept();
}
#[test]
fn test_read_line_interactive_history_navigate_up_down_middle_of_line() {
ReadLineInteractiveTest::default()
.set_prompt("? ")
.add_output(CapturedOut::Write("? ".to_string()))
.add_output(CapturedOut::SyncNow)
.set_history(
vec!["a".to_owned(), "long-line".to_owned(), "zzzz".to_owned()],
vec!["a".to_owned(), "long-line".to_owned(), "zzzz".to_owned()],
)
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::Write("zzzz".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("zzzz".len() as i16)))
.add_output(CapturedOut::Write("long-line".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("long-line".len() as i16) + 4))
.add_output(CapturedOut::Write("a".to_string()))
.add_output(CapturedOut::Write(" ".to_string()))
.add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowUp)
.add_key(Key::ArrowDown)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("a".len() as i16)))
.add_output(CapturedOut::Write("long-line".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowDown)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("long-line".len() as i16) + 6))
.add_output(CapturedOut::Write("zzzz".to_string()))
.add_output(CapturedOut::Write(" ".to_string()))
.add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowDown)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("zzzz".len() as i16)))
.add_output(CapturedOut::Write(" ".to_string()))
.add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowDown)
.accept();
}
#[test]
fn test_read_line_interactive_history_navigate_and_edit() {
ReadLineInteractiveTest::default()
.set_prompt("? ")
.add_output(CapturedOut::Write("? ".to_string()))
.add_output(CapturedOut::SyncNow)
.set_history(
vec!["first".to_owned(), "second".to_owned(), "third".to_owned()],
vec![
"first".to_owned(),
"second".to_owned(),
"third".to_owned(),
"sec ond".to_owned(),
],
)
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::Write("third".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-5))
.add_output(CapturedOut::Write("second".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key_chars(" ")
.add_output(CapturedOut::HideCursor)
.add_output_bytes(" ")
.add_output(CapturedOut::Write("ond".to_string()))
.add_output(CapturedOut::MoveWithinLine(-3))
.add_output(CapturedOut::ShowCursor)
.set_line("sec ond")
.accept();
}
#[test]
fn test_read_line_ignored_keys() {
ReadLineInteractiveTest::default()
.add_key_chars("not ")
.add_output_bytes("not ")
.add_key(Key::Escape)
.add_key(Key::PageDown)
.add_key(Key::PageUp)
.add_key(Key::Tab)
.add_key_chars("affected")
.add_output_bytes("affected")
.set_line("not affected")
.accept();
}
#[test]
fn test_read_line_without_echo() {
ReadLineInteractiveTest::default()
.set_echo(false)
.set_prompt("> ")
.set_previous("pass1234")
.add_output(CapturedOut::Write("> ********".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key_chars("56")
.add_output_bytes("**")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write("**".to_string()))
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-3))
.add_output(CapturedOut::ShowCursor)
.add_output(CapturedOut::HideCursor)
.add_key_chars("7")
.add_output(CapturedOut::Write("***".to_string()))
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.set_line("pass123756")
.accept();
ReadLineInteractiveTest::default()
.set_echo(false)
.set_prompt("> ")
.set_previous("pass1234")
.add_output(CapturedOut::Write("> ********".to_string()))
.add_output(CapturedOut::SyncNow)
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::Delete)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::Write("*".to_string()))
.add_output_bytes(" ")
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.set_line("pass124")
.accept();
}
#[test]
fn test_read_line_secure_trivial_test() {
let mut console = MockConsole::default();
console.set_interactive(true);
console.add_input_keys(&[Key::Char('1'), Key::Char('5'), Key::NewLine]);
console.set_size_chars(CharsXY::new(15, 5));
let line = block_on(read_line_secure(&mut console, "> ")).unwrap();
assert_eq!("15", &line);
assert_eq!(
&[
CapturedOut::Write("> ".to_string()),
CapturedOut::SyncNow,
CapturedOut::Write("*".to_string()),
CapturedOut::Write("*".to_string()),
CapturedOut::Print("".to_owned()),
],
console.captured_out()
);
}
#[test]
fn test_read_line_secure_unsupported_in_noninteractive_console() {
let mut console = MockConsole::default();
let err = block_on(read_line_secure(&mut console, "> ")).unwrap_err();
assert!(format!("{}", err).contains("Cannot read secure"));
}
}