From 305dbde131900e895ca5951b187610b878eb600f Mon Sep 17 00:00:00 2001 From: awaisdev5765 Date: Fri, 30 Oct 2026 18:55:11 +0500 Subject: [PATCH 1/2] Adjust ANSI colors in logs and console for theme legibility ANSI colors are fixed terminal colors, so some of them (e.g. ansi-black, ansi-white, ansi-bright-white) are effectively invisible against the corresponding light or dark themed background (see #6345). textSpansFromAnsi now takes an optional background color, which all console/logging call sites pass as the theme surface. Foreground colors whose contrast ratio against that background is below 3:1 are blended toward black (on a light background) or white (on a dark background) to restore legibility; sufficiently contrasting colors are left untouched. Background ANSI colors are not adjusted since blending into the surface does not harm legibility. Adds unit tests for the contrast adjustment behavior. Fixes #6345 --- .../src/screens/logging/_message_column.dart | 2 + .../object_inspector/vm_instance_display.dart | 1 + .../lib/src/shared/console/console.dart | 1 + .../console/widgets/display_provider.dart | 1 + .../lib/src/shared/primitives/utils.dart | 40 ++++++++++++-- .../test/shared/primitives/utils_test.dart | 53 +++++++++++++++++++ 6 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/logging/_message_column.dart b/packages/devtools_app/lib/src/screens/logging/_message_column.dart index ead7f9fa0c4..23b63b2cad9 100644 --- a/packages/devtools_app/lib/src/screens/logging/_message_column.dart +++ b/packages/devtools_app/lib/src/screens/logging/_message_column.dart @@ -74,6 +74,7 @@ class MessageColumn extends ColumnData // The current summary is generally the first 200 chars of details. data.summary!, theme.regularTextStyle, + backgroundColor: theme.colorScheme.surface, ), if (hasSummary && hasDetails()) TextSpan(text: ' • ', style: theme.subtleTextStyle), @@ -81,6 +82,7 @@ class MessageColumn extends ColumnData ...textSpansFromAnsi( detailsComputed ? data.details! : '', theme.subtleTextStyle, + backgroundColor: theme.colorScheme.surface, ), ], ), diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_instance_display.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_instance_display.dart index 7c2f36b922d..f9fc8edbac8 100644 --- a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_instance_display.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_instance_display.dart @@ -171,6 +171,7 @@ class DisplayProvider extends StatelessWidget { children: textSpansFromAnsi( variable.text ?? '', theme.subtleFixedFontStyle, + backgroundColor: theme.colorScheme.surface, ), ), ), diff --git a/packages/devtools_app/lib/src/shared/console/console.dart b/packages/devtools_app/lib/src/shared/console/console.dart index 53a8adca1f3..0a04528a62c 100644 --- a/packages/devtools_app/lib/src/shared/console/console.dart +++ b/packages/devtools_app/lib/src/shared/console/console.dart @@ -196,6 +196,7 @@ class _ConsoleOutputState extends State<_ConsoleOutput> children: textSpansFromAnsi( line.text, theme.regularTextStyle, + backgroundColor: theme.colorScheme.surface, ), ), ); diff --git a/packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart b/packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart index b8d398b27d6..ec3932dcf10 100644 --- a/packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart +++ b/packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart @@ -75,6 +75,7 @@ class _DisplayProviderState extends State { children: textSpansFromAnsi( widget.variable.text!, theme.subtleFixedFontStyle, + backgroundColor: theme.colorScheme.surface, ), ), ), diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart index e40cf699791..e06139e3e64 100644 --- a/packages/devtools_app/lib/src/shared/primitives/utils.dart +++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart @@ -545,7 +545,11 @@ class DebugTimingLogger { } } -List textSpansFromAnsi(String input, TextStyle defaultStyle) { +List textSpansFromAnsi( + String input, + TextStyle defaultStyle, { + Color? backgroundColor, +}) { final parser = AnsiParser(input); return parser.parse().map((entry) { final styled = entry.bold || entry.fgColor != null || entry.bgColor != null; @@ -553,7 +557,7 @@ List textSpansFromAnsi(String input, TextStyle defaultStyle) { text: entry.text, style: styled ? TextStyle( - color: ansiToColor(entry.fgColor), + color: ansiToColor(entry.fgColor, background: backgroundColor), backgroundColor: ansiToColor(entry.bgColor), fontWeight: entry.bold ? FontWeight.bold : FontWeight.normal, ) @@ -562,13 +566,41 @@ List textSpansFromAnsi(String input, TextStyle defaultStyle) { }).toList(); } -Color? ansiToColor(List? ansiInput) { +Color? ansiToColor(List? ansiInput, {Color? background}) { if (ansiInput == null) { return null; } assert(ansiInput.length == 3, 'Ansi color list should contain 3 elements'); - return Color.fromRGBO(ansiInput[0], ansiInput[1], ansiInput[2], 1); + final color = Color.fromRGBO(ansiInput[0], ansiInput[1], ansiInput[2], 1); + if (background == null) { + return color; + } + return _adjustForContrast(color, background); +} + +/// Returns [color] adjusted so that it remains legible against [background]. +/// +/// ANSI colors are fixed terminal colors, and some of them (e.g. ansi-black, +/// ansi-white, ansi-bright-white) are effectively invisible against the +/// corresponding light or dark themed background. When the contrast between +/// [color] and [background] is too low, blend the color toward black (on a +/// light background) or white (on a dark background) to restore legibility. +Color _adjustForContrast(Color color, Color background) { + final fgLuminance = color.computeLuminance(); + final bgLuminance = background.computeLuminance(); + final contrastRatio = + (max(fgLuminance, bgLuminance) + 0.05) / + (min(fgLuminance, bgLuminance) + 0.05); + if (contrastRatio >= 3.0) { + // A 3:1 contrast ratio is sufficient for terminal-style output. + return color; + } + return Color.lerp( + color, + bgLuminance > 0.5 ? Colors.black : Colors.white, + 0.6, + )!; } /// An extension on [LogicalKeySet] to provide user-facing names for key diff --git a/packages/devtools_app/test/shared/primitives/utils_test.dart b/packages/devtools_app/test/shared/primitives/utils_test.dart index 98581e53e7e..a1ad8978993 100644 --- a/packages/devtools_app/test/shared/primitives/utils_test.dart +++ b/packages/devtools_app/test/shared/primitives/utils_test.dart @@ -11,6 +11,59 @@ import 'package:material_ui/material_ui.dart'; void main() { group('utils', () { + group('textSpansFromAnsi', () { + const defaultStyle = TextStyle(); + // Theme surface colors used by DevTools (see devtools_app_shared). + const lightSurface = Color(0xFFFFFFFF); + const darkSurface = Color(0xFF1B1B1F); + + test('adjusts low-contrast foreground for a light background', () { + // ansi-white is invisible on a white background. + final spans = textSpansFromAnsi( + '\x1B[37mhello\x1B[0m', + defaultStyle, + backgroundColor: lightSurface, + ); + final color = spans.single.style!.color!; + expect(color.computeLuminance(), lessThan(1.0)); + expect(color, isNot(equals(lightSurface))); + }); + + test('adjusts low-contrast foreground for a dark background', () { + // ansi-black is invisible on a near-black background. + final spans = textSpansFromAnsi( + '\x1B[30mhello\x1B[0m', + defaultStyle, + backgroundColor: darkSurface, + ); + final color = spans.single.style!.color!; + expect( + color.computeLuminance(), + greaterThan(darkSurface.computeLuminance()), + ); + }); + + test('keeps colors that already have sufficient contrast', () { + // ansi-red is legible on a white background and must be unchanged. + final spans = textSpansFromAnsi( + '\x1B[31mhello\x1B[0m', + defaultStyle, + backgroundColor: lightSurface, + ); + expect( + spans.single.style!.color, + equals(const Color.fromRGBO(187, 0, 0, 1)), + ); + }); + + test('does not adjust colors when no background is provided', () { + final spans = textSpansFromAnsi('\x1B[37mhello\x1B[0m', defaultStyle); + expect( + spans.single.style!.color, + equals(const Color.fromRGBO(255, 255, 255, 1)), + ); + }); + }); group('durationText', () { test('infers unit based on duration', () { expect(durationText(Duration.zero), equals('0 μs')); From 5867bc67f675d82f3eb148b556298b4e76e0121b Mon Sep 17 00:00:00 2001 From: awaisdev5765 Date: Fri, 30 Oct 2026 19:01:13 +0500 Subject: [PATCH 2/2] Extract magic numbers in _adjustForContrast into named constants Per code review feedback and the DevTools style guide. --- .../devtools_app/lib/src/shared/primitives/utils.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart index e06139e3e64..df7aee309d2 100644 --- a/packages/devtools_app/lib/src/shared/primitives/utils.dart +++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart @@ -587,19 +587,23 @@ Color? ansiToColor(List? ansiInput, {Color? background}) { /// [color] and [background] is too low, blend the color toward black (on a /// light background) or white (on a dark background) to restore legibility. Color _adjustForContrast(Color color, Color background) { + // A 3:1 contrast ratio is sufficient for terminal-style output (WCAG AA + // large-text threshold). + const minContrastRatio = 3.0; + const contrastBlendFactor = 0.6; + final fgLuminance = color.computeLuminance(); final bgLuminance = background.computeLuminance(); final contrastRatio = (max(fgLuminance, bgLuminance) + 0.05) / (min(fgLuminance, bgLuminance) + 0.05); - if (contrastRatio >= 3.0) { - // A 3:1 contrast ratio is sufficient for terminal-style output. + if (contrastRatio >= minContrastRatio) { return color; } return Color.lerp( color, bgLuminance > 0.5 ? Colors.black : Colors.white, - 0.6, + contrastBlendFactor, )!; }