use crate::console::{Console, Key, LineBuffer};
use std::borrow::Cow;
use std::io;
const SECURE_CHAR: &str = "*";
fn update_line(
console: &mut dyn Console,
pos: usize,
clear_len: usize,
line: &LineBuffer,
) -> io::Result<()> {
console.hide_cursor()?;
if pos > 0 {
console.move_within_line(-(pos as i16))?;
}
if !line.is_empty() {
console.write(&line.to_string())?;
}
let line_len = line.len();
if line_len < clear_len {
let diff = clear_len - line_len;
console.write(&" ".repeat(diff))?;
console.move_within_line(-(diff as i16))?;
}
console.show_cursor()
}
async fn read_line_interactive(
console: &mut dyn Console,
prompt: &str,
previous: &str,
mut history: Option<&mut Vec<String>>,
echo: bool,
) -> io::Result<String> {
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 mut line = LineBuffer::from(previous);
if !prompt.is_empty() || !line.is_empty() {
if echo {
console.write(&format!("{}{}", prompt, line))?;
} else {
console.write(&format!("{}{}", prompt, "*".repeat(line.len())))?;
}
console.sync_now()?;
}
let width = {
console_width - prompt_len
};
let mut pos = line.len();
let mut history_pos = match history.as_mut() {
Some(history) => {
history.push(line.to_string());
history.len() - 1
}
None => 0,
};
loop {
match console.read_key().await? {
Key::ArrowUp => {
if let Some(history) = history.as_mut() {
if history_pos == 0 {
continue;
}
let clear_len = line.len();
history[history_pos] = line.into_inner();
history_pos -= 1;
line = LineBuffer::from(&history[history_pos]);
update_line(console, pos, clear_len, &line)?;
pos = line.len();
}
}
Key::ArrowDown => {
if let Some(history) = history.as_mut() {
if history_pos == history.len() - 1 {
continue;
}
let clear_len = line.len();
history[history_pos] = line.to_string();
history_pos += 1;
line = LineBuffer::from(&history[history_pos]);
update_line(console, pos, clear_len, &line)?;
pos = line.len();
}
}
Key::ArrowLeft => {
if pos > 0 {
console.move_within_line(-1)?;
pos -= 1;
}
}
Key::ArrowRight => {
if pos < line.len() {
console.move_within_line(1)?;
pos += 1;
}
}
Key::Backspace => {
if pos > 0 {
console.hide_cursor()?;
console.move_within_line(-1)?;
if echo {
console.write(&line.end(pos))?;
} else {
console.write(&SECURE_CHAR.repeat(line.len() - pos))?;
}
console.write(" ")?;
console.move_within_line(-((line.len() - pos) as i16 + 1))?;
console.show_cursor()?;
line.remove(pos - 1);
pos -= 1;
}
}
Key::CarriageReturn => {
if cfg!(not(target_os = "windows")) {
console.print("")?;
break;
}
}
Key::Char(ch) => {
let line_len = line.len();
debug_assert!(line_len < width);
if line_len == width - 1 {
continue;
}
if pos < line_len {
console.hide_cursor()?;
if echo {
let mut buf = [0u8; 4];
console.write(ch.encode_utf8(&mut buf))?;
console.write(&line.end(pos))?;
} else {
console.write(&SECURE_CHAR.repeat(line_len - pos + 1))?;
}
console.move_within_line(-((line_len - pos) as i16))?;
console.show_cursor()?;
line.insert(pos, ch);
} else {
if echo {
let mut buf = [0u8; 4];
console.write(ch.encode_utf8(&mut buf))?;
} else {
console.write(SECURE_CHAR)?;
}
line.insert(line_len, ch);
}
pos += 1;
}
Key::End => {
let offset = line.len() - pos;
if offset > 0 {
console.move_within_line(offset as i16)?;
pos += offset;
}
}
Key::Eof => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
Key::Escape => {
}
Key::Home => {
if pos > 0 {
console.move_within_line(-(pos as i16))?;
pos = 0;
}
}
Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
Key::NewLine => {
console.print("")?;
break;
}
Key::PageDown | Key::PageUp => {
}
Key::Tab => {
}
Key::Unknown(_) => (),
}
}
if let Some(history) = history.as_mut() {
if line.is_empty() {
history.pop();
} else {
let last = history.len() - 1;
history[last] = line.to_string();
}
}
Ok(line.into_inner())
}
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::End | Key::Home => (),
Key::Escape => (),
Key::Eof => 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(bad_input) => line += &bad_input,
}
}
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::new(
io::ErrorKind::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 = match self.history.as_mut() {
Some(history) => block_on(read_line_interactive(
&mut console,
self.prompt,
self.previous,
Some(history),
self.echo,
))
.unwrap(),
None => block_on(read_line_interactive(
&mut console,
self.prompt,
self.previous,
None,
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);
}
}
#[test]
fn test_read_line_interactive_empty() {
ReadLineInteractiveTest::default().accept();
ReadLineInteractiveTest::default().add_key(Key::Backspace).accept();
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")
.set_line("h")
.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")
.set_line("h")
.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")
.set_line("he")
.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")
.set_line("h")
.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_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_not_implemented() {
ReadLineInteractiveTest::default()
.add_key_chars("1234567890123456789")
.add_output_bytes("12345678901234")
.set_line("12345678901234")
.accept();
ReadLineInteractiveTest::default()
.add_key_chars("1234567890123456789")
.add_output_bytes("12345678901234")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key_chars("these will all be ignored")
.set_line("12345678901234")
.accept();
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")
.set_line("678901234")
.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("long second line".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowUp)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("long 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("long second line".to_string()))
.add_output(CapturedOut::ShowCursor)
.add_key(Key::ArrowDown)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-("long 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();
}
#[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"));
}
}