Skip to main content

endbasic_std/console/
readline.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Interactive line reader.
18
19use crate::console::{Console, Key, LineBuffer};
20use std::borrow::Cow;
21use std::io;
22
23/// Character to print when typing a secure string.
24const SECURE_CHAR: &str = "*";
25
26/// Result of dispatching a key press while editing a line.
27enum DispatchResult {
28    Continue,
29    Submit,
30}
31
32/// Stateful interactive line editor.
33struct Readline<'a, 'h> {
34    console: &'a mut dyn Console,
35    history: Option<&'h mut Vec<String>>,
36    echo: bool,
37    input_width: usize,
38    line: LineBuffer,
39    pos: usize,
40    view_start: usize,
41    rendered: String,
42    rendered_len: usize,
43    history_pos: usize,
44}
45
46impl<'a, 'h> Readline<'a, 'h> {
47    /// Adjusts the viewport start to ensure `pos` remains visible.
48    fn adjust_view(&self, pos: usize, view_start: usize) -> usize {
49        if self.input_width == 0 {
50            0
51        } else if pos < view_start {
52            pos
53        } else if pos > view_start + self.input_width {
54            pos - self.input_width
55        } else {
56            view_start
57        }
58    }
59
60    /// Returns the cursor offset within the currently-rendered viewport.
61    fn current_pos(&self) -> usize {
62        self.pos - self.view_start
63    }
64
65    /// Returns the text to render for the current viewport.
66    fn render_line(&self, view_start: usize) -> String {
67        debug_assert!(view_start <= self.line.len());
68
69        if !self.echo {
70            SECURE_CHAR.repeat((self.line.len() - view_start).min(self.input_width))
71        } else {
72            self.line.range(view_start, view_start + self.input_width)
73        }
74    }
75
76    /// Recomputes the rendered text and its display width.
77    fn sync_rendered(&mut self) {
78        self.rendered = self.render_line(self.view_start);
79        self.rendered_len = self.rendered.chars().count();
80    }
81
82    /// Moves the viewport so that the end of the line is visible.
83    fn reset_view_to_end(&mut self) {
84        self.view_start =
85            if self.input_width == 0 { 0 } else { self.pos.saturating_sub(self.input_width) };
86        self.sync_rendered();
87    }
88
89    /// Redraws the rendered viewport and places the cursor at the desired offset.
90    fn redraw_line(&mut self, old_pos: usize, clear_len: usize, new_pos: usize) -> io::Result<()> {
91        self.console.hide_cursor()?;
92        if old_pos > 0 {
93            self.console.move_within_line(-(old_pos as i16))?;
94        }
95        if !self.rendered.is_empty() {
96            self.console.write(&self.rendered)?;
97        }
98        let written_len = self.rendered_len.max(clear_len);
99        if self.rendered_len < clear_len {
100            let diff = clear_len - self.rendered_len;
101            self.console.write(&" ".repeat(diff))?;
102        }
103        debug_assert!(new_pos <= written_len);
104        let reset = written_len - new_pos;
105        if reset > 0 {
106            self.console.move_within_line(-(reset as i16))?;
107        }
108        self.console.show_cursor()
109    }
110
111    /// Erases a character and redraws the visible tail of the line.
112    fn erase_at(&mut self, cursor_pos: usize, remove_pos: usize) -> io::Result<()> {
113        debug_assert!(remove_pos < self.line.len());
114        debug_assert!(remove_pos <= cursor_pos);
115
116        self.console.hide_cursor()?;
117        let delta = cursor_pos - remove_pos;
118        if delta > 0 {
119            self.console.move_within_line(-(delta as i16))?;
120        }
121        let tail_len = self.line.len() - remove_pos - 1;
122        if self.echo {
123            self.console.write(&self.line.end(remove_pos + 1))?;
124        } else {
125            self.console.write(&SECURE_CHAR.repeat(tail_len))?;
126        }
127        self.console.write(" ")?;
128        self.console.move_within_line(-((tail_len + 1) as i16))?;
129        self.console.show_cursor()?;
130        self.line.remove(remove_pos);
131        Ok(())
132    }
133
134    /// Handles a backspace key press.
135    fn do_backspace(&mut self) -> io::Result<DispatchResult> {
136        if self.pos == 0 {
137            return Ok(DispatchResult::Continue);
138        }
139
140        if self.view_start == 0 && self.line.len() <= self.input_width {
141            self.erase_at(self.pos, self.pos - 1)?;
142            self.pos -= 1;
143            self.sync_rendered();
144        } else {
145            let old_pos = self.current_pos();
146            let clear_len = self.rendered_len;
147            self.line.remove(self.pos - 1);
148            self.pos -= 1;
149            self.view_start = self.adjust_view(self.pos, self.view_start);
150            self.sync_rendered();
151            self.redraw_line(old_pos, clear_len, self.current_pos())?;
152        }
153        Ok(DispatchResult::Continue)
154    }
155
156    /// Handles a carriage return key press.
157    fn do_carriage_return(&mut self) -> io::Result<DispatchResult> {
158        // TODO(jmmv): This is here because the integration tests may be checked out with
159        // CRLF line endings on Windows, which means we'd see two characters to end a line
160        // instead of one.  Not sure if we should do this or if instead we should ensure
161        // the golden data we feed to the tests has single-character line endings.
162        if cfg!(not(target_os = "windows")) {
163            self.console.print("")?;
164            Ok(DispatchResult::Submit)
165        } else {
166            Ok(DispatchResult::Continue)
167        }
168    }
169
170    /// Handles a regular character insertion.
171    fn do_char(&mut self, ch: char) -> io::Result<DispatchResult> {
172        if self.input_width == 0 {
173            return Ok(DispatchResult::Continue);
174        }
175
176        let line_len = self.line.len();
177        let new_view_start = self.adjust_view(self.pos + 1, self.view_start);
178        if new_view_start == self.view_start && line_len < self.input_width {
179            if self.pos < line_len {
180                self.console.hide_cursor()?;
181                if self.echo {
182                    let mut buf = [0u8; 4];
183                    self.console.write(ch.encode_utf8(&mut buf))?;
184                    self.console.write(&self.line.end(self.pos))?;
185                } else {
186                    self.console.write(&SECURE_CHAR.repeat(line_len - self.pos + 1))?;
187                }
188                self.console.move_within_line(-((line_len - self.pos) as i16))?;
189                self.console.show_cursor()?;
190                self.line.insert(self.pos, ch);
191            } else {
192                if self.echo {
193                    let mut buf = [0u8; 4];
194                    self.console.write(ch.encode_utf8(&mut buf))?;
195                } else {
196                    self.console.write(SECURE_CHAR)?;
197                }
198                self.line.insert(line_len, ch);
199            }
200            self.pos += 1;
201            self.sync_rendered();
202        } else {
203            let old_pos = self.current_pos();
204            let clear_len = self.rendered_len;
205            self.line.insert(self.pos, ch);
206            self.pos += 1;
207            self.view_start = new_view_start;
208            self.sync_rendered();
209            self.redraw_line(old_pos, clear_len, self.current_pos())?;
210        }
211        Ok(DispatchResult::Continue)
212    }
213
214    /// Handles deletion of the character under the cursor.
215    fn do_delete(&mut self) -> io::Result<DispatchResult> {
216        if self.pos >= self.line.len() {
217            return Ok(DispatchResult::Continue);
218        }
219
220        if self.view_start == 0 && self.line.len() <= self.input_width {
221            self.erase_at(self.pos, self.pos)?;
222            self.sync_rendered();
223        } else {
224            let old_pos = self.current_pos();
225            let clear_len = self.rendered_len;
226            self.line.remove(self.pos);
227            self.view_start = self.adjust_view(self.pos, self.view_start);
228            self.sync_rendered();
229            self.redraw_line(old_pos, clear_len, self.current_pos())?;
230        }
231        Ok(DispatchResult::Continue)
232    }
233
234    /// Moves the cursor to the end of the line.
235    fn do_end(&mut self) -> io::Result<DispatchResult> {
236        self.do_move_to(self.line.len())
237    }
238
239    /// Treats EOF as delete while text exists, or reports end-of-input otherwise.
240    fn do_eof_or_delete(&mut self) -> io::Result<DispatchResult> {
241        if self.line.is_empty() {
242            Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF"))
243        } else {
244            self.do_delete()
245        }
246    }
247
248    /// Moves the cursor to the start of the line.
249    fn do_home(&mut self) -> io::Result<DispatchResult> {
250        self.do_move_to(0)
251    }
252
253    /// Ignores a key press that has no editing effect.
254    fn do_ignore(&mut self) -> io::Result<DispatchResult> {
255        Ok(DispatchResult::Continue)
256    }
257
258    /// Reports an interrupt triggered by the user.
259    fn do_interrupt(&mut self) -> io::Result<DispatchResult> {
260        Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C"))
261    }
262
263    /// Moves the cursor to a new position, scrolling if needed.
264    fn do_move_to(&mut self, new_pos: usize) -> io::Result<DispatchResult> {
265        if new_pos == self.pos {
266            return Ok(DispatchResult::Continue);
267        }
268
269        let old_pos = self.current_pos();
270        let new_view_start = self.adjust_view(new_pos, self.view_start);
271        if new_view_start == self.view_start {
272            let delta = new_pos as i16 - self.pos as i16;
273            self.console.move_within_line(delta)?;
274            self.pos = new_pos;
275        } else {
276            let clear_len = self.rendered_len;
277            self.pos = new_pos;
278            self.view_start = new_view_start;
279            self.sync_rendered();
280            self.redraw_line(old_pos, clear_len, self.current_pos())?;
281        }
282        Ok(DispatchResult::Continue)
283    }
284
285    /// Accepts the current line.
286    fn do_newline(&mut self) -> io::Result<DispatchResult> {
287        self.console.print("")?;
288        Ok(DispatchResult::Submit)
289    }
290
291    /// Navigates through the command history.
292    fn do_up_down(&mut self, delta: isize) -> io::Result<DispatchResult> {
293        let (new_history_pos, new_line) = {
294            let history = match self.history.as_deref_mut() {
295                Some(history) => history,
296                None => return Ok(DispatchResult::Continue),
297            };
298
299            let new_history_pos = if delta < 0 {
300                if self.history_pos == 0 {
301                    return Ok(DispatchResult::Continue);
302                }
303                self.history_pos - 1
304            } else {
305                if self.history_pos == history.len() - 1 {
306                    return Ok(DispatchResult::Continue);
307                }
308                self.history_pos + 1
309            };
310
311            history[self.history_pos] = self.line.to_string();
312            (new_history_pos, history[new_history_pos].clone())
313        };
314
315        let old_pos = self.current_pos();
316        let clear_len = self.rendered_len;
317        self.history_pos = new_history_pos;
318        self.line = LineBuffer::from(&new_line);
319        self.pos = self.line.len();
320        self.reset_view_to_end();
321        self.redraw_line(old_pos, clear_len, self.current_pos())?;
322        Ok(DispatchResult::Continue)
323    }
324
325    /// Finalizes history bookkeeping and returns the edited line.
326    fn finish(mut self) -> String {
327        if let Some(history) = self.history.as_mut() {
328            if self.line.is_empty() {
329                history.pop();
330            } else {
331                let last = history.len() - 1;
332                history[last] = self.line.to_string();
333            }
334        }
335        self.line.into_inner()
336    }
337
338    /// Creates a new interactive line editor and renders its initial state.
339    fn new(
340        console: &'a mut dyn Console,
341        prompt: &str,
342        previous: &str,
343        mut history: Option<&'h mut Vec<String>>,
344        echo: bool,
345    ) -> io::Result<Self> {
346        let console_width = {
347            let console_size = console.size_chars()?;
348            usize::from(console_size.x)
349        };
350
351        let mut prompt = Cow::from(prompt);
352        let mut prompt_len = prompt.len();
353        if prompt_len >= console_width {
354            // TODO(jmmv): This slices by bytes and can split non-ASCII prompts.
355            if console_width >= 5 {
356                prompt = Cow::from(format!("{}...", &prompt[0..console_width - 5]));
357            } else {
358                prompt = Cow::from("");
359            }
360            prompt_len = prompt.len();
361        }
362
363        let input_width = {
364            // Assumes that the prompt was printed at column 0.  If that was not the case, line
365            // length calculation does not work.
366            let width = console_width - prompt_len;
367            width.saturating_sub(1)
368        };
369
370        let line = LineBuffer::from(previous);
371        let pos = line.len();
372        let view_start = if input_width == 0 { 0 } else { pos.saturating_sub(input_width) };
373        let history_pos = match history.as_mut() {
374            Some(history) => {
375                history.push(line.to_string());
376                history.len() - 1
377            }
378            None => 0,
379        };
380
381        let mut readline = Self {
382            console,
383            history,
384            echo,
385            input_width,
386            line,
387            pos,
388            view_start,
389            rendered: String::new(),
390            rendered_len: 0,
391            history_pos,
392        };
393        readline.view_start = readline.adjust_view(readline.pos, readline.view_start);
394        readline.sync_rendered();
395
396        if !prompt.is_empty() || !readline.rendered.is_empty() {
397            readline.console.write(&format!("{}{}", prompt, readline.rendered))?;
398            readline.console.sync_now()?;
399        }
400
401        Ok(readline)
402    }
403
404    /// Runs the interactive key-processing loop until the line is accepted.
405    async fn run(mut self) -> io::Result<String> {
406        loop {
407            let result = match self.console.read_key().await? {
408                Key::ArrowDown => self.do_up_down(1),
409                Key::ArrowLeft => self.do_move_to(self.pos.saturating_sub(1)),
410                Key::ArrowRight => self.do_move_to((self.pos + 1).min(self.line.len())),
411                Key::ArrowUp => self.do_up_down(-1),
412                Key::Backspace => self.do_backspace(),
413                Key::CarriageReturn => self.do_carriage_return(),
414                Key::Char(ch) => self.do_char(ch),
415                Key::Delete => self.do_delete(),
416                Key::End => self.do_end(),
417                Key::EofOrDelete => self.do_eof_or_delete(),
418                Key::Escape => self.do_ignore(),
419                Key::Home => self.do_home(),
420                Key::Interrupt => self.do_interrupt(),
421                Key::NewLine => self.do_newline(),
422                Key::PageDown | Key::PageUp | Key::Tab | Key::Unknown => self.do_ignore(),
423            }?;
424
425            if let DispatchResult::Submit = result {
426                break;
427            }
428        }
429
430        Ok(self.finish())
431    }
432}
433
434/// Reads a line of text interactively from the console, using the given `prompt` and pre-filling
435/// the input with `previous`.  If `history` is not `None`, then this appends the newly entered line
436/// into the history and allows navigating through it.
437async fn read_line_interactive(
438    console: &mut dyn Console,
439    prompt: &str,
440    previous: &str,
441    mut history: Option<&mut Vec<String>>,
442    echo: bool,
443) -> io::Result<String> {
444    Readline::new(console, prompt, previous, history.take(), echo)?.run().await
445}
446
447/// Reads a line of text interactively from the console, which is not expected to be a TTY.
448async fn read_line_raw(console: &mut dyn Console) -> io::Result<String> {
449    let mut line = String::new();
450    loop {
451        match console.read_key().await? {
452            Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight => (),
453            Key::Backspace => {
454                if !line.is_empty() {
455                    line.pop();
456                }
457            }
458            Key::CarriageReturn => {
459                // TODO(jmmv): This is here because the integration tests may be checked out with
460                // CRLF line endings on Windows, which means we'd see two characters to end a line
461                // instead of one.  Not sure if we should do this or if instead we should ensure
462                // the golden data we feed to the tests has single-character line endings.
463                if cfg!(not(target_os = "windows")) {
464                    break;
465                }
466            }
467            Key::Char(ch) => line.push(ch),
468            Key::Delete => (),
469            Key::End | Key::Home => (),
470            Key::Escape => (),
471            Key::EofOrDelete => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
472            Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
473            Key::NewLine => break,
474            Key::PageDown | Key::PageUp => (),
475            Key::Tab => (),
476            Key::Unknown => line.push('?'),
477        }
478    }
479    Ok(line)
480}
481
482/// Reads a line from the console.  If the console is interactive, this does fancy line editing and
483/// uses the given `prompt` and pre-fills the input with `previous`.
484pub async fn read_line(
485    console: &mut dyn Console,
486    prompt: &str,
487    previous: &str,
488    history: Option<&mut Vec<String>>,
489) -> io::Result<String> {
490    if console.is_interactive() {
491        read_line_interactive(console, prompt, previous, history, true).await
492    } else {
493        read_line_raw(console).await
494    }
495}
496
497/// Reads a line from the console without echo using the given `prompt`.
498///
499/// The console must be interactive for this to work, as otherwise we do not have a mechanism to
500/// suppress echo.
501pub async fn read_line_secure(console: &mut dyn Console, prompt: &str) -> io::Result<String> {
502    if !console.is_interactive() {
503        return Err(io::Error::other("Cannot read secure strings from a raw console".to_owned()));
504    }
505    read_line_interactive(console, prompt, "", None, false).await
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511    use crate::console::CharsXY;
512    use crate::testutils::*;
513    use futures_lite::future::block_on;
514
515    /// Builder pattern to construct a test for `read_line_interactive`.
516    #[must_use]
517    struct ReadLineInteractiveTest {
518        size_chars: CharsXY,
519        keys: Vec<Key>,
520        prompt: &'static str,
521        previous: &'static str,
522        history: Option<Vec<String>>,
523        echo: bool,
524        exp_line: &'static str,
525        exp_output: Vec<CapturedOut>,
526        exp_history: Option<Vec<String>>,
527    }
528
529    impl Default for ReadLineInteractiveTest {
530        /// Constructs a new test that feeds no input to the function, with no prompt or previous
531        /// text, and expects an empty return line and no changes to the console.
532        fn default() -> Self {
533            Self {
534                size_chars: CharsXY::new(15, 5),
535                keys: vec![],
536                prompt: "",
537                previous: "",
538                history: None,
539                echo: true,
540                exp_line: "",
541                exp_output: vec![],
542                exp_history: None,
543            }
544        }
545    }
546
547    impl ReadLineInteractiveTest {
548        /// Adds `key` to the golden input.
549        fn add_key(mut self, key: Key) -> Self {
550            self.keys.push(key);
551            self
552        }
553
554        /// Adds a bunch of `chars` as individual key presses to the golden input.
555        fn add_key_chars(mut self, chars: &'static str) -> Self {
556            for ch in chars.chars() {
557                self.keys.push(Key::Char(ch));
558            }
559            self
560        }
561
562        /// Adds a single state change to the expected output.
563        fn add_output(mut self, output: CapturedOut) -> Self {
564            self.exp_output.push(output);
565            self
566        }
567
568        /// Adds a bunch of `bytes` as separate console writes to the expected output.
569        fn add_output_bytes(mut self, bytes: &'static str) -> Self {
570            if bytes.is_empty() {
571                self.exp_output.push(CapturedOut::Write("".to_string()))
572            } else {
573                for b in bytes.chars() {
574                    let mut buf = [0u8; 4];
575                    self.exp_output.push(CapturedOut::Write(b.encode_utf8(&mut buf).to_string()));
576                }
577            }
578            self
579        }
580
581        /// Sets the size of the console.
582        fn set_size_chars(mut self, size: CharsXY) -> Self {
583            self.size_chars = size;
584            self
585        }
586
587        /// Sets the expected resulting line for the test.
588        fn set_line(mut self, line: &'static str) -> Self {
589            self.exp_line = line;
590            self
591        }
592
593        /// Sets the prompt to use for the test.
594        fn set_prompt(mut self, prompt: &'static str) -> Self {
595            self.prompt = prompt;
596            self
597        }
598
599        /// Sets the previous text to use for the test.
600        fn set_previous(mut self, previous: &'static str) -> Self {
601            self.previous = previous;
602            self
603        }
604
605        /// Enables history tracking and sets the history to use for the test as `history` and
606        /// expects that `history` matches `exp_history` upon test completion.
607        fn set_history(mut self, history: Vec<String>, exp_history: Vec<String>) -> Self {
608            self.history = Some(history);
609            self.exp_history = Some(exp_history);
610            self
611        }
612
613        /// Sets whether read_line echoes characters or not.
614        fn set_echo(mut self, echo: bool) -> Self {
615            self.echo = echo;
616            self
617        }
618
619        /// Adds a final return key to the golden input, a newline to the expected output, and
620        /// executes the test.
621        fn accept(mut self) {
622            self.keys.push(Key::NewLine);
623            self.exp_output.push(CapturedOut::Print("".to_owned()));
624
625            let mut console = MockConsole::default();
626            console.add_input_keys(&self.keys);
627            console.set_size_chars(self.size_chars);
628            let line = block_on(read_line_interactive(
629                &mut console,
630                self.prompt,
631                self.previous,
632                self.history.as_mut(),
633                self.echo,
634            ))
635            .unwrap();
636            assert_eq!(self.exp_line, &line);
637            assert_eq!(self.exp_output.as_slice(), console.captured_out());
638            assert_eq!(self.exp_history, self.history);
639        }
640
641        /// Executes the test and expects the given error kind.
642        fn expect_err(mut self, exp_kind: io::ErrorKind) {
643            let mut console = MockConsole::default();
644            console.add_input_keys(&self.keys);
645            console.set_size_chars(self.size_chars);
646            let err = block_on(read_line_interactive(
647                &mut console,
648                self.prompt,
649                self.previous,
650                self.history.as_mut(),
651                self.echo,
652            ))
653            .expect_err("read_line_interactive should fail");
654            assert_eq!(exp_kind, err.kind());
655            assert_eq!(self.exp_output.as_slice(), console.captured_out());
656            assert_eq!(self.exp_history, self.history);
657        }
658    }
659
660    #[test]
661    fn test_read_line_interactive_empty() {
662        ReadLineInteractiveTest::default().accept();
663        ReadLineInteractiveTest::default().add_key(Key::Backspace).accept();
664        ReadLineInteractiveTest::default().add_key(Key::Delete).accept();
665        ReadLineInteractiveTest::default()
666            .add_key(Key::EofOrDelete)
667            .expect_err(io::ErrorKind::UnexpectedEof);
668        ReadLineInteractiveTest::default().add_key(Key::ArrowLeft).accept();
669        ReadLineInteractiveTest::default().add_key(Key::ArrowRight).accept();
670    }
671
672    #[test]
673    fn test_read_line_with_prompt() {
674        ReadLineInteractiveTest::default()
675            .set_prompt("Ready> ")
676            .add_output(CapturedOut::Write("Ready> ".to_string()))
677            .add_output(CapturedOut::SyncNow)
678            // -
679            .add_key_chars("hello")
680            .add_output_bytes("hello")
681            // -
682            .set_line("hello")
683            .accept();
684
685        ReadLineInteractiveTest::default()
686            .set_prompt("Cannot delete")
687            .add_output(CapturedOut::Write("Cannot delete".to_string()))
688            .add_output(CapturedOut::SyncNow)
689            // -
690            .add_key(Key::Backspace)
691            .accept();
692    }
693
694    #[test]
695    fn test_read_line_with_prompt_larger_than_screen() {
696        ReadLineInteractiveTest::default()
697            .set_size_chars(CharsXY::new(15, 5))
698            .set_prompt("This is larger than the screen> ")
699            .add_output(CapturedOut::Write("This is la...".to_string()))
700            .add_output(CapturedOut::SyncNow)
701            // -
702            .add_key_chars("hello")
703            .add_output_bytes("h")
704            .add_output(CapturedOut::HideCursor)
705            .add_output(CapturedOut::MoveWithinLine(-1))
706            .add_output(CapturedOut::Write("e".to_string()))
707            .add_output(CapturedOut::ShowCursor)
708            .add_output(CapturedOut::HideCursor)
709            .add_output(CapturedOut::MoveWithinLine(-1))
710            .add_output(CapturedOut::Write("l".to_string()))
711            .add_output(CapturedOut::ShowCursor)
712            .add_output(CapturedOut::HideCursor)
713            .add_output(CapturedOut::MoveWithinLine(-1))
714            .add_output(CapturedOut::Write("l".to_string()))
715            .add_output(CapturedOut::ShowCursor)
716            .add_output(CapturedOut::HideCursor)
717            .add_output(CapturedOut::MoveWithinLine(-1))
718            .add_output(CapturedOut::Write("o".to_string()))
719            .add_output(CapturedOut::ShowCursor)
720            // -
721            .set_line("hello")
722            .accept();
723    }
724
725    #[test]
726    fn test_read_line_with_prompt_equal_to_screen() {
727        ReadLineInteractiveTest::default()
728            .set_size_chars(CharsXY::new(10, 5))
729            .set_prompt("0123456789")
730            .add_output(CapturedOut::Write("01234...".to_string()))
731            .add_output(CapturedOut::SyncNow)
732            // -
733            .add_key_chars("hello")
734            .add_output_bytes("h")
735            .add_output(CapturedOut::HideCursor)
736            .add_output(CapturedOut::MoveWithinLine(-1))
737            .add_output(CapturedOut::Write("e".to_string()))
738            .add_output(CapturedOut::ShowCursor)
739            .add_output(CapturedOut::HideCursor)
740            .add_output(CapturedOut::MoveWithinLine(-1))
741            .add_output(CapturedOut::Write("l".to_string()))
742            .add_output(CapturedOut::ShowCursor)
743            .add_output(CapturedOut::HideCursor)
744            .add_output(CapturedOut::MoveWithinLine(-1))
745            .add_output(CapturedOut::Write("l".to_string()))
746            .add_output(CapturedOut::ShowCursor)
747            .add_output(CapturedOut::HideCursor)
748            .add_output(CapturedOut::MoveWithinLine(-1))
749            .add_output(CapturedOut::Write("o".to_string()))
750            .add_output(CapturedOut::ShowCursor)
751            // -
752            .set_line("hello")
753            .accept();
754    }
755
756    #[test]
757    fn test_read_line_with_prompt_larger_than_tiny_screen() {
758        ReadLineInteractiveTest::default()
759            .set_size_chars(CharsXY::new(3, 5))
760            .set_prompt("This is larger than the screen> ")
761            // -
762            .add_key_chars("hello")
763            .add_output_bytes("he")
764            .add_output(CapturedOut::HideCursor)
765            .add_output(CapturedOut::MoveWithinLine(-2))
766            .add_output(CapturedOut::Write("el".to_string()))
767            .add_output(CapturedOut::ShowCursor)
768            .add_output(CapturedOut::HideCursor)
769            .add_output(CapturedOut::MoveWithinLine(-2))
770            .add_output(CapturedOut::Write("ll".to_string()))
771            .add_output(CapturedOut::ShowCursor)
772            .add_output(CapturedOut::HideCursor)
773            .add_output(CapturedOut::MoveWithinLine(-2))
774            .add_output(CapturedOut::Write("lo".to_string()))
775            .add_output(CapturedOut::ShowCursor)
776            // -
777            .set_line("hello")
778            .accept();
779    }
780
781    #[test]
782    fn test_read_line_with_prompt_shorter_than_tiny_screen() {
783        ReadLineInteractiveTest::default()
784            .set_size_chars(CharsXY::new(3, 5))
785            .set_prompt("?")
786            .add_output(CapturedOut::Write("?".to_string()))
787            .add_output(CapturedOut::SyncNow)
788            // -
789            .add_key_chars("hello")
790            .add_output_bytes("h")
791            .add_output(CapturedOut::HideCursor)
792            .add_output(CapturedOut::MoveWithinLine(-1))
793            .add_output(CapturedOut::Write("e".to_string()))
794            .add_output(CapturedOut::ShowCursor)
795            .add_output(CapturedOut::HideCursor)
796            .add_output(CapturedOut::MoveWithinLine(-1))
797            .add_output(CapturedOut::Write("l".to_string()))
798            .add_output(CapturedOut::ShowCursor)
799            .add_output(CapturedOut::HideCursor)
800            .add_output(CapturedOut::MoveWithinLine(-1))
801            .add_output(CapturedOut::Write("l".to_string()))
802            .add_output(CapturedOut::ShowCursor)
803            .add_output(CapturedOut::HideCursor)
804            .add_output(CapturedOut::MoveWithinLine(-1))
805            .add_output(CapturedOut::Write("o".to_string()))
806            .add_output(CapturedOut::ShowCursor)
807            // -
808            .set_line("hello")
809            .accept();
810    }
811
812    #[test]
813    fn test_read_line_interactive_trailing_input() {
814        ReadLineInteractiveTest::default()
815            .add_key_chars("hello")
816            .add_output_bytes("hello")
817            // -
818            .set_line("hello")
819            .accept();
820
821        ReadLineInteractiveTest::default()
822            .set_previous("123")
823            .add_output(CapturedOut::Write("123".to_string()))
824            .add_output(CapturedOut::SyncNow)
825            // -
826            .add_key_chars("hello")
827            .add_output_bytes("hello")
828            // -
829            .set_line("123hello")
830            .accept();
831    }
832
833    #[test]
834    fn test_read_line_interactive_middle_input() {
835        ReadLineInteractiveTest::default()
836            .add_key_chars("some text")
837            .add_output_bytes("some text")
838            // -
839            .add_key(Key::ArrowLeft)
840            .add_output(CapturedOut::MoveWithinLine(-1))
841            // -
842            .add_key(Key::ArrowLeft)
843            .add_output(CapturedOut::MoveWithinLine(-1))
844            // -
845            .add_key(Key::ArrowLeft)
846            .add_output(CapturedOut::MoveWithinLine(-1))
847            // -
848            .add_key(Key::ArrowRight)
849            .add_output(CapturedOut::MoveWithinLine(1))
850            // -
851            .add_key_chars(" ")
852            .add_output(CapturedOut::HideCursor)
853            .add_output_bytes(" ")
854            .add_output(CapturedOut::Write("xt".to_string()))
855            .add_output(CapturedOut::MoveWithinLine(-2))
856            .add_output(CapturedOut::ShowCursor)
857            // -
858            .add_key_chars(".")
859            .add_output(CapturedOut::HideCursor)
860            .add_output_bytes(".")
861            .add_output(CapturedOut::Write("xt".to_string()))
862            .add_output(CapturedOut::MoveWithinLine(-2))
863            .add_output(CapturedOut::ShowCursor)
864            // -
865            .set_line("some te .xt")
866            .accept();
867    }
868
869    #[test]
870    fn test_read_line_interactive_utf8_basic() {
871        ReadLineInteractiveTest::default()
872            .add_key_chars("é")
873            .add_output(CapturedOut::Write("é".to_string()))
874            // -
875            .set_line("é")
876            .accept();
877    }
878
879    #[test]
880    fn test_read_line_interactive_utf8_remove_2byte_char() {
881        ReadLineInteractiveTest::default()
882            .add_key_chars("é")
883            .add_output(CapturedOut::Write("é".to_string()))
884            // -
885            .add_key(Key::Backspace)
886            .add_output(CapturedOut::HideCursor)
887            .add_output(CapturedOut::MoveWithinLine(-1))
888            .add_output_bytes("")
889            .add_output_bytes(" ")
890            .add_output(CapturedOut::MoveWithinLine(-1))
891            .add_output(CapturedOut::ShowCursor)
892            // -
893            .set_line("")
894            .accept();
895    }
896
897    #[test]
898    fn test_read_line_interactive_utf8_add_and_remove_last() {
899        ReadLineInteractiveTest::default()
900            .add_key_chars("àé")
901            .add_output(CapturedOut::Write("à".to_string()))
902            .add_output(CapturedOut::Write("é".to_string()))
903            // -
904            .add_key(Key::Backspace)
905            .add_output(CapturedOut::HideCursor)
906            .add_output(CapturedOut::MoveWithinLine(-1))
907            .add_output_bytes("")
908            .add_output_bytes(" ")
909            .add_output(CapturedOut::MoveWithinLine(-1))
910            .add_output(CapturedOut::ShowCursor)
911            // -
912            .set_line("à")
913            .accept();
914    }
915
916    #[test]
917    fn test_read_line_interactive_utf8_navigate_2byte_chars() {
918        ReadLineInteractiveTest::default()
919            .add_key_chars("àé")
920            .add_output(CapturedOut::Write("à".to_string()))
921            .add_output(CapturedOut::Write("é".to_string()))
922            // -
923            .add_key(Key::ArrowLeft)
924            .add_output(CapturedOut::MoveWithinLine(-1))
925            // -
926            .add_key(Key::ArrowLeft)
927            .add_output(CapturedOut::MoveWithinLine(-1))
928            // -
929            .add_key(Key::ArrowLeft)
930            // -
931            .add_key(Key::ArrowRight)
932            .add_output(CapturedOut::MoveWithinLine(1))
933            // -
934            .add_key(Key::Backspace)
935            .add_output(CapturedOut::HideCursor)
936            .add_output(CapturedOut::MoveWithinLine(-1))
937            .add_output(CapturedOut::Write("é".to_string()))
938            .add_output_bytes(" ")
939            .add_output(CapturedOut::MoveWithinLine(-2))
940            .add_output(CapturedOut::ShowCursor)
941            // -
942            .set_line("é")
943            .accept();
944    }
945
946    #[test]
947    fn test_read_line_interactive_trailing_backspace() {
948        ReadLineInteractiveTest::default()
949            .add_key_chars("bar")
950            .add_output_bytes("bar")
951            // -
952            .add_key(Key::Backspace)
953            .add_output(CapturedOut::HideCursor)
954            .add_output(CapturedOut::MoveWithinLine(-1))
955            .add_output_bytes("")
956            .add_output_bytes(" ")
957            .add_output(CapturedOut::MoveWithinLine(-1))
958            .add_output(CapturedOut::ShowCursor)
959            // -
960            .add_key_chars("zar")
961            .add_output_bytes("zar")
962            // -
963            .set_line("bazar")
964            .accept();
965    }
966
967    #[test]
968    fn test_read_line_interactive_middle_backspace() {
969        ReadLineInteractiveTest::default()
970            .add_key_chars("has a tYpo")
971            .add_output_bytes("has a tYpo")
972            // -
973            .add_key(Key::ArrowLeft)
974            .add_output(CapturedOut::MoveWithinLine(-1))
975            // -
976            .add_key(Key::ArrowLeft)
977            .add_output(CapturedOut::MoveWithinLine(-1))
978            // -
979            .add_key(Key::Backspace)
980            .add_output(CapturedOut::HideCursor)
981            .add_output(CapturedOut::MoveWithinLine(-1))
982            .add_output(CapturedOut::Write("po".to_string()))
983            .add_output_bytes(" ")
984            .add_output(CapturedOut::MoveWithinLine(-3))
985            .add_output(CapturedOut::ShowCursor)
986            // -
987            .add_key_chars("y")
988            .add_output(CapturedOut::HideCursor)
989            .add_output_bytes("y")
990            .add_output(CapturedOut::Write("po".to_string()))
991            .add_output(CapturedOut::MoveWithinLine(-2))
992            .add_output(CapturedOut::ShowCursor)
993            // -
994            .set_line("has a typo")
995            .accept();
996    }
997
998    #[test]
999    fn test_read_line_interactive_middle_delete() {
1000        ReadLineInteractiveTest::default()
1001            .add_key_chars("has a typo")
1002            .add_output_bytes("has a typo")
1003            // -
1004            .add_key(Key::ArrowLeft)
1005            .add_output(CapturedOut::MoveWithinLine(-1))
1006            // -
1007            .add_key(Key::ArrowLeft)
1008            .add_output(CapturedOut::MoveWithinLine(-1))
1009            // -
1010            .add_key(Key::Delete)
1011            .add_output(CapturedOut::HideCursor)
1012            .add_output(CapturedOut::Write("o".to_string()))
1013            .add_output_bytes(" ")
1014            .add_output(CapturedOut::MoveWithinLine(-2))
1015            .add_output(CapturedOut::ShowCursor)
1016            // -
1017            .add_key_chars("e")
1018            .add_output(CapturedOut::HideCursor)
1019            .add_output_bytes("e")
1020            .add_output(CapturedOut::Write("o".to_string()))
1021            .add_output(CapturedOut::MoveWithinLine(-1))
1022            .add_output(CapturedOut::ShowCursor)
1023            // -
1024            .set_line("has a tyeo")
1025            .accept();
1026    }
1027
1028    #[test]
1029    fn test_read_line_interactive_middle_ctrl_d_deletes() {
1030        ReadLineInteractiveTest::default()
1031            .add_key_chars("has a typo")
1032            .add_output_bytes("has a typo")
1033            // -
1034            .add_key(Key::ArrowLeft)
1035            .add_output(CapturedOut::MoveWithinLine(-1))
1036            // -
1037            .add_key(Key::ArrowLeft)
1038            .add_output(CapturedOut::MoveWithinLine(-1))
1039            // -
1040            .add_key(Key::EofOrDelete)
1041            .add_output(CapturedOut::HideCursor)
1042            .add_output(CapturedOut::Write("o".to_string()))
1043            .add_output_bytes(" ")
1044            .add_output(CapturedOut::MoveWithinLine(-2))
1045            .add_output(CapturedOut::ShowCursor)
1046            // -
1047            .add_key_chars("e")
1048            .add_output(CapturedOut::HideCursor)
1049            .add_output_bytes("e")
1050            .add_output(CapturedOut::Write("o".to_string()))
1051            .add_output(CapturedOut::MoveWithinLine(-1))
1052            .add_output(CapturedOut::ShowCursor)
1053            // -
1054            .set_line("has a tyeo")
1055            .accept();
1056    }
1057
1058    #[test]
1059    fn test_read_line_interactive_utf8_delete_2byte_char() {
1060        ReadLineInteractiveTest::default()
1061            .add_key_chars("àé")
1062            .add_output(CapturedOut::Write("à".to_string()))
1063            .add_output(CapturedOut::Write("é".to_string()))
1064            // -
1065            .add_key(Key::ArrowLeft)
1066            .add_output(CapturedOut::MoveWithinLine(-1))
1067            // -
1068            .add_key(Key::Delete)
1069            .add_output(CapturedOut::HideCursor)
1070            .add_output_bytes("")
1071            .add_output_bytes(" ")
1072            .add_output(CapturedOut::MoveWithinLine(-1))
1073            .add_output(CapturedOut::ShowCursor)
1074            // -
1075            .set_line("à")
1076            .accept();
1077    }
1078
1079    #[test]
1080    fn test_read_line_interactive_delete_at_end_is_ignored() {
1081        ReadLineInteractiveTest::default()
1082            .set_previous("sample")
1083            .add_output(CapturedOut::Write("sample".to_string()))
1084            .add_output(CapturedOut::SyncNow)
1085            // -
1086            .add_key(Key::Delete)
1087            // -
1088            .set_line("sample")
1089            .accept();
1090    }
1091
1092    #[test]
1093    fn test_read_line_interactive_ctrl_d_at_end_is_ignored() {
1094        ReadLineInteractiveTest::default()
1095            .set_previous("sample")
1096            .add_output(CapturedOut::Write("sample".to_string()))
1097            .add_output(CapturedOut::SyncNow)
1098            // -
1099            .add_key(Key::EofOrDelete)
1100            // -
1101            .set_line("sample")
1102            .accept();
1103    }
1104
1105    #[test]
1106    fn test_read_line_interactive_test_move_bounds() {
1107        ReadLineInteractiveTest::default()
1108            .set_previous("12")
1109            .add_output(CapturedOut::Write("12".to_string()))
1110            .add_output(CapturedOut::SyncNow)
1111            // -
1112            .add_key(Key::ArrowLeft)
1113            .add_output(CapturedOut::MoveWithinLine(-1))
1114            // -
1115            .add_key(Key::ArrowLeft)
1116            .add_output(CapturedOut::MoveWithinLine(-1))
1117            // -
1118            .add_key(Key::ArrowLeft)
1119            .add_key(Key::ArrowLeft)
1120            .add_key(Key::ArrowLeft)
1121            .add_key(Key::ArrowLeft)
1122            // -
1123            .add_key(Key::ArrowRight)
1124            .add_output(CapturedOut::MoveWithinLine(1))
1125            // -
1126            .add_key(Key::ArrowRight)
1127            .add_output(CapturedOut::MoveWithinLine(1))
1128            // -
1129            .add_key(Key::ArrowRight)
1130            .add_key(Key::ArrowRight)
1131            // -
1132            .add_key_chars("3")
1133            .add_output_bytes("3")
1134            // -
1135            .set_line("123")
1136            .accept();
1137    }
1138
1139    #[test]
1140    fn test_read_line_interactive_test_home_end() {
1141        ReadLineInteractiveTest::default()
1142            .set_previous("sample text")
1143            .add_output(CapturedOut::Write("sample text".to_string()))
1144            .add_output(CapturedOut::SyncNow)
1145            // -
1146            .add_key(Key::End)
1147            // -
1148            .add_key(Key::Home)
1149            .add_output(CapturedOut::MoveWithinLine(-11))
1150            // -
1151            .add_key(Key::Home)
1152            // -
1153            .add_key(Key::Char('>'))
1154            .add_output(CapturedOut::HideCursor)
1155            .add_output_bytes(">")
1156            .add_output(CapturedOut::Write("sample text".to_string()))
1157            .add_output(CapturedOut::MoveWithinLine(-11))
1158            .add_output(CapturedOut::ShowCursor)
1159            // -
1160            .add_key(Key::End)
1161            .add_output(CapturedOut::MoveWithinLine(11))
1162            // -
1163            .add_key(Key::Char('<'))
1164            .add_output_bytes("<")
1165            // -
1166            .add_key(Key::ArrowLeft)
1167            .add_output(CapturedOut::MoveWithinLine(-1))
1168            // -
1169            .add_key(Key::ArrowLeft)
1170            .add_output(CapturedOut::MoveWithinLine(-1))
1171            // -
1172            .add_key(Key::ArrowLeft)
1173            .add_output(CapturedOut::MoveWithinLine(-1))
1174            // -
1175            .add_key(Key::ArrowLeft)
1176            .add_output(CapturedOut::MoveWithinLine(-1))
1177            // -
1178            .add_key(Key::ArrowLeft)
1179            .add_output(CapturedOut::MoveWithinLine(-1))
1180            // -
1181            .add_key(Key::Backspace)
1182            .add_output(CapturedOut::HideCursor)
1183            .add_output(CapturedOut::MoveWithinLine(-1))
1184            .add_output(CapturedOut::Write("text<".to_string()))
1185            .add_output_bytes(" ")
1186            .add_output(CapturedOut::MoveWithinLine(-6))
1187            .add_output(CapturedOut::ShowCursor)
1188            // -
1189            .set_line(">sampletext<")
1190            .accept();
1191    }
1192
1193    #[test]
1194    fn test_read_line_interactive_horizontal_scrolling_while_appending() {
1195        ReadLineInteractiveTest::default()
1196            .add_key_chars("12345678901234")
1197            .add_output_bytes("12345678901234")
1198            // -
1199            .add_key_chars("5")
1200            .add_output(CapturedOut::HideCursor)
1201            .add_output(CapturedOut::MoveWithinLine(-14))
1202            .add_output(CapturedOut::Write("23456789012345".to_string()))
1203            .add_output(CapturedOut::ShowCursor)
1204            // -
1205            .set_line("123456789012345")
1206            .accept();
1207    }
1208
1209    #[test]
1210    fn test_read_line_interactive_horizontal_scrolling_while_navigating() {
1211        ReadLineInteractiveTest::default()
1212            .set_size_chars(CharsXY::new(6, 5))
1213            .set_previous("abcdef")
1214            .add_output(CapturedOut::Write("bcdef".to_string()))
1215            .add_output(CapturedOut::SyncNow)
1216            // -
1217            .add_key(Key::ArrowLeft)
1218            .add_output(CapturedOut::MoveWithinLine(-1))
1219            .add_key(Key::ArrowLeft)
1220            .add_output(CapturedOut::MoveWithinLine(-1))
1221            .add_key(Key::ArrowLeft)
1222            .add_output(CapturedOut::MoveWithinLine(-1))
1223            .add_key(Key::ArrowLeft)
1224            .add_output(CapturedOut::MoveWithinLine(-1))
1225            .add_key(Key::ArrowLeft)
1226            .add_output(CapturedOut::MoveWithinLine(-1))
1227            .add_key(Key::ArrowLeft)
1228            .add_output(CapturedOut::HideCursor)
1229            .add_output(CapturedOut::Write("abcde".to_string()))
1230            .add_output(CapturedOut::MoveWithinLine(-5))
1231            .add_output(CapturedOut::ShowCursor)
1232            // -
1233            .add_key(Key::ArrowRight)
1234            .add_output(CapturedOut::MoveWithinLine(1))
1235            .add_key(Key::ArrowRight)
1236            .add_output(CapturedOut::MoveWithinLine(1))
1237            .add_key(Key::ArrowRight)
1238            .add_output(CapturedOut::MoveWithinLine(1))
1239            .add_key(Key::ArrowRight)
1240            .add_output(CapturedOut::MoveWithinLine(1))
1241            .add_key(Key::ArrowRight)
1242            .add_output(CapturedOut::MoveWithinLine(1))
1243            .add_key(Key::ArrowRight)
1244            .add_output(CapturedOut::HideCursor)
1245            .add_output(CapturedOut::MoveWithinLine(-5))
1246            .add_output(CapturedOut::Write("bcdef".to_string()))
1247            .add_output(CapturedOut::ShowCursor)
1248            // -
1249            .set_line("abcdef")
1250            .accept();
1251    }
1252
1253    #[test]
1254    fn test_read_line_interactive_horizontal_scrolling_with_prompt_and_previous() {
1255        ReadLineInteractiveTest::default()
1256            .set_prompt("12345")
1257            .set_previous("67890")
1258            .add_output(CapturedOut::Write("1234567890".to_string()))
1259            .add_output(CapturedOut::SyncNow)
1260            // -
1261            .add_key_chars("1234567890")
1262            .add_output_bytes("1234")
1263            .add_output(CapturedOut::HideCursor)
1264            .add_output(CapturedOut::MoveWithinLine(-9))
1265            .add_output(CapturedOut::Write("789012345".to_string()))
1266            .add_output(CapturedOut::ShowCursor)
1267            .add_output(CapturedOut::HideCursor)
1268            .add_output(CapturedOut::MoveWithinLine(-9))
1269            .add_output(CapturedOut::Write("890123456".to_string()))
1270            .add_output(CapturedOut::ShowCursor)
1271            .add_output(CapturedOut::HideCursor)
1272            .add_output(CapturedOut::MoveWithinLine(-9))
1273            .add_output(CapturedOut::Write("901234567".to_string()))
1274            .add_output(CapturedOut::ShowCursor)
1275            .add_output(CapturedOut::HideCursor)
1276            .add_output(CapturedOut::MoveWithinLine(-9))
1277            .add_output(CapturedOut::Write("012345678".to_string()))
1278            .add_output(CapturedOut::ShowCursor)
1279            .add_output(CapturedOut::HideCursor)
1280            .add_output(CapturedOut::MoveWithinLine(-9))
1281            .add_output(CapturedOut::Write("123456789".to_string()))
1282            .add_output(CapturedOut::ShowCursor)
1283            .add_output(CapturedOut::HideCursor)
1284            .add_output(CapturedOut::MoveWithinLine(-9))
1285            .add_output(CapturedOut::Write("234567890".to_string()))
1286            .add_output(CapturedOut::ShowCursor)
1287            // -
1288            .set_line("678901234567890")
1289            .accept();
1290    }
1291
1292    #[test]
1293    fn test_read_line_interactive_horizontal_scrolling_secure() {
1294        ReadLineInteractiveTest::default()
1295            .set_size_chars(CharsXY::new(6, 5))
1296            .set_echo(false)
1297            .add_key_chars("abcdef")
1298            .add_output_bytes("*****")
1299            .add_output(CapturedOut::HideCursor)
1300            .add_output(CapturedOut::MoveWithinLine(-5))
1301            .add_output(CapturedOut::Write("*****".to_string()))
1302            .add_output(CapturedOut::ShowCursor)
1303            // -
1304            .set_line("abcdef")
1305            .accept();
1306    }
1307
1308    #[test]
1309    fn test_read_line_interactive_horizontal_scrolling_delete() {
1310        ReadLineInteractiveTest::default()
1311            .set_size_chars(CharsXY::new(6, 5))
1312            .set_previous("abcdef")
1313            .add_output(CapturedOut::Write("bcdef".to_string()))
1314            .add_output(CapturedOut::SyncNow)
1315            // -
1316            .add_key(Key::ArrowLeft)
1317            .add_output(CapturedOut::MoveWithinLine(-1))
1318            .add_key(Key::Delete)
1319            .add_output(CapturedOut::HideCursor)
1320            .add_output(CapturedOut::MoveWithinLine(-4))
1321            .add_output(CapturedOut::Write("bcde".to_string()))
1322            .add_output(CapturedOut::Write(" ".to_string()))
1323            .add_output(CapturedOut::MoveWithinLine(-1))
1324            .add_output(CapturedOut::ShowCursor)
1325            // -
1326            .set_line("abcde")
1327            .accept();
1328    }
1329
1330    #[test]
1331    fn test_read_line_interactive_history_not_enabled_by_default() {
1332        ReadLineInteractiveTest::default().add_key(Key::ArrowUp).accept();
1333        ReadLineInteractiveTest::default().add_key(Key::ArrowDown).accept();
1334    }
1335
1336    #[test]
1337    fn test_read_line_interactive_history_empty() {
1338        ReadLineInteractiveTest::default()
1339            .set_history(vec![], vec!["foobarbaz".to_owned()])
1340            //
1341            .add_key_chars("foo")
1342            .add_output_bytes("foo")
1343            //
1344            .add_key(Key::ArrowUp)
1345            //
1346            .add_key_chars("bar")
1347            .add_output_bytes("bar")
1348            //
1349            .add_key(Key::ArrowDown)
1350            //
1351            .add_key_chars("baz")
1352            .add_output_bytes("baz")
1353            //
1354            .set_line("foobarbaz")
1355            .accept();
1356    }
1357
1358    #[test]
1359    fn test_read_line_interactive_skips_empty_lines() {
1360        ReadLineInteractiveTest::default()
1361            .set_history(vec!["first".to_owned()], vec!["first".to_owned()])
1362            // -
1363            .add_key_chars("x")
1364            .add_output(CapturedOut::Write("x".to_string()))
1365            // -
1366            .add_key(Key::Backspace)
1367            .add_output(CapturedOut::HideCursor)
1368            .add_output(CapturedOut::MoveWithinLine(-1))
1369            .add_output_bytes("")
1370            .add_output_bytes(" ")
1371            .add_output(CapturedOut::MoveWithinLine(-1))
1372            .add_output(CapturedOut::ShowCursor)
1373            // -
1374            .accept();
1375    }
1376
1377    #[test]
1378    fn test_read_line_interactive_history_navigate_up_down_end_of_line() {
1379        ReadLineInteractiveTest::default()
1380            .set_prompt("? ")
1381            .add_output(CapturedOut::Write("? ".to_string()))
1382            .add_output(CapturedOut::SyncNow)
1383            //
1384            .set_history(
1385                vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()],
1386                vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()],
1387            )
1388            //
1389            .add_key(Key::ArrowUp)
1390            .add_output(CapturedOut::HideCursor)
1391            .add_output(CapturedOut::Write("last".to_string()))
1392            .add_output(CapturedOut::ShowCursor)
1393            //
1394            .add_key(Key::ArrowUp)
1395            .add_output(CapturedOut::HideCursor)
1396            .add_output(CapturedOut::MoveWithinLine(-("last".len() as i16)))
1397            .add_output(CapturedOut::Write(" second line".to_string()))
1398            .add_output(CapturedOut::ShowCursor)
1399            //
1400            .add_key(Key::ArrowUp)
1401            .add_output(CapturedOut::HideCursor)
1402            .add_output(CapturedOut::MoveWithinLine(-(" second line".len() as i16)))
1403            .add_output(CapturedOut::Write("first".to_string()))
1404            .add_output(CapturedOut::Write("       ".to_string()))
1405            .add_output(CapturedOut::MoveWithinLine(-("       ".len() as i16)))
1406            .add_output(CapturedOut::ShowCursor)
1407            //
1408            .add_key(Key::ArrowUp)
1409            //
1410            .add_key(Key::ArrowDown)
1411            .add_output(CapturedOut::HideCursor)
1412            .add_output(CapturedOut::MoveWithinLine(-("first".len() as i16)))
1413            .add_output(CapturedOut::Write(" second line".to_string()))
1414            .add_output(CapturedOut::ShowCursor)
1415            //
1416            .add_key(Key::ArrowDown)
1417            .add_output(CapturedOut::HideCursor)
1418            .add_output(CapturedOut::MoveWithinLine(-(" second line".len() as i16)))
1419            .add_output(CapturedOut::Write("last".to_string()))
1420            .add_output(CapturedOut::Write("        ".to_string()))
1421            .add_output(CapturedOut::MoveWithinLine(-("        ".len() as i16)))
1422            .add_output(CapturedOut::ShowCursor)
1423            //
1424            .add_key(Key::ArrowDown)
1425            .add_output(CapturedOut::HideCursor)
1426            .add_output(CapturedOut::MoveWithinLine(-("last".len() as i16)))
1427            .add_output(CapturedOut::Write("    ".to_string()))
1428            .add_output(CapturedOut::MoveWithinLine(-("    ".len() as i16)))
1429            .add_output(CapturedOut::ShowCursor)
1430            //
1431            .add_key(Key::ArrowDown)
1432            //
1433            .accept();
1434    }
1435
1436    #[test]
1437    fn test_read_line_interactive_history_navigate_up_down_middle_of_line() {
1438        ReadLineInteractiveTest::default()
1439            .set_prompt("? ")
1440            .add_output(CapturedOut::Write("? ".to_string()))
1441            .add_output(CapturedOut::SyncNow)
1442            //
1443            .set_history(
1444                vec!["a".to_owned(), "long-line".to_owned(), "zzzz".to_owned()],
1445                vec!["a".to_owned(), "long-line".to_owned(), "zzzz".to_owned()],
1446            )
1447            //
1448            .add_key(Key::ArrowUp)
1449            .add_output(CapturedOut::HideCursor)
1450            .add_output(CapturedOut::Write("zzzz".to_string()))
1451            .add_output(CapturedOut::ShowCursor)
1452            //
1453            .add_key(Key::ArrowUp)
1454            .add_output(CapturedOut::HideCursor)
1455            .add_output(CapturedOut::MoveWithinLine(-("zzzz".len() as i16)))
1456            .add_output(CapturedOut::Write("long-line".to_string()))
1457            .add_output(CapturedOut::ShowCursor)
1458            //
1459            .add_key(Key::ArrowLeft)
1460            .add_output(CapturedOut::MoveWithinLine(-1))
1461            .add_key(Key::ArrowLeft)
1462            .add_output(CapturedOut::MoveWithinLine(-1))
1463            .add_key(Key::ArrowLeft)
1464            .add_output(CapturedOut::MoveWithinLine(-1))
1465            .add_key(Key::ArrowLeft)
1466            .add_output(CapturedOut::MoveWithinLine(-1))
1467            //
1468            .add_key(Key::ArrowUp)
1469            .add_output(CapturedOut::HideCursor)
1470            .add_output(CapturedOut::MoveWithinLine(-("long-line".len() as i16) + 4))
1471            .add_output(CapturedOut::Write("a".to_string()))
1472            .add_output(CapturedOut::Write("        ".to_string()))
1473            .add_output(CapturedOut::MoveWithinLine(-("        ".len() as i16)))
1474            .add_output(CapturedOut::ShowCursor)
1475            //
1476            .add_key(Key::ArrowUp)
1477            //
1478            .add_key(Key::ArrowDown)
1479            .add_output(CapturedOut::HideCursor)
1480            .add_output(CapturedOut::MoveWithinLine(-("a".len() as i16)))
1481            .add_output(CapturedOut::Write("long-line".to_string()))
1482            .add_output(CapturedOut::ShowCursor)
1483            //
1484            .add_key(Key::ArrowLeft)
1485            .add_output(CapturedOut::MoveWithinLine(-1))
1486            .add_key(Key::ArrowLeft)
1487            .add_output(CapturedOut::MoveWithinLine(-1))
1488            .add_key(Key::ArrowLeft)
1489            .add_output(CapturedOut::MoveWithinLine(-1))
1490            .add_key(Key::ArrowLeft)
1491            .add_output(CapturedOut::MoveWithinLine(-1))
1492            .add_key(Key::ArrowLeft)
1493            .add_output(CapturedOut::MoveWithinLine(-1))
1494            .add_key(Key::ArrowLeft)
1495            .add_output(CapturedOut::MoveWithinLine(-1))
1496            //
1497            .add_key(Key::ArrowDown)
1498            .add_output(CapturedOut::HideCursor)
1499            .add_output(CapturedOut::MoveWithinLine(-("long-line".len() as i16) + 6))
1500            .add_output(CapturedOut::Write("zzzz".to_string()))
1501            .add_output(CapturedOut::Write("     ".to_string()))
1502            .add_output(CapturedOut::MoveWithinLine(-("     ".len() as i16)))
1503            .add_output(CapturedOut::ShowCursor)
1504            //
1505            .add_key(Key::ArrowDown)
1506            .add_output(CapturedOut::HideCursor)
1507            .add_output(CapturedOut::MoveWithinLine(-("zzzz".len() as i16)))
1508            .add_output(CapturedOut::Write("    ".to_string()))
1509            .add_output(CapturedOut::MoveWithinLine(-("    ".len() as i16)))
1510            .add_output(CapturedOut::ShowCursor)
1511            //
1512            .add_key(Key::ArrowDown)
1513            //
1514            .accept();
1515    }
1516
1517    #[test]
1518    fn test_read_line_interactive_history_navigate_and_edit() {
1519        ReadLineInteractiveTest::default()
1520            .set_prompt("? ")
1521            .add_output(CapturedOut::Write("? ".to_string()))
1522            .add_output(CapturedOut::SyncNow)
1523            //
1524            .set_history(
1525                vec!["first".to_owned(), "second".to_owned(), "third".to_owned()],
1526                vec![
1527                    "first".to_owned(),
1528                    "second".to_owned(),
1529                    "third".to_owned(),
1530                    "sec ond".to_owned(),
1531                ],
1532            )
1533            //
1534            .add_key(Key::ArrowUp)
1535            .add_output(CapturedOut::HideCursor)
1536            .add_output(CapturedOut::Write("third".to_string()))
1537            .add_output(CapturedOut::ShowCursor)
1538            //
1539            .add_key(Key::ArrowUp)
1540            .add_output(CapturedOut::HideCursor)
1541            .add_output(CapturedOut::MoveWithinLine(-5))
1542            .add_output(CapturedOut::Write("second".to_string()))
1543            .add_output(CapturedOut::ShowCursor)
1544            //
1545            .add_key(Key::ArrowLeft)
1546            .add_output(CapturedOut::MoveWithinLine(-1))
1547            // -
1548            .add_key(Key::ArrowLeft)
1549            .add_output(CapturedOut::MoveWithinLine(-1))
1550            // -
1551            .add_key(Key::ArrowLeft)
1552            .add_output(CapturedOut::MoveWithinLine(-1))
1553            // -
1554            .add_key_chars(" ")
1555            .add_output(CapturedOut::HideCursor)
1556            .add_output_bytes(" ")
1557            .add_output(CapturedOut::Write("ond".to_string()))
1558            .add_output(CapturedOut::MoveWithinLine(-3))
1559            .add_output(CapturedOut::ShowCursor)
1560            // -
1561            .set_line("sec ond")
1562            .accept();
1563    }
1564
1565    #[test]
1566    fn test_read_line_ignored_keys() {
1567        ReadLineInteractiveTest::default()
1568            .add_key_chars("not ")
1569            .add_output_bytes("not ")
1570            // -
1571            .add_key(Key::Escape)
1572            .add_key(Key::PageDown)
1573            .add_key(Key::PageUp)
1574            .add_key(Key::Tab)
1575            // -
1576            .add_key_chars("affected")
1577            .add_output_bytes("affected")
1578            // -
1579            .set_line("not affected")
1580            .accept();
1581    }
1582
1583    #[test]
1584    fn test_read_line_without_echo() {
1585        ReadLineInteractiveTest::default()
1586            .set_echo(false)
1587            .set_prompt("> ")
1588            .set_previous("pass1234")
1589            .add_output(CapturedOut::Write("> ********".to_string()))
1590            .add_output(CapturedOut::SyncNow)
1591            // -
1592            .add_key_chars("56")
1593            .add_output_bytes("**")
1594            // -
1595            .add_key(Key::ArrowLeft)
1596            .add_output(CapturedOut::MoveWithinLine(-1))
1597            // -
1598            .add_key(Key::ArrowLeft)
1599            .add_output(CapturedOut::MoveWithinLine(-1))
1600            // -
1601            .add_key(Key::Backspace)
1602            .add_output(CapturedOut::HideCursor)
1603            .add_output(CapturedOut::MoveWithinLine(-1))
1604            .add_output(CapturedOut::Write("**".to_string()))
1605            .add_output_bytes(" ")
1606            .add_output(CapturedOut::MoveWithinLine(-3))
1607            .add_output(CapturedOut::ShowCursor)
1608            // -
1609            .add_output(CapturedOut::HideCursor)
1610            .add_key_chars("7")
1611            .add_output(CapturedOut::Write("***".to_string()))
1612            .add_output(CapturedOut::MoveWithinLine(-2))
1613            .add_output(CapturedOut::ShowCursor)
1614            // -
1615            .set_line("pass123756")
1616            .accept();
1617
1618        ReadLineInteractiveTest::default()
1619            .set_echo(false)
1620            .set_prompt("> ")
1621            .set_previous("pass1234")
1622            .add_output(CapturedOut::Write("> ********".to_string()))
1623            .add_output(CapturedOut::SyncNow)
1624            // -
1625            .add_key(Key::ArrowLeft)
1626            .add_output(CapturedOut::MoveWithinLine(-1))
1627            // -
1628            .add_key(Key::ArrowLeft)
1629            .add_output(CapturedOut::MoveWithinLine(-1))
1630            // -
1631            .add_key(Key::Delete)
1632            .add_output(CapturedOut::HideCursor)
1633            .add_output(CapturedOut::Write("*".to_string()))
1634            .add_output_bytes(" ")
1635            .add_output(CapturedOut::MoveWithinLine(-2))
1636            .add_output(CapturedOut::ShowCursor)
1637            // -
1638            .set_line("pass124")
1639            .accept();
1640    }
1641
1642    #[test]
1643    fn test_read_line_secure_trivial_test() {
1644        let mut console = MockConsole::default();
1645        console.set_interactive(true);
1646        console.add_input_keys(&[Key::Char('1'), Key::Char('5'), Key::NewLine]);
1647        console.set_size_chars(CharsXY::new(15, 5));
1648        let line = block_on(read_line_secure(&mut console, "> ")).unwrap();
1649        assert_eq!("15", &line);
1650        assert_eq!(
1651            &[
1652                CapturedOut::Write("> ".to_string()),
1653                CapturedOut::SyncNow,
1654                CapturedOut::Write("*".to_string()),
1655                CapturedOut::Write("*".to_string()),
1656                CapturedOut::Print("".to_owned()),
1657            ],
1658            console.captured_out()
1659        );
1660    }
1661
1662    #[test]
1663    fn test_read_line_secure_unsupported_in_noninteractive_console() {
1664        let mut console = MockConsole::default();
1665        let err = block_on(read_line_secure(&mut console, "> ")).unwrap_err();
1666        assert!(format!("{}", err).contains("Cannot read secure"));
1667    }
1668}