Skip to main content

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 => line.push('?'),
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::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    /// Builder pattern to construct a test for `read_line_interactive`.
337    #[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        /// Constructs a new test that feeds no input to the function, with no prompt or previous
352        /// text, and expects an empty return line and no changes to the console.
353        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        /// Adds `key` to the golden input.
370        fn add_key(mut self, key: Key) -> Self {
371            self.keys.push(key);
372            self
373        }
374
375        /// Adds a bunch of `chars` as individual key presses to the golden input.
376        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        /// Adds a single state change to the expected output.
384        fn add_output(mut self, output: CapturedOut) -> Self {
385            self.exp_output.push(output);
386            self
387        }
388
389        /// Adds a bunch of `bytes` as separate console writes to the expected output.
390        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        /// Sets the size of the console.
403        fn set_size_chars(mut self, size: CharsXY) -> Self {
404            self.size_chars = size;
405            self
406        }
407
408        /// Sets the expected resulting line for the test.
409        fn set_line(mut self, line: &'static str) -> Self {
410            self.exp_line = line;
411            self
412        }
413
414        /// Sets the prompt to use for the test.
415        fn set_prompt(mut self, prompt: &'static str) -> Self {
416            self.prompt = prompt;
417            self
418        }
419
420        /// Sets the previous text to use for the test.
421        fn set_previous(mut self, previous: &'static str) -> Self {
422            self.previous = previous;
423            self
424        }
425
426        /// Enables history tracking and sets the history to use for the test as `history` and
427        /// expects that `history` matches `exp_history` upon test completion.
428        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        /// Sets whether read_line echoes characters or not.
435        fn set_echo(mut self, echo: bool) -> Self {
436            self.echo = echo;
437            self
438        }
439
440        /// Adds a final return key to the golden input, a newline to the expected output, and
441        /// executes the test.
442        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            // -
488            .add_key_chars("hello")
489            .add_output_bytes("hello")
490            // -
491            .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            // -
499            .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            // -
511            .add_key_chars("hello")
512            .add_output_bytes("h")
513            // -
514            .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            // -
526            .add_key_chars("hello")
527            .add_output_bytes("h")
528            // -
529            .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            // -
539            .add_key_chars("hello")
540            .add_output_bytes("he")
541            // -
542            .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            // -
554            .add_key_chars("hello")
555            .add_output_bytes("h")
556            // -
557            .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            // -
567            .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            // -
575            .add_key_chars("hello")
576            .add_output_bytes("hello")
577            // -
578            .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            // -
588            .add_key(Key::ArrowLeft)
589            .add_output(CapturedOut::MoveWithinLine(-1))
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::ArrowRight)
598            .add_output(CapturedOut::MoveWithinLine(1))
599            // -
600            .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            // -
607            .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            // -
614            .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            // -
624            .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            // -
634            .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            // -
642            .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            // -
653            .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            // -
661            .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            // -
672            .add_key(Key::ArrowLeft)
673            .add_output(CapturedOut::MoveWithinLine(-1))
674            // -
675            .add_key(Key::ArrowLeft)
676            .add_output(CapturedOut::MoveWithinLine(-1))
677            // -
678            .add_key(Key::ArrowLeft)
679            // -
680            .add_key(Key::ArrowRight)
681            .add_output(CapturedOut::MoveWithinLine(1))
682            // -
683            .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            // -
691            .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            // -
701            .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            // -
709            .add_key_chars("zar")
710            .add_output_bytes("zar")
711            // -
712            .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            // -
722            .add_key(Key::ArrowLeft)
723            .add_output(CapturedOut::MoveWithinLine(-1))
724            // -
725            .add_key(Key::ArrowLeft)
726            .add_output(CapturedOut::MoveWithinLine(-1))
727            // -
728            .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            // -
736            .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            // -
743            .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            // -
754            .add_key(Key::ArrowLeft)
755            .add_output(CapturedOut::MoveWithinLine(-1))
756            // -
757            .add_key(Key::ArrowLeft)
758            .add_output(CapturedOut::MoveWithinLine(-1))
759            // -
760            .add_key(Key::ArrowLeft)
761            .add_key(Key::ArrowLeft)
762            .add_key(Key::ArrowLeft)
763            .add_key(Key::ArrowLeft)
764            // -
765            .add_key(Key::ArrowRight)
766            .add_output(CapturedOut::MoveWithinLine(1))
767            // -
768            .add_key(Key::ArrowRight)
769            .add_output(CapturedOut::MoveWithinLine(1))
770            // -
771            .add_key(Key::ArrowRight)
772            .add_key(Key::ArrowRight)
773            // -
774            .add_key_chars("3")
775            .add_output_bytes("3")
776            // -
777            .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            // -
788            .add_key(Key::End)
789            // -
790            .add_key(Key::Home)
791            .add_output(CapturedOut::MoveWithinLine(-11))
792            // -
793            .add_key(Key::Home)
794            // -
795            .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            // -
802            .add_key(Key::End)
803            .add_output(CapturedOut::MoveWithinLine(11))
804            // -
805            .add_key(Key::Char('<'))
806            .add_output_bytes("<")
807            // -
808            .add_key(Key::ArrowLeft)
809            .add_output(CapturedOut::MoveWithinLine(-1))
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::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            // -
831            .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            // -
841            .set_line("12345678901234")
842            .accept();
843
844        ReadLineInteractiveTest::default()
845            .add_key_chars("1234567890123456789")
846            .add_output_bytes("12345678901234")
847            // -
848            .add_key(Key::ArrowLeft)
849            .add_output(CapturedOut::MoveWithinLine(-1))
850            // -
851            .add_key(Key::ArrowLeft)
852            .add_output(CapturedOut::MoveWithinLine(-1))
853            // -
854            .add_key_chars("these will all be ignored")
855            // -
856            .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            // -
865            .add_key_chars("1234567890")
866            .add_output_bytes("1234")
867            // -
868            .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            //
883            .add_key_chars("foo")
884            .add_output_bytes("foo")
885            //
886            .add_key(Key::ArrowUp)
887            //
888            .add_key_chars("bar")
889            .add_output_bytes("bar")
890            //
891            .add_key(Key::ArrowDown)
892            //
893            .add_key_chars("baz")
894            .add_output_bytes("baz")
895            //
896            .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            // -
905            .add_key_chars("x")
906            .add_output(CapturedOut::Write("x".to_string()))
907            // -
908            .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            // -
916            .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            //
926            .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            //
931            .add_key(Key::ArrowUp)
932            .add_output(CapturedOut::HideCursor)
933            .add_output(CapturedOut::Write("last".to_string()))
934            .add_output(CapturedOut::ShowCursor)
935            //
936            .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            //
942            .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            //
950            .add_key(Key::ArrowUp)
951            //
952            .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            //
958            .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            //
966            .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            //
973            .add_key(Key::ArrowDown)
974            //
975            .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            //
985            .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            //
990            .add_key(Key::ArrowUp)
991            .add_output(CapturedOut::HideCursor)
992            .add_output(CapturedOut::Write("zzzz".to_string()))
993            .add_output(CapturedOut::ShowCursor)
994            //
995            .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            //
1001            .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            //
1010            .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            //
1018            .add_key(Key::ArrowUp)
1019            //
1020            .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            //
1026            .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            //
1039            .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            //
1047            .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            //
1054            .add_key(Key::ArrowDown)
1055            //
1056            .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            //
1066            .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            //
1076            .add_key(Key::ArrowUp)
1077            .add_output(CapturedOut::HideCursor)
1078            .add_output(CapturedOut::Write("third".to_string()))
1079            .add_output(CapturedOut::ShowCursor)
1080            //
1081            .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            //
1087            .add_key(Key::ArrowLeft)
1088            .add_output(CapturedOut::MoveWithinLine(-1))
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_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            // -
1103            .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            // -
1113            .add_key(Key::Escape)
1114            .add_key(Key::PageDown)
1115            .add_key(Key::PageUp)
1116            .add_key(Key::Tab)
1117            // -
1118            .add_key_chars("affected")
1119            .add_output_bytes("affected")
1120            // -
1121            .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            // -
1134            .add_key_chars("56")
1135            .add_output_bytes("**")
1136            // -
1137            .add_key(Key::ArrowLeft)
1138            .add_output(CapturedOut::MoveWithinLine(-1))
1139            // -
1140            .add_key(Key::ArrowLeft)
1141            .add_output(CapturedOut::MoveWithinLine(-1))
1142            // -
1143            .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            // -
1151            .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            // -
1157            .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}