diff --git a/CHANGELOG.md b/CHANGELOG.md index 5326c32fd..cb1c3cfd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 with brackets. To disable, set the new `ui.unique-prefixes` option to `none` * `jj print` was renamed to `jj cat`. `jj print` remains as an alias. + +* In content that goes to the terminal, the ANSI escape byte (0x1b) is replaced + by a "␛" character. That prevents them from interfering with the ANSI escapes + jj itself writes. ### Fixed bugs diff --git a/src/formatter.rs b/src/formatter.rs index e3c12b4df..9ad91059e 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -93,14 +93,17 @@ pub struct FormatterFactory { #[derive(Clone, Debug)] enum FormatterFactoryKind { PlainText, + Sanitized, Color { rules: Arc }, } impl FormatterFactory { - pub fn prepare(config: &config::Config, color: bool) -> Self { + pub fn prepare(config: &config::Config, color: bool, sanitized: bool) -> Self { let kind = if color { let rules = Arc::new(rules_from_config(config)); FormatterFactoryKind::Color { rules } + } else if sanitized { + FormatterFactoryKind::Sanitized } else { FormatterFactoryKind::PlainText }; @@ -113,6 +116,7 @@ impl FormatterFactory { ) -> Box { match &self.kind { FormatterFactoryKind::PlainText => Box::new(PlainTextFormatter::new(output)), + FormatterFactoryKind::Sanitized => Box::new(SanitizingFormatter::new(output)), FormatterFactoryKind::Color { rules } => { Box::new(ColorFormatter::new(output, rules.clone())) } @@ -154,6 +158,41 @@ impl Formatter for PlainTextFormatter { } } +pub struct SanitizingFormatter { + output: W, +} + +impl SanitizingFormatter { + pub fn new(output: W) -> SanitizingFormatter { + Self { output } + } +} + +impl Write for SanitizingFormatter { + fn write(&mut self, data: &[u8]) -> Result { + write_sanitized(&mut self.output, data)?; + Ok(data.len()) + } + + fn flush(&mut self) -> Result<(), Error> { + self.output.flush() + } +} + +impl Formatter for SanitizingFormatter { + fn raw(&mut self) -> &mut dyn Write { + &mut self.output + } + + fn push_label(&mut self, _label: &str) -> io::Result<()> { + Ok(()) + } + + fn pop_label(&mut self) -> io::Result<()> { + Ok(()) + } +} + #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Style { pub fg_color: Option, @@ -451,6 +490,26 @@ mod tests { insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"hello"); } + #[test] + fn test_plaintext_formatter_ansi_codes_in_text() { + // Test that ANSI codes in the input text are NOT escaped. + let mut output: Vec = vec![]; + let mut formatter = PlainTextFormatter::new(&mut output); + formatter.write_str("\x1b[1mactually bold\x1b[0m").unwrap(); + insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"actually bold"); + } + + #[test] + fn test_sanitizing_formatter_ansi_codes_in_text() { + // Test that ANSI codes in the input text are escaped. + let mut output: Vec = vec![]; + let mut formatter = SanitizingFormatter::new(&mut output); + formatter + .write_str("\x1b[1mnot actually bold\x1b[0m") + .unwrap(); + insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"␛[1mnot actually bold␛[0m"); + } + #[test] fn test_color_formatter_color_codes() { // Test the color code for each color. diff --git a/src/ui.rs b/src/ui.rs index d347c9a87..7b03141c3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -109,8 +109,11 @@ fn pager_setting(config: &config::Config) -> FullCommandArgs { impl Ui { pub fn with_config(config: &config::Config) -> Ui { let color = use_color(color_setting(config)); + // Sanitize ANSI escape codes if we're printing to a terminal. Doesn't affect + // ANSI escape codes that originate from the formatter itself. + let sanitize = io::stdout().is_tty(); + let formatter_factory = FormatterFactory::prepare(config, color, sanitize); let progress_indicator = progress_indicator_setting(config); - let formatter_factory = FormatterFactory::prepare(config, color); Ui { color, formatter_factory, @@ -125,7 +128,8 @@ impl Ui { self.color = use_color(color_setting(config)); self.pager_cmd = pager_setting(config); self.progress_indicator = progress_indicator_setting(config); - self.formatter_factory = FormatterFactory::prepare(config, self.color); + let sanitize = io::stdout().is_tty(); + self.formatter_factory = FormatterFactory::prepare(config, self.color, sanitize); } /// Sets the pagination value.