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}]"; }