| // Copyright (c) 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]. | |
| final 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) | |
| final 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 | |
| ]; |