Skip to content
Draft
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
4 changes: 4 additions & 0 deletions pkgs/io/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 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);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

There's a potential division by zero here if text.length is 1. length - 1 would be 0, and i / (length - 1) would result in NaN. This leads to a runtime error when rgb() is called with NaN values because they cannot be assigned to int. You should handle this edge case.

Suggested change
final ratio = i / (length - 1);
final ratio = length > 1 ? i / (length - 1) : 0.0;

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
76 changes: 72 additions & 4 deletions pkgs/io/lib/src/ansi_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,17 @@ class AnsiCodeType {
/// [Source](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
class AnsiCode {
/// The numeric value associated with this code.
///
/// `-1` if this code is a composite code with multiple integer values.
/// See [codes].
final int code;

/// The numeric values associated with this code.
///
/// A composite code may have more than one integer value, in which case the
/// [code] property will be `-1`.
Iterable<int> get codes => [code];

/// The [AnsiCode] that resets this value, if one exists.
///
/// Otherwise, `null`.
Expand All @@ -76,10 +85,10 @@ class AnsiCode {
const AnsiCode._(this.name, this.type, this.code, this.reset);

/// Represents the value escaped for use in terminal output.
String get escape => '$_ansiEscapeLiteral[${code}m';
String get escape => '$_ansiEscapeLiteral[${codes.join(';')}m';

/// Represents the value as an unescaped literal suitable for scripts.
String get escapeForScript => '$_ansiEscapeForScript[${code}m';
String get escapeForScript => '$_ansiEscapeForScript[${codes.join(';')}m';

String _escapeValue({bool forScript = false}) =>
forScript ? escapeForScript : escape;
Expand All @@ -104,6 +113,34 @@ 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
Iterable<int> get codes => [_prefix, 2, red, green, blue];
}

/// Returns a [String] formatted with [codes].
///
/// If [forScript] is `true`, the return value is an unescaped literal. The
Expand Down Expand Up @@ -150,14 +187,45 @@ String? wrapWith(String? value, Iterable<AnsiCode> codes,
break;
}
}
final codeParts = myCodes.expand((c) => c.codes).map((c) => c.toString());

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.');
Copy link
Member Author

Choose a reason for hiding this comment

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

@lrhn - would RangeError be preferred here?

}
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
24 changes: 22 additions & 2 deletions 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 @@ -152,7 +160,7 @@ void main() {

_test('multi', () {
final expected =
'$escapeLiteral[1;4;34;107m$sampleInput$escapeLiteral[0m';
'$escapeLiteral[34;107;1;4m$sampleInput$escapeLiteral[0m';

expect(
wrapWith(sampleInput,
Expand All @@ -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
Loading