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 => line.push('?'),
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::other("Cannot read secure strings from a raw console".to_owned()));
325 }
326 read_line_interactive(console, prompt, "", None, false).await
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::console::CharsXY;
333 use crate::testutils::*;
334 use futures_lite::future::block_on;
335
336 #[must_use]
338 struct ReadLineInteractiveTest {
339 size_chars: CharsXY,
340 keys: Vec<Key>,
341 prompt: &'static str,
342 previous: &'static str,
343 history: Option<Vec<String>>,
344 echo: bool,
345 exp_line: &'static str,
346 exp_output: Vec<CapturedOut>,
347 exp_history: Option<Vec<String>>,
348 }
349
350 impl Default for ReadLineInteractiveTest {
351 fn default() -> Self {
354 Self {
355 size_chars: CharsXY::new(15, 5),
356 keys: vec![],
357 prompt: "",
358 previous: "",
359 history: None,
360 echo: true,
361 exp_line: "",
362 exp_output: vec![],
363 exp_history: None,
364 }
365 }
366 }
367
368 impl ReadLineInteractiveTest {
369 fn add_key(mut self, key: Key) -> Self {
371 self.keys.push(key);
372 self
373 }
374
375 fn add_key_chars(mut self, chars: &'static str) -> Self {
377 for ch in chars.chars() {
378 self.keys.push(Key::Char(ch));
379 }
380 self
381 }
382
383 fn add_output(mut self, output: CapturedOut) -> Self {
385 self.exp_output.push(output);
386 self
387 }
388
389 fn add_output_bytes(mut self, bytes: &'static str) -> Self {
391 if bytes.is_empty() {
392 self.exp_output.push(CapturedOut::Write("".to_string()))
393 } else {
394 for b in bytes.chars() {
395 let mut buf = [0u8; 4];
396 self.exp_output.push(CapturedOut::Write(b.encode_utf8(&mut buf).to_string()));
397 }
398 }
399 self
400 }
401
402 fn set_size_chars(mut self, size: CharsXY) -> Self {
404 self.size_chars = size;
405 self
406 }
407
408 fn set_line(mut self, line: &'static str) -> Self {
410 self.exp_line = line;
411 self
412 }
413
414 fn set_prompt(mut self, prompt: &'static str) -> Self {
416 self.prompt = prompt;
417 self
418 }
419
420 fn set_previous(mut self, previous: &'static str) -> Self {
422 self.previous = previous;
423 self
424 }
425
426 fn set_history(mut self, history: Vec<String>, exp_history: Vec<String>) -> Self {
429 self.history = Some(history);
430 self.exp_history = Some(exp_history);
431 self
432 }
433
434 fn set_echo(mut self, echo: bool) -> Self {
436 self.echo = echo;
437 self
438 }
439
440 fn accept(mut self) {
443 self.keys.push(Key::NewLine);
444 self.exp_output.push(CapturedOut::Print("".to_owned()));
445
446 let mut console = MockConsole::default();
447 console.add_input_keys(&self.keys);
448 console.set_size_chars(self.size_chars);
449 let line = match self.history.as_mut() {
450 Some(history) => block_on(read_line_interactive(
451 &mut console,
452 self.prompt,
453 self.previous,
454 Some(history),
455 self.echo,
456 ))
457 .unwrap(),
458 None => block_on(read_line_interactive(
459 &mut console,
460 self.prompt,
461 self.previous,
462 None,
463 self.echo,
464 ))
465 .unwrap(),
466 };
467 assert_eq!(self.exp_line, &line);
468 assert_eq!(self.exp_output.as_slice(), console.captured_out());
469 assert_eq!(self.exp_history, self.history);
470 }
471 }
472
473 #[test]
474 fn test_read_line_interactive_empty() {
475 ReadLineInteractiveTest::default().accept();
476 ReadLineInteractiveTest::default().add_key(Key::Backspace).accept();
477 ReadLineInteractiveTest::default().add_key(Key::ArrowLeft).accept();
478 ReadLineInteractiveTest::default().add_key(Key::ArrowRight).accept();
479 }
480
481 #[test]
482 fn test_read_line_with_prompt() {
483 ReadLineInteractiveTest::default()
484 .set_prompt("Ready> ")
485 .add_output(CapturedOut::Write("Ready> ".to_string()))
486 .add_output(CapturedOut::SyncNow)
487 .add_key_chars("hello")
489 .add_output_bytes("hello")
490 .set_line("hello")
492 .accept();
493
494 ReadLineInteractiveTest::default()
495 .set_prompt("Cannot delete")
496 .add_output(CapturedOut::Write("Cannot delete".to_string()))
497 .add_output(CapturedOut::SyncNow)
498 .add_key(Key::Backspace)
500 .accept();
501 }
502
503 #[test]
504 fn test_read_line_with_prompt_larger_than_screen() {
505 ReadLineInteractiveTest::default()
506 .set_size_chars(CharsXY::new(15, 5))
507 .set_prompt("This is larger than the screen> ")
508 .add_output(CapturedOut::Write("This is la...".to_string()))
509 .add_output(CapturedOut::SyncNow)
510 .add_key_chars("hello")
512 .add_output_bytes("h")
513 .set_line("h")
515 .accept();
516 }
517
518 #[test]
519 fn test_read_line_with_prompt_equal_to_screen() {
520 ReadLineInteractiveTest::default()
521 .set_size_chars(CharsXY::new(10, 5))
522 .set_prompt("0123456789")
523 .add_output(CapturedOut::Write("01234...".to_string()))
524 .add_output(CapturedOut::SyncNow)
525 .add_key_chars("hello")
527 .add_output_bytes("h")
528 .set_line("h")
530 .accept();
531 }
532
533 #[test]
534 fn test_read_line_with_prompt_larger_than_tiny_screen() {
535 ReadLineInteractiveTest::default()
536 .set_size_chars(CharsXY::new(3, 5))
537 .set_prompt("This is larger than the screen> ")
538 .add_key_chars("hello")
540 .add_output_bytes("he")
541 .set_line("he")
543 .accept();
544 }
545
546 #[test]
547 fn test_read_line_with_prompt_shorter_than_tiny_screen() {
548 ReadLineInteractiveTest::default()
549 .set_size_chars(CharsXY::new(3, 5))
550 .set_prompt("?")
551 .add_output(CapturedOut::Write("?".to_string()))
552 .add_output(CapturedOut::SyncNow)
553 .add_key_chars("hello")
555 .add_output_bytes("h")
556 .set_line("h")
558 .accept();
559 }
560
561 #[test]
562 fn test_read_line_interactive_trailing_input() {
563 ReadLineInteractiveTest::default()
564 .add_key_chars("hello")
565 .add_output_bytes("hello")
566 .set_line("hello")
568 .accept();
569
570 ReadLineInteractiveTest::default()
571 .set_previous("123")
572 .add_output(CapturedOut::Write("123".to_string()))
573 .add_output(CapturedOut::SyncNow)
574 .add_key_chars("hello")
576 .add_output_bytes("hello")
577 .set_line("123hello")
579 .accept();
580 }
581
582 #[test]
583 fn test_read_line_interactive_middle_input() {
584 ReadLineInteractiveTest::default()
585 .add_key_chars("some text")
586 .add_output_bytes("some text")
587 .add_key(Key::ArrowLeft)
589 .add_output(CapturedOut::MoveWithinLine(-1))
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::ArrowRight)
598 .add_output(CapturedOut::MoveWithinLine(1))
599 .add_key_chars(" ")
601 .add_output(CapturedOut::HideCursor)
602 .add_output_bytes(" ")
603 .add_output(CapturedOut::Write("xt".to_string()))
604 .add_output(CapturedOut::MoveWithinLine(-2))
605 .add_output(CapturedOut::ShowCursor)
606 .add_key_chars(".")
608 .add_output(CapturedOut::HideCursor)
609 .add_output_bytes(".")
610 .add_output(CapturedOut::Write("xt".to_string()))
611 .add_output(CapturedOut::MoveWithinLine(-2))
612 .add_output(CapturedOut::ShowCursor)
613 .set_line("some te .xt")
615 .accept();
616 }
617
618 #[test]
619 fn test_read_line_interactive_utf8_basic() {
620 ReadLineInteractiveTest::default()
621 .add_key_chars("é")
622 .add_output(CapturedOut::Write("é".to_string()))
623 .set_line("é")
625 .accept();
626 }
627
628 #[test]
629 fn test_read_line_interactive_utf8_remove_2byte_char() {
630 ReadLineInteractiveTest::default()
631 .add_key_chars("é")
632 .add_output(CapturedOut::Write("é".to_string()))
633 .add_key(Key::Backspace)
635 .add_output(CapturedOut::HideCursor)
636 .add_output(CapturedOut::MoveWithinLine(-1))
637 .add_output_bytes("")
638 .add_output_bytes(" ")
639 .add_output(CapturedOut::MoveWithinLine(-1))
640 .add_output(CapturedOut::ShowCursor)
641 .set_line("")
643 .accept();
644 }
645
646 #[test]
647 fn test_read_line_interactive_utf8_add_and_remove_last() {
648 ReadLineInteractiveTest::default()
649 .add_key_chars("àé")
650 .add_output(CapturedOut::Write("à".to_string()))
651 .add_output(CapturedOut::Write("é".to_string()))
652 .add_key(Key::Backspace)
654 .add_output(CapturedOut::HideCursor)
655 .add_output(CapturedOut::MoveWithinLine(-1))
656 .add_output_bytes("")
657 .add_output_bytes(" ")
658 .add_output(CapturedOut::MoveWithinLine(-1))
659 .add_output(CapturedOut::ShowCursor)
660 .set_line("à")
662 .accept();
663 }
664
665 #[test]
666 fn test_read_line_interactive_utf8_navigate_2byte_chars() {
667 ReadLineInteractiveTest::default()
668 .add_key_chars("àé")
669 .add_output(CapturedOut::Write("à".to_string()))
670 .add_output(CapturedOut::Write("é".to_string()))
671 .add_key(Key::ArrowLeft)
673 .add_output(CapturedOut::MoveWithinLine(-1))
674 .add_key(Key::ArrowLeft)
676 .add_output(CapturedOut::MoveWithinLine(-1))
677 .add_key(Key::ArrowLeft)
679 .add_key(Key::ArrowRight)
681 .add_output(CapturedOut::MoveWithinLine(1))
682 .add_key(Key::Backspace)
684 .add_output(CapturedOut::HideCursor)
685 .add_output(CapturedOut::MoveWithinLine(-1))
686 .add_output(CapturedOut::Write("é".to_string()))
687 .add_output_bytes(" ")
688 .add_output(CapturedOut::MoveWithinLine(-2))
689 .add_output(CapturedOut::ShowCursor)
690 .set_line("é")
692 .accept();
693 }
694
695 #[test]
696 fn test_read_line_interactive_trailing_backspace() {
697 ReadLineInteractiveTest::default()
698 .add_key_chars("bar")
699 .add_output_bytes("bar")
700 .add_key(Key::Backspace)
702 .add_output(CapturedOut::HideCursor)
703 .add_output(CapturedOut::MoveWithinLine(-1))
704 .add_output_bytes("")
705 .add_output_bytes(" ")
706 .add_output(CapturedOut::MoveWithinLine(-1))
707 .add_output(CapturedOut::ShowCursor)
708 .add_key_chars("zar")
710 .add_output_bytes("zar")
711 .set_line("bazar")
713 .accept();
714 }
715
716 #[test]
717 fn test_read_line_interactive_middle_backspace() {
718 ReadLineInteractiveTest::default()
719 .add_key_chars("has a tYpo")
720 .add_output_bytes("has a tYpo")
721 .add_key(Key::ArrowLeft)
723 .add_output(CapturedOut::MoveWithinLine(-1))
724 .add_key(Key::ArrowLeft)
726 .add_output(CapturedOut::MoveWithinLine(-1))
727 .add_key(Key::Backspace)
729 .add_output(CapturedOut::HideCursor)
730 .add_output(CapturedOut::MoveWithinLine(-1))
731 .add_output(CapturedOut::Write("po".to_string()))
732 .add_output_bytes(" ")
733 .add_output(CapturedOut::MoveWithinLine(-3))
734 .add_output(CapturedOut::ShowCursor)
735 .add_key_chars("y")
737 .add_output(CapturedOut::HideCursor)
738 .add_output_bytes("y")
739 .add_output(CapturedOut::Write("po".to_string()))
740 .add_output(CapturedOut::MoveWithinLine(-2))
741 .add_output(CapturedOut::ShowCursor)
742 .set_line("has a typo")
744 .accept();
745 }
746
747 #[test]
748 fn test_read_line_interactive_test_move_bounds() {
749 ReadLineInteractiveTest::default()
750 .set_previous("12")
751 .add_output(CapturedOut::Write("12".to_string()))
752 .add_output(CapturedOut::SyncNow)
753 .add_key(Key::ArrowLeft)
755 .add_output(CapturedOut::MoveWithinLine(-1))
756 .add_key(Key::ArrowLeft)
758 .add_output(CapturedOut::MoveWithinLine(-1))
759 .add_key(Key::ArrowLeft)
761 .add_key(Key::ArrowLeft)
762 .add_key(Key::ArrowLeft)
763 .add_key(Key::ArrowLeft)
764 .add_key(Key::ArrowRight)
766 .add_output(CapturedOut::MoveWithinLine(1))
767 .add_key(Key::ArrowRight)
769 .add_output(CapturedOut::MoveWithinLine(1))
770 .add_key(Key::ArrowRight)
772 .add_key(Key::ArrowRight)
773 .add_key_chars("3")
775 .add_output_bytes("3")
776 .set_line("123")
778 .accept();
779 }
780
781 #[test]
782 fn test_read_line_interactive_test_home_end() {
783 ReadLineInteractiveTest::default()
784 .set_previous("sample text")
785 .add_output(CapturedOut::Write("sample text".to_string()))
786 .add_output(CapturedOut::SyncNow)
787 .add_key(Key::End)
789 .add_key(Key::Home)
791 .add_output(CapturedOut::MoveWithinLine(-11))
792 .add_key(Key::Home)
794 .add_key(Key::Char('>'))
796 .add_output(CapturedOut::HideCursor)
797 .add_output_bytes(">")
798 .add_output(CapturedOut::Write("sample text".to_string()))
799 .add_output(CapturedOut::MoveWithinLine(-11))
800 .add_output(CapturedOut::ShowCursor)
801 .add_key(Key::End)
803 .add_output(CapturedOut::MoveWithinLine(11))
804 .add_key(Key::Char('<'))
806 .add_output_bytes("<")
807 .add_key(Key::ArrowLeft)
809 .add_output(CapturedOut::MoveWithinLine(-1))
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::Backspace)
824 .add_output(CapturedOut::HideCursor)
825 .add_output(CapturedOut::MoveWithinLine(-1))
826 .add_output(CapturedOut::Write("text<".to_string()))
827 .add_output_bytes(" ")
828 .add_output(CapturedOut::MoveWithinLine(-6))
829 .add_output(CapturedOut::ShowCursor)
830 .set_line(">sampletext<")
832 .accept();
833 }
834
835 #[test]
836 fn test_read_line_interactive_horizontal_scrolling_not_implemented() {
837 ReadLineInteractiveTest::default()
838 .add_key_chars("1234567890123456789")
839 .add_output_bytes("12345678901234")
840 .set_line("12345678901234")
842 .accept();
843
844 ReadLineInteractiveTest::default()
845 .add_key_chars("1234567890123456789")
846 .add_output_bytes("12345678901234")
847 .add_key(Key::ArrowLeft)
849 .add_output(CapturedOut::MoveWithinLine(-1))
850 .add_key(Key::ArrowLeft)
852 .add_output(CapturedOut::MoveWithinLine(-1))
853 .add_key_chars("these will all be ignored")
855 .set_line("12345678901234")
857 .accept();
858
859 ReadLineInteractiveTest::default()
860 .set_prompt("12345")
861 .set_previous("67890")
862 .add_output(CapturedOut::Write("1234567890".to_string()))
863 .add_output(CapturedOut::SyncNow)
864 .add_key_chars("1234567890")
866 .add_output_bytes("1234")
867 .set_line("678901234")
869 .accept();
870 }
871
872 #[test]
873 fn test_read_line_interactive_history_not_enabled_by_default() {
874 ReadLineInteractiveTest::default().add_key(Key::ArrowUp).accept();
875 ReadLineInteractiveTest::default().add_key(Key::ArrowDown).accept();
876 }
877
878 #[test]
879 fn test_read_line_interactive_history_empty() {
880 ReadLineInteractiveTest::default()
881 .set_history(vec![], vec!["foobarbaz".to_owned()])
882 .add_key_chars("foo")
884 .add_output_bytes("foo")
885 .add_key(Key::ArrowUp)
887 .add_key_chars("bar")
889 .add_output_bytes("bar")
890 .add_key(Key::ArrowDown)
892 .add_key_chars("baz")
894 .add_output_bytes("baz")
895 .set_line("foobarbaz")
897 .accept();
898 }
899
900 #[test]
901 fn test_read_line_interactive_skips_empty_lines() {
902 ReadLineInteractiveTest::default()
903 .set_history(vec!["first".to_owned()], vec!["first".to_owned()])
904 .add_key_chars("x")
906 .add_output(CapturedOut::Write("x".to_string()))
907 .add_key(Key::Backspace)
909 .add_output(CapturedOut::HideCursor)
910 .add_output(CapturedOut::MoveWithinLine(-1))
911 .add_output_bytes("")
912 .add_output_bytes(" ")
913 .add_output(CapturedOut::MoveWithinLine(-1))
914 .add_output(CapturedOut::ShowCursor)
915 .accept();
917 }
918
919 #[test]
920 fn test_read_line_interactive_history_navigate_up_down_end_of_line() {
921 ReadLineInteractiveTest::default()
922 .set_prompt("? ")
923 .add_output(CapturedOut::Write("? ".to_string()))
924 .add_output(CapturedOut::SyncNow)
925 .set_history(
927 vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()],
928 vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()],
929 )
930 .add_key(Key::ArrowUp)
932 .add_output(CapturedOut::HideCursor)
933 .add_output(CapturedOut::Write("last".to_string()))
934 .add_output(CapturedOut::ShowCursor)
935 .add_key(Key::ArrowUp)
937 .add_output(CapturedOut::HideCursor)
938 .add_output(CapturedOut::MoveWithinLine(-("last".len() as i16)))
939 .add_output(CapturedOut::Write("long second line".to_string()))
940 .add_output(CapturedOut::ShowCursor)
941 .add_key(Key::ArrowUp)
943 .add_output(CapturedOut::HideCursor)
944 .add_output(CapturedOut::MoveWithinLine(-("long second line".len() as i16)))
945 .add_output(CapturedOut::Write("first".to_string()))
946 .add_output(CapturedOut::Write(" ".to_string()))
947 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
948 .add_output(CapturedOut::ShowCursor)
949 .add_key(Key::ArrowUp)
951 .add_key(Key::ArrowDown)
953 .add_output(CapturedOut::HideCursor)
954 .add_output(CapturedOut::MoveWithinLine(-("first".len() as i16)))
955 .add_output(CapturedOut::Write("long second line".to_string()))
956 .add_output(CapturedOut::ShowCursor)
957 .add_key(Key::ArrowDown)
959 .add_output(CapturedOut::HideCursor)
960 .add_output(CapturedOut::MoveWithinLine(-("long second line".len() as i16)))
961 .add_output(CapturedOut::Write("last".to_string()))
962 .add_output(CapturedOut::Write(" ".to_string()))
963 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
964 .add_output(CapturedOut::ShowCursor)
965 .add_key(Key::ArrowDown)
967 .add_output(CapturedOut::HideCursor)
968 .add_output(CapturedOut::MoveWithinLine(-("last".len() as i16)))
969 .add_output(CapturedOut::Write(" ".to_string()))
970 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
971 .add_output(CapturedOut::ShowCursor)
972 .add_key(Key::ArrowDown)
974 .accept();
976 }
977
978 #[test]
979 fn test_read_line_interactive_history_navigate_up_down_middle_of_line() {
980 ReadLineInteractiveTest::default()
981 .set_prompt("? ")
982 .add_output(CapturedOut::Write("? ".to_string()))
983 .add_output(CapturedOut::SyncNow)
984 .set_history(
986 vec!["a".to_owned(), "long-line".to_owned(), "zzzz".to_owned()],
987 vec!["a".to_owned(), "long-line".to_owned(), "zzzz".to_owned()],
988 )
989 .add_key(Key::ArrowUp)
991 .add_output(CapturedOut::HideCursor)
992 .add_output(CapturedOut::Write("zzzz".to_string()))
993 .add_output(CapturedOut::ShowCursor)
994 .add_key(Key::ArrowUp)
996 .add_output(CapturedOut::HideCursor)
997 .add_output(CapturedOut::MoveWithinLine(-("zzzz".len() as i16)))
998 .add_output(CapturedOut::Write("long-line".to_string()))
999 .add_output(CapturedOut::ShowCursor)
1000 .add_key(Key::ArrowLeft)
1002 .add_output(CapturedOut::MoveWithinLine(-1))
1003 .add_key(Key::ArrowLeft)
1004 .add_output(CapturedOut::MoveWithinLine(-1))
1005 .add_key(Key::ArrowLeft)
1006 .add_output(CapturedOut::MoveWithinLine(-1))
1007 .add_key(Key::ArrowLeft)
1008 .add_output(CapturedOut::MoveWithinLine(-1))
1009 .add_key(Key::ArrowUp)
1011 .add_output(CapturedOut::HideCursor)
1012 .add_output(CapturedOut::MoveWithinLine(-("long-line".len() as i16) + 4))
1013 .add_output(CapturedOut::Write("a".to_string()))
1014 .add_output(CapturedOut::Write(" ".to_string()))
1015 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
1016 .add_output(CapturedOut::ShowCursor)
1017 .add_key(Key::ArrowUp)
1019 .add_key(Key::ArrowDown)
1021 .add_output(CapturedOut::HideCursor)
1022 .add_output(CapturedOut::MoveWithinLine(-("a".len() as i16)))
1023 .add_output(CapturedOut::Write("long-line".to_string()))
1024 .add_output(CapturedOut::ShowCursor)
1025 .add_key(Key::ArrowLeft)
1027 .add_output(CapturedOut::MoveWithinLine(-1))
1028 .add_key(Key::ArrowLeft)
1029 .add_output(CapturedOut::MoveWithinLine(-1))
1030 .add_key(Key::ArrowLeft)
1031 .add_output(CapturedOut::MoveWithinLine(-1))
1032 .add_key(Key::ArrowLeft)
1033 .add_output(CapturedOut::MoveWithinLine(-1))
1034 .add_key(Key::ArrowLeft)
1035 .add_output(CapturedOut::MoveWithinLine(-1))
1036 .add_key(Key::ArrowLeft)
1037 .add_output(CapturedOut::MoveWithinLine(-1))
1038 .add_key(Key::ArrowDown)
1040 .add_output(CapturedOut::HideCursor)
1041 .add_output(CapturedOut::MoveWithinLine(-("long-line".len() as i16) + 6))
1042 .add_output(CapturedOut::Write("zzzz".to_string()))
1043 .add_output(CapturedOut::Write(" ".to_string()))
1044 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
1045 .add_output(CapturedOut::ShowCursor)
1046 .add_key(Key::ArrowDown)
1048 .add_output(CapturedOut::HideCursor)
1049 .add_output(CapturedOut::MoveWithinLine(-("zzzz".len() as i16)))
1050 .add_output(CapturedOut::Write(" ".to_string()))
1051 .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16)))
1052 .add_output(CapturedOut::ShowCursor)
1053 .add_key(Key::ArrowDown)
1055 .accept();
1057 }
1058
1059 #[test]
1060 fn test_read_line_interactive_history_navigate_and_edit() {
1061 ReadLineInteractiveTest::default()
1062 .set_prompt("? ")
1063 .add_output(CapturedOut::Write("? ".to_string()))
1064 .add_output(CapturedOut::SyncNow)
1065 .set_history(
1067 vec!["first".to_owned(), "second".to_owned(), "third".to_owned()],
1068 vec![
1069 "first".to_owned(),
1070 "second".to_owned(),
1071 "third".to_owned(),
1072 "sec ond".to_owned(),
1073 ],
1074 )
1075 .add_key(Key::ArrowUp)
1077 .add_output(CapturedOut::HideCursor)
1078 .add_output(CapturedOut::Write("third".to_string()))
1079 .add_output(CapturedOut::ShowCursor)
1080 .add_key(Key::ArrowUp)
1082 .add_output(CapturedOut::HideCursor)
1083 .add_output(CapturedOut::MoveWithinLine(-5))
1084 .add_output(CapturedOut::Write("second".to_string()))
1085 .add_output(CapturedOut::ShowCursor)
1086 .add_key(Key::ArrowLeft)
1088 .add_output(CapturedOut::MoveWithinLine(-1))
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_chars(" ")
1097 .add_output(CapturedOut::HideCursor)
1098 .add_output_bytes(" ")
1099 .add_output(CapturedOut::Write("ond".to_string()))
1100 .add_output(CapturedOut::MoveWithinLine(-3))
1101 .add_output(CapturedOut::ShowCursor)
1102 .set_line("sec ond")
1104 .accept();
1105 }
1106
1107 #[test]
1108 fn test_read_line_ignored_keys() {
1109 ReadLineInteractiveTest::default()
1110 .add_key_chars("not ")
1111 .add_output_bytes("not ")
1112 .add_key(Key::Escape)
1114 .add_key(Key::PageDown)
1115 .add_key(Key::PageUp)
1116 .add_key(Key::Tab)
1117 .add_key_chars("affected")
1119 .add_output_bytes("affected")
1120 .set_line("not affected")
1122 .accept();
1123 }
1124
1125 #[test]
1126 fn test_read_line_without_echo() {
1127 ReadLineInteractiveTest::default()
1128 .set_echo(false)
1129 .set_prompt("> ")
1130 .set_previous("pass1234")
1131 .add_output(CapturedOut::Write("> ********".to_string()))
1132 .add_output(CapturedOut::SyncNow)
1133 .add_key_chars("56")
1135 .add_output_bytes("**")
1136 .add_key(Key::ArrowLeft)
1138 .add_output(CapturedOut::MoveWithinLine(-1))
1139 .add_key(Key::ArrowLeft)
1141 .add_output(CapturedOut::MoveWithinLine(-1))
1142 .add_key(Key::Backspace)
1144 .add_output(CapturedOut::HideCursor)
1145 .add_output(CapturedOut::MoveWithinLine(-1))
1146 .add_output(CapturedOut::Write("**".to_string()))
1147 .add_output_bytes(" ")
1148 .add_output(CapturedOut::MoveWithinLine(-3))
1149 .add_output(CapturedOut::ShowCursor)
1150 .add_output(CapturedOut::HideCursor)
1152 .add_key_chars("7")
1153 .add_output(CapturedOut::Write("***".to_string()))
1154 .add_output(CapturedOut::MoveWithinLine(-2))
1155 .add_output(CapturedOut::ShowCursor)
1156 .set_line("pass123756")
1158 .accept();
1159 }
1160
1161 #[test]
1162 fn test_read_line_secure_trivial_test() {
1163 let mut console = MockConsole::default();
1164 console.set_interactive(true);
1165 console.add_input_keys(&[Key::Char('1'), Key::Char('5'), Key::NewLine]);
1166 console.set_size_chars(CharsXY::new(15, 5));
1167 let line = block_on(read_line_secure(&mut console, "> ")).unwrap();
1168 assert_eq!("15", &line);
1169 assert_eq!(
1170 &[
1171 CapturedOut::Write("> ".to_string()),
1172 CapturedOut::SyncNow,
1173 CapturedOut::Write("*".to_string()),
1174 CapturedOut::Write("*".to_string()),
1175 CapturedOut::Print("".to_owned()),
1176 ],
1177 console.captured_out()
1178 );
1179 }
1180
1181 #[test]
1182 fn test_read_line_secure_unsupported_in_noninteractive_console() {
1183 let mut console = MockConsole::default();
1184 let err = block_on(read_line_secure(&mut console, "> ")).unwrap_err();
1185 assert!(format!("{}", err).contains("Cannot read secure"));
1186 }
1187}