Skip to content

Fixes #4000. Named colors as enums. #4005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Mar 29, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions Terminal.Gui/Configuration/ColorJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using ColorHelper;

namespace Terminal.Gui;

Expand Down Expand Up @@ -40,11 +39,9 @@ public override Color Read (ref Utf8JsonReader reader, Type typeToConvert, JsonS
// Get the color string
ReadOnlySpan<char> colorString = reader.GetString ();

// Check if the color string is a color name
if (ColorStrings.TryParseW3CColorName (colorString.ToString (), out Color color1))
if (ColorStrings.TryParseNamedColor (colorString, out Color namedColor))
{
// Return the parsed color
return new (color1);
return namedColor;
}

if (Color.TryParse (colorString, null, out Color parsedColor))
Expand Down
70 changes: 70 additions & 0 deletions Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#nullable enable

using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;

namespace Terminal.Gui;

/// <summary>
/// Color name resolver for <see cref="ColorName16"/>.
/// </summary>
public class AnsiColorNameResolver : IColorNameResolver
{
private static readonly ImmutableArray<string> AnsiColorNames = ImmutableArray.Create(Enum.GetNames<ColorName16>());

/// <inheritdoc/>
public IEnumerable<string> GetColorNames ()
{
return AnsiColorNames;
}

/// <inheritdoc/>
public bool TryNameColor (Color color, [NotNullWhen (true)] out string? name)
{
if (Color.TryGetExactNamedColor16 (color, out ColorName16 colorName16))
{
name = Color16Name (colorName16);
return true;
}
name = null;
return false;
}

/// <inheritdoc/>
public bool TryParseColor (ReadOnlySpan<char> name, out Color color)
{
if (Enum.TryParse (name, ignoreCase: true, out ColorName16 colorName16) &&
// Any numerical value converts to undefined enum value.
Enum.IsDefined (colorName16))
{
color = new Color (colorName16);
return true;
}
color = default;
return false;
}

private static string Color16Name (ColorName16 color16)
{
return color16 switch
{
ColorName16.Black => nameof (ColorName16.Black),
ColorName16.Blue => nameof (ColorName16.Blue),
ColorName16.Green => nameof (ColorName16.Green),
ColorName16.Cyan => nameof (ColorName16.Cyan),
ColorName16.Red => nameof (ColorName16.Red),
ColorName16.Magenta => nameof (ColorName16.Magenta),
ColorName16.Yellow => nameof (ColorName16.Yellow),
ColorName16.Gray => nameof (ColorName16.Gray),
ColorName16.DarkGray => nameof (ColorName16.DarkGray),
ColorName16.BrightBlue => nameof (ColorName16.BrightBlue),
ColorName16.BrightGreen => nameof (ColorName16.BrightGreen),
ColorName16.BrightCyan => nameof (ColorName16.BrightCyan),
ColorName16.BrightRed => nameof (ColorName16.BrightRed),
ColorName16.BrightMagenta => nameof (ColorName16.BrightMagenta),
ColorName16.BrightYellow => nameof (ColorName16.BrightYellow),
ColorName16.White => nameof (ColorName16.White),
_ => throw new NotSupportedException ($"ColorName16 '{color16}' is not supported.")
};
}
}
148 changes: 67 additions & 81 deletions Terminal.Gui/Drawing/Color/Color.Formatting.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Runtime.CompilerServices;

Expand Down Expand Up @@ -267,90 +266,79 @@ public static Color Parse (ReadOnlySpan<char> text, IFormatProvider? formatProvi
return text switch
{
// Null string or empty span provided
{ IsEmpty: true } when formatProvider is null => throw new ColorParseException (
in text,
"The text provided was null or empty.",
in text
),
{ IsEmpty: true } when formatProvider is null =>
throw new ColorParseException (in text, "The text provided was null or empty.", in text),

// A valid ICustomColorFormatter was specified and the text wasn't null or empty
{ IsEmpty: false } when formatProvider is ICustomColorFormatter f => f.Parse (text),

// Input string is only whitespace
{ Length: > 0 } when text.IsWhiteSpace () => throw new ColorParseException (
in text,
"The text provided consisted of only whitespace characters.",
in text
),
{ Length: > 0 } when text.IsWhiteSpace () =>
throw new ColorParseException (in text, "The text provided consisted of only whitespace characters.", in text),

// Any string too short to possibly be any supported format.
{ Length: > 0 and < 3 } => throw new ColorParseException (
in text,
"Text was too short to be any possible supported format.",
in text
),

// The various hexadecimal cases
['#', ..] hexString => hexString switch
{
// #RGB
['#', var rChar, var gChar, var bChar] chars when chars [1..]
.IsAllAsciiHexDigits () =>
new Color (
byte.Parse ([rChar, rChar], NumberStyles.HexNumber),
byte.Parse ([gChar, gChar], NumberStyles.HexNumber),
byte.Parse ([bChar, bChar], NumberStyles.HexNumber)
),

// #ARGB
['#', var aChar, var rChar, var gChar, var bChar] chars when chars [1..]
.IsAllAsciiHexDigits () =>
new Color (
byte.Parse ([rChar, rChar], NumberStyles.HexNumber),
byte.Parse ([gChar, gChar], NumberStyles.HexNumber),
byte.Parse ([bChar, bChar], NumberStyles.HexNumber),
byte.Parse ([aChar, aChar], NumberStyles.HexNumber)
),

// #RRGGBB
[
'#', var r1Char, var r2Char, var g1Char, var g2Char, var b1Char,
var b2Char
] chars when chars [1..].IsAllAsciiHexDigits () =>
new Color (
byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber),
byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber),
byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber)
),

// #AARRGGBB
[
'#', var a1Char, var a2Char, var r1Char, var r2Char, var g1Char,
var g2Char, var b1Char, var b2Char
] chars when chars [1..].IsAllAsciiHexDigits () =>
new Color (
byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber),
byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber),
byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber),
byte.Parse ([a1Char, a2Char], NumberStyles.HexNumber)
),
_ => throw new ColorParseException (
in hexString,
$"Color hex string {hexString} was not in a supported format",
in hexString
)
},

