From bf40860c7f369ddfe665927ea1d8560e4c23c873 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Mon, 22 Apr 2024 20:47:16 -0700 Subject: [PATCH] Fix ANSI escape codes provided in input from being stripped when rendering Fixes #248 > A recent commit (possibly 8e515d1#diff-546b6385118f60f64674170f786acf59f0ccce53d5d6ad4400409fc8363cfce1R74) has made it so that ANSI escape codes are now stripped. This makes it impossible to have colorised text inside prompt messages, e.g. if you use Confirm with a string that contains color text the color won't show up. This is a regression as this was possible with older versions (at least with 0.5.0). --- inquire/src/terminal/mod.rs | 3 + inquire/src/terminal/test.rs | 137 +++++++++++++++++++++++++++++++ inquire/src/ui/api/style.rs | 11 +++ inquire/src/ui/frame_renderer.rs | 35 +++++++- 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 inquire/src/terminal/test.rs diff --git a/inquire/src/terminal/mod.rs b/inquire/src/terminal/mod.rs index 0e15d00b..0cbf379a 100644 --- a/inquire/src/terminal/mod.rs +++ b/inquire/src/terminal/mod.rs @@ -17,6 +17,9 @@ pub mod termion; #[cfg_attr(docsrs, doc(cfg(feature = "console")))] pub mod console; +#[cfg(test)] +pub(crate) mod test; + pub type TerminalSize = Dimension; pub trait Terminal: Sized { diff --git a/inquire/src/terminal/test.rs b/inquire/src/terminal/test.rs new file mode 100644 index 00000000..dc0c7add --- /dev/null +++ b/inquire/src/terminal/test.rs @@ -0,0 +1,137 @@ +use std::{collections::VecDeque, fmt::Display}; + +use crate::ui::{Key, Styled}; + +use super::{Terminal, TerminalSize}; + +pub struct MockTerminal { + pub size: TerminalSize, + pub input: VecDeque, + pub output: VecDeque, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum MockTerminalToken { + Text(Styled), + ClearLine, + ClearUntilNewLine, + CursorHide, + CursorShow, + CursorUp(u16), + CursorDown(u16), + CursorLeft(u16), + CursorRight(u16), + CursorMoveToColumn(u16), +} + +impl From for MockTerminalToken +where + T: Display, +{ + fn from(val: T) -> Self { + MockTerminalToken::Text(Styled::new(val.to_string())) + } +} + +impl MockTerminal { + pub fn new() -> Self { + Self { + size: TerminalSize::new(80, 40), + input: VecDeque::new(), + output: VecDeque::new(), + } + } + + pub fn with_size(mut self, size: TerminalSize) -> Self { + self.size = size; + self + } + + pub fn find_and_expect_token(&mut self, token: MockTerminalToken) { + while let Some(actual) = self.output.pop_front() { + if actual == token { + return; + } + } + + panic!("Expected token not found: {:?}", token); + } +} + +impl Terminal for MockTerminal { + fn get_size(&self) -> std::io::Result { + Ok(self.size) + } + + fn write(&mut self, val: T) -> std::io::Result<()> { + let styled = Styled::new(format!("{}", val)); + let token = MockTerminalToken::Text(styled); + self.output.push_back(token); + Ok(()) + } + + fn write_styled(&mut self, val: &Styled) -> std::io::Result<()> { + let styled = Styled::new(format!("{}", val.content)).with_style_sheet(val.style); + let token = MockTerminalToken::Text(styled); + self.output.push_back(token); + Ok(()) + } + + fn clear_line(&mut self) -> std::io::Result<()> { + let token = MockTerminalToken::ClearLine; + self.output.push_back(token); + Ok(()) + } + + fn clear_until_new_line(&mut self) -> std::io::Result<()> { + let token = MockTerminalToken::ClearUntilNewLine; + self.output.push_back(token); + Ok(()) + } + + fn cursor_hide(&mut self) -> std::io::Result<()> { + let token = MockTerminalToken::CursorHide; + self.output.push_back(token); + Ok(()) + } + + fn cursor_show(&mut self) -> std::io::Result<()> { + let token = MockTerminalToken::CursorShow; + self.output.push_back(token); + Ok(()) + } + + fn cursor_up(&mut self, cnt: u16) -> std::io::Result<()> { + let token = MockTerminalToken::CursorUp(cnt); + self.output.push_back(token); + Ok(()) + } + + fn cursor_down(&mut self, cnt: u16) -> std::io::Result<()> { + let token = MockTerminalToken::CursorDown(cnt); + self.output.push_back(token); + Ok(()) + } + + fn cursor_left(&mut self, cnt: u16) -> std::io::Result<()> { + let token = MockTerminalToken::CursorLeft(cnt); + self.output.push_back(token); + Ok(()) + } + + fn cursor_right(&mut self, cnt: u16) -> std::io::Result<()> { + let token = MockTerminalToken::CursorRight(cnt); + self.output.push_back(token); + Ok(()) + } + + fn cursor_move_to_column(&mut self, idx: u16) -> std::io::Result<()> { + let token = MockTerminalToken::CursorMoveToColumn(idx); + self.output.push_back(token); + Ok(()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/inquire/src/ui/api/style.rs b/inquire/src/ui/api/style.rs index a9de81c3..42e75d88 100644 --- a/inquire/src/ui/api/style.rs +++ b/inquire/src/ui/api/style.rs @@ -216,3 +216,14 @@ where Self::new(from) } } + +impl PartialEq for Styled +where + T: PartialEq + Display, +{ + fn eq(&self, other: &Self) -> bool { + self.style == other.style && self.content == other.content + } +} + +impl Eq for Styled where T: Eq + Display {} diff --git a/inquire/src/ui/frame_renderer.rs b/inquire/src/ui/frame_renderer.rs index ea1cf07d..6544b2be 100644 --- a/inquire/src/ui/frame_renderer.rs +++ b/inquire/src/ui/frame_renderer.rs @@ -71,9 +71,10 @@ impl FrameState { let current_char = match piece { AnsiAwareChar::Char(c) => c, - AnsiAwareChar::AnsiEscapeSequence(_) => { + AnsiAwareChar::AnsiEscapeSequence(seq) => { // we don't care for escape sequences when calculating cursor position // and box size + self.current_styled.content.push_str(seq); continue; } }; @@ -438,3 +439,35 @@ where let _unused = self.terminal.flush(); } } + +#[cfg(test)] +mod test { + use crate::{ + error::InquireResult, + terminal::{test::MockTerminal, TerminalSize}, + }; + + use super::FrameRenderer; + + #[test] + fn ensure_inline_ansi_codes_are_maintained() -> InquireResult<()> { + let terminal = MockTerminal::new().with_size(TerminalSize::new(200, 200)); + let mut renderer = FrameRenderer::new(terminal)?; + + renderer.start_frame()?; + renderer.write("Hello")?; + renderer.write("World")?; + renderer.write("\n")?; + renderer.write("\x1b[1;31mWhat\x1b[0m is your name?")?; + renderer.finish_current_frame(false)?; + + let terminal = &mut renderer.terminal; + + terminal.find_and_expect_token("Hello".into()); + terminal.find_and_expect_token("World".into()); + terminal.find_and_expect_token("\n".into()); + terminal.find_and_expect_token("\x1b[1;31mWhat\x1b[0m is your name?".into()); + + Ok(()) + } +}