using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace Terminal.Gui { /// /// Colors that can be used to set the foreground and background colors in console applications. /// /// /// The value indicates either no-color has been set or the color is invalid. /// [JsonConverter (typeof (ColorJsonConverter))] public enum Color { /// /// The black color. /// Black, /// /// The blue color. /// Blue, /// /// The green color. /// Green, /// /// The cyan color. /// Cyan, /// /// The red color. /// Red, /// /// The magenta color. /// Magenta, /// /// The brown color. /// Brown, /// /// The gray color. /// Gray, /// /// The dark gray color. /// DarkGray, /// /// The bright bBlue color. /// BrightBlue, /// /// The bright green color. /// BrightGreen, /// /// The bright cyan color. /// BrightCyan, /// /// The bright red color. /// BrightRed, /// /// The bright magenta color. /// BrightMagenta, /// /// The bright yellow color. /// BrightYellow, /// /// The White color. /// White } /// /// Indicates the RGB for true colors. /// [JsonConverter (typeof (TrueColorJsonConverter))] public readonly struct TrueColor : IEquatable { private static readonly ImmutableDictionary TrueColorToConsoleColorMap = new Dictionary () { { new TrueColor (0,0,0),Color.Black }, { new TrueColor (0, 0, 0x80),Color.Blue }, { new TrueColor (0, 0x80, 0),Color.Green}, { new TrueColor (0, 0x80, 0x80),Color.Cyan}, { new TrueColor (0x80, 0, 0),Color.Red}, { new TrueColor (0x80, 0, 0x80),Color.Magenta}, { new TrueColor (0xC1, 0x9C, 0x00),Color.Brown}, // TODO confirm this { new TrueColor (0xC0, 0xC0, 0xC0),Color.Gray}, { new TrueColor (0x80, 0x80, 0x80),Color.DarkGray}, { new TrueColor (0, 0, 0xFF),Color.BrightBlue}, { new TrueColor (0, 0xFF, 0),Color.BrightGreen}, { new TrueColor (0, 0xFF, 0xFF),Color.BrightCyan}, { new TrueColor (0xFF, 0, 0),Color.BrightRed}, { new TrueColor (0xFF, 0, 0xFF),Color.BrightMagenta }, { new TrueColor (0xFF, 0xFF, 0),Color.BrightYellow}, { new TrueColor (0xFF, 0xFF, 0xFF),Color.White}, }.ToImmutableDictionary (); /// /// Red color component. /// public int Red { get; } /// /// Green color component. /// public int Green { get; } /// /// Blue color component. /// public int Blue { get; } /// /// Initializes a new instance of the struct. /// /// /// /// public TrueColor (int red, int green, int blue) { Red = red; Green = green; Blue = blue; } /// /// Converts the provided text to a . /// /// The text to analyze. /// The parsed value. /// A boolean value indcating whether it was successful. public static bool TryParse (string text, [NotNullWhen (true)] out TrueColor? trueColor) { // empty color if ((text == null) || (text.Length == 0)) { trueColor = null; return false; } // #RRGGBB or #RGB if ((text [0] == '#') && ((text.Length == 7) || (text.Length == 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); trueColor = new TrueColor (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); trueColor = new TrueColor (r, g, b); } return true; } // rgb(XX,YY,ZZ) 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); trueColor = new TrueColor (r, g, b); return true; } trueColor = null; return false; } /// /// Converts a to a using a default mapping. /// /// The to convert. /// public static TrueColor? FromConsoleColor (Color consoleColor) { return consoleColor switch { Color.Black => new TrueColor (0, 0, 0), Color.Blue => new TrueColor (0, 0, 0x80), Color.Green => new TrueColor (0, 0x80, 0), Color.Cyan => new TrueColor (0, 0x80, 0x80), Color.Red => new TrueColor (0x80, 0, 0), Color.Magenta => new TrueColor (0x80, 0, 0x80), Color.Brown => new TrueColor (0xC1, 0x9C, 0x00) // TODO confirm this , Color.Gray => new TrueColor (0xC0, 0xC0, 0xC0), Color.DarkGray => new TrueColor (0x80, 0x80, 0x80), Color.BrightBlue => new TrueColor (0, 0, 0xFF), Color.BrightGreen => new TrueColor (0, 0xFF, 0), Color.BrightCyan => new TrueColor (0, 0xFF, 0xFF), Color.BrightRed => new TrueColor (0xFF, 0, 0), Color.BrightMagenta => new TrueColor (0xFF, 0, 0xFF), Color.BrightYellow => new TrueColor (0xFF, 0xFF, 0), Color.White => new TrueColor (0xFF, 0xFF, 0xFF), var _ => null }; ; } /// /// Converts the provided to using a default mapping. /// /// /// public static Color ToConsoleColor (TrueColor? trueColor) { if (trueColor.HasValue) { return TrueColorToConsoleColorMap.MinBy (kv => CalculateDistance (kv.Key, trueColor.Value)).Value; } else { return (Color)(-1); } } private static float CalculateDistance (TrueColor color1, TrueColor color2) { // use RGB distance return Math.Abs (color1.Red - color2.Red) + Math.Abs (color1.Green - color2.Green) + Math.Abs (color1.Blue - color2.Blue); } /// public static bool operator == (TrueColor left, TrueColor right) { return left.Equals (right); } /// public static bool operator != (TrueColor left, TrueColor right) { return !left.Equals (right); } /// public override bool Equals (object obj) { return obj is TrueColor other && Equals (other); } /// public bool Equals (TrueColor other) { return Red == other.Red && Green == other.Green && Blue == other.Blue; } /// public override int GetHashCode () { return HashCode.Combine (Red, Green, Blue); } /// public override string ToString () { return $"#{Red:X2}{Green:X2}{Blue:X2}"; } } /// /// 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 struct Attribute : IEquatable { /// /// Default empty attribute. /// public static readonly Attribute Default = new Attribute (Color.White, Color.Black); /// /// The -specific color value. If is /// the value of this property is invalid (typically because the Attribute was created before a driver was loaded) /// and the attribute should be re-made (see ) before it is used. /// [JsonIgnore (Condition = JsonIgnoreCondition.Always)] internal int Value { get; } /// /// The foreground color. /// [JsonConverter (typeof (ColorJsonConverter))] public Color Foreground { get; private init; } /// /// The background color. /// [JsonConverter (typeof (ColorJsonConverter))] public Color Background { get; private init; } /// /// Gets the TrueColor foreground color. /// [JsonConverter (typeof (TrueColorJsonConverter))] public TrueColor? TrueColorForeground { get; private init; } /// /// Gets the TrueColor background color. /// [JsonConverter (typeof (TrueColorJsonConverter))] public TrueColor? TrueColorBackground { get; private init; } /// /// Initializes a new instance with a platform-specific color value. /// /// Value. internal Attribute (int value) { Color foreground = default; Color background = default; Initialized = false; if (Application.Driver != null) { Application.Driver.GetColors (value, out foreground, out background); Initialized = true; } Value = value; Foreground = foreground; Background = background; TrueColorForeground = TrueColor.FromConsoleColor (foreground); TrueColorBackground = TrueColor.FromConsoleColor (background); } /// /// Initializes a new instance of the struct. /// /// platform-dependent color value. /// Foreground /// Background public Attribute (int value, Color foreground, Color background) { Foreground = foreground; Background = background; TrueColorForeground = TrueColor.FromConsoleColor (foreground); TrueColorBackground = TrueColor.FromConsoleColor (background); Value = value; Initialized = true; } /// /// Initializes a new instance of the struct. /// /// Foreground /// Background public Attribute (Color foreground = new Color (), Color background = new Color ()) { Foreground = foreground; Background = background; TrueColorForeground = TrueColor.FromConsoleColor (foreground); TrueColorBackground = TrueColor.FromConsoleColor (background); var make = Make (foreground, background); Initialized = make.Initialized; Value = make.Value; } /// /// Initializes a new instance of the class. Populates /// and . Also computes /// and (basic console colors) in case /// driver does not support true color rendering. /// /// /// public Attribute (TrueColor? trueColorForeground, TrueColor? trueColorBackground) { Foreground = TrueColor.ToConsoleColor (trueColorForeground); Background = TrueColor.ToConsoleColor (trueColorBackground); TrueColorForeground = trueColorForeground; TrueColorBackground = trueColorBackground; var make = Make (Foreground, Background); Value = make.Value; Initialized = make.Initialized; } /// /// /// Initializes a new instance of the class. Populates /// and with explicit /// fallback values for and (in case /// driver does not support true color rendering). /// /// If you do not want to manually specify the fallback colors use /// instead which auto calculates these. /// /// True color RGB values you would like to use. /// True color RGB values you would like to use. /// Simple console color replacement if driver does not support true color. /// Simple console color replacement if driver does not support true color. public Attribute (TrueColor trueColorForeground, TrueColor trueColorBackground, Color foreground, Color background) { Foreground = foreground; Background = background; TrueColorForeground = trueColorForeground; TrueColorBackground = trueColorBackground; var make = Make (Foreground, Background); Value = make.Value; Initialized = make.Initialized; } /// /// 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) { return obj is Attribute other && Equals (other); } /// public bool Equals (Attribute other) { if (TrueColorForeground.HasValue || TrueColorBackground.HasValue) { return TrueColorForeground == other.TrueColorForeground && TrueColorBackground == other.TrueColorBackground; } return Value == other.Value && Foreground == other.Foreground && Background == other.Background; } /// public override int GetHashCode () => HashCode.Combine (Value, Foreground, Background, TrueColorForeground, TrueColorBackground); /// /// Creates an from the specified foreground and background colors. /// /// /// If a has not been loaded (Application.Driver == null) this /// method will return an attribute with set to . /// /// The new attribute. /// Foreground color to use. /// Background color to use. public static Attribute Make (Color foreground, Color background) { if (Application.Driver == null) { // Create the attribute, but show it's not been initialized return new Attribute () { Initialized = false, Foreground = foreground, Background = background }; } return Application.Driver.MakeAttribute (foreground, background); } /// /// Gets the current from the driver. /// /// The current attribute. public static Attribute Get () { if (Application.Driver == null) { throw new InvalidOperationException ("The Application has not been initialized"); } return Application.Driver.GetAttribute (); } /// /// If the attribute has been initialized by a and /// thus has that is valid for that driver. If the /// and colors may have been set '-1' but /// the attribute has not been mapped to a specific color value. /// /// /// Attributes that have not been initialized must eventually be initialized before being passed to a driver. /// [JsonIgnore] public bool Initialized { get; internal set; } /// /// Returns if the Attribute is valid (both foreground and background have valid color values). /// /// [JsonIgnore] public bool HasValidColors => (int)Foreground > -1 && (int)Background > -1; /// public override string ToString () { // Note, Unit tests are dependent on this format return $"{Foreground},{Background}"; } } /// /// Defines the color s for common visible elements in a . /// Containers such as and use to determine /// the colors used by sub-views. /// /// /// See also: . /// [JsonConverter (typeof (ColorSchemeJsonConverter))] public class ColorScheme : IEquatable { Attribute _normal = Attribute.Default; Attribute _focus = Attribute.Default; Attribute _hotNormal = Attribute.Default; Attribute _hotFocus = Attribute.Default; Attribute _disabled = Attribute.Default; /// /// Used by and to track which ColorScheme /// is being accessed. /// internal string schemeBeingSet = ""; /// /// Creates a new instance. /// public ColorScheme () { } /// /// Creates a new instance, initialized with the values from . /// /// The scheme to initialize the new instance with. public ColorScheme (ColorScheme scheme) : base () { if (scheme != null) { _normal = scheme.Normal; _focus = scheme.Focus; _hotNormal = scheme.HotNormal; _disabled = scheme.Disabled; _hotFocus = scheme.HotFocus; } } /// /// Creates a new instance, initialized with the values from . /// /// The attribute to initialize the new instance with. public ColorScheme (Attribute attribute) { _normal = attribute; _focus = attribute; _hotNormal = attribute; _disabled = attribute; _hotFocus = attribute; } /// /// The foreground and background color for text when the view is not focused, hot, or disabled. /// public Attribute Normal { get { return _normal; } set { if (!value.HasValidColors) { return; } _normal = value; } } /// /// The foreground and background color for text when the view has the focus. /// public Attribute Focus { get { return _focus; } set { if (!value.HasValidColors) { return; } _focus = value; } } /// /// The foreground and background color for text when the view is highlighted (hot). /// public Attribute HotNormal { get { return _hotNormal; } set { if (!value.HasValidColors) { return; } _hotNormal = value; } } /// /// The foreground and background color for text when the view is highlighted (hot) and has focus. /// public Attribute HotFocus { get { return _hotFocus; } set { if (!value.HasValidColors) { return; } _hotFocus = value; } } /// /// The default foreground and background color for text, when the view is disabled. /// public Attribute Disabled { get { return _disabled; } set { if (!value.HasValidColors) { return; } _disabled = value; } } /// /// Compares two objects for equality. /// /// /// true if the two objects are equal public override bool Equals (object obj) { return Equals (obj as ColorScheme); } /// /// Compares two objects for equality. /// /// /// true if the two objects are equal public bool Equals (ColorScheme other) { return other != null && EqualityComparer.Default.Equals (_normal, other._normal) && EqualityComparer.Default.Equals (_focus, other._focus) && EqualityComparer.Default.Equals (_hotNormal, other._hotNormal) && EqualityComparer.Default.Equals (_hotFocus, other._hotFocus) && EqualityComparer.Default.Equals (_disabled, other._disabled); } /// /// Returns a hashcode for this instance. /// /// hashcode for this instance public override int GetHashCode () { int hashCode = -1242460230; hashCode = hashCode * -1521134295 + _normal.GetHashCode (); hashCode = hashCode * -1521134295 + _focus.GetHashCode (); hashCode = hashCode * -1521134295 + _hotNormal.GetHashCode (); hashCode = hashCode * -1521134295 + _hotFocus.GetHashCode (); hashCode = hashCode * -1521134295 + _disabled.GetHashCode (); return hashCode; } /// /// Compares two objects for equality. /// /// /// /// true if the two objects are equivalent public static bool operator == (ColorScheme left, ColorScheme right) { return EqualityComparer.Default.Equals (left, right); } /// /// Compares two objects for inequality. /// /// /// /// true if the two objects are not equivalent public static bool operator != (ColorScheme left, ColorScheme right) { return !(left == right); } internal void Initialize () { // If the new scheme was created before a driver was loaded, we need to re-make // the attributes if (!_normal.Initialized) { _normal = new Attribute (_normal.Foreground, _normal.Background); } if (!_focus.Initialized) { _focus = new Attribute (_focus.Foreground, _focus.Background); } if (!_hotNormal.Initialized) { _hotNormal = new Attribute (_hotNormal.Foreground, _hotNormal.Background); } if (!_hotFocus.Initialized) { _hotFocus = new Attribute (_hotFocus.Foreground, _hotFocus.Background); } if (!_disabled.Initialized) { _disabled = new Attribute (_disabled.Foreground, _disabled.Background); } } } /// /// The default s for the application. /// /// /// This property can be set in a Theme to change the default for the application. /// public static class Colors { private class SchemeNameComparerIgnoreCase : IEqualityComparer { public bool Equals (string x, string y) { if (x != null && y != null) { return string.Equals (x, y, StringComparison.InvariantCultureIgnoreCase); } return false; } public int GetHashCode (string obj) { return obj.ToLowerInvariant ().GetHashCode (); } } static Colors () { ColorSchemes = Create (); } /// /// Creates a new dictionary of new objects. /// public static Dictionary Create () { // Use reflection to dynamically create the default set of ColorSchemes from the list defined // by the class. return typeof (Colors).GetProperties () .Where (p => p.PropertyType == typeof (ColorScheme)) .Select (p => new KeyValuePair (p.Name, new ColorScheme ())) .ToDictionary (t => t.Key, t => t.Value, comparer: new SchemeNameComparerIgnoreCase ()); } /// /// The application Toplevel color scheme, for the default Toplevel views. /// /// /// /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["TopLevel"]; /// /// public static ColorScheme TopLevel { get => GetColorScheme (); set => SetColorScheme (value); } /// /// The base color scheme, for the default Toplevel views. /// /// /// /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Base"]; /// /// public static ColorScheme Base { get => GetColorScheme (); set => SetColorScheme (value); } /// /// The dialog color scheme, for standard popup dialog boxes /// /// /// /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Dialog"]; /// /// public static ColorScheme Dialog { get => GetColorScheme (); set => SetColorScheme (value); } /// /// The menu bar color /// /// /// /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Menu"]; /// /// public static ColorScheme Menu { get => GetColorScheme (); set => SetColorScheme (value); } /// /// The color scheme for showing errors. /// /// /// /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Error"]; /// /// public static ColorScheme Error { get => GetColorScheme (); set => SetColorScheme (value); } static ColorScheme GetColorScheme ([CallerMemberName] string schemeBeingSet = null) { return ColorSchemes [schemeBeingSet]; } static void SetColorScheme (ColorScheme colorScheme, [CallerMemberName] string schemeBeingSet = null) { ColorSchemes [schemeBeingSet] = colorScheme; colorScheme.schemeBeingSet = schemeBeingSet; } /// /// Provides the defined s. /// [SerializableConfigurationProperty (Scope = typeof (ThemeScope), OmitClassName = true)] [JsonConverter (typeof (DictionaryJsonConverter))] public static Dictionary ColorSchemes { get; private set; } } }