Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions packages/cli_tools/lib/src/logger/loggers/std_out_logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -159,9 +153,12 @@ class StdOutLogger extends Logger {
if (type is BoxLogType) {
message = _formatAsBox(
wrapColumn: wrapTextColumn ?? _defaultColumnWrap,
message: message,
title: type.title,
message: _stripAnsiCodes(message),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest moving the ansi code wrapping to the _log() method, and avoid wrapping, unwrapping, and wrapping again.

title: type.title != null ? _stripAnsiCodes(type.title!) : null,
);
if (ansiSupported) {
message = _styleByLevel(message, logLevel);
Comment on lines 154 to +160

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't drop caller-supplied ANSI styling from boxed logs.

Lines 156-160 build the box from stripped message/title and only reapply _styleByLevel afterward. That removes any inline styling the caller passed in, and LogLevel.info / LogLevel.nothing boxes become plain text on ANSI-capable terminals. Use stripped text for width calculation only; the final render should preserve the original styled content.

}
} else if (type is TextLogType) {
switch (type.style) {
case TextLogStyle.command:
Expand Down Expand Up @@ -237,6 +234,27 @@ 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) {
// 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, '');
}

/// wrap text based on column width
String _wrapText(final String text, final int columnWidth) {
final textLines = text.split('\n');
Expand Down
75 changes: 75 additions & 0 deletions packages/cli_tools/test/std_out_logger_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -137,4 +144,72 @@ 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'),
),
ansiSupported: true,
);

final stripped = _stripAnsi(stdout.output);

expect(stdout.output.contains('\x1B['), isTrue);
expect(
stripped,
'┌─ warn ──┐\n'
'│ warning │\n'
'└─────────┘\n',
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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'),
),
ansiSupported: true,
);

final stripped = _stripAnsi(stdout.output);

expect(stdout.output.contains('\x1B'), isTrue);
expect(
stripped,
'┌─ warn ─┐\n'
'│ click │\n'
'└───────┘\n',
);
expect(stderr.output, '');
});
});
}

5 changes: 3 additions & 2 deletions packages/cli_tools/test/test_utils/io_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ Future<({MockStdout stdout, MockStdout stderr, MockStdin stdin})>
final FutureOr<T> Function() runner, {
final List<String> stdinLines = const [],
final List<int> keyInputs = const [],
final bool ansiSupported = false,
}) async {
final standardOut = MockStdout();
final standardError = MockStdout();
final standardOut = MockStdout(ansiSupported: ansiSupported);
final standardError = MockStdout(ansiSupported: ansiSupported);
final standardIn = MockStdin(textInputs: stdinLines, keyInputs: keyInputs);

await IOOverrides.runZoned(
Expand Down
5 changes: 4 additions & 1 deletion packages/cli_tools/test/test_utils/mock_stdout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down