endbasic_std/console/
readline.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Interactive line reader.
17
18use crate::console::{Console, Key, LineBuffer};
19use std::borrow::Cow;
20use std::io;
21
22/// Character to print when typing a secure string.
23const SECURE_CHAR: &str = "*";
24
25/// Refreshes the current input line to display `line` assuming that the cursor is currently
26/// offset by `pos` characters from the beginning of the input and that the previous line was
27/// `clear_len` characters long.
28fn 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
50/// Reads a line of text interactively from the console, using the given `prompt` and pre-filling
51/// the input with `previous`.  If `history` is not `None`, then this appends the newly entered line
52/// into the history and allows navigating through it.
53async 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        // Assumes that the prompt was printed at column 0.  If that was not the case, line length
88        // calculation does not work.
89        console_width - prompt_len
90    };
91
92    // Insertion position *within* the line, without accounting for the prompt.
93    // TODO(zenria): Handle UTF-8 graphemes.
94    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                // TODO(jmmv): This is here because the integration tests may be checked out with
175                // CRLF line endings on Windows, which means we'd see two characters to end a line
176                // instead of one.  Not sure if we should do this or if instead we should ensure
177                // the golden data we feed to the tests has single-character line endings.
178                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                    // TODO(jmmv): Implement support for lines that exceed the width of the input
189                    // field (the width of the screen).
190                    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                // Intentionally ignored.
229            }
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                // Intentionally ignored.
247            }
248
249            Key::Tab => {
250                // TODO(jmmv): Would be nice to have some form of auto-completion.
251            }
252
253            // TODO(jmmv): Should do something smarter with unknown keys.
254            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
269/// Reads a line of text interactively from the console, which is not expected to be a TTY.
270async 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                // TODO(jmmv): This is here because the integration tests may be checked out with
282                // CRLF line endings on Windows, which means we'd see two characters to end a line
283                // instead of one.  Not sure if we should do this or if instead we should ensure
284                // the golden data we feed to the tests has single-character line endings.
285                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
303/// Reads a line from the console.  If the console is interactive, this does fancy line editing and
304/// uses the given `prompt` and pre-fills the input with `previous`.
305pub 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
318/// Reads a line from the console without echo using the given `prompt`.
319///
320/// The console must be interactive for this to work, as otherwise we do not have a mechanism to
321/// suppress echo.
322pub 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    /// Builder pattern to construct a test for `read_line_interactive`.
340    #[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        /// Constructs a new test that feeds no input to the function, with no prompt or previous
355        /// text, and expects an empty return line and no changes to the console.
356        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        /// Adds `key` to the golden input.
373        fn add_key(mut self, key: Key) -> Self {
374            self.keys.push(key);
375            self
376        }
377
378        /// Adds a bunch of `chars` as individual key presses to the golden input.
379        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        /// Adds a single state change to the expected output.
387        fn add_output(mut self, output: CapturedOut) -> Self {
388            self.exp_output.push(output);
389            self
390        }
391
392        /// Adds a bunch of `bytes` as separate console writes to the expected output.
393        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        /// Sets the size of the console.
406        fn set_size_chars(mut self, size: CharsXY) -> Self {
407            self.size_chars = size;
408            self
409        }
410
411        /// Sets the expected resulting line for the test.
412        fn set_line(mut self, line: &'static str) -> Self {
413            self.exp_line = line;
414            self
415        }
416
417        /// Sets the prompt to use for the test.
418        fn set_prompt(mut self, prompt: &'static str) -> Self {
419            self.prompt = prompt;
420            self
421        }
422
423        /// Sets the previous text to use for the test.
424        fn set_previous(mut self, previous: &'static str) -> Self {
425            self.previous = previous;
426            self
427        }
428
429        /// Enables history tracking and sets the history to use for the test as `history` and
430        /// expects that `history` matches `exp_history` upon test completion.
431        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        /// Sets whether read_line echoes characters or not.
438        fn set_echo(mut self, echo: bool) -> Self {
439            self.echo = echo;
440            self
441        }
442
443        /// Adds a final return key to the golden input, a newline to the expected output, and
444        /// executes the test.
445        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            // -
491            .add_key_chars("hello")
492            .add_output_bytes("hello")
493            // -
494            .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            // -
502            .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            // -
514            .add_key_chars("hello")
515            .add_output_bytes("h")
516            // -
517            .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            // -
529            .add_key_chars("hello")
530            .add_output_bytes("h")
531            // -
532            .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            // -
542            .add_key_chars("hello")
543            .add_output_bytes("he")
544            // -
545            .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            // -
557            .add_key_chars("hello")
558            .add_output_bytes("h")
559            // -
560            .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            // -
570            .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            // -
578            .add_key_chars("hello")
579            .add_output_bytes("hello")
580            // -
581            .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            // -
591            .add_key(Key::ArrowLeft)
592            .add_output(CapturedOut::MoveWithinLine(-1))
593            // -
594            .add_key(Key::ArrowLeft)
595            .add_output(CapturedOut::MoveWithinLine(-1))
596            // -
597            .add_key(Key::ArrowLeft)
598            .add_output(CapturedOut::MoveWithinLine(-1))
599            // -
600            .add_key(Key::ArrowRight)
601            .add_output(CapturedOut::MoveWithinLine(1))
602            // -
603            .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            // -
610            .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            // -
617            .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            // -
627            .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            // -
637            .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            // -
645            .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            // -
656            .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            // -
664            .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            // -
675            .add_key(Key::ArrowLeft)
676            .add_output(CapturedOut::MoveWithinLine(-1))
677            // -
678            .add_key(Key::ArrowLeft)
679            .add_output(CapturedOut::MoveWithinLine(-1))
680            // -
681            .add_key(Key::ArrowLeft)
682            // -
683            .add_key(Key::ArrowRight)
684            .add_output(CapturedOut::MoveWithinLine(1))
685            // -
686            .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            // -
694            .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            // -
704            .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            // -
712            .add_key_chars("zar")
713            .add_output_bytes("zar")
714            // -
715            .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            // -
725            .add_key(Key::ArrowLeft)
726            .add_output(CapturedOut::MoveWithinLine(-1))
727            // -
728            .add_key(Key::ArrowLeft)
729            .add_output(CapturedOut::MoveWithinLine(-1))
730            // -
731            .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            // -
739            .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            // -
746            .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            // -
757            .add_key(Key::ArrowLeft)
758            .add_output(CapturedOut::MoveWithinLine(-1))
759            // -
760            .add_key(Key::ArrowLeft)
761            .add_output(CapturedOut::MoveWithinLine(-1))
762            // -
763            .add_key(Key::ArrowLeft)
764            .add_key(Key::ArrowLeft)
765            .add_key(Key::ArrowLeft)
766            .add_key(Key::ArrowLeft)
767            // -
768            .add_key(Key::ArrowRight)
769            .add_output(CapturedOut::MoveWithinLine(1))
770            // -
771            .add_key(Key::ArrowRight)
772            .add_output(CapturedOut::MoveWithinLine(1))
773            // -
774            .add_key(Key::ArrowRight)
775            .add_key(Key::ArrowRight)
776            // -
777            .add_key_chars("3")
778            .add_output_bytes("3")
779            // -
780            .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            // -
791            .add_key(Key::End)
792            // -
793            .add_key(Key::Home)
794            .add_output(CapturedOut::MoveWithinLine(-11))
795            // -
796            .add_key(Key::Home)
797            // -
798            .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            // -
805            .add_key(Key::End)
806            .add_output(CapturedOut::MoveWithinLine(11))
807            // -
808            .add_key(Key::Char('<'))
809            .add_output_bytes("<")
810            // -
811            .add_key(Key::ArrowLeft)
812            .add_output(CapturedOut::MoveWithinLine(-1))
813            // -
814            .add_key(Key::ArrowLeft)
815            .add_output(CapturedOut::MoveWithinLine(-1))
816            // -
817            .add_key(Key::ArrowLeft)
818            .add_output(CapturedOut::MoveWithinLine(-1))
819            // -
820            .add_key(Key::ArrowLeft)
821            .add_output(CapturedOut::MoveWithinLine(-1))
822            // -
823            .add_key(Key::ArrowLeft)
824            .add_output(CapturedOut::MoveWithinLine(-1))
825            // -
826            .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            // -
834            .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            // -
844            .set_line("12345678901234")
845            .accept();
846
847        ReadLineInteractiveTest::default()
848            .add_key_chars("1234567890123456789")
849            .add_output_bytes("12345678901234")
850            // -
851            .add_key(Key::ArrowLeft)
852            .add_output(CapturedOut::MoveWithinLine(-1))
853            // -
854            .add_key(Key::ArrowLeft)
855            .add_output(CapturedOut::MoveWithinLine(-1))
856            // -
857            .add_key_chars("these will all be ignored")
858            // -
859            .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            // -
868            .add_key_chars("1234567890")
869            .add_output_bytes("1234")
870            // -
871            .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            //
886            .add_key_chars("foo")
887            .add_output_bytes("foo")
888            //
889            .add_key(Key::ArrowUp)
890            //
891            .add_key_chars("bar")
892            .add_output_bytes("bar")
893            //
894            .add_key(Key::ArrowDown)
895            //
896            .add_key_chars("baz")
897            .add_output_bytes("baz")
898            //
899            .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            // -
908            .add_key_chars("x")
909            .add_output(CapturedOut::Write("x".to_string()))
910            // -
911            .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            // -
919            .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            //
929            .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            //
934            .add_key(Key::ArrowUp)
935            .add_output(CapturedOut::HideCursor)
936            .add_output(CapturedOut::Write("last".to_string()))
937            .add_output(CapturedOut::ShowCursor)
938            //
939            .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            //
945            .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            //
953            .add_key(Key::ArrowUp)
954            //
955            .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            //
961            .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            //
969            .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            //
976            .add_key(Key::ArrowDown)
977            //
978            .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            //
988            .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            //
993            .add_key(Key::ArrowUp)
994            .add_output(CapturedOut::HideCursor)
995            .add_output(CapturedOut::Write("zzzz".to_string()))
996            .add_output(CapturedOut::ShowCursor)
997            //
998            .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            //
1004            .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            //
1013            .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            //
1021            .add_key(Key::ArrowUp)
1022            //
1023            .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            //
1029            .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            //
1042            .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            //
1050            .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            //
1057            .add_key(Key::ArrowDown)
1058            //
1059            .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            //
1069            .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            //
1079            .add_key(Key::ArrowUp)
1080            .add_output(CapturedOut::HideCursor)
1081            .add_output(CapturedOut::Write("third".to_string()))
1082            .add_output(CapturedOut::ShowCursor)
1083            //
1084            .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            //
1090            .add_key(Key::ArrowLeft)
1091            .add_output(CapturedOut::MoveWithinLine(-1))
1092            // -
1093            .add_key(Key::ArrowLeft)
1094            .add_output(CapturedOut::MoveWithinLine(-1))
1095            // -
1096            .add_key(Key::ArrowLeft)
1097            .add_output(CapturedOut::MoveWithinLine(-1))
1098            // -
1099            .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            // -
1106            .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            // -
1116            .add_key(Key::Escape)
1117            .add_key(Key::PageDown)
1118            .add_key(Key::PageUp)
1119            .add_key(Key::Tab)
1120            // -
1121            .add_key_chars("affected")
1122            .add_output_bytes("affected")
1123            // -
1124            .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            // -
1137            .add_key_chars("56")
1138            .add_output_bytes("**")
1139            // -
1140            .add_key(Key::ArrowLeft)
1141            .add_output(CapturedOut::MoveWithinLine(-1))
1142            // -
1143            .add_key(Key::ArrowLeft)
1144            .add_output(CapturedOut::MoveWithinLine(-1))
1145            // -
1146            .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            // -
1154            .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            // -
1160            .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}