From d77b6b4e4136917463861d576dd59d030e22f679 Mon Sep 17 00:00:00 2001 From: Lexedia Date: Thu, 18 Dec 2025 19:34:32 +0100 Subject: [PATCH 1/4] [io] support rgb colours for ansi escape codes --- pkgs/io/example/example.dart | 22 +++++++++ pkgs/io/lib/src/ansi_code.dart | 85 +++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/pkgs/io/example/example.dart b/pkgs/io/example/example.dart index 8e358fdbb..c854da6e4 100644 --- a/pkgs/io/example/example.dart +++ b/pkgs/io/example/example.dart @@ -17,6 +17,28 @@ void main(List args) { _preview('Foreground', foregroundColors, forScript); _preview('Background', backgroundColors, forScript); _preview('Styles', styles, forScript); + _preview('Rgb', [rgb(255, 0, 0), rgb(0, 255, 0), rgb(0, 0, 255)], forScript); + _gradient('** Gradient Text Sample **', forScript); +} + +void _gradient(String text, bool forScript) { + final length = text.length; + final buffer = StringBuffer(); + for (var i = 0; i < length; i++) { + final ratio = i / (length - 1); + int red, green, blue; + if (ratio < .5) { + red = ((1 - (ratio * 2)) * 255).round(); + green = (ratio * 2 * 255).round(); + blue = 0; + } else { + red = 0; + green = ((1 - ((ratio - .5) * 2)) * 255).round(); + blue = (((ratio - .5) * 2) * 255).round(); + } + buffer.write(rgb(red, green, blue).wrap(text[i], forScript: forScript)); + } + print(buffer.toString()); } void _preview(String name, List values, bool forScript) { diff --git a/pkgs/io/lib/src/ansi_code.dart b/pkgs/io/lib/src/ansi_code.dart index c9a22c541..052c4c348 100644 --- a/pkgs/io/lib/src/ansi_code.dart +++ b/pkgs/io/lib/src/ansi_code.dart @@ -104,6 +104,42 @@ class AnsiCode { String toString() => '$name ${type._name} ($code)'; } +/// An ANSI escape code for RGB colours. +/// +/// Represents a true colour (24-bit RGB) escape sequence that can be used for +/// both foreground and background colours. +/// +/// Use [rgb] to create an instance of this class. +/// +/// [See also](https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit) +class AnsiRgbCode extends AnsiCode { + /// The red value (0-255). + final int red; + + /// The green value (0-255). + final int green; + + /// The blue value (0-255). + final int blue; + + @override + // ignore: overridden_fields + final AnsiCodeType type; + + /// Creates an RGB [AnsiCode] for the given [type]. + AnsiRgbCode._(this.red, this.green, this.blue, this.type) + : super._('rgb($red,$green,$blue)', type, -1, resetAll); + + int get _prefix => type == AnsiCodeType.background ? 48 : 38; + + @override + String get escape => '$_ansiEscapeLiteral[$_prefix;2;$red;$green;${blue}m'; + + @override + String get escapeForScript => + '$_ansiEscapeForScript[$_prefix;2;$red;$green;${blue}m'; +} + /// Returns a [String] formatted with [codes]. /// /// If [forScript] is `true`, the return value is an unescaped literal. The @@ -127,6 +163,8 @@ String? wrapWith(String? value, Iterable codes, return value; } + final codeParts = []; + var foreground = 0, background = 0; for (var code in myCodes) { switch (code.type) { @@ -149,15 +187,58 @@ String? wrapWith(String? value, Iterable codes, // Ignore. break; } + + if (code is! AnsiRgbCode && code.code != -1) { + codeParts.add(code.code.toString()); + } + } + + codeParts.sort(); + + for (var code in myCodes) { + if (code is AnsiRgbCode) { + final prefix = code.type == AnsiCodeType.background ? '48' : '38'; + codeParts.add('$prefix;2;${code.red};${code.green};${code.blue}'); + } } - final sortedCodes = myCodes.map((ac) => ac.code).toList()..sort(); final escapeValue = forScript ? _ansiEscapeForScript : _ansiEscapeLiteral; - return "$escapeValue[${sortedCodes.join(';')}m$value" + return "$escapeValue[${codeParts.join(';')}m$value" '${resetAll._escapeValue(forScript: forScript)}'; } +/// Creates an [AnsiRgbCode] with the given RGB colour values. +/// +/// The [red], [green], and [blue] parameters must be between 0 and 255. +/// +/// By default, it creates a foreground colour. Pass [type] as +/// [AnsiCodeType.background] to create a background colour. +/// +/// Throws an [ArgumentError] if any colour value is outside the 0-255 range or +/// if [type] is neither foreground nor background. +AnsiCode rgb( + int red, + int green, + int blue, { + AnsiCodeType type = AnsiCodeType.foreground, +}) { + if (red < 0 || red > 255) { + throw ArgumentError.value(red, 'red', 'Must be between 0 and 255.'); + } + if (green < 0 || green > 255) { + throw ArgumentError.value(green, 'green', 'Must be between 0 and 255.'); + } + if (blue < 0 || blue > 255) { + throw ArgumentError.value(blue, 'blue', 'Must be between 0 and 255.'); + } + if (type != AnsiCodeType.foreground && type != AnsiCodeType.background) { + throw ArgumentError.value( + type, 'type', 'Must be either foreground or background.'); + } + return AnsiRgbCode._(red, green, blue, type); +} + // // Style values // From fd3669874dc621e1407dccae4268f915471e3577 Mon Sep 17 00:00:00 2001 From: Lexedia Date: Thu, 18 Dec 2025 21:01:50 +0100 Subject: [PATCH 2/4] add tests --- pkgs/io/lib/src/ansi_code.dart | 23 ++++++++++++++--------- pkgs/io/test/ansi_code_test.dart | 22 +++++++++++++++++++++- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/pkgs/io/lib/src/ansi_code.dart b/pkgs/io/lib/src/ansi_code.dart index 052c4c348..c93845a45 100644 --- a/pkgs/io/lib/src/ansi_code.dart +++ b/pkgs/io/lib/src/ansi_code.dart @@ -163,7 +163,8 @@ String? wrapWith(String? value, Iterable codes, return value; } - final codeParts = []; + final numericCodes = []; + AnsiRgbCode? ansiRgbCode; var foreground = 0, background = 0; for (var code in myCodes) { @@ -188,18 +189,22 @@ String? wrapWith(String? value, Iterable codes, break; } - if (code is! AnsiRgbCode && code.code != -1) { - codeParts.add(code.code.toString()); + if (code is AnsiRgbCode) { + ansiRgbCode = code; + } else if (code.code != -1) { + numericCodes.add(code.code); } } - codeParts.sort(); + numericCodes.sort(); + final codeParts = numericCodes.map((c) => c.toString()).toList(); - for (var code in myCodes) { - if (code is AnsiRgbCode) { - final prefix = code.type == AnsiCodeType.background ? '48' : '38'; - codeParts.add('$prefix;2;${code.red};${code.green};${code.blue}'); - } + if (ansiRgbCode != null) { + final prefix = ansiRgbCode.type == AnsiCodeType.background ? 48 : 38; + codeParts.add( + '$prefix;2;${ansiRgbCode.red};' + '${ansiRgbCode.green};${ansiRgbCode.blue}', + ); } final escapeValue = forScript ? _ansiEscapeForScript : _ansiEscapeLiteral; diff --git a/pkgs/io/test/ansi_code_test.dart b/pkgs/io/test/ansi_code_test.dart index 98ae68b63..078ced1d2 100644 --- a/pkgs/io/test/ansi_code_test.dart +++ b/pkgs/io/test/ansi_code_test.dart @@ -33,7 +33,7 @@ void main() { }); }); - test('forScript variaents ignore `ansiOutputEnabled`', () { + test('forScript variants ignore `ansiOutputEnabled`', () { const expected = '$_ansiEscapeForScript[34m$sampleInput$_ansiEscapeForScript[0m'; @@ -112,6 +112,14 @@ void main() { test(null, () { expect(blue.wrap(null, forScript: forScript), isNull); }); + + test('rgb', () { + final rgbCode = rgb(128, 64, 32); + final expected = + '$escapeLiteral[38;2;128;64;32m$sampleInput$escapeLiteral[0m'; + + expect(rgbCode.wrap(sampleInput, forScript: forScript), expected); + }); }); group('wrapWith', () { @@ -178,6 +186,18 @@ void main() { forScript: forScript), isNull); }); + + _test('rgb', () { + final rgbCode = rgb(128, 64, 32); + final expected = + '$escapeLiteral[4;38;2;128;64;32m$sampleInput$escapeLiteral[0m'; + + expect( + wrapWith(sampleInput, [styleUnderlined, rgbCode], + forScript: forScript), + expected, + ); + }); }); }); } From 3ca09d2976d2e5accc8110b463d06182e5967db6 Mon Sep 17 00:00:00 2001 From: Lexedia Date: Thu, 18 Dec 2025 21:05:11 +0100 Subject: [PATCH 3/4] Adds a changelog entry --- pkgs/io/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkgs/io/CHANGELOG.md b/pkgs/io/CHANGELOG.md index 8c0057f2b..16d9a79ec 100644 --- a/pkgs/io/CHANGELOG.md +++ b/pkgs/io/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.2.0-wip +* Added an `AnsiRgbCode` class and a `rgb` utility function. + ## 1.1.0-wip * Add a `deepCopyLinks` argument to `copyPath` and `copyPathSync`. From ad41251c735665bf1f30c76fe8e1b22d6b9a64eb Mon Sep 17 00:00:00 2001 From: Lexedia Date: Thu, 18 Dec 2025 21:19:20 +0100 Subject: [PATCH 4/4] remove overriden field --- pkgs/io/lib/src/ansi_code.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkgs/io/lib/src/ansi_code.dart b/pkgs/io/lib/src/ansi_code.dart index c93845a45..73ad3134f 100644 --- a/pkgs/io/lib/src/ansi_code.dart +++ b/pkgs/io/lib/src/ansi_code.dart @@ -122,12 +122,8 @@ class AnsiRgbCode extends AnsiCode { /// The blue value (0-255). final int blue; - @override - // ignore: overridden_fields - final AnsiCodeType type; - /// Creates an RGB [AnsiCode] for the given [type]. - AnsiRgbCode._(this.red, this.green, this.blue, this.type) + AnsiRgbCode._(this.red, this.green, this.blue, AnsiCodeType type) : super._('rgb($red,$green,$blue)', type, -1, resetAll); int get _prefix => type == AnsiCodeType.background ? 48 : 38;