diff --git a/repl/src/editor.rs b/repl/src/editor.rs index 8ddf7cba..d50e6b34 100644 --- a/repl/src/editor.rs +++ b/repl/src/editor.rs @@ -148,7 +148,7 @@ impl Editor { } status.truncate(width); - console.locate(CharsXY::new(0, console_size.y - 1))?; + console.locate(CharsXY::new(0, console_size.y.saturating_sub(1)))?; console.set_color(STATUS_COLOR.0, STATUS_COLOR.1)?; console.write(&status)?; Ok(()) @@ -160,6 +160,8 @@ impl Editor { /// after calling this function, and the caller should also hide the cursor before calling this /// function. fn refresh(&self, console: &mut dyn Console, console_size: CharsXY) -> io::Result<()> { + let max_rows = usize::from(console_size.y.saturating_sub(1)); + console.set_color(TEXT_COLOR.0, TEXT_COLOR.1)?; console.clear(ClearType::All)?; self.refresh_status(console, console_size)?; @@ -168,7 +170,7 @@ impl Editor { let mut row = self.viewport_pos.line; let mut printed_rows = 0; - while row < self.content.len() && printed_rows < console_size.y - 1 { + while row < self.content.len() && printed_rows < max_rows { let line = &self.content[row]; let line_len = line.len(); if line_len > self.viewport_pos.col { @@ -213,27 +215,28 @@ impl Editor { /// Internal implementation of the interactive editor, which interacts with the `console`. async fn edit_interactively(&mut self, console: &mut dyn Console) -> io::Result<()> { - let console_size = console.size_chars()?; - if self.content.is_empty() { self.content.push(LineBuffer::default()); } let mut need_refresh = true; + let mut last_console_size = None; loop { + let console_size = console.size_chars()?; + if last_console_size != Some(console_size) { + need_refresh = true; + last_console_size = Some(console_size); + } + // The key handling below only deals with moving the insertion position within the file // but does not bother to update the viewport. Adjust it now, if necessary. - let width = usize::from(console_size.x); - let height = usize::from(console_size.y); + let width = usize::from(console_size.x).max(1); + let text_rows = usize::from(console_size.y.saturating_sub(1)).max(1); if self.file_pos.line < self.viewport_pos.line { self.viewport_pos.line = self.file_pos.line; need_refresh = true; - } else if self.file_pos.line > self.viewport_pos.line + height - 2 { - if self.file_pos.line > height - 2 { - self.viewport_pos.line = self.file_pos.line - (height - 2); - } else { - self.viewport_pos.line = 0; - } + } else if self.file_pos.line >= self.viewport_pos.line + text_rows { + self.viewport_pos.line = self.file_pos.line + 1 - text_rows; need_refresh = true; } @@ -241,7 +244,7 @@ impl Editor { self.viewport_pos.col = self.file_pos.col; need_refresh = true; } else if self.file_pos.col >= self.viewport_pos.col + width { - self.viewport_pos.col = self.file_pos.col - width + 1; + self.viewport_pos.col = self.file_pos.col + 1 - width; need_refresh = true; } @@ -357,7 +360,7 @@ impl Editor { self.file_pos.col += 1; self.insert_col = self.file_pos.col; - if cursor_pos.x < console_size.x - 1 && !need_refresh { + if usize::from(cursor_pos.x) + 1 < width && !need_refresh { console.write(ch.encode_utf8(&mut buf))?; } @@ -399,9 +402,9 @@ impl Editor { self.dirty = true; } - Key::PageDown => self.move_down(usize::from(console_size.y - 2)), + Key::PageDown => self.move_down(text_rows.saturating_sub(1)), - Key::PageUp => self.move_up(usize::from(console_size.y - 2)), + Key::PageUp => self.move_up(text_rows.saturating_sub(1)), Key::Tab => { let line = &mut self.content[self.file_pos.line]; @@ -476,6 +479,7 @@ impl Program for Editor { #[cfg(test)] mod tests { use super::*; + use endbasic_std::console::CharsXY; use endbasic_std::testutils::*; use futures_lite::future::block_on; @@ -597,6 +601,12 @@ mod tests { self } + /// Changes the console size used to compute subsequent expected output. + fn set_console_size(mut self, console_size: CharsXY) -> Self { + self.console_size = console_size; + self + } + /// Finalizes the list of expected side-effects on the console. fn build(self) -> Vec { let mut output = self.output; @@ -970,6 +980,33 @@ mod tests { ); } + #[test] + fn test_resize_height_forces_refresh_and_updates_page_movement() { + let mut console = MockConsole::default(); + console.set_interactive(true); + console.set_size_chars(yx(10, 40)); + + let mut ob = OutputBuilder::new(yx(10, 40)); + ob = ob.refresh(linecol(0, 0), &["1", "2", "3", "4", "5", "6", "7", "8", "9"], yx(0, 0)); + + console.add_input_keys(&[Key::PageDown]); + ob = ob.quick_refresh(linecol(8, 0), yx(8, 0)); + + console.add_resize_event(yx(5, 40)); + console.add_input_keys(&[Key::Unknown]); + ob = ob.set_console_size(yx(5, 40)); + ob = ob.refresh(linecol(8, 0), &["6", "7", "8", "9"], yx(3, 0)); + + console.add_input_keys(&[Key::PageDown, Key::Escape]); + ob = ob.refresh(linecol(11, 0), &["9", "10", "11", "12"], yx(3, 0)); + + let mut editor = Editor::default(); + editor.load(Some(TEST_FILENAME), "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n"); + block_on(editor.edit(&mut console)).unwrap(); + + assert_eq!(ob.build(), console.captured_out()); + } + #[test] fn test_tab_append() { let mut cb = MockConsole::default(); @@ -1443,6 +1480,32 @@ mod tests { run_editor("ab\n\nxyz\n", "ab\n12345678901234567890123456789012345678ABC\nxyz\n", cb, ob); } + #[test] + fn test_resize_width_forces_refresh_and_clamps_cursor() { + let mut console = MockConsole::default(); + console.set_interactive(true); + console.set_size_chars(yx(10, 40)); + + let mut ob = OutputBuilder::new(yx(10, 40)); + ob = ob.refresh(linecol(0, 0), &["1234567890123456789012345678901234567890"], yx(0, 0)); + + console.add_input_keys(&[Key::End]); + ob = ob.refresh(linecol(0, 40), &["234567890123456789012345678901234567890"], yx(0, 39)); + + console.add_resize_event(yx(10, 25)); + console.add_input_keys(&[Key::Unknown]); + ob = ob.set_console_size(yx(10, 25)); + ob = ob.refresh(linecol(0, 40), &["789012345678901234567890"], yx(0, 24)); + + console.add_input_keys(&[Key::Escape]); + + let mut editor = Editor::default(); + editor.load(Some(TEST_FILENAME), "1234567890123456789012345678901234567890\n"); + block_on(editor.edit(&mut console)).unwrap(); + + assert_eq!(ob.build(), console.captured_out()); + } + #[test] fn test_vertical_scrolling() { let mut cb = MockConsole::default(); diff --git a/std/src/console/format.rs b/std/src/console/format.rs index 971d4cf4..54b87847 100644 --- a/std/src/console/format.rs +++ b/std/src/console/format.rs @@ -129,7 +129,7 @@ pub(crate) async fn refill_and_page, P: IntoIterator>( paragraphs: P, indent: &str, ) -> io::Result<()> { - for line in refill_many(paragraphs, indent, usize::from(pager.columns())) { + for line in refill_many(paragraphs, indent, usize::from(pager.columns()?)) { pager.print(&line).await?; } Ok(()) diff --git a/std/src/console/pager.rs b/std/src/console/pager.rs index 74bffc48..ebeab33a 100644 --- a/std/src/console/pager.rs +++ b/std/src/console/pager.rs @@ -15,12 +15,15 @@ //! A simple paginator for commands that produce long outputs -use super::{CharsXY, Console, Key, is_narrow}; +use super::{Console, Key, is_narrow}; use crate::Yielder; use std::cell::RefCell; use std::io; use std::rc::Rc; +#[cfg(test)] +use super::CharsXY; + /// Message to print on a narrow console when the screen is full. const MORE_MESSAGE_NARROW: &str = " << More >> "; @@ -35,12 +38,6 @@ pub(crate) struct Pager<'a> { /// Optional hook to yield output production back to the host. yielder: Option>>, - /// Cached size of the console. - size: CharsXY, - - /// The message to print when the screen is full. - more_message: &'static str, - /// Number of columns printed so far on the current line. cur_columns: usize, @@ -54,14 +51,17 @@ impl<'a> Pager<'a> { console: &'a mut dyn Console, yielder: Option>>, ) -> io::Result { - let size = console.size_chars()?; - let more_message = if is_narrow(console) { MORE_MESSAGE_NARROW } else { MORE_MESSAGE_WIDE }; - Ok(Self { console, yielder, size, more_message, cur_columns: 0, cur_lines: 0 }) + Ok(Self { console, yielder, cur_columns: 0, cur_lines: 0 }) } /// Returns the maximum number of columns of the console. - pub(crate) fn columns(&self) -> u16 { - self.size.x + pub(crate) fn columns(&self) -> io::Result { + Ok(self.console.size_chars()?.x) + } + + /// Returns the message to print when the screen is full. + fn more_message(&self) -> &'static str { + if is_narrow(self.console) { MORE_MESSAGE_NARROW } else { MORE_MESSAGE_WIDE } } /// Gets the console's current foreground and background colors. @@ -88,15 +88,16 @@ impl<'a> Pager<'a> { yielder.yield_now().await; } + let size = self.console.size_chars()?; self.cur_columns += text.len(); - self.cur_lines += (self.cur_columns / usize::from(self.size.x)) + 1; + self.cur_lines += (self.cur_columns / usize::from(size.x)) + 1; - if self.cur_lines >= usize::from(self.size.y) - 1 { + if self.cur_lines >= usize::from(size.y) - 1 { let previous_color = self.console.color(); if previous_color != (None, None) { self.console.set_color(None, None)?; } - self.console.print(self.more_message)?; + self.console.print(self.more_message())?; if previous_color != (None, None) { self.console.set_color(previous_color.0, previous_color.1)?; } @@ -359,4 +360,64 @@ mod tests { cb.captured_out() ); } + + #[tokio::test] + async fn test_columns_follow_resize() { + let mut cb = MockConsole::default(); + cb.set_interactive(true); + cb.set_size_chars(CharsXY::new(10, 3)); + let size = cb.size_chars_handle(); + + let pager = Pager::new(&mut cb, None).unwrap(); + assert_eq!(10, pager.columns().unwrap()); + + *size.borrow_mut() = CharsXY::new(20, 3); + assert_eq!(20, pager.columns().unwrap()); + } + + #[tokio::test] + async fn test_paging_message_follows_resize() { + let mut cb = MockConsole::default(); + cb.set_interactive(true); + cb.set_size_chars(CharsXY::new(10, 3)); + cb.add_input_keys(&[Key::NewLine]); + let size = cb.size_chars_handle(); + + let mut pager = Pager::new(&mut cb, None).unwrap(); + pager.print("line 1").await.unwrap(); + *size.borrow_mut() = CharsXY::new(60, 3); + pager.print("line 2").await.unwrap(); + + assert_eq!( + [ + CapturedOut::Print("line 1".to_owned()), + CapturedOut::Print("line 2".to_owned()), + CapturedOut::Print(MORE_MESSAGE_WIDE.to_owned()), + ], + cb.captured_out() + ); + } + + #[tokio::test] + async fn test_paging_height_follows_resize() { + let mut cb = MockConsole::default(); + cb.set_interactive(true); + cb.set_size_chars(CharsXY::new(10, 5)); + cb.add_input_keys(&[Key::NewLine]); + let size = cb.size_chars_handle(); + + let mut pager = Pager::new(&mut cb, None).unwrap(); + pager.print("line 1").await.unwrap(); + *size.borrow_mut() = CharsXY::new(10, 3); + pager.print("line 2").await.unwrap(); + + assert_eq!( + [ + CapturedOut::Print("line 1".to_owned()), + CapturedOut::Print("line 2".to_owned()), + CapturedOut::Print(MORE_MESSAGE_NARROW.to_owned()), + ], + cb.captured_out() + ); + } } diff --git a/std/src/console/readline.rs b/std/src/console/readline.rs index 1bb000a3..df3eb9d1 100644 --- a/std/src/console/readline.rs +++ b/std/src/console/readline.rs @@ -16,12 +16,52 @@ //! Interactive line reader. use crate::console::{Console, Key, LineBuffer}; -use std::borrow::Cow; use std::io; /// Character to print when typing a secure string. const SECURE_CHAR: &str = "*"; +struct PromptInfo { + prompt: String, + prompt_len: usize, + max_line_len: usize, +} + +struct LineState { + prompt_len: usize, + pos: usize, + line_len: usize, +} + +/// Returns the current prompt information based on the console width. +fn prompt_info(prompt: &str, console_width: usize) -> PromptInfo { + let prompt = if prompt.len() >= console_width { + if console_width >= 5 { + format!("{}...", &prompt[0..console_width - 5]) + } else { + String::new() + } + } else { + prompt.to_owned() + }; + let prompt_len = prompt.len(); + let max_line_len = console_width.saturating_sub(prompt_len).saturating_sub(1); + PromptInfo { prompt, prompt_len, max_line_len } +} + +/// Truncates `line` to fit within `max_line_len` and clamps `pos` within the resulting line. +fn fit_line(line: &mut LineBuffer, pos: &mut usize, max_line_len: usize) { + if line.len() > max_line_len { + line.split_off(max_line_len); + } + *pos = (*pos).min(line.len()); +} + +/// Returns the text to display for `line`. +fn display_line(line: &LineBuffer, echo: bool) -> String { + if echo { line.to_string() } else { SECURE_CHAR.repeat(line.len()) } +} + /// Refreshes the current input line to display `line` assuming that the cursor is currently /// offset by `pos` characters from the beginning of the input and that the previous line was /// `clear_len` characters long. @@ -30,13 +70,15 @@ fn update_line( pos: usize, clear_len: usize, line: &LineBuffer, + echo: bool, ) -> io::Result<()> { console.hide_cursor()?; if pos > 0 { console.move_within_line(-(pos as i16))?; } - if !line.is_empty() { - console.write(&line.to_string())?; + let line_text = display_line(line, echo); + if !line_text.is_empty() { + console.write(&line_text)?; } let line_len = line.len(); if line_len < clear_len { @@ -47,6 +89,41 @@ fn update_line( console.show_cursor() } +/// Refreshes the current input line, including the prompt. +fn redraw_line( + console: &mut dyn Console, + old_state: LineState, + prompt: &str, + line: &LineBuffer, + pos: usize, + echo: bool, +) -> io::Result<()> { + console.hide_cursor()?; + let old_abs_pos = old_state.prompt_len + old_state.pos; + if old_abs_pos > 0 { + console.move_within_line(-(old_abs_pos as i16))?; + } + if !prompt.is_empty() { + console.write(prompt)?; + } + let line_text = display_line(line, echo); + if !line_text.is_empty() { + console.write(&line_text)?; + } + let old_total_len = old_state.prompt_len + old_state.line_len; + let new_total_len = prompt.len() + line.len(); + if new_total_len < old_total_len { + let diff = old_total_len - new_total_len; + console.write(&" ".repeat(diff))?; + console.move_within_line(-(diff as i16))?; + } + let line_len = line.len(); + if pos < line_len { + console.move_within_line(-((line_len - pos) as i16))?; + } + console.show_cursor() +} + /// Reads a line of text interactively from the console, using the given `prompt` and pre-filling /// the input with `previous`. If `history` is not `None`, then this appends the newly entered line /// into the history and allows navigating through it. @@ -57,41 +134,21 @@ async fn read_line_interactive( mut history: Option<&mut Vec>, echo: bool, ) -> io::Result { - let console_width = { - let console_size = console.size_chars()?; - usize::from(console_size.x) - }; - - let mut prompt = Cow::from(prompt); - let mut prompt_len = prompt.len(); - if prompt_len >= console_width { - if console_width >= 5 { - prompt = Cow::from(format!("{}...", &prompt[0..console_width - 5])); - } else { - prompt = Cow::from(""); - } - prompt_len = prompt.len(); - } - + let mut current_prompt = prompt_info(prompt, usize::from(console.size_chars()?.x)); let mut line = LineBuffer::from(previous); - if !prompt.is_empty() || !line.is_empty() { + let mut pos = line.len(); + fit_line(&mut line, &mut pos, current_prompt.max_line_len); + if !current_prompt.prompt.is_empty() || !line.is_empty() { if echo { - console.write(&format!("{}{}", prompt, line))?; + console.write(&format!("{}{}", current_prompt.prompt, line))?; } else { - console.write(&format!("{}{}", prompt, "*".repeat(line.len())))?; + console.write(&format!("{}{}", current_prompt.prompt, "*".repeat(line.len())))?; } console.sync_now()?; } - let width = { - // Assumes that the prompt was printed at column 0. If that was not the case, line length - // calculation does not work. - console_width - prompt_len - }; - // Insertion position *within* the line, without accounting for the prompt. // TODO(zenria): Handle UTF-8 graphemes. - let mut pos = line.len(); let mut history_pos = match history.as_mut() { Some(history) => { @@ -102,6 +159,17 @@ async fn read_line_interactive( }; loop { + let new_prompt_info = prompt_info(prompt, usize::from(console.size_chars()?.x)); + if current_prompt.prompt != new_prompt_info.prompt + || current_prompt.max_line_len != new_prompt_info.max_line_len + { + let old_prompt_len = current_prompt.prompt_len; + let old_state = LineState { prompt_len: old_prompt_len, pos, line_len: line.len() }; + fit_line(&mut line, &mut pos, new_prompt_info.max_line_len); + redraw_line(console, old_state, &new_prompt_info.prompt, &line, pos, echo)?; + current_prompt = new_prompt_info; + } + match console.read_key().await? { Key::ArrowUp => { if let Some(history) = history.as_mut() { @@ -109,14 +177,15 @@ async fn read_line_interactive( continue; } - let clear_len = line.len(); + let old_line_len = line.len(); + let old_pos = pos; history[history_pos] = line.into_inner(); history_pos -= 1; line = LineBuffer::from(&history[history_pos]); - - update_line(console, pos, clear_len, &line)?; - + pos = line.len(); + fit_line(&mut line, &mut pos, current_prompt.max_line_len); + update_line(console, old_pos, old_line_len, &line, echo)?; pos = line.len(); } } @@ -127,14 +196,15 @@ async fn read_line_interactive( continue; } - let clear_len = line.len(); + let old_line_len = line.len(); + let old_pos = pos; history[history_pos] = line.to_string(); history_pos += 1; line = LineBuffer::from(&history[history_pos]); - - update_line(console, pos, clear_len, &line)?; - + pos = line.len(); + fit_line(&mut line, &mut pos, current_prompt.max_line_len); + update_line(console, old_pos, old_line_len, &line, echo)?; pos = line.len(); } } @@ -183,8 +253,7 @@ async fn read_line_interactive( Key::Char(ch) => { let line_len = line.len(); - debug_assert!(line_len < width); - if line_len == width - 1 { + if line_len >= current_prompt.max_line_len { // TODO(jmmv): Implement support for lines that exceed the width of the input // field (the width of the screen). continue; @@ -869,6 +938,60 @@ mod tests { .accept(); } + #[test] + fn test_read_line_interactive_resize_truncates_existing_line() { + let mut console = MockConsole::default(); + console.set_interactive(true); + console.set_size_chars(CharsXY::new(15, 5)); + console.add_resize_event(CharsXY::new(10, 5)); + console.add_input_keys(&[Key::Unknown, Key::NewLine]); + + let line = block_on(read_line_interactive(&mut console, "", "12345678901234", None, true)) + .unwrap(); + assert_eq!("123456789", &line); + assert_eq!( + &[ + CapturedOut::Write("12345678901234".to_string()), + CapturedOut::SyncNow, + CapturedOut::HideCursor, + CapturedOut::MoveWithinLine(-14), + CapturedOut::Write("123456789".to_string()), + CapturedOut::Write(" ".to_string()), + CapturedOut::MoveWithinLine(-5), + CapturedOut::ShowCursor, + CapturedOut::Print("".to_owned()), + ], + console.captured_out() + ); + } + + #[test] + fn test_read_line_interactive_resize_truncates_prompt_and_line() { + let mut console = MockConsole::default(); + console.set_interactive(true); + console.set_size_chars(CharsXY::new(15, 5)); + console.add_resize_event(CharsXY::new(3, 5)); + console.add_input_keys(&[Key::Unknown, Key::NewLine]); + + let line = + block_on(read_line_interactive(&mut console, "Ready> ", "1234", None, true)).unwrap(); + assert_eq!("12", &line); + assert_eq!( + &[ + CapturedOut::Write("Ready> 1234".to_string()), + CapturedOut::SyncNow, + CapturedOut::HideCursor, + CapturedOut::MoveWithinLine(-11), + CapturedOut::Write("12".to_string()), + CapturedOut::Write(" ".to_string()), + CapturedOut::MoveWithinLine(-9), + CapturedOut::ShowCursor, + CapturedOut::Print("".to_owned()), + ], + console.captured_out() + ); + } + #[test] fn test_read_line_interactive_history_not_enabled_by_default() { ReadLineInteractiveTest::default().add_key(Key::ArrowUp).accept(); @@ -925,7 +1048,7 @@ mod tests { // .set_history( vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()], - vec!["first".to_owned(), "long second line".to_owned(), "last".to_owned()], + vec!["first".to_owned(), "long second ".to_owned(), "last".to_owned()], ) // .add_key(Key::ArrowUp) @@ -936,15 +1059,15 @@ mod tests { .add_key(Key::ArrowUp) .add_output(CapturedOut::HideCursor) .add_output(CapturedOut::MoveWithinLine(-("last".len() as i16))) - .add_output(CapturedOut::Write("long second line".to_string())) + .add_output(CapturedOut::Write("long second ".to_string())) .add_output(CapturedOut::ShowCursor) // .add_key(Key::ArrowUp) .add_output(CapturedOut::HideCursor) - .add_output(CapturedOut::MoveWithinLine(-("long second line".len() as i16))) + .add_output(CapturedOut::MoveWithinLine(-("long second ".len() as i16))) .add_output(CapturedOut::Write("first".to_string())) - .add_output(CapturedOut::Write(" ".to_string())) - .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16))) + .add_output(CapturedOut::Write(" ".to_string())) + .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16))) .add_output(CapturedOut::ShowCursor) // .add_key(Key::ArrowUp) @@ -952,15 +1075,15 @@ mod tests { .add_key(Key::ArrowDown) .add_output(CapturedOut::HideCursor) .add_output(CapturedOut::MoveWithinLine(-("first".len() as i16))) - .add_output(CapturedOut::Write("long second line".to_string())) + .add_output(CapturedOut::Write("long second ".to_string())) .add_output(CapturedOut::ShowCursor) // .add_key(Key::ArrowDown) .add_output(CapturedOut::HideCursor) - .add_output(CapturedOut::MoveWithinLine(-("long second line".len() as i16))) + .add_output(CapturedOut::MoveWithinLine(-("long second ".len() as i16))) .add_output(CapturedOut::Write("last".to_string())) - .add_output(CapturedOut::Write(" ".to_string())) - .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16))) + .add_output(CapturedOut::Write(" ".to_string())) + .add_output(CapturedOut::MoveWithinLine(-(" ".len() as i16))) .add_output(CapturedOut::ShowCursor) // .add_key(Key::ArrowDown) diff --git a/std/src/testutils.rs b/std/src/testutils.rs index 9405da7a..a607daad 100644 --- a/std/src/testutils.rs +++ b/std/src/testutils.rs @@ -108,16 +108,26 @@ pub enum CapturedOut { SetSync(bool), } +/// Input events to feed to the mock console. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum InputEvent { + /// Represents a regular key press. + Key(Key), + + /// Represents a resize of the text console. + ResizeChars(CharsXY), +} + /// A console that supplies golden input and captures all output. pub struct MockConsole { - /// Sequence of keys to yield on `read_key` calls. - golden_in: VecDeque, + /// Sequence of input events to yield on `read_key` calls. + golden_in: VecDeque, /// Sequence of all messages printed. captured_out: Vec, /// The size of the mock text console. - size_chars: CharsXY, + size_chars: Rc>, /// The size of the mock graphical console. size_pixels: Option, @@ -141,7 +151,7 @@ impl MockConsole { Self { golden_in: VecDeque::new(), captured_out: vec![], - size_chars: CharsXY::new(u16::MAX, u16::MAX), + size_chars: Rc::from(RefCell::from(CharsXY::new(u16::MAX, u16::MAX))), size_pixels: None, interactive: false, signals_tx, @@ -155,16 +165,21 @@ impl MockConsole { pub fn add_input_chars(&mut self, s: &str) { for ch in s.chars() { match ch { - '\n' => self.golden_in.push_back(Key::NewLine), - '\r' => self.golden_in.push_back(Key::CarriageReturn), - ch => self.golden_in.push_back(Key::Char(ch)), + '\n' => self.golden_in.push_back(InputEvent::Key(Key::NewLine)), + '\r' => self.golden_in.push_back(InputEvent::Key(Key::CarriageReturn)), + ch => self.golden_in.push_back(InputEvent::Key(Key::Char(ch))), } } } /// Adds a bunch of keys as golden input. pub fn add_input_keys(&mut self, keys: &[Key]) { - self.golden_in.extend(keys.iter().cloned()); + self.golden_in.extend(keys.iter().cloned().map(InputEvent::Key)); + } + + /// Adds a resize of the text console as golden input. + pub fn add_resize_event(&mut self, size: CharsXY) { + self.golden_in.push_back(InputEvent::ResizeChars(size)); } /// Obtains a reference to the captured output. @@ -182,7 +197,12 @@ impl MockConsole { /// Sets the size of the mock text console. pub fn set_size_chars(&mut self, size: CharsXY) { - self.size_chars = size; + *self.size_chars.borrow_mut() = size; + } + + /// Returns a shared handle to the size of the mock text console. + pub fn size_chars_handle(&self) -> Rc> { + self.size_chars.clone() } /// Sets the size of the mock graphical console. @@ -194,13 +214,24 @@ impl MockConsole { pub fn set_interactive(&mut self, interactive: bool) { self.interactive = interactive; } + + /// Consumes queued resize events and returns the next available key, if any. + fn pop_input_key(&mut self) -> Option { + loop { + match self.golden_in.pop_front() { + Some(InputEvent::Key(key)) => return Some(key), + Some(InputEvent::ResizeChars(size)) => *self.size_chars.borrow_mut() = size, + None => return None, + } + } + } } impl Drop for MockConsole { fn drop(&mut self) { assert!( self.golden_in.is_empty(), - "Not all golden input chars were consumed; {} left", + "Not all golden input events were consumed; {} left", self.golden_in.len() ); } @@ -247,8 +278,9 @@ impl Console for MockConsole { } fn locate(&mut self, pos: CharsXY) -> io::Result<()> { - assert!(pos.x < self.size_chars.x); - assert!(pos.y < self.size_chars.y); + let size_chars = *self.size_chars.borrow(); + assert!(pos.x < size_chars.x); + assert!(pos.y < size_chars.y); self.captured_out.push(CapturedOut::Locate(pos)); Ok(()) } @@ -266,7 +298,7 @@ impl Console for MockConsole { } async fn poll_key(&mut self) -> io::Result> { - match self.golden_in.pop_front() { + match self.pop_input_key() { Some(ch) => { if ch == Key::Interrupt && let Some(signals_tx) = &self.signals_tx @@ -280,7 +312,7 @@ impl Console for MockConsole { } async fn read_key(&mut self) -> io::Result { - match self.golden_in.pop_front() { + match self.pop_input_key() { Some(ch) => { if ch == Key::Interrupt && let Some(signals_tx) = &self.signals_tx @@ -299,7 +331,7 @@ impl Console for MockConsole { } fn size_chars(&self) -> io::Result { - Ok(self.size_chars) + Ok(*self.size_chars.borrow()) } fn size_pixels(&self) -> io::Result {