From db2ab952fe5d63aedf2173c7e069c1bac195cb5c Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 9 Mar 2026 11:32:42 -0700 Subject: [PATCH 1/7] fix(logger): strip ansi escapes before box layout --- .../src/logger/loggers/std_out_logger.dart | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart b/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart index 60fbe48..7ff48c5 100644 --- a/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart +++ b/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart @@ -159,9 +159,12 @@ class StdOutLogger extends Logger { if (type is BoxLogType) { message = _formatAsBox( wrapColumn: wrapTextColumn ?? _defaultColumnWrap, - message: message, + message: _stripAnsiCodes(message), title: type.title, ); + if (ansiSupported) { + message = _styleByLevel(message, logLevel); + } } else if (type is TextLogType) { switch (type.style) { case TextLogStyle.command: @@ -237,6 +240,21 @@ class StdOutLogger extends Logger { } } +String _styleByLevel(final String message, final LogLevel level) { + return switch (level) { + LogLevel.debug => AnsiStyle.darkGray.wrap(message), + LogLevel.info => message, + LogLevel.warning => AnsiStyle.yellow.wrap(message), + LogLevel.error => AnsiStyle.red.wrap(message), + LogLevel.nothing => message, + }; +} + +String _stripAnsiCodes(final String input) { + final ansiRegex = RegExp(r'\x1B\[[0-9;]*m'); + return input.replaceAll(ansiRegex, ''); +} + /// wrap text based on column width String _wrapText(final String text, final int columnWidth) { final textLines = text.split('\n'); From 56df24b130666a847a9d1d50e2e4511b7910c516 Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 9 Mar 2026 11:43:42 -0700 Subject: [PATCH 2/7] refactor(logger): reuse _styleByLevel in log --- .../cli_tools/lib/src/logger/loggers/std_out_logger.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart b/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart index 7ff48c5..a0c0b65 100644 --- a/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart +++ b/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart @@ -94,13 +94,7 @@ class StdOutLogger extends Logger { final LogType type = TextLogType.normal, }) { if (ansiSupported) { - final ansiMessage = switch (level) { - LogLevel.debug => AnsiStyle.darkGray.wrap(message), - LogLevel.info => message, - LogLevel.warning => AnsiStyle.yellow.wrap(message), - LogLevel.error => AnsiStyle.red.wrap(message), - LogLevel.nothing => message, - }; + final ansiMessage = _styleByLevel(message, level); _log(ansiMessage, level, newParagraph, type); } else { From 54dc621cfd4dc01483ec55d4ef4993d5aa35f7e9 Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 9 Mar 2026 11:51:46 -0700 Subject: [PATCH 3/7] fix(logger): strip ANSI from box titles before width calc --- packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart b/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart index a0c0b65..adc65e2 100644 --- a/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart +++ b/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart @@ -154,7 +154,7 @@ class StdOutLogger extends Logger { message = _formatAsBox( wrapColumn: wrapTextColumn ?? _defaultColumnWrap, message: _stripAnsiCodes(message), - title: type.title, + title: type.title != null ? _stripAnsiCodes(type.title!) : null, ); if (ansiSupported) { message = _styleByLevel(message, logLevel); From a479bc308ab6d201bcf3c6d6dce57365b10e16de Mon Sep 17 00:00:00 2001 From: Sergio Date: Tue, 10 Mar 2026 01:18:00 -0700 Subject: [PATCH 4/7] test(logger): add box log coverage for plain and ANSI-styled messages --- .../cli_tools/test/std_out_logger_test.dart | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/cli_tools/test/std_out_logger_test.dart b/packages/cli_tools/test/std_out_logger_test.dart index d30f81f..84dc4c7 100644 --- a/packages/cli_tools/test/std_out_logger_test.dart +++ b/packages/cli_tools/test/std_out_logger_test.dart @@ -137,4 +137,45 @@ void main() { expect(stderr.output, 'ERROR: error message\n'); }); }); + + group('Given a StdOutLogger logging BoxLogType messages', () { + final logger = StdOutLogger(LogLevel.debug); + + test( + 'when logging boxed plain text ' + 'then it renders the expected box framing', () async { + final (:stdout, :stderr, :stdin) = await collectOutput( + () => logger.info('hello world', type: BoxLogType(title: 'title')), + ); + + expect( + stdout.output, + '┌─ title ─────┐\n' + '│ hello world │\n' + '└─────────────┘\n', + ); + expect(stderr.output, ''); + }); + + test( + 'when logging boxed ANSI-styled text ' + 'then ANSI codes are stripped before width calculation', () async { + final (:stdout, :stderr, :stdin) = await collectOutput( + () => logger.warning( + '\x1B[33mwarning\x1B[0m', + type: BoxLogType(title: '\x1B[31mwarn\x1B[0m'), + ), + ); + + expect(stdout.output.contains('\x1B['), isFalse); + expect( + stdout.output, + '┌─ warn ──┐\n' + '│ warning │\n' + '└─────────┘\n', + ); + expect(stderr.output, ''); + }); + }); } + From 99ab33efcfa7995533e5f24050f1b3228bab5833 Mon Sep 17 00:00:00 2001 From: Sergio Date: Tue, 10 Mar 2026 02:17:20 -0700 Subject: [PATCH 5/7] fix(logger): strip non-SGR ANSI control sequences in box width calc --- .../src/logger/loggers/std_out_logger.dart | 8 ++++++- .../cli_tools/test/std_out_logger_test.dart | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart b/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart index adc65e2..334a12e 100644 --- a/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart +++ b/packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart @@ -245,7 +245,13 @@ String _styleByLevel(final String message, final LogLevel level) { } String _stripAnsiCodes(final String input) { - final ansiRegex = RegExp(r'\x1B\[[0-9;]*m'); + // Matches ANSI/VT escape sequences including: + // - C1 control chars introduced by ESC + // - CSI sequences (ESC [ ... final-byte) + // - OSC sequences (ESC ] ... BEL or ESC \\) + final ansiRegex = RegExp( + r'\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])', + ); return input.replaceAll(ansiRegex, ''); } diff --git a/packages/cli_tools/test/std_out_logger_test.dart b/packages/cli_tools/test/std_out_logger_test.dart index 84dc4c7..f6f8ce1 100644 --- a/packages/cli_tools/test/std_out_logger_test.dart +++ b/packages/cli_tools/test/std_out_logger_test.dart @@ -176,6 +176,27 @@ void main() { ); expect(stderr.output, ''); }); + + test( + 'when logging boxed text containing non-SGR ANSI sequences ' + 'then control sequences are stripped before width calculation', + () async { + final (:stdout, :stderr, :stdin) = await collectOutput( + () => logger.warning( + '\x1B]8;;https://example.com\x07click\x1B]8;;\x07', + type: BoxLogType(title: '\x1B[2Kwarn\x1B[0G'), + ), + ); + + expect(stdout.output.contains('\x1B'), isFalse); + expect( + stdout.output, + '┌─ warn ─┐\n' + '│ click │\n' + '└───────┘\n', + ); + expect(stderr.output, ''); + }); }); } From 94e69c0c9f265c4d5be1396f87813c687f007139 Mon Sep 17 00:00:00 2001 From: Sergio Date: Tue, 10 Mar 2026 02:29:05 -0700 Subject: [PATCH 6/7] test(logger): exercise ANSI-enabled box rendering path --- .../cli_tools/test/std_out_logger_test.dart | 20 +++++++++++++++---- .../cli_tools/test/test_utils/io_helper.dart | 3 ++- .../test/test_utils/mock_stdout.dart | 5 ++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/cli_tools/test/std_out_logger_test.dart b/packages/cli_tools/test/std_out_logger_test.dart index f6f8ce1..88b5351 100644 --- a/packages/cli_tools/test/std_out_logger_test.dart +++ b/packages/cli_tools/test/std_out_logger_test.dart @@ -165,11 +165,17 @@ void main() { '\x1B[33mwarning\x1B[0m', type: BoxLogType(title: '\x1B[31mwarn\x1B[0m'), ), + ansiSupported: true, ); - expect(stdout.output.contains('\x1B['), isFalse); + final stripped = stdout.output.replaceAll( + RegExp(r'\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\\\)|\x1B[@-Z\\\\-_]'), + '', + ); + + expect(stdout.output.contains('\x1B['), isTrue); expect( - stdout.output, + stripped, '┌─ warn ──┐\n' '│ warning │\n' '└─────────┘\n', @@ -186,11 +192,17 @@ void main() { '\x1B]8;;https://example.com\x07click\x1B]8;;\x07', type: BoxLogType(title: '\x1B[2Kwarn\x1B[0G'), ), + ansiSupported: true, ); - expect(stdout.output.contains('\x1B'), isFalse); + final stripped = stdout.output.replaceAll( + RegExp(r'\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\\\)|\x1B[@-Z\\\\-_]'), + '', + ); + + expect(stdout.output.contains('\x1B'), isTrue); expect( - stdout.output, + stripped, '┌─ warn ─┐\n' '│ click │\n' '└───────┘\n', diff --git a/packages/cli_tools/test/test_utils/io_helper.dart b/packages/cli_tools/test/test_utils/io_helper.dart index 453cf5e..1f6ff80 100644 --- a/packages/cli_tools/test/test_utils/io_helper.dart +++ b/packages/cli_tools/test/test_utils/io_helper.dart @@ -9,8 +9,9 @@ Future<({MockStdout stdout, MockStdout stderr, MockStdin stdin})> final FutureOr Function() runner, { final List stdinLines = const [], final List keyInputs = const [], + final bool ansiSupported = false, }) async { - final standardOut = MockStdout(); + final standardOut = MockStdout(ansiSupported: ansiSupported); final standardError = MockStdout(); final standardIn = MockStdin(textInputs: stdinLines, keyInputs: keyInputs); diff --git a/packages/cli_tools/test/test_utils/mock_stdout.dart b/packages/cli_tools/test/test_utils/mock_stdout.dart index 326901b..8a13e31 100644 --- a/packages/cli_tools/test/test_utils/mock_stdout.dart +++ b/packages/cli_tools/test/test_utils/mock_stdout.dart @@ -2,6 +2,9 @@ import 'dart:convert'; import 'dart:io'; class MockStdout implements Stdout { + MockStdout({this.ansiSupported = false}); + + final bool ansiSupported; final _buffer = StringBuffer(); @override @@ -49,7 +52,7 @@ class MockStdout implements Stdout { IOSink get nonBlocking => throw UnimplementedError(); @override - bool get supportsAnsiEscapes => false; + bool get supportsAnsiEscapes => ansiSupported; @override int get terminalColumns => 80; From 120a5ae853ff28ed281b2e5e96965ab5f72d3c1b Mon Sep 17 00:00:00 2001 From: Sergio Date: Tue, 10 Mar 2026 02:36:46 -0700 Subject: [PATCH 7/7] test(logger): propagate ansi flag to stderr and dedupe strip helper --- .../cli_tools/test/std_out_logger_test.dart | 17 +++++++++-------- .../cli_tools/test/test_utils/io_helper.dart | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/cli_tools/test/std_out_logger_test.dart b/packages/cli_tools/test/std_out_logger_test.dart index 88b5351..da05985 100644 --- a/packages/cli_tools/test/std_out_logger_test.dart +++ b/packages/cli_tools/test/std_out_logger_test.dart @@ -6,6 +6,13 @@ import 'package:test/test.dart'; import 'test_utils/io_helper.dart'; + +final _ansiRegex = RegExp( + r'\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\\\)|\x1B[@-Z\\\\-_]', +); + +String _stripAnsi(final String input) => input.replaceAll(_ansiRegex, ''); + void main() { group('Given a StdOutLogger with default settings', () { final logger = StdOutLogger(LogLevel.debug); @@ -168,10 +175,7 @@ void main() { ansiSupported: true, ); - final stripped = stdout.output.replaceAll( - RegExp(r'\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\\\)|\x1B[@-Z\\\\-_]'), - '', - ); + final stripped = _stripAnsi(stdout.output); expect(stdout.output.contains('\x1B['), isTrue); expect( @@ -195,10 +199,7 @@ void main() { ansiSupported: true, ); - final stripped = stdout.output.replaceAll( - RegExp(r'\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\\\)|\x1B[@-Z\\\\-_]'), - '', - ); + final stripped = _stripAnsi(stdout.output); expect(stdout.output.contains('\x1B'), isTrue); expect( diff --git a/packages/cli_tools/test/test_utils/io_helper.dart b/packages/cli_tools/test/test_utils/io_helper.dart index 1f6ff80..38d9a75 100644 --- a/packages/cli_tools/test/test_utils/io_helper.dart +++ b/packages/cli_tools/test/test_utils/io_helper.dart @@ -12,7 +12,7 @@ Future<({MockStdout stdout, MockStdout stderr, MockStdin stdin})> final bool ansiSupported = false, }) async { final standardOut = MockStdout(ansiSupported: ansiSupported); - final standardError = MockStdout(); + final standardError = MockStdout(ansiSupported: ansiSupported); final standardIn = MockStdin(textInputs: stdinLines, keyInputs: keyInputs); await IOOverrides.runZoned(