// rgb(r,g,b) or rgb(r,g,b,a)
['r', 'g', 'b', '(', .., ')'] => ParseRgbaFormat (in text, 4),

// rgba(r,g,b,a) or rgba(r,g,b)
['r', 'g', 'b', 'a', '(', .., ')'] => ParseRgbaFormat (in text, 5),

// Attempt to parse as a named color from the ColorStrings resources
{ } when char.IsLetter (text [0]) && ColorStrings.TryParseW3CColorName (text.ToString (), out Color color) =>
new Color (color),
{ Length: > 0 and < 3 } =>
throw new ColorParseException (in text, "Text was too short to be any possible supported format.", in text),

// The various hexadecimal cases
['#', ..] hexString => hexString switch
{
// #RGB
['#', var rChar, var gChar, var bChar] chars when chars [1..]
.IsAllAsciiHexDigits () =>
new Color (
byte.Parse ([rChar, rChar], NumberStyles.HexNumber),
byte.Parse ([gChar, gChar], NumberStyles.HexNumber),
byte.Parse ([bChar, bChar], NumberStyles.HexNumber)
),

// #ARGB
['#', var aChar, var rChar, var gChar, var bChar] chars when chars [1..]
.IsAllAsciiHexDigits () =>
new Color (
byte.Parse ([rChar, rChar], NumberStyles.HexNumber),
byte.Parse ([gChar, gChar], NumberStyles.HexNumber),
byte.Parse ([bChar, bChar], NumberStyles.HexNumber),
byte.Parse ([aChar, aChar], NumberStyles.HexNumber)
),

// #RRGGBB
[
'#', var r1Char, var r2Char, var g1Char, var g2Char, var b1Char, var b2Char
] chars when chars [1..].IsAllAsciiHexDigits () =>
new Color (
byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber),
byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber),
byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber)
),

// #AARRGGBB
[
'#', var a1Char, var a2Char,
var r1Char, var r2Char,
var g1Char, var g2Char,
var b1Char, var b2Char
] chars when chars [1..].IsAllAsciiHexDigits () =>
new Color (
byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber),
byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber),
byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber),
byte.Parse ([a1Char, a2Char], NumberStyles.HexNumber)
),
_ => throw new ColorParseException (
in hexString,
$"Color hex string {hexString} was not in a supported format",
in hexString
)
},

// rgb(r,g,b) or rgb(r,g,b,a)
['r', 'g', 'b', '(', .., ')'] => ParseRgbaFormat (in text, 4),

// rgba(r,g,b,a) or rgba(r,g,b)
['r', 'g', 'b', 'a', '(', .., ')'] => ParseRgbaFormat (in text, 5),
// Attempt named colors
{ } when char.IsLetter (text [0]) && ColorStrings.TryParseNamedColor (text, out Color color) => color,
// Any other input
_ => throw new ColorParseException (in text, "Text did not match any expected format.", in text, [])
};
Expand Down Expand Up @@ -585,11 +573,9 @@ public static bool TryParse (ReadOnlySpan<byte> utf8Text, IFormatProvider? provi
[SkipLocalsInit]
public override string ToString ()
{
string? name = ColorStrings.GetW3CColorName (this);

if (name is { })
if (ColorStrings.GetColorName (this) is string colorName)
{
return name;
return colorName;
}

return $"#{R:X2}{G:X2}{B:X2}";
Expand Down
11 changes: 9 additions & 2 deletions Terminal.Gui/Drawing/Color/Color.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#nullable enable
using System.Collections.Frozen;
using System.Diagnostics.Contracts;
using System.Drawing;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -236,6 +234,15 @@ internal static ColorName16 GetClosestNamedColor16 (Color inputColor)
return ColorExtensions.ColorToName16Map.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value;
}

/// <summary>Converts the given color value to exact named color represented by <see cref="ColorName16"/>.</summary>
/// <param name="inputColor"></param>
/// <param name="colorName16">Successfully converted named color.</param>
/// <returns>True if conversion succeeded; otherwise false.</returns>
internal static bool TryGetExactNamedColor16 (Color inputColor, out ColorName16 colorName16)
{
return ColorExtensions.ColorToName16Map.TryGetValue (inputColor, out colorName16);
}

[SkipLocalsInit]
private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); }

Expand Down
Loading
Loading