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
3 changes: 3 additions & 0 deletions pkgs/io/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
22 changes: 22 additions & 0 deletions pkgs/io/example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ void main(List<String> 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());
Comment on lines +24 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The _gradient function can produce NaN for ratio when text contains a single character (e.g., length is 1), leading to a runtime error due to division by zero. This can be fixed by handling the single-character case explicitly to prevent this issue, improving the robustness of the function.

Suggested change
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 _gradient(String text, bool forScript) {
final length = text.length;
if (length == 0) return;
final buffer = StringBuffer();
for (var i = 0; i < length; i++) {
final ratio = length == 1 ? 0.5 : 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<AnsiCode> values, bool forScript) {
Expand Down
86 changes: 84 additions & 2 deletions pkgs/io/lib/src/ansi_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -127,6 +159,9 @@ String? wrapWith(String? value, Iterable<AnsiCode> codes,
return value;
}

final numericCodes = <int>[];
AnsiRgbCode? ansiRgbCode;

var foreground = 0, background = 0;
for (var code in myCodes) {
switch (code.type) {
Expand All @@ -149,15 +184,62 @@ String? wrapWith(String? value, Iterable<AnsiCode> codes,
// Ignore.
break;
}

if (code is AnsiRgbCode) {
ansiRgbCode = code;
} else if (code.code != -1) {
numericCodes.add(code.code);
Comment on lines +188 to +191
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The wrapWith function currently allows both a standard foreground/background AnsiCode and an AnsiRgbCode of the same type to be present in the codes iterable. This can lead to ambiguous or incorrect ANSI escape sequences (e.g., ESC[34;38;2;255;0;0m). The foreground and background counters should also account for AnsiRgbCode instances to ensure only one color type is applied per foreground/background. This improves correctness and maintainability.

    if (code is AnsiRgbCode) {
      if (code.type == AnsiCodeType.foreground) {
        foreground++;
      } else if (code.type == AnsiCodeType.background) {
        background++;
      }
      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
//
Expand Down
22 changes: 21 additions & 1 deletion pkgs/io/test/ansi_code_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void main() {
});
});

test('forScript variaents ignore `ansiOutputEnabled`', () {
test('forScript variants ignore `ansiOutputEnabled`', () {
const expected =
'$_ansiEscapeForScript[34m$sampleInput$_ansiEscapeForScript[0m';

Expand Down Expand Up @@ -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', () {
Expand Down Expand Up @@ -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,
);
});
});
});
}
Expand Down