global using Attribute = Terminal.Gui.Attribute;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace Terminal.Gui;
///
/// Defines the 16 legacy color names and values that can be used to set the
/// foreground and background colors in Terminal.Gui apps. Used with .
///
///
///
/// These colors match the 16 colors defined for ANSI escape sequences for 4-bit (16) colors.
///
///
/// For terminals that support 24-bit color (TrueColor), the RGB values for each of these colors can be configured
/// using the
/// property.
///
///
public enum ColorName {
///
/// The black color. ANSI escape sequence: \u001b[30m.
///
Black,
///
/// The blue color. ANSI escape sequence: \u001b[34m.
///
Blue,
///
/// The green color. ANSI escape sequence: \u001b[32m.
///
Green,
///
/// The cyan color. ANSI escape sequence: \u001b[36m.
///
Cyan,
///
/// The red color. ANSI escape sequence: \u001b[31m.
///
Red,
///
/// The magenta color. ANSI escape sequence: \u001b[35m.
///
Magenta,
///
/// The yellow color (also known as Brown). ANSI escape sequence: \u001b[33m.
///
Yellow,
///
/// The gray color (also known as White). ANSI escape sequence: \u001b[37m.
///
Gray,
///
/// The dark gray color (also known as Bright Black). ANSI escape sequence: \u001b[30;1m.
///
DarkGray,
///
/// The bright blue color. ANSI escape sequence: \u001b[34;1m.
///
BrightBlue,
///
/// The bright green color. ANSI escape sequence: \u001b[32;1m.
///
BrightGreen,
///
/// The bright cyan color. ANSI escape sequence: \u001b[36;1m.
///
BrightCyan,
///
/// The bright red color. ANSI escape sequence: \u001b[31;1m.
///
BrightRed,
///
/// The bright magenta color. ANSI escape sequence: \u001b[35;1m.
///
BrightMagenta,
///
/// The bright yellow color. ANSI escape sequence: \u001b[33;1m.
///
BrightYellow,
///
/// The White color (also known as Bright White). ANSI escape sequence: \u001b[37;1m.
///
White
}
///
/// The 16 foreground color codes used by ANSI Esc sequences for 256 color terminals. Add 10 to these values for background
/// color.
///
public enum AnsiColorCode {
///
/// The ANSI color code for Black.
///
BLACK = 30,
///
/// The ANSI color code for Red.
///
RED = 31,
///
/// The ANSI color code for Green.
///
GREEN = 32,
///
/// The ANSI color code for Yellow.
///
YELLOW = 33,
///
/// The ANSI color code for Blue.
///
BLUE = 34,
///
/// The ANSI color code for Magenta.
///
MAGENTA = 35,
///
/// The ANSI color code for Cyan.
///
CYAN = 36,
///
/// The ANSI color code for White.
///
WHITE = 37,
///
/// The ANSI color code for Bright Black.
///
BRIGHT_BLACK = 90,
///
/// The ANSI color code for Bright Red.
///
BRIGHT_RED = 91,
///
/// The ANSI color code for Bright Green.
///
BRIGHT_GREEN = 92,
///
/// The ANSI color code for Bright Yellow.
///
BRIGHT_YELLOW = 93,
///
/// The ANSI color code for Bright Blue.
///
BRIGHT_BLUE = 94,
///
/// The ANSI color code for Bright Magenta.
///
BRIGHT_MAGENTA = 95,
///
/// The ANSI color code for Bright Cyan.
///
BRIGHT_CYAN = 96,
///
/// The ANSI color code for Bright White.
///
BRIGHT_WHITE = 97
}
///
/// Represents a 24-bit color. Provides automatic mapping between the legacy 4-bit (16 color) system and 24-bit colors (see
/// ). Used with .
///
[JsonConverter (typeof (ColorJsonConverter))]
public readonly struct Color : IEquatable {
// TODO: Make this map configurable via ConfigurationManager
// TODO: This does not need to be a Dictionary, but can be an 16 element array.
///
/// Maps legacy 16-color values to the corresponding 24-bit RGB value.
///
internal static ImmutableDictionary _colorToNameMap = new Dictionary {
// using "Windows 10 Console/PowerShell 6" here: https://i.stack.imgur.com/9UVnC.png
// See also: https://en.wikipedia.org/wiki/ANSI_escape_code
{ new Color (12, 12, 12), ColorName.Black },
{ new Color (0, 55, 218), ColorName.Blue },
{ new Color (19, 161, 14), ColorName.Green },
{ new Color (58, 150, 221), ColorName.Cyan },
{ new Color (197, 15, 31), ColorName.Red },
{ new Color (136, 23, 152), ColorName.Magenta },
{ new Color (128, 64, 32), ColorName.Yellow },
{ new Color (204, 204, 204), ColorName.Gray },
{ new Color (118, 118, 118), ColorName.DarkGray },
{ new Color (59, 120, 255), ColorName.BrightBlue },
{ new Color (22, 198, 12), ColorName.BrightGreen },
{ new Color (97, 214, 214), ColorName.BrightCyan },
{ new Color (231, 72, 86), ColorName.BrightRed },
{ new Color (180, 0, 158), ColorName.BrightMagenta },
{ new Color (249, 241, 165), ColorName.BrightYellow },
{ new Color (242, 242, 242), ColorName.White }
}.ToImmutableDictionary ();
///
/// Defines the 16 legacy color names and values that can be used to set the
///
internal static ImmutableDictionary _colorNameToAnsiColorMap = new Dictionary {
{ ColorName.Black, AnsiColorCode.BLACK },
{ ColorName.Blue, AnsiColorCode.BLUE },
{ ColorName.Green, AnsiColorCode.GREEN },
{ ColorName.Cyan, AnsiColorCode.CYAN },
{ ColorName.Red, AnsiColorCode.RED },
{ ColorName.Magenta, AnsiColorCode.MAGENTA },
{ ColorName.Yellow, AnsiColorCode.YELLOW },
{ ColorName.Gray, AnsiColorCode.WHITE },
{ ColorName.DarkGray, AnsiColorCode.BRIGHT_BLACK },
{ ColorName.BrightBlue, AnsiColorCode.BRIGHT_BLUE },
{ ColorName.BrightGreen, AnsiColorCode.BRIGHT_GREEN },
{ ColorName.BrightCyan, AnsiColorCode.BRIGHT_CYAN },
{ ColorName.BrightRed, AnsiColorCode.BRIGHT_RED },
{ ColorName.BrightMagenta, AnsiColorCode.BRIGHT_MAGENTA },
{ ColorName.BrightYellow, AnsiColorCode.BRIGHT_YELLOW },
{ ColorName.White, AnsiColorCode.BRIGHT_WHITE }
}.ToImmutableDictionary ();
///
/// Initializes a new instance of the class.
///
/// The red 8-bits.
/// The green 8-bits.
/// The blue 8-bits.
/// Optional; defaults to 0xFF. The Alpha channel is not supported by Terminal.Gui.
public Color (int red, int green, int blue, int alpha = 0xFF)
{
R = red;
G = green;
B = blue;
A = alpha;
}
///
/// Initializes a new instance of the class with an encoded 24-bit color value.
///
/// The encoded 24-bit color value (see ).
public Color (int rgba)
{
A = (byte)(rgba >> 24 & 0xFF);
R = (byte)(rgba >> 16 & 0xFF);
G = (byte)(rgba >> 8 & 0xFF);
B = (byte)(rgba & 0xFF);
}
///
/// Initializes a new instance of the color from a legacy 16-color value.
///
/// The 16-color value.
public Color (ColorName colorName)
{
var c = FromColorName (colorName);
R = c.R;
G = c.G;
B = c.B;
A = c.A;
}
///
/// Initializes a new instance of the color from string. See
/// for details.
///
///
///
public Color (string colorString)
{
if (!TryParse (colorString, out var c)) {
throw new ArgumentOutOfRangeException (nameof (colorString));
}
R = c.R;
G = c.G;
B = c.B;
A = c.A;
}
///
/// Initializes a new instance of the .
///
public Color ()
{
R = 0;
G = 0;
B = 0;
A = 0xFF;
}
///
/// Red color component.
///
public int R { get; }
///
/// Green color component.
///
public int G { get; }
///
/// Blue color component.
///
public int B { get; }
///
/// Alpha color component.
///
///
/// The Alpha channel is not supported by Terminal.Gui.
///
public int A { get; } // Not currently supported; here for completeness.
///
/// Gets or sets the color value encoded as ARGB32.
///
/// (<see cref="A"/> << 24) | (<see cref="R"/> << 16) | (<see cref="G"/> << 8) | <see cref="B"/>
///
///
[JsonIgnore]
public int Rgba => A << 24 | R << 16 | G << 8 | B;
///
/// Gets or sets the 24-bit color value for each of the legacy 16-color values.
///
[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
public static Dictionary Colors {
get =>
// Transform _colorToNameMap into a Dictionary
_colorToNameMap.ToDictionary (kvp => kvp.Value, kvp => $"#{kvp.Key.R:X2}{kvp.Key.G:X2}{kvp.Key.B:X2}");
set {
// Transform Dictionary into _colorToNameMap
var newMap = value.ToDictionary (kvp => new Color (kvp.Value), kvp => {
if (Enum.TryParse (kvp.Key.ToString (), true, out var colorName)) {
return colorName;
}
throw new ArgumentException ($"Invalid color name: {kvp.Key}");
});
_colorToNameMap = newMap.ToImmutableDictionary ();
}
}
///
/// Gets the using a legacy 16-color value.
/// will return the closest 16 color match to the true color when no exact value is found.
///
///
/// Get returns the of the closest 24-bit color value. Set sets the RGB value using a hard-coded
/// map.
///
[JsonIgnore]
public ColorName ColorName => FindClosestColor (this);
///
/// Gets the using a legacy 16-color value.
/// will return the closest 16 color match to the true color when no exact value is found.
///
///
/// Get returns the of the closest 24-bit color value. Set sets the RGB value using a hard-coded
/// map.
///
[JsonIgnore]
public AnsiColorCode AnsiColorCode => _colorNameToAnsiColorMap [ColorName];
///
/// Converts a legacy to a 24-bit .
///
/// The to convert.
///
static Color FromColorName (ColorName colorName) => _colorToNameMap.FirstOrDefault (x => x.Value == colorName).Key;
// Iterates through the entries in the _colorNames dictionary, calculates the
// Euclidean distance between the input color and each dictionary color in RGB space,
// and keeps track of the closest entry found so far. The function returns a KeyValuePair
// representing the closest color entry and its associated color name.
internal static ColorName FindClosestColor (Color inputColor)
{
var closestColor = ColorName.Black; // Default to Black
var closestDistance = double.MaxValue;
foreach (var colorEntry in _colorToNameMap) {
var distance = CalculateColorDistance (inputColor, colorEntry.Key);
if (distance < closestDistance) {
closestDistance = distance;
closestColor = colorEntry.Value;
}
}
return closestColor;
}
static double CalculateColorDistance (Color color1, Color color2)
{
// Calculate the Euclidean distance between two colors
var deltaR = color1.R - (double)color2.R;
var deltaG = color1.G - (double)color2.G;
var deltaB = color1.B - (double)color2.B;
return Math.Sqrt (deltaR * deltaR + deltaG * deltaG + deltaB * deltaB);
}
///
/// Converts the provided string to a new instance.
///
///
/// The text to analyze. Formats supported are
/// "#RGB", "#RRGGBB", "#RGBA", "#RRGGBBAA", "rgb(r,g,b)", "rgb(r,g,b,a)", and any of the
/// .
///
/// The parsed value.
/// A boolean value indicating whether parsing was successful.
///
/// While supports the alpha channel , Terminal.Gui does not.
///
public static bool TryParse (string text, [NotNullWhen (true)] out Color color)
{
// empty color
if (string.IsNullOrEmpty (text)) {
color = new Color ();
return false;
}
// #RRGGBB, #RGB
if (text [0] == '#' && text.Length is 7 or 4) {
if (text.Length == 7) {
var r = Convert.ToInt32 (text.Substring (1, 2), 16);
var g = Convert.ToInt32 (text.Substring (3, 2), 16);
var b = Convert.ToInt32 (text.Substring (5, 2), 16);
color = new Color (r, g, b);
} else {
var rText = char.ToString (text [1]);
var gText = char.ToString (text [2]);
var bText = char.ToString (text [3]);
var r = Convert.ToInt32 (rText + rText, 16);
var g = Convert.ToInt32 (gText + gText, 16);
var b = Convert.ToInt32 (bText + bText, 16);
color = new Color (r, g, b);
}
return true;
}
// #RRGGBB, #RGBA
if (text [0] == '#' && text.Length is 8 or 5) {
if (text.Length == 7) {
var r = Convert.ToInt32 (text.Substring (1, 2), 16);
var g = Convert.ToInt32 (text.Substring (3, 2), 16);
var b = Convert.ToInt32 (text.Substring (5, 2), 16);
var a = Convert.ToInt32 (text.Substring (7, 2), 16);
color = new Color (a, r, g, b);
} else {
var rText = char.ToString (text [1]);
var gText = char.ToString (text [2]);
var bText = char.ToString (text [3]);
var aText = char.ToString (text [4]);
var r = Convert.ToInt32 (aText + aText, 16);
var g = Convert.ToInt32 (rText + rText, 16);
var b = Convert.ToInt32 (gText + gText, 16);
var a = Convert.ToInt32 (bText + bText, 16);
color = new Color (r, g, b, a);
}
return true;
}
// rgb(r,g,b)
var match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+)\)");
if (match.Success) {
var r = int.Parse (match.Groups [1].Value);
var g = int.Parse (match.Groups [2].Value);
var b = int.Parse (match.Groups [3].Value);
color = new Color (r, g, b);
return true;
}
// rgb(r,g,b,a)
match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+),(\d+)\)");
if (match.Success) {
var r = int.Parse (match.Groups [1].Value);
var g = int.Parse (match.Groups [2].Value);
var b = int.Parse (match.Groups [3].Value);
var a = int.Parse (match.Groups [4].Value);
color = new Color (r, g, b, a);
return true;
}
if (Enum.TryParse (text, true, out var colorName)) {
color = new Color (colorName);
return true;
}
color = new Color ();
return false;
}
///
/// Converts the color to a string representation.
///
///
///
/// If the color is a named color, the name is returned. Otherwise, the color is returned as a hex string.
///
///
/// (Alpha channel) is ignored and the returned string will not include it.
///
///
///
public override string ToString ()
{
// If Values has an exact match with a named color (in _colorNames), use that.
if (_colorToNameMap.TryGetValue (this, out var colorName)) {
return Enum.GetName (typeof (ColorName), colorName);
}
// Otherwise return as an RGB hex value.
return $"#{R:X2}{G:X2}{B:X2}";
}
#region Legacy Color Names
///
/// The black color.
///
public const ColorName Black = ColorName.Black;
///
/// The blue color.
///
public const ColorName Blue = ColorName.Blue;
///
/// The green color.
///
public const ColorName Green = ColorName.Green;
///
/// The cyan color.
///
public const ColorName Cyan = ColorName.Cyan;
///
/// The red color.
///
public const ColorName Red = ColorName.Red;
///
/// The magenta color.
///
public const ColorName Magenta = ColorName.Magenta;
///
/// The yellow color.
///
public const ColorName Yellow = ColorName.Yellow;
///
/// The gray color.
///
public const ColorName Gray = ColorName.Gray;
///
/// The dark gray color.
///
public const ColorName DarkGray = ColorName.DarkGray;
///
/// The bright bBlue color.
///
public const ColorName BrightBlue = ColorName.BrightBlue;
///
/// The bright green color.
///
public const ColorName BrightGreen = ColorName.BrightGreen;
///
/// The bright cyan color.
///
public const ColorName BrightCyan = ColorName.BrightCyan;
///
/// The bright red color.
///
public const ColorName BrightRed = ColorName.BrightRed;
///
/// The bright magenta color.
///
public const ColorName BrightMagenta = ColorName.BrightMagenta;
///
/// The bright yellow color.
///
public const ColorName BrightYellow = ColorName.BrightYellow;
///
/// The White color.
///
public const ColorName White = ColorName.White;
#endregion
// TODO: Verify implict/explicit are correct for below
#region Operators
///
/// Cast from int.
///
///
public static implicit operator Color (int rgba) => new (rgba);
///
/// Cast to int.
///
///
public static implicit operator int (Color color) => color.Rgba;
///
/// Cast from . May fail if the color is not a named color.
///
///
public static explicit operator Color (ColorName colorName) => new (colorName);
///
/// Cast to . May fail if the color is not a named color.
///
///
public static explicit operator ColorName (Color color) => color.ColorName;
///
/// Equality operator for two objects..
///
///
///
///
public static bool operator == (Color left, Color right) => left.Equals (right);
///
/// Inequality operator for two objects.
///
///
///
///
public static bool operator != (Color left, Color right) => !left.Equals (right);
///
/// Equality operator for and objects.
///
///
///
///
public static bool operator == (ColorName left, Color right) => left == right.ColorName;
///
/// Inequality operator for and objects.
///
///
///
///
public static bool operator != (ColorName left, Color right) => left != right.ColorName;
///
/// Equality operator for and objects.
///
///
///
///
public static bool operator == (Color left, ColorName right) => left.ColorName == right;
///
/// Inequality operator for and objects.
///
///
///
///
public static bool operator != (Color left, ColorName right) => left.ColorName != right;
///
public override bool Equals (object obj) => obj is Color other && Equals (other);
///
public bool Equals (Color other) => R == other.R &&
G == other.G &&
B == other.B &&
A == other.A;
///
public override int GetHashCode () => HashCode.Combine (R, G, B, A);
#endregion
}
///
/// Attributes represent how text is styled when displayed in the terminal.
///
///
/// provides a platform independent representation of colors (and someday other forms of text
/// styling).
/// They encode both the foreground and the background color and are used in the
/// class to define color schemes that can be used in an application.
///
[JsonConverter (typeof (AttributeJsonConverter))]
public readonly struct Attribute : IEquatable {
///
/// Default empty attribute.
///
public static readonly Attribute Default = new (Color.White, Color.Black);
///
/// The -specific color value.
///
[JsonIgnore (Condition = JsonIgnoreCondition.Always)]
internal int PlatformColor { get; }
///
/// The foreground color.
///
[JsonConverter (typeof (ColorJsonConverter))]
public Color Foreground { get; }
///
/// The background color.
///
[JsonConverter (typeof (ColorJsonConverter))]
public Color Background { get; }
///
/// Initializes a new instance with default values.
///
public Attribute ()
{
PlatformColor = -1;
Foreground = new Color (Default.Foreground.ColorName);
Background = new Color (Default.Background.ColorName);
}
///
/// Initializes a new instance from an existing instance.
///
public Attribute (Attribute attr)
{
PlatformColor = -1;
Foreground = new Color (attr.Foreground.ColorName);
Background = new Color (attr.Background.ColorName);
}
///
/// Initializes a new instance with platform specific color value.
///
/// Value.
internal Attribute (int platformColor)
{
PlatformColor = platformColor;
Foreground = new Color (Default.Foreground.ColorName);
Background = new Color (Default.Background.ColorName);
}
///
/// Initializes a new instance of the struct.
///
/// platform-dependent color value.
/// Foreground
/// Background
internal Attribute (int platformColor, Color foreground, Color background)
{
Foreground = foreground;
Background = background;
PlatformColor = platformColor;
}
///
/// Initializes a new instance of the struct.
///
/// platform-dependent color value.
/// Foreground
/// Background
internal Attribute (int platformColor, ColorName foreground, ColorName background) : this (platformColor, new Color (foreground), new Color (background)) { }
///
/// Initializes a new instance of the struct.
///
/// Foreground
/// Background
public Attribute (Color foreground, Color background)
{
Foreground = foreground;
Background = background;
// TODO: Once CursesDriver supports truecolor all the PlatformColor stuff goes away
if (Application.Driver == null) {
PlatformColor = -1;
return;
}
var make = Application.Driver.MakeColor (foreground, background);
PlatformColor = make.PlatformColor;
}
///
/// Initializes a new instance with a value. Both and
/// will be set to the specified color.
///
/// Value.
internal Attribute (ColorName colorName) : this (colorName, colorName) { }
///
/// Initializes a new instance of the struct.
///
/// Foreground
/// Background
public Attribute (ColorName foregroundName, ColorName backgroundName) : this (new Color (foregroundName), new Color (backgroundName)) { }
///
/// Initializes a new instance of the struct.
///
/// Foreground
/// Background
public Attribute (ColorName foregroundName, Color background) : this (new Color (foregroundName), background) { }
///
/// Initializes a new instance of the struct.
///
/// Foreground
/// Background
public Attribute (Color foreground, ColorName backgroundName) : this (foreground, new Color (backgroundName)) { }
///
/// Initializes a new instance of the struct
/// with the same colors for the foreground and background.
///
/// The color.
public Attribute (Color color) : this (color, color) { }
///
/// Compares two attributes for equality.
///
///
///
///
public static bool operator == (Attribute left, Attribute right) => left.Equals (right);
///
/// Compares two attributes for inequality.
///
///
///
///
public static bool operator != (Attribute left, Attribute right) => !(left == right);
///
public override bool Equals (object obj) => obj is Attribute other && Equals (other);
///
public bool Equals (Attribute other) => PlatformColor == other.PlatformColor &&
Foreground == other.Foreground &&
Background == other.Background;
///
public override int GetHashCode () => HashCode.Combine (PlatformColor, Foreground, Background);
///
public override string ToString () =>
// Note: Unit tests are dependent on this format
$"[{Foreground},{Background}]";
}