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`. 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..73ad3134f 100644 --- a/pkgs/io/lib/src/ansi_code.dart +++ b/pkgs/io/lib/src/ansi_code.dart @@ -104,6 +104,38 @@ 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; + + /// Creates an RGB [AnsiCode] for the given [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; + + @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 +159,9 @@ String? wrapWith(String? value, Iterable codes, return value; } + final numericCodes = []; + AnsiRgbCode? ansiRgbCode; + var foreground = 0, background = 0; for (var code in myCodes) { switch (code.type) { @@ -149,15 +184,62 @@ String? wrapWith(String? value, Iterable codes, // Ignore. break; } + + if (code is AnsiRgbCode) { + ansiRgbCode = code; + } else if (code.code != -1) { + numericCodes.add(code.code); + } + } + + numericCodes.sort(); + final codeParts = numericCodes.map((c) => c.toString()).toList(); + + if (ansiRgbCode != null) { + final prefix = ansiRgbCode.type == AnsiCodeType.background ? 48 : 38; + codeParts.add( + '$prefix;2;${ansiRgbCode.red};' + '${ansiRgbCode.green};${ansiRgbCode.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 // 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, + ); + }); }); }); }