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