1use crate::console::{Console, Key, LineBuffer};
19use std::borrow::Cow;
20use std::io;
21
22const SECURE_CHAR: &str = "*";
24
25fn update_line(
29 console: &mut dyn Console,
30 pos: usize,
31 clear_len: usize,
32 line: &LineBuffer,
33) -> io::Result<()> {
34 console.hide_cursor()?;
35 if pos > 0 {
36 console.move_within_line(-(pos as i16))?;
37 }
38 if !line.is_empty() {
39 console.write(&line.to_string())?;
40 }
41 let line_len = line.len();
42 if line_len < clear_len {
43 let diff = clear_len - line_len;
44 console.write(&" ".repeat(diff))?;
45 console.move_within_line(-(diff as i16))?;
46 }
47 console.show_cursor()
48}
49
50async fn read_line_interactive(
54 console: &mut dyn Console,
55 prompt: &str,
56 previous: &str,
57 mut history: Option<&mut Vec<String>>,
58 echo: bool,
59) -> io::Result<String> {
60 let console_width = {
61 let console_size = console.size_chars()?;
62 usize::from(console_size.x)
63 };
64
65 let mut prompt = Cow::from(prompt);
66 let mut prompt_len = prompt.len();
67 if prompt_len >= console_width {
68 if console_width >= 5 {
69 prompt = Cow::from(format!("{}...", &prompt[0..console_width - 5]));
70 } else {
71 prompt = Cow::from("");
72 }
73 prompt_len = prompt.len();
74 }
75
76 let mut line = LineBuffer::from(previous);
77 if !prompt.is_empty() || !line.is_empty() {
78 if echo {
79 console.write(&format!("{}{}", prompt, line))?;
80 } else {
81 console.write(&format!("{}{}", prompt, "*".repeat(line.len())))?;
82 }
83 console.sync_now()?;
84 }
85
86 let width = {
87 console_width - prompt_len
90 };
91
92 let mut pos = line.len();
95
96 let mut history_pos = match history.as_mut() {
97 Some(history) => {
98 history.push(line.to_string());
99 history.len() - 1
100 }
101 None => 0,
102 };
103
104 loop {
105 match console.read_key().await? {
106 Key::ArrowUp => {
107 if let Some(history) = history.as_mut() {
108 if history_pos == 0 {
109 continue;
110 }
111
112 let clear_len = line.len();
113
114 history[history_pos] = line.into_inner();
115 history_pos -= 1;
116 line = LineBuffer::from(&history[history_pos]);
117
118 update_line(console, pos, clear_len, &line)?;
119
120 pos = line.len();
121 }
122 }
123
124 Key::ArrowDown => {
125 if let Some(history) = history.as_mut() {
126 if history_pos == history.len() - 1 {
127 continue;
128 }
129
130 let clear_len = line.len();
131
132 history[history_pos] = line.to_string();
133 history_pos += 1;
134 line = LineBuffer::from(&history[history_pos]);
135
136 update_line(console, pos, clear_len, &line)?;
137
138 pos = line.len();
139 }
140 }
141
142 Key::ArrowLeft => {
143 if pos > 0 {
144 console.move_within_line(-1)?;
145 pos -= 1;
146 }
147 }
148
149 Key::ArrowRight => {
150 if pos < line.len() {
151 console.move_within_line(1)?;
152 pos += 1;
153 }
154 }
155
156 Key::Backspace => {
157 if pos > 0 {
158 console.hide_cursor()?;
159 console.move_within_line(-1)?;
160 if echo {
161 console.write(&line.end(pos))?;
162 } else {
163 console.write(&SECURE_CHAR.repeat(line.len() - pos))?;
164 }
165 console.write(" ")?;
166 console.move_within_line(-((line.len() - pos) as i16 + 1))?;
167 console.show_cursor()?;
168 line.remove(pos - 1);
169 pos -= 1;
170 }
171 }
172
173 Key::CarriageReturn => {
174 if cfg!(not(target_os = "windows")) {
179 console.print("")?;
180 break;
181 }
182 }
183
184 Key::Char(ch) => {
185 let line_len = line.len();
186 debug_assert!(line_len < width);
187 if line_len == width - 1 {
188 continue;
191 }
192
193 if pos < line_len {
194 console.hide_cursor()?;
195 if echo {
196 let mut buf = [0u8; 4];
197 console.write(ch.encode_utf8(&mut buf))?;
198 console.write(&line.end(pos))?;
199 } else {
200 console.write(&SECURE_CHAR.repeat(line_len - pos + 1))?;
201 }
202 console.move_within_line(-((line_len - pos) as i16))?;
203 console.show_cursor()?;
204 line.insert(pos, ch);
205 } else {
206 if echo {
207 let mut buf = [0u8; 4];
208 console.write(ch.encode_utf8(&mut buf))?;
209 } else {
210 console.write(SECURE_CHAR)?;
211 }
212 line.insert(line_len, ch);
213 }
214 pos += 1;
215 }
216
217 Key::End => {
218 let offset = line.len() - pos;
219 if offset > 0 {
220 console.move_within_line(offset as i16)?;
221 pos += offset;
222 }
223 }
224
225 Key::Eof => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
226
227 Key::Escape => {
228 }
230
231 Key::Home => {
232 if pos > 0 {
233 console.move_within_line(-(pos as i16))?;
234 pos = 0;
235 }
236 }
237
238 Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
239
240 Key::NewLine => {
241 console.print("")?;
242 break;
243 }
244
245 Key::PageDown | Key::PageUp => {
246 }
248
249 Key::Tab => {
250 }
252
253 Key::Unknown(_) => (),
255 }
256 }
257
258 if let Some(history) = history.as_mut() {
259 if line.is_empty() {
260 history.pop();
261 } else {
262 let last = history.len() - 1;
263 history[last] = line.to_string();
264 }
265 }
266 Ok(line.into_inner())
267}
268
269async fn read_line_raw(console: &mut dyn Console) -> io::Result<String> {
271 let mut line = String::new();
272 loop {
273 match console.read_key().await? {
274 Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight => (),
275 Key::Backspace => {
276 if !line.is_empty() {
277 line.pop();
278 }
279 }
280 Key::CarriageReturn => {
281 if cfg!(not(target_os = "windows")) {
286 break;
287 }
288 }
289 Key::Char(ch) => line.push(ch),
290 Key::End | Key::Home => (),
291 Key::Escape => (),
292 Key::Eof => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
293 Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
294 Key::NewLine => break,
295 Key::PageDown | Key::PageUp => (),
296 Key::Tab => (),
297 Key::Unknown(bad_input) => line += &bad_input,
298 }
299 }
300 Ok(line)
301}
302
303pub async fn read_line(
306 console: &mut dyn Console,
307 prompt: &str,
308 previous: &str,
309 history: Option<&mut Vec<String>>,
310) -> io::Result<String> {
311 if console.is_interactive() {
312 read_line_interactive(console, prompt, previous, history, true).await
313 } else {
314 read_line_raw(console).await
315 }
316}
317
318pub async fn read_line_secure(console: &mut dyn Console, prompt: &str) -> io::Result<String> {
323 if !console.is_interactive() {
324 return Err(io::Error::new(
325 io::ErrorKind::Other,
326 "Cannot read secure strings from a raw console".to_owned(),
327 ));
328 }
329 read_line_interactive(console, prompt, "", None, false).await
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::console::CharsXY;
336 use crate::testutils::*;
337 use futures_lite::future::block_on;
338
339 #[must_use]
341 struct ReadLineInteractiveTest {
342 size_chars: CharsXY,
343 keys: Vec<Key>,
344 prompt: &'static str,
345 previous: &'static str,
346 history: Option<Vec<String>>,
347 echo: bool,
348 exp_line: &'static str,
349 exp_output: Vec<CapturedOut>,
350 exp_history: Option<Vec<String>>,
351 }
352
353 impl Default for ReadLineInteractiveTest {
354 fn default() -> Self {
357 Self {
358 size_chars: CharsXY::new(15, 5),
359 keys: vec![],
360 prompt: "",
361 previous: "",
362 history: None,
363 echo: true,
364 exp_line: "",
365 exp_output: vec![],
366 exp_history: None,
367 }
368 }
369 }
370
371 impl ReadLineInteractiveTest {
372 fn add_key(mut self, key: Key) -> Self {
374 self.keys.push(key);
375 self
376 }
377
378 fn add_key_chars(mut self, chars: &'static str) -> Self {
380 for ch in chars.chars() {
381 self.keys.push(Key::Char(ch));
382 }
383 self
384 }
385
386 fn add_output(mut self, output: CapturedOut) -> Self {
388 self.exp_output.push(output);
389 self
390 }
391
392 fn add_output_bytes(mut self, bytes: &'static str) -> Self {
394 if bytes.is_empty() {
395 self.exp_output.push(CapturedOut::Write("".to_string()))
396 } else {
397 for b in bytes.chars() {
398 let mut buf = [0u8; 4];
399 self.exp_output.push(CapturedOut::Write(b.encode_utf8(&mut buf).to_string()));
400 }
401 }
402 self
403 }
404
405 fn set_size_chars(mut self, size: CharsXY) -> Self {
407 self.size_chars = size;
408 self
409 }
410
411 fn set_line(mut self, line: &'static str) -> Self {
413 self.exp_line = line;
414 self
415 }
416
417 fn set_prompt(mut self, prompt: &'static str) -> Self {
419 self.prompt = prompt;
420 self
421 }
422
423 fn set_previous(mut self, previous: &'static str) -> Self {
425 self.previous = previous;
426 self
427 }
428
429 fn set_history(mut self, history: Vec<String>, exp_history: Vec<String>) -> Self {
432 self.history = Some(history);
433 self.exp_history = Some(exp_history);
434 self
435 }
436
437 fn set_echo(mut self, echo: bool) -> Self {
439 self.echo = echo;
440 self
441 }
442
443 fn accept(mut self) {
446 self.keys.push(Key::NewLine);
447 self.exp_output.push(CapturedOut::Print("".to_owned()));
448
449 let mut console = MockConsole::default();
450 console.add_input_keys(&self.keys);
451 console.set_size_chars(self.size_chars);
452 let line = match self.history.as_mut() {
453 Some(history) => block_on(read_line_interactive(
454 &mut console,
455 self.prompt,
456 self.previous,
457 Some(history),
458 self.echo,
459 ))
460 .unwrap(),
461 None => block_on(read_line_interactive(
462 &mut console,
463 self.prompt,
464 self.previous,
465 None,
466 self.echo,
467 ))
468 .unwrap(),
469 };
470 assert_eq!(self.exp_line, &line);
471 assert_eq!(self.exp_output.as_slice(), console.captured_out());
472 assert_eq!(self.exp_history, self.history);
473 }
474 }
475
476 #[test]
477 fn test_read_line_interactive_empty() {
478 ReadLineInteractiveTest::default().accept();
479 ReadLineInteractiveTest::default().add_key(Key::Backspace).accept();
480 ReadLineInteractiveTest::default().add_key(Key::ArrowLeft).accept();
481 ReadLineInteractiveTest::default().add_key(Key::ArrowRight).accept();
482 }
483
484 #[test]
485 fn test_read_line_with_prompt() {
486 ReadLineInteractiveTest::default()
487 .set_prompt("Ready> ")
488 .add_output(CapturedOut::Write("Ready> ".to_string()))
489 .add_output(CapturedOut::SyncNow)
490 .add_key_chars("hello")
492 .add_output_bytes("hello")
493 .set_line("hello")
495 .accept();
496
497 ReadLineInteractiveTest::default()
498 .set_prompt("Cannot delete")
499 .add_output(CapturedOut::Write("Cannot delete".to_string()))
500 .add_output(CapturedOut::SyncNow)
501 .add_key(Key::Backspace)
503 .accept();
504 }
505
506 #[test]
507 fn test_read_line_with_prompt_larger_than_screen() {
508 ReadLineInteractiveTest::default()
509 .set_size_chars(CharsXY::new(15, 5))
510 .set_prompt("This is larger than the screen> ")
511 .add_output(CapturedOut::Write("This is la...".to_string()))
512 .add_output(CapturedOut::SyncNow)
513 .add_key_chars("hello")
515 .add_output_bytes("h")
516 .set_line("h")
518 .accept();
519 }
520
521 #[test]
522 fn test_read_line_with_prompt_equal_to_screen() {
523 ReadLineInteractiveTest::default()
524 .set_size_chars(CharsXY::new(10, 5))
525 .set_prompt("0123456789")
526 .add_output(CapturedOut::Write("01234...".to_string()))
527 .add_output(CapturedOut::SyncNow)
528 .add_key_chars("hello")
530 .add_output_bytes("h")
531 .set_line("h")
533 .accept();
534 }
535
536 #[test]
537 fn test_read_line_with_prompt_larger_than_tiny_screen() {
538 ReadLineInteractiveTest::default()
539 .set_size_chars(CharsXY::new(3, 5))
540 .set_prompt("This is larger than the screen> ")
541 .add_key_chars("hello")
543 .add_output_bytes("he")
544 .set_line("he")
546 .accept();
547 }
548
549 #[test]
550 fn test_read_line_with_prompt_shorter_than_tiny_screen() {
551 ReadLineInteractiveTest::default()
552 .set_size_chars(CharsXY::new(3, 5))
553 .set_prompt("?")
554 .add_output(CapturedOut::Write("?".to_string()))
555 .add_output(CapturedOut::SyncNow)
556 .add_key_chars("hello")
558 .add_output_bytes("h")
559 .set_line("h")
561 .accept();
562 }
563
564 #[test]
565 fn test_read_line_interactive_trailing_input() {
566 ReadLineInteractiveTest::default()
567 .add_key_chars("hello")
568 .add_output_bytes("hello")
569 .set_line("hello")
571 .accept();
572
573 ReadLineInteractiveTest::default()
574 .set_previous("123")
575 .add_output(CapturedOut::Write("123".to_string()))
576 .add_output(CapturedOut::SyncNow)
577 .add_key_chars("hello")
579 .add_output_bytes("hello")
580 .set_line("123hello")
582 .accept();
583 }
584
585 #[test]
586 fn test_read_line_interactive_middle_input() {
587 ReadLineInteractiveTest::default()
588 .add_key_chars("some text")
589 .add_output_bytes("some text")
590 .add_key(Key::ArrowLeft)
592 .add_output(CapturedOut::MoveWithinLine(-1))
593 .add_key(Key::ArrowLeft)
595 .add_output(CapturedOut::MoveWithinLine(-1))
596 .add_key(Key::ArrowLeft)
598 .add_output(CapturedOut::MoveWithinLine(-1))
599 .add_key(Key::ArrowRight)
601 .add_output(CapturedOut::MoveWithinLine(1))
602 .add_key_chars(" ")
604 .add_output(CapturedOut::HideCursor)
605 .add_output_bytes(" ")
606 .add_output(CapturedOut::Write("xt".to_string()))
607 .add_output(CapturedOut::MoveWithinLine(-2))
608 .add_output(CapturedOut::ShowCursor)
609 .add_key_chars(".")
611 .add_output(CapturedOut::HideCursor)
612 .add_output_bytes(".")
613 .add_output(CapturedOut::Write("xt".to_string()))
614 .add_output(CapturedOut::MoveWithinLine(-2))
615 .add_output(CapturedOut::ShowCursor)
616 .set_line("some te .xt")
618 .accept();
619 }
620
621 #[test]
622 fn test_read_line_interactive_utf8_basic() {
623 ReadLineInteractiveTest::default()
624 .add_key_chars("é")
625 .add_output(CapturedOut::Write("é".to_string()))
626 .set_line("é")
628 .accept();
629 }
630
631 #[test]
632 fn test_read_line_interactive_utf8_remove_2byte_char() {
633 ReadLineInteractiveTest::default()
634 .add_key_chars("é")
635 .add_output(CapturedOut::Write("é".to_string()))
636 .add_key(Key::Backspace)
638 .add_output(CapturedOut::HideCursor)
639 .add_output(CapturedOut::MoveWithinLine(-1))
640 .add_output_bytes("")
641 .add_output_bytes(" ")
642 .add_output(CapturedOut::MoveWithinLine(-1))
643 .add_output(CapturedOut::ShowCursor)
644 .set_line("")
646 .accept();
647 }
648
649 #[test]
650 fn test_read_line_interactive_utf8_add_and_remove_last() {
651 ReadLineInteractiveTest::default()
652 .add_key_chars("àé")
653 .add_output(CapturedOut::Write("à".to_string()))
654 .add_output(CapturedOut::Write("é".to_string()))
655 .add_key(Key::Backspace)
657 .add_output(CapturedOut::HideCursor)
658 .add_output(CapturedOut::MoveWithinLine(-1))
659 .add_output_bytes("")
660 .add_output_bytes(" ")
661 .add_output(CapturedOut::MoveWithinLine(-1))
662 .add_output(CapturedOut::ShowCursor)
663 .set_line("à")
665 .accept();
666 }
667
668 #[test]
669 fn test_read_line_interactive_utf8_navigate_2byte_chars() {
670 ReadLineInteractiveTest::default()
671 .add_key_chars("àé")
672 .add_output(CapturedOut::Write("à".to_string()))
673 .add_output(CapturedOut::Write("é".to_string()))
674 .add_key(Key::ArrowLeft)
676 .add_output(CapturedOut::MoveWithinLine(-1))
677 .add_key(Key::ArrowLeft)
679 .add_output(CapturedOut::MoveWithinLine(-1))
680 .add_key(Key::ArrowLeft)
682 .add_key(Key::ArrowRight)
684 .add_output(CapturedOut::MoveWithinLine(1))
685 .add_key(Key::Backspace)
687 .add_output(CapturedOut::HideCursor)
688 .add_output(CapturedOut::MoveWithinLine(-1))
689 .add_output(CapturedOut::Write("é".to_string()))
690 .add_output_bytes(" ")
691 .add_output(CapturedOut::MoveWithinLine(-2))
692 .add_output(CapturedOut::ShowCursor)
693 .set_line("é")
695 .accept();
696 }
697
698 #[test]
699 fn test_read_line_interactive_trailing_backspace() {
700 ReadLineInteractiveTest::default()
701 .add_key_chars("bar")
702 .add_output_bytes("bar")
703 .add_key(Key::Backspace)
705 .add_output(CapturedOut::HideCursor)
706 .add_output(CapturedOut::MoveWithinLine(-1))
707 .add_output_bytes("")
708 .add_output_bytes(" ")
709 .add_output(CapturedOut::MoveWithinLine(-1))
710 .add_output(CapturedOut::ShowCursor)
711 .add_key_chars("zar")
713 .add_output_bytes("zar")
714 .set_line("bazar")
716 .accept();
717 }
718
719 #[test]
720 fn test_read_line_interactive_middle_backspace() {
721 ReadLineInteractiveTest::default()
722 .add_key_chars("has a tYpo")
723 .add_output_bytes("has a tYpo")
724 .add_key(Key::ArrowLeft)
726 .add_output(CapturedOut::MoveWithinLine(-1))
727 .add_key(Key::ArrowLeft)
729 .add_output(CapturedOut::MoveWithinLine(-1))
730 .add_key(Key::Backspace)
732 .add_output(CapturedOut::HideCursor)
733 .add_output(CapturedOut::MoveWithinLine(-1))
734 .add_output(CapturedOut::Write("po".to_string()))
735 .add_output_bytes(" ")
736 .add_output(CapturedOut::MoveWithinLine(-3))
737 .add_output(CapturedOut::ShowCursor)
738 .add_key_chars("y")
740 .add_output(CapturedOut::HideCursor)
741 .add_output_bytes("y")
742 .add_output(CapturedOut::Write("po".to_string()))
743 .add_output(CapturedOut::MoveWithinLine(-2))
744 .add_output(CapturedOut::ShowCursor)
745 .set_line("has a typo")
747 .accept();
748 }
749
750 #[test]
751 fn test_read_line_interactive_test_move_bounds() {
752 ReadLineInteractiveTest::default()
753 .set_previous("12")
754 .add_output(CapturedOut::Write("12".to_string()))
755 .add_output(CapturedOut::SyncNow)
756 .add_key(Key::ArrowLeft)
758 .add_output(CapturedOut::MoveWithinLine(-1))
759 .add_key(Key::ArrowLeft)
761 .add_output(CapturedOut::MoveWithinLine(-1))
762 .add_key(Key::ArrowLeft)
764 .add_key(Key::ArrowLeft)
765 .add_key(Key::ArrowLeft)
766 .add_key(Key::ArrowLeft)
767 .add_key(Key::ArrowRight)
769 .add_output(CapturedOut::MoveWithinLine(1))
770 .add_key(Key::ArrowRight)
772 .add_output(CapturedOut::MoveWithinLine(1))
773 .add_key(Key::ArrowRight)
775 .add_key(Key::ArrowRight)
776 .add_key_chars("3")
778 .add_output_bytes("3")
779 .set_line("123")
781 .accept();
782 }
783
784 #[test]
785 fn test_read_line_interactive_test_home_end() {
786 ReadLineInteractiveTest::default()
787 .set_previous("sample text")
788 .add_output(CapturedOut::Write("sample text".to_string()))
789 .add_output(CapturedOut::SyncNow)
790 .add_key(Key::End)
792 .add_key(Key::Home)
794 .add_output(CapturedOut::MoveWithinLine(-11))
795 .add_key(Key::Home)
797 .add_key(Key::Char('>'))
799 .add_output(CapturedOut::HideCursor)
800 .add_output_bytes(">")
801 .add_output(CapturedOut::Write("sample text".to_string()))
802 .add_output(CapturedOut::MoveWithinLine(-11))
803 .add_output(CapturedOut::ShowCursor)
804 .add_key(Key::End)
806 .add_output(CapturedOut::MoveWithinLine(11))
807 .add_key(Key::Char('<'))
809 .add_output_bytes("<")
810 .add_key(Key::ArrowLeft)
812 .add_output(CapturedOut::MoveWithinLine(-1))
813 .add_key(Key::ArrowLeft)
815 .add_output(CapturedOut::MoveWithinLine(-1))
816 .add_key(Key::ArrowLeft)
818 .add_output(CapturedOut::MoveWithinLine(-1))
819 .add_key(Key::ArrowLeft)
821 .add_output(CapturedOut::MoveWithinLine(-1))
822 .add_key(Key::ArrowLeft)
824 .add_output(CapturedOut::MoveWithinLine(-1))
825 .add_key(Key::Backspace)
827 .add_output(CapturedOut::HideCursor)
828 .add_output(CapturedOut::MoveWithinLine(-1))
829 .add_output(CapturedOut::Write("text<".to_string()))
830 .add_output_bytes(" ")
831 .add_output(CapturedOut::MoveWithinLine(-6))
832 .add_output(CapturedOut::ShowCursor)
833 .set_line(">sampletext<")
835 .accept();
836 }
837
838 #[test]
839 fn test_read_line_interactive_horizontal_scrolling_not_implemented() {
840 ReadLineInteractiveTest::default()
841 .add_key_chars("1234567890123456789")
842 .add_output_bytes("12345678901234")
843 .set_line("12345678901234")
845 .accept();
846
847 ReadLineInteractiveTest::default()
848 .add_key_chars("1234567890123456789")
849 .add_output_bytes("12345678901234")
850 .add_key(Key::ArrowLeft)
852 .add_output(CapturedOut::MoveWithinLine(-1))
853 .add_key(Key::ArrowLeft)
855 .add_output(CapturedOut::MoveWithinLine(-1))
856 .add_key_chars("these will all be ignored")
858 .set_line("12345678901234")
860 .accept();
861
862 ReadLineInteractiveTest::default()
863 .set_prompt("12345")
864 .set_previous("67890")
865 .add_output(CapturedOut::Write("1234567890".to_string()))
866 .add_output(CapturedOut::SyncNow)
867 .add_key_chars("1234567890")
869 .add_output_bytes("1234")
870 .set_line("678901234")
872 .accept();
873 }
874
875 #[test]
876 fn test_read_line_interactive_history_not_enabled_by_default() {
877 ReadLineInteractiveTest::default().add_key(Key::ArrowUp).accept();
878 ReadLineInteractiveTest::default().add_key(Key::ArrowDown).accept();
879 }
880
881 #[test]
882 fn test_read_line_interactive_history_empty() {
883 ReadLineInteractiveTest::default()
884 .set_history(vec![], vec!["foobarbaz".to_owned()])
885 .add_key_chars("foo")
887 .add_output_bytes("foo")
888 .add_key(Key::ArrowUp)
890 .add_key_chars("bar")
892 .add_output_bytes("bar")
893 .add_key(Key::ArrowDown)
895 .add_key_chars("baz")
897 .add_output_bytes("baz")
898 .set_line("foobarbaz")
900 .accept();
901 }
902
903 #[test]
904 fn test_read_line_interactive_skips_empty_lines() {
905 ReadLineInteractiveTest::default()
906 .set_history(vec!["first".to_owned()], vec!["first".to_owned()])
907 .add_key_chars("x")
909 .add_output(CapturedOut::Write("x".to_string()))
910 .add_key(Key::Backspace)
912 .add_output(CapturedOut::HideCursor)
913 .add_output(CapturedOut::MoveWithinLine(-1))
914 .add_output_bytes("")
915 .add_output_bytes(" ")
916 .add_output(CapturedOut::MoveWithinLine(-1))
917 .add_output(CapturedOut::ShowCursor)
918 .accept();
920 }
921
922 #[test]
923 fn test_read_line_interactive_history_navigate_up_down_end_of_line() {
924 ReadLineInteractiveTest::default()
925 .set_prompt("? ")
926 .add_output(CapturedOut::Write("? ".to_string()))
927 .add_output(CapturedOut::SyncNow)
928 .set_history(
930 vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()],
931 vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()],
932 )
933 .add_key(Key::ArrowUp)
935 .add_output(CapturedOut::HideCursor)
936 .add_output(CapturedOut::Write("last".to_string()))
937 .add_output(CapturedOut::ShowCursor)
938 .add_key(Key::ArrowUp)
940 .add_output(CapturedOut::HideCursor)
941 .add_output(CapturedOut::MoveWithinLine(-("last".len() as i16)))
942 .add_output(CapturedOut::Write("long second line".to_string()))
943 .add_output(CapturedOut::ShowCursor)
944 .add_key(Key::ArrowUp)
946 .add_output(CapturedOut::HideCursor)
947 .add_output(CapturedOut::MoveWithinLine(-("long second line".len() as i16)))
948 .add_output(CapturedOut::Write("first".to_string()))
949 .add_output(CapturedOut::Write(" ".to_string()))
950 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
951 .add_output(CapturedOut::ShowCursor)
952 .add_key(Key::ArrowUp)
954 .add_key(Key::ArrowDown)
956 .add_output(CapturedOut::HideCursor)
957 .add_output(CapturedOut::MoveWithinLine(-("first".len() as i16)))
958 .add_output(CapturedOut::Write("long second line".to_string()))
959 .add_output(CapturedOut::ShowCursor)
960 .add_key(Key::ArrowDown)
962 .add_output(CapturedOut::HideCursor)
963 .add_output(CapturedOut::MoveWithinLine(-("long second line".len() as i16)))
964 .add_output(CapturedOut::Write("last".to_string()))
965 .add_output(CapturedOut::Write(" ".to_string()))
966 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
967 .add_output(CapturedOut::ShowCursor)
968 .add_key(Key::ArrowDown)
970 .add_output(CapturedOut::HideCursor)
971 .add_output(CapturedOut::MoveWithinLine(-("last".len() as i16)))
972 .add_output(CapturedOut::Write(" ".to_string()))
973 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
974 .add_output(CapturedOut::ShowCursor)
975 .add_key(Key::ArrowDown)
977 .accept();
979 }
980
981 #[test]
982 fn test_read_line_interactive_history_navigate_up_down_middle_of_line() {
983 ReadLineInteractiveTest::default()
984 .set_prompt("? ")
985 .add_output(CapturedOut::Write("? ".to_string()))
986 .add_output(CapturedOut::SyncNow)
987 .set_history(
989 vec!["a".to_owned(), "long-line".to_owned(), "zzzz".to_owned()],
990 vec!["a".to_owned(), "long-line".to_owned(), "zzzz".to_owned()],
991 )
992 .add_key(Key::ArrowUp)
994 .add_output(CapturedOut::HideCursor)
995 .add_output(CapturedOut::Write("zzzz".to_string()))
996 .add_output(CapturedOut::ShowCursor)
997 .add_key(Key::ArrowUp)
999 .add_output(CapturedOut::HideCursor)
1000 .add_output(CapturedOut::MoveWithinLine(-("zzzz".len() as i16)))
1001 .add_output(CapturedOut::Write("long-line".to_string()))
1002 .add_output(CapturedOut::ShowCursor)
1003 .add_key(Key::ArrowLeft)
1005 .add_output(CapturedOut::MoveWithinLine(-1))
1006 .add_key(Key::ArrowLeft)
1007 .add_output(CapturedOut::MoveWithinLine(-1))
1008 .add_key(Key::ArrowLeft)
1009 .add_output(CapturedOut::MoveWithinLine(-1))
1010 .add_key(Key::ArrowLeft)
1011 .add_output(CapturedOut::MoveWithinLine(-1))
1012 .add_key(Key::ArrowUp)
1014 .add_output(CapturedOut::HideCursor)
1015 .add_output(CapturedOut::MoveWithinLine(-("long-line".len() as i16) + 4))
1016 .add_output(CapturedOut::Write("a".to_string()))
1017 .add_output(CapturedOut::Write(" ".to_string()))
1018 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
1019 .add_output(CapturedOut::ShowCursor)
1020 .add_key(Key::ArrowUp)
1022 .add_key(Key::ArrowDown)
1024 .add_output(CapturedOut::HideCursor)
1025 .add_output(CapturedOut::MoveWithinLine(-("a".len() as i16)))
1026 .add_output(CapturedOut::Write("long-line".to_string()))
1027 .add_output(CapturedOut::ShowCursor)
1028 .add_key(Key::ArrowLeft)
1030 .add_output(CapturedOut::MoveWithinLine(-1))
1031 .add_key(Key::ArrowLeft)
1032 .add_output(CapturedOut::MoveWithinLine(-1))
1033 .add_key(Key::ArrowLeft)
1034 .add_output(CapturedOut::MoveWithinLine(-1))
1035 .add_key(Key::ArrowLeft)
1036 .add_output(CapturedOut::MoveWithinLine(-1))
1037 .add_key(Key::ArrowLeft)
1038 .add_output(CapturedOut::MoveWithinLine(-1))
1039 .add_key(Key::ArrowLeft)
1040 .add_output(CapturedOut::MoveWithinLine(-1))
1041 .add_key(Key::ArrowDown)
1043 .add_output(CapturedOut::HideCursor)
1044 .add_output(CapturedOut::MoveWithinLine(-("long-line".len() as i16) + 6))
1045 .add_output(CapturedOut::Write("zzzz".to_string()))
1046 .add_output(CapturedOut::Write(" ".to_string()))
1047 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
1048 .add_output(CapturedOut::ShowCursor)
1049 .add_key(Key::ArrowDown)
1051 .add_output(CapturedOut::HideCursor)
1052 .add_output(CapturedOut::MoveWithinLine(-("zzzz".len() as i16)))
1053 .add_output(CapturedOut::Write(" ".to_string()))
1054 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
1055 .add_output(CapturedOut::ShowCursor)
1056 .add_key(Key::ArrowDown)
1058 .accept();
1060 }
1061
1062 #[test]
1063 fn test_read_line_interactive_history_navigate_and_edit() {
1064 ReadLineInteractiveTest::default()
1065 .set_prompt("? ")
1066 .add_output(CapturedOut::Write("? ".to_string()))
1067 .add_output(CapturedOut::SyncNow)
1068 .set_history(
1070 vec!["first".to_owned(), "second".to_owned(), "third".to_owned()],
1071 vec![
1072 "first".to_owned(),
1073 "second".to_owned(),
1074 "third".to_owned(),
1075 "sec ond".to_owned(),
1076 ],
1077 )
1078 .add_key(Key::ArrowUp)
1080 .add_output(CapturedOut::HideCursor)
1081 .add_output(CapturedOut::Write("third".to_string()))
1082 .add_output(CapturedOut::ShowCursor)
1083 .add_key(Key::ArrowUp)
1085 .add_output(CapturedOut::HideCursor)
1086 .add_output(CapturedOut::MoveWithinLine(-5))
1087 .add_output(CapturedOut::Write("second".to_string()))
1088 .add_output(CapturedOut::ShowCursor)
1089 .add_key(Key::ArrowLeft)
1091 .add_output(CapturedOut::MoveWithinLine(-1))
1092 .add_key(Key::ArrowLeft)
1094 .add_output(CapturedOut::MoveWithinLine(-1))
1095 .add_key(Key::ArrowLeft)
1097 .add_output(CapturedOut::MoveWithinLine(-1))
1098 .add_key_chars(" ")
1100 .add_output(CapturedOut::HideCursor)
1101 .add_output_bytes(" ")
1102 .add_output(CapturedOut::Write("ond".to_string()))
1103 .add_output(CapturedOut::MoveWithinLine(-3))
1104 .add_output(CapturedOut::ShowCursor)
1105 .set_line("sec ond")
1107 .accept();
1108 }
1109
1110 #[test]
1111 fn test_read_line_ignored_keys() {
1112 ReadLineInteractiveTest::default()
1113 .add_key_chars("not ")
1114 .add_output_bytes("not ")
1115 .add_key(Key::Escape)
1117 .add_key(Key::PageDown)
1118 .add_key(Key::PageUp)
1119 .add_key(Key::Tab)
1120 .add_key_chars("affected")
1122 .add_output_bytes("affected")
1123 .set_line("not affected")
1125 .accept();
1126 }
1127
1128 #[test]
1129 fn test_read_line_without_echo() {
1130 ReadLineInteractiveTest::default()
1131 .set_echo(false)
1132 .set_prompt("> ")
1133 .set_previous("pass1234")
1134 .add_output(CapturedOut::Write("> ********".to_string()))
1135 .add_output(CapturedOut::SyncNow)
1136 .add_key_chars("56")
1138 .add_output_bytes("**")
1139 .add_key(Key::ArrowLeft)
1141 .add_output(CapturedOut::MoveWithinLine(-1))
1142 .add_key(Key::ArrowLeft)
1144 .add_output(CapturedOut::MoveWithinLine(-1))
1145 .add_key(Key::Backspace)
1147 .add_output(CapturedOut::HideCursor)
1148 .add_output(CapturedOut::MoveWithinLine(-1))
1149 .add_output(CapturedOut::Write("**".to_string()))
1150 .add_output_bytes(" ")
1151 .add_output(CapturedOut::MoveWithinLine(-3))
1152 .add_output(CapturedOut::ShowCursor)
1153 .add_output(CapturedOut::HideCursor)
1155 .add_key_chars("7")
1156 .add_output(CapturedOut::Write("***".to_string()))
1157 .add_output(CapturedOut::MoveWithinLine(-2))
1158 .add_output(CapturedOut::ShowCursor)
1159 .set_line("pass123756")
1161 .accept();
1162 }
1163
1164 #[test]
1165 fn test_read_line_secure_trivial_test() {
1166 let mut console = MockConsole::default();
1167 console.set_interactive(true);
1168 console.add_input_keys(&[Key::Char('1'), Key::Char('5'), Key::NewLine]);
1169 console.set_size_chars(CharsXY::new(15, 5));
1170 let line = block_on(read_line_secure(&mut console, "> ")).unwrap();
1171 assert_eq!("15", &line);
1172 assert_eq!(
1173 &[
1174 CapturedOut::Write("> ".to_string()),
1175 CapturedOut::SyncNow,
1176 CapturedOut::Write("*".to_string()),
1177 CapturedOut::Write("*".to_string()),
1178 CapturedOut::Print("".to_owned()),
1179 ],
1180 console.captured_out()
1181 );
1182 }
1183
1184 #[test]
1185 fn test_read_line_secure_unsupported_in_noninteractive_console() {
1186 let mut console = MockConsole::default();
1187 let err = block_on(read_line_secure(&mut console, "> ")).unwrap_err();
1188 assert!(format!("{}", err).contains("Cannot read secure"));
1189 }
1190}