blob: c023f53f0ca65de79ebe96fe6d323e9d740c1d80 [file] [log] [blame] [edit]
// Copyright 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:io' as io;
const _ansiEscapeLiteral = '\x1B';
const _ansiEscapeForScript = r'\033';
/// Whether formatted ANSI output is enabled for [wrapWith] and [AnsiCode.wrap].
///
/// By default, returns `true` if both `stdout.supportsAnsiEscapes` and
/// `stderr.supportsAnsiEscapes` from `dart:io` are `true`.
///
/// The default can be overridden by setting the [Zone] variable [AnsiCode] to
/// either `true` or `false`.
///
/// [overrideAnsiOutput] is provided to make this easy.
bool get ansiOutputEnabled =>
Zone.current[AnsiCode] as bool? ??
(io.stdout.supportsAnsiEscapes && io.stderr.supportsAnsiEscapes);
/// Returns `true` no formatting is required for [input].
bool _isNoop(bool skip, String? input, bool? forScript) =>
skip ||
input == null ||
input.isEmpty ||
!((forScript ?? false) || ansiOutputEnabled);
/// Allows overriding [ansiOutputEnabled] to [enableAnsiOutput] for the code run
/// within [body].
T overrideAnsiOutput<T>(bool enableAnsiOutput, T Function() body) =>
runZoned(body, zoneValues: <Object, Object>{AnsiCode: enableAnsiOutput});
/// The type of code represented by [AnsiCode].
class AnsiCodeType {
final String _name;
/// A foreground color.
static const AnsiCodeType foreground = AnsiCodeType._('foreground');
/// A style.
static const AnsiCodeType style = AnsiCodeType._('style');
/// A background color.
static const AnsiCodeType background = AnsiCodeType._('background');
/// A reset value.
static const AnsiCodeType reset = AnsiCodeType._('reset');
const AnsiCodeType._(this._name);
@override
String toString() => 'AnsiType.$_name';
}
/// Standard ANSI escape code for customizing terminal text output.
///
/// [Source](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
class AnsiCode {
/// The numeric value associated with this code.
final int code;
/// The [AnsiCode] that resets this value, if one exists.
///
/// Otherwise, `null`.
final AnsiCode? reset;
/// A description of this code.
final String name;
/// The type of code that is represented.
final AnsiCodeType type;
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';
/// Represents the value as an unescaped literal suitable for scripts.
String get escapeForScript => '$_ansiEscapeForScript[${code}m';
String _escapeValue({bool forScript = false}) =>
forScript ? escapeForScript : escape;
/// Wraps [value] with the [escape] value for this code, followed by
/// [resetAll].
///
/// If [forScript] is `true`, the return value is an unescaped literal. The
/// value of [ansiOutputEnabled] is also ignored.
///
/// Returns `value` unchanged if
/// * [value] is `null` or empty
/// * both [ansiOutputEnabled] and [forScript] are `false`.
/// * [type] is [AnsiCodeType.reset]
String? wrap(String? value, {bool forScript = false}) =>
_isNoop(type == AnsiCodeType.reset, value, forScript)
? value
: '${_escapeValue(forScript: forScript)}$value'
'${reset!._escapeValue(forScript: forScript)}';
@override
String toString() => '$name ${type._name} ($code)';
}
/// Returns a [String] formatted with [codes].
///
/// If [forScript] is `true`, the return value is an unescaped literal. The
/// value of [ansiOutputEnabled] is also ignored.
///
/// Returns `value` unchanged if
/// * [value] is `null` or empty.
/// * both [ansiOutputEnabled] and [forScript] are `false`.
/// * [codes] is empty.
///
/// Throws an [ArgumentError] if
/// * [codes] contains more than one value of type [AnsiCodeType.foreground].
/// * [codes] contains more than one value of type [AnsiCodeType.background].
/// * [codes] contains any value of type [AnsiCodeType.reset].
String? wrapWith(String? value, Iterable<AnsiCode> codes,
{bool forScript = false}) {
// Eliminate duplicates
final myCodes = codes.toSet();
if (_isNoop(myCodes.isEmpty, value, forScript)) {
return value;
}
var foreground = 0, background = 0;
for (var code in myCodes) {
switch (code.type) {
case AnsiCodeType.foreground:
foreground++;
if (foreground > 1) {
throw ArgumentError.value(codes, 'codes',
'Cannot contain more than one foreground color code.');
}
case AnsiCodeType.background:
background++;
if (background > 1) {
throw ArgumentError.value(codes, 'codes',
'Cannot contain more than one foreground color code.');
}
case AnsiCodeType.reset:
throw ArgumentError.value(
codes, 'codes', 'Cannot contain reset codes.');
case AnsiCodeType.style:
// Ignore.
break;
}
}
final sortedCodes = myCodes.map((ac) => ac.code).toList()..sort();
final escapeValue = forScript ? _ansiEscapeForScript : _ansiEscapeLiteral;
return "$escapeValue[${sortedCodes.join(';')}m$value"
'${resetAll._escapeValue(forScript: forScript)}';
}
//
// Style values
//
const styleBold = AnsiCode._('bold', AnsiCodeType.style, 1, resetBold);
const styleDim = AnsiCode._('dim', AnsiCodeType.style, 2, resetDim);
const styleItalic = AnsiCode._('italic', AnsiCodeType.style, 3, resetItalic);
const styleUnderlined =
AnsiCode._('underlined', AnsiCodeType.style, 4, resetUnderlined);
const styleBlink = AnsiCode._('blink', AnsiCodeType.style, 5, resetBlink);
const styleReverse = AnsiCode._('reverse', AnsiCodeType.style, 7, resetReverse);
/// Not widely supported.
const styleHidden = AnsiCode._('hidden', AnsiCodeType.style, 8, resetHidden);
/// Not widely supported.
const styleCrossedOut =
AnsiCode._('crossed out', AnsiCodeType.style, 9, resetCrossedOut);
//
// Reset values
//
const resetAll = AnsiCode._('all', AnsiCodeType.reset, 0, null);
// NOTE: bold is weird. The reset code seems to be 22 sometimes – not 21
// See https://gitlab.com/gnachman/iterm2/issues/3208
const resetBold = AnsiCode._('bold', AnsiCodeType.reset, 22, null);
const resetDim = AnsiCode._('dim', AnsiCodeType.reset, 22, null);
const resetItalic = AnsiCode._('italic', AnsiCodeType.reset, 23, null);
const resetUnderlined = AnsiCode._('underlined', AnsiCodeType.reset, 24, null);
const resetBlink = AnsiCode._('blink', AnsiCodeType.reset, 25, null);
const resetReverse = AnsiCode._('reverse', AnsiCodeType.reset, 27, null);
const resetHidden = AnsiCode._('hidden', AnsiCodeType.reset, 28, null);
const resetCrossedOut = AnsiCode._('crossed out', AnsiCodeType.reset, 29, null);
//
// Foreground values
//
const black = AnsiCode._('black', AnsiCodeType.foreground, 30, resetAll);
const red = AnsiCode._('red', AnsiCodeType.foreground, 31, resetAll);
const green = AnsiCode._('green', AnsiCodeType.foreground, 32, resetAll);
const yellow = AnsiCode._('yellow', AnsiCodeType.foreground, 33, resetAll);
const blue = AnsiCode._('blue', AnsiCodeType.foreground, 34, resetAll);
const magenta = AnsiCode._('magenta', AnsiCodeType.foreground, 35, resetAll);
const cyan = AnsiCode._('cyan', AnsiCodeType.foreground, 36, resetAll);
const lightGray =
AnsiCode._('light gray', AnsiCodeType.foreground, 37, resetAll);
const defaultForeground =
AnsiCode._('default', AnsiCodeType.foreground, 39, resetAll);
const darkGray = AnsiCode._('dark gray', AnsiCodeType.foreground, 90, resetAll);
const lightRed = AnsiCode._('light red', AnsiCodeType.foreground, 91, resetAll);
const lightGreen =
AnsiCode._('light green', AnsiCodeType.foreground, 92, resetAll);
const lightYellow =
AnsiCode._('light yellow', AnsiCodeType.foreground, 93, resetAll);
const lightBlue =
AnsiCode._('light blue', AnsiCodeType.foreground, 94, resetAll);
const lightMagenta =
AnsiCode._('light magenta', AnsiCodeType.foreground, 95, resetAll);
const lightCyan =
AnsiCode._('light cyan', AnsiCodeType.foreground, 96, resetAll);
const white = AnsiCode._('white', AnsiCodeType.foreground, 97, resetAll);
//
// Background values
//
const backgroundBlack =
AnsiCode._('black', AnsiCodeType.background, 40, resetAll);
const backgroundRed = AnsiCode._('red', AnsiCodeType.background, 41, resetAll);
const backgroundGreen =
AnsiCode._('green', AnsiCodeType.background, 42, resetAll);
const backgroundYellow =
AnsiCode._('yellow', AnsiCodeType.background, 43, resetAll);
const backgroundBlue =
AnsiCode._('blue', AnsiCodeType.background, 44, resetAll);
const backgroundMagenta =
AnsiCode._('magenta', AnsiCodeType.background, 45, resetAll);
const backgroundCyan =
AnsiCode._('cyan', AnsiCodeType.background, 46, resetAll);
const backgroundLightGray =
AnsiCode._('light gray', AnsiCodeType.background, 47, resetAll);
const backgroundDefault =
AnsiCode._('default', AnsiCodeType.background, 49, resetAll);
const backgroundDarkGray =
AnsiCode._('dark gray', AnsiCodeType.background, 100, resetAll);
const backgroundLightRed =
AnsiCode._('light red', AnsiCodeType.background, 101, resetAll);
const backgroundLightGreen =
AnsiCode._('light green', AnsiCodeType.background, 102, resetAll);
const backgroundLightYellow =
AnsiCode._('light yellow', AnsiCodeType.background, 103, resetAll);
const backgroundLightBlue =
AnsiCode._('light blue', AnsiCodeType.background, 104, resetAll);
const backgroundLightMagenta =
AnsiCode._('light magenta', AnsiCodeType.background, 105, resetAll);
const backgroundLightCyan =
AnsiCode._('light cyan', AnsiCodeType.background, 106, resetAll);
const backgroundWhite =
AnsiCode._('white', AnsiCodeType.background, 107, resetAll);
/// All of the [AnsiCode] values that represent [AnsiCodeType.style].
const List<AnsiCode> styles = [
styleBold,
styleDim,
styleItalic,
styleUnderlined,
styleBlink,
styleReverse,
styleHidden,
styleCrossedOut
];
/// All of the [AnsiCode] values that represent [AnsiCodeType.foreground].
const List<AnsiCode> foregroundColors = [
black,
red,
green,
yellow,
blue,
magenta,
cyan,
lightGray,
defaultForeground,
darkGray,
lightRed,
lightGreen,
lightYellow,
lightBlue,
lightMagenta,
lightCyan,
white
];
/// All of the [AnsiCode] values that represent [AnsiCodeType.background].
const List<AnsiCode> backgroundColors = [
backgroundBlack,
backgroundRed,
backgroundGreen,
backgroundYellow,
backgroundBlue,
backgroundMagenta,
backgroundCyan,
backgroundLightGray,
backgroundDefault,
backgroundDarkGray,
backgroundLightRed,
backgroundLightGreen,
backgroundLightYellow,
backgroundLightBlue,
backgroundLightMagenta,
backgroundLightCyan,
backgroundWhite
];