Преглед изворни кода

Fixed a bunch of Key related issues

Tig пре 10 месеци
родитељ
комит
525d332c9b

+ 10 - 1
Terminal.Gui/Configuration/ColorJsonConverter.cs

@@ -4,7 +4,16 @@ using ColorHelper;
 
 namespace Terminal.Gui;
 
-/// <summary>Json converter for the <see cref="Color"/> class.</summary>
+/// <summary>
+/// Json converter for the <see cref="Color"/> class.
+/// <para>
+///     Serialization outputs a string with the color name if the color matches a name in <see cref="ColorStrings"/>
+///     or the "#RRGGBB" hexadecimal representation (e.g. "#FF0000" for red).
+/// </para>
+/// <para>
+///     Deserialization formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
+///     "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", or any W3C color name.</para>
+/// </summary>
 internal class ColorJsonConverter : JsonConverter<Color>
 {
     private static ColorJsonConverter _instance;

+ 1 - 1
Terminal.Gui/Drawing/Color.Formatting.cs

@@ -501,7 +501,7 @@ public readonly partial record struct Color
     /// </summary>
     /// <param name="text">
     ///     The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
-    ///     "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="GetClosestNamedColor16(Terminal.Gui.Color)"/> string
+    ///     "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any W3C color name."/> string
     ///     values.
     /// </param>
     /// <param name="formatProvider">

+ 93 - 47
Terminal.Gui/Input/Key.cs

@@ -1,6 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
-using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
 
@@ -80,7 +79,7 @@ public class Key : EventArgs, IEquatable<Key>
     public Key (KeyCode k) { KeyCode = k; }
 
     /// <summary>
-    /// Copy constructor.
+    ///     Copy constructor.
     /// </summary>
     /// <param name="key">The Key to copy</param>
     public Key (Key key)
@@ -155,18 +154,13 @@ public class Key : EventArgs, IEquatable<Key>
     /// </remarks>
     public Rune AsRune => ToRune (KeyCode);
 
-    private bool _handled = false;
-
     /// <summary>
     ///     Indicates if the current Key event has already been processed and the driver should stop notifying any other
-    ///     event subscriber. It's important to set this value to true specially when updating any View's layout from inside the
+    ///     event subscriber. It's important to set this value to true specially when updating any View's layout from inside
+    ///     the
     ///     subscriber method.
     /// </summary>
-    public bool Handled
-    {
-        get => _handled;
-        set => _handled = value;
-    }
+    public bool Handled { get; set; }
 
     /// <summary>Gets a value indicating whether the Alt key was pressed (real or synthesized)</summary>
     /// <value><see langword="true"/> if is alternate; otherwise, <see langword="false"/>.</value>
@@ -339,11 +333,11 @@ public class Key : EventArgs, IEquatable<Key>
         switch (baseKey)
         {
             case >= KeyCode.A and <= KeyCode.Z when !key.HasFlag (KeyCode.ShiftMask):
-                return new Rune ((uint)(baseKey + 32));
+                return new ((uint)(baseKey + 32));
             case >= KeyCode.A and <= KeyCode.Z when key.HasFlag (KeyCode.ShiftMask):
-                return new Rune ((uint)baseKey);
+                return new ((uint)baseKey);
             case > KeyCode.Null and < KeyCode.A:
-                return new Rune ((uint)baseKey);
+                return new ((uint)baseKey);
         }
 
         if (Enum.IsDefined (typeof (KeyCode), baseKey))
@@ -351,7 +345,7 @@ public class Key : EventArgs, IEquatable<Key>
             return default (Rune);
         }
 
-        return new Rune ((uint)baseKey);
+        return new ((uint)baseKey);
     }
 
     #region Operators
@@ -381,17 +375,17 @@ public class Key : EventArgs, IEquatable<Key>
 
     /// <summary>Cast <see cref="KeyCode"/> to a <see cref="Key"/>.</summary>
     /// <param name="keyCode"></param>
-    public static implicit operator Key (KeyCode keyCode) { return new Key (keyCode); }
+    public static implicit operator Key (KeyCode keyCode) { return new (keyCode); }
 
     /// <summary>Cast <see langword="char"/> to a <see cref="Key"/>.</summary>
     /// <remarks>See <see cref="Key(char)"/> for more information.</remarks>
     /// <param name="ch"></param>
-    public static implicit operator Key (char ch) { return new Key (ch); }
+    public static implicit operator Key (char ch) { return new (ch); }
 
     /// <summary>Cast <see langword="string"/> to a <see cref="Key"/>.</summary>
     /// <remarks>See <see cref="Key(string)"/> for more information.</remarks>
     /// <param name="str"></param>
-    public static implicit operator Key (string str) { return new Key (str); }
+    public static implicit operator Key (string str) { return new (str); }
 
     /// <summary>Cast a <see cref="Key"/> to a <see langword="string"/>.</summary>
     /// <remarks>See <see cref="Key(string)"/> for more information.</remarks>
@@ -399,10 +393,7 @@ public class Key : EventArgs, IEquatable<Key>
     public static implicit operator string (Key key) { return key.ToString (); }
 
     /// <inheritdoc/>
-    public override bool Equals (object obj)
-    {
-        return obj is Key k && k.KeyCode == KeyCode && k.Handled == Handled;
-    }
+    public override bool Equals (object obj) { return obj is Key k && k.KeyCode == KeyCode && k.Handled == Handled; }
 
     bool IEquatable<Key>.Equals (Key other) { return Equals (other); }
 
@@ -568,7 +559,10 @@ public class Key : EventArgs, IEquatable<Key>
     /// <summary>Converts the provided string to a new <see cref="Key"/> instance.</summary>
     /// <param name="text">
     ///     The text to analyze. Formats supported are "Ctrl+X", "Alt+X", "Shift+X", "Ctrl+Alt+X",
-    ///     "Ctrl+Shift+X", "Alt+Shift+X", "Ctrl+Alt+Shift+X", and "X".
+    ///     "Ctrl+Shift+X", "Alt+Shift+X", "Ctrl+Alt+Shift+X", "X", and "120" (Unicode codepoint).
+    ///     <para>
+    ///         The separator can be any character, not just <see cref="Key.Separator"/> (e.g. "Ctrl@Alt@X").
+    ///     </para>
     /// </param>
     /// <param name="key">The parsed value.</param>
     /// <returns>A boolean value indicating whether parsing was successful.</returns>
@@ -577,38 +571,88 @@ public class Key : EventArgs, IEquatable<Key>
     {
         if (string.IsNullOrEmpty (text))
         {
-            key = Key.Empty;
+            key = Empty;
 
             return true;
         }
 
+        switch (text)
+        {
+            case "Ctrl":
+                key = KeyCode.CtrlMask;
+
+                return true;
+            case "Alt":
+                key = KeyCode.AltMask;
+
+                return true;
+            case "Shift":
+                key = KeyCode.ShiftMask;
+
+                return true;
+        }
+
         key = null;
 
-        // Split the string into parts
-        string [] parts = text.Split ('+', '-', (char)Separator.Value);
+        Rune separator = Separator;
 
-        if (parts.Length is 0 or > 4 || parts.Any (string.IsNullOrEmpty))
+        // Perhaps the separator was written using a different Key.Separator? Does the string
+        // start with "Ctrl", "Alt" or "Shift"? If so, get the char after the modifier string and use that as the separator.
+        if (text.StartsWith ("Ctrl", StringComparison.InvariantCultureIgnoreCase))
+        {
+            separator = (Rune)text [4];
+        }
+        else if (text.StartsWith ("Alt", StringComparison.InvariantCultureIgnoreCase))
+        {
+            separator = (Rune)text [3];
+        }
+        else if (text.StartsWith ("Shift", StringComparison.InvariantCultureIgnoreCase))
+        {
+            separator = (Rune)text [5];
+        }
+        else if (text.EndsWith ("Ctrl", StringComparison.InvariantCultureIgnoreCase))
         {
+            separator = (Rune)text [^5];
+        }
+        else if (text.EndsWith ("Alt", StringComparison.InvariantCultureIgnoreCase))
+        {
+            separator = (Rune)text [^4];
+        }
+        else if (text.EndsWith ("Shift", StringComparison.InvariantCultureIgnoreCase))
+        {
+            separator = (Rune)text [^6];
+        }
+
+        // Split the string into parts using the set Separator
+        string [] parts = text.Split ((char)separator.Value);
+
+        if (parts.Length is > 4)
+        {
+            // Invalid
             return false;
         }
 
-        // if it's just a shift key
-        if (parts.Length == 1)
+        // e.g. "Ctrl++"
+        if ((Rune)text [^1] != separator && parts.Any (string.IsNullOrEmpty))
         {
-            switch (parts [0])
-            {
-                case "Ctrl":
-                    key = KeyCode.CtrlMask;
+            // Invalid
+            return false;
+        }
 
-                    return true;
-                case "Alt":
-                    key = KeyCode.AltMask;
+        if ((Rune)text [^1] == separator)
+        {
+            parts [^1] = separator.Value.ToString ();
+            key = (char)separator.Value;
+        }
 
-                    return true;
-                case "Shift":
-                    key = KeyCode.ShiftMask;
+        if (separator != Separator && (parts.Length is 1 || (key is { } && parts.Length is 2)))
+        {
+            parts = text.Split ((char)separator.Value);
 
-                    return true;
+            if (parts.Length is 0 or > 4 || parts.Any (string.IsNullOrEmpty))
+            {
+                // Invalid
+                return false;
             }
         }
 
@@ -649,12 +693,12 @@ public class Key : EventArgs, IEquatable<Key>
                 {
                     if (parsedKeyCode is >= KeyCode.A and <= KeyCode.Z && modifiers == 0)
                     {
-                        key = new Key (parsedKeyCode | KeyCode.ShiftMask);
+                        key = new (parsedKeyCode | KeyCode.ShiftMask);
 
                         return true;
                     }
 
-                    key = new Key (parsedKeyCode | modifiers);
+                    key = new (parsedKeyCode | modifiers);
 
                     return true;
                 }
@@ -664,7 +708,8 @@ public class Key : EventArgs, IEquatable<Key>
             {
                 keyCode = keyCode & ~KeyCode.Space;
             }
-            key = new Key (keyCode | modifiers);
+
+            key = new (keyCode | modifiers);
 
             return true;
         }
@@ -675,7 +720,7 @@ public class Key : EventArgs, IEquatable<Key>
             {
                 if (parsedKeyCode is >= KeyCode.A and <= KeyCode.Z && modifiers == 0)
                 {
-                    key = new Key (parsedKeyCode | KeyCode.ShiftMask);
+                    key = new (parsedKeyCode | KeyCode.ShiftMask);
 
                     return true;
                 }
@@ -684,7 +729,8 @@ public class Key : EventArgs, IEquatable<Key>
                 {
                     parsedKeyCode = parsedKeyCode & ~KeyCode.Space;
                 }
-                key = new Key (parsedKeyCode | modifiers);
+
+                key = new (parsedKeyCode | modifiers);
 
                 return true;
             }
@@ -705,12 +751,12 @@ public class Key : EventArgs, IEquatable<Key>
 
             if ((KeyCode)parsedInt is >= KeyCode.A and <= KeyCode.Z && modifiers == 0)
             {
-                key = new Key ((KeyCode)parsedInt | KeyCode.ShiftMask);
+                key = new ((KeyCode)parsedInt | KeyCode.ShiftMask);
 
                 return true;
             }
 
-            key = new Key ((KeyCode)parsedInt);
+            key = new ((KeyCode)parsedInt);
 
             return true;
         }
@@ -722,7 +768,7 @@ public class Key : EventArgs, IEquatable<Key>
 
         if (GetIsKeyCodeAtoZ (parsedKeyCode))
         {
-            key = new Key (parsedKeyCode | (modifiers & ~KeyCode.Space));
+            key = new (parsedKeyCode | (modifiers & ~KeyCode.Space));
 
             return true;
         }

+ 32 - 0
UnitTests/Configuration/AttributeJsonConverterTests.cs

@@ -0,0 +1,32 @@
+using System.Text.Json;
+
+namespace Terminal.Gui.ConfigurationTests;
+
+public class AttributeJsonConverterTests
+{
+    [Fact]
+    public void TestDeserialize ()
+    {
+        // Test deserializing from human-readable color names
+        var json = "{\"Foreground\":\"Blue\",\"Background\":\"Green\"}";
+        var attribute = JsonSerializer.Deserialize<Attribute> (json, ConfigurationManagerTests._jsonOptions);
+        Assert.Equal (Color.Blue, attribute.Foreground.GetClosestNamedColor16 ());
+        Assert.Equal (Color.Green, attribute.Background.GetClosestNamedColor16 ());
+
+        // Test deserializing from RGB values
+        json = "{\"Foreground\":\"rgb(255,0,0)\",\"Background\":\"rgb(0,255,0)\"}";
+        attribute = JsonSerializer.Deserialize<Attribute> (json, ConfigurationManagerTests._jsonOptions);
+        Assert.Equal (Color.Red, attribute.Foreground.GetClosestNamedColor16 ());
+        Assert.Equal (Color.BrightGreen, attribute.Background.GetClosestNamedColor16 ());
+    }
+
+    [Fact]
+    [AutoInitShutdown]
+    public void TestSerialize ()
+    {
+        // Test serializing to human-readable color names
+        var attribute = new Attribute (Color.Blue, Color.Green);
+        string json = JsonSerializer.Serialize (attribute, ConfigurationManagerTests._jsonOptions);
+        Assert.Equal ("{\"Foreground\":\"Blue\",\"Background\":\"Green\"}", json);
+    }
+}

+ 180 - 0
UnitTests/Configuration/ColorJsonConverterTests.cs

@@ -0,0 +1,180 @@
+using System.Text;
+using System.Text.Json;
+
+namespace Terminal.Gui.ConfigurationTests;
+
+public class ColorJsonConverterTests
+{
+    [Theory]
+    [InlineData ("\"#000000\"", 0, 0, 0)]
+    public void DeserializesFromHexCode (string hexCode, int r, int g, int b)
+    {
+        // Arrange
+        var expected = new Color (r, g, b);
+
+        // Act
+        var actual = JsonSerializer.Deserialize<Color> (
+                                                        hexCode,
+                                                        new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
+                                                       );
+
+        //Assert
+        Assert.Equal (expected, actual);
+    }
+
+    [Theory]
+    [InlineData ("\"rgb(0,0,0)\"", 0, 0, 0)]
+    public void DeserializesFromRgb (string rgb, int r, int g, int b)
+    {
+        // Arrange
+        var expected = new Color (r, g, b);
+
+        // Act
+        var actual = JsonSerializer.Deserialize<Color> (
+                                                        rgb,
+                                                        new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
+                                                       );
+
+        //Assert
+        Assert.Equal (expected, actual);
+    }
+
+    [Theory]
+    [InlineData (ColorName16.Black, "Black")]
+    [InlineData (ColorName16.Blue, "Blue")]
+    [InlineData (ColorName16.Green, "Green")]
+    [InlineData (ColorName16.Cyan, "Cyan")]
+    [InlineData (ColorName16.Gray, "Gray")]
+    [InlineData (ColorName16.Red, "Red")]
+    [InlineData (ColorName16.Magenta, "Magenta")]
+    [InlineData (ColorName16.Yellow, "Yellow")]
+    [InlineData (ColorName16.DarkGray, "DarkGray")]
+    [InlineData (ColorName16.BrightBlue, "BrightBlue")]
+    [InlineData (ColorName16.BrightGreen, "BrightGreen")]
+    [InlineData (ColorName16.BrightCyan, "BrightCyan")]
+    [InlineData (ColorName16.BrightRed, "BrightRed")]
+    [InlineData (ColorName16.BrightMagenta, "BrightMagenta")]
+    [InlineData (ColorName16.BrightYellow, "BrightYellow")]
+    [InlineData (ColorName16.White, "White")]
+    public void SerializesColorName16ValuesAsStrings (ColorName16 colorName, string expectedJson)
+    {
+        var converter = new ColorJsonConverter ();
+        var options = new JsonSerializerOptions { Converters = { converter } };
+
+        string serialized = JsonSerializer.Serialize (new Color (colorName), options);
+
+        Assert.Equal ($"\"{expectedJson}\"", serialized);
+    }
+
+    [Theory]
+    [InlineData (1, 0, 0, "\"#010000\"")]
+    [InlineData (0, 0, 1, "\"#000001\"")]
+    public void SerializesToHexCode (int r, int g, int b, string expected)
+    {
+        // Arrange
+
+        // Act
+        string actual = JsonSerializer.Serialize (
+                                                  new Color (r, g, b),
+                                                  new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
+                                                 );
+
+        //Assert
+        Assert.Equal (expected, actual);
+    }
+
+    [Theory]
+    [InlineData ("Black", Color.Black)]
+    [InlineData ("Blue", Color.Blue)]
+    [InlineData ("BrightBlue", Color.BrightBlue)]
+    [InlineData ("BrightCyan", Color.BrightCyan)]
+    [InlineData ("BrightGreen", Color.BrightGreen)]
+    [InlineData ("BrightMagenta", Color.BrightMagenta)]
+    [InlineData ("BrightRed", Color.BrightRed)]
+    [InlineData ("BrightYellow", Color.BrightYellow)]
+    [InlineData ("Yellow", Color.Yellow)]
+    [InlineData ("Cyan", Color.Cyan)]
+    [InlineData ("DarkGray", Color.DarkGray)]
+    [InlineData ("Gray", Color.Gray)]
+    [InlineData ("Green", Color.Green)]
+    [InlineData ("Magenta", Color.Magenta)]
+    [InlineData ("Red", Color.Red)]
+    [InlineData ("White", Color.White)]
+    public void TestColorDeserializationFromHumanReadableColorName16 (string colorName, ColorName16 expectedColor)
+    {
+        // Arrange
+        var json = $"\"{colorName}\"";
+
+        // Act
+        var actualColor = JsonSerializer.Deserialize<Color> (json, ConfigurationManagerTests._jsonOptions);
+
+        // Assert
+        Assert.Equal (new Color (expectedColor), actualColor);
+    }
+
+    [Fact]
+    public void TestDeserializeColor_Black ()
+    {
+        // Arrange
+        var json = "\"Black\"";
+        var expectedColor = new Color ("Black");
+
+        // Act
+        var color = JsonSerializer.Deserialize<Color> (
+                                                       json,
+                                                       new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
+                                                      );
+
+        // Assert
+        Assert.Equal (expectedColor, color);
+    }
+
+    [Fact]
+    public void TestDeserializeColor_BrightRed ()
+    {
+        // Arrange
+        var json = "\"BrightRed\"";
+        var expectedColor = Color.BrightRed;
+
+        // Act
+        var color = JsonSerializer.Deserialize<Color> (
+                                                       json,
+                                                       new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
+                                                      );
+
+        // Assert
+        Assert.Equal (expectedColor, color);
+    }
+
+    [Fact]
+    public void TestSerializeColor_Black ()
+    {
+        // Arrange
+        var expectedJson = "\"Black\"";
+
+        // Act
+        string json = JsonSerializer.Serialize (
+                                                new Color (Color.Black),
+                                                new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
+                                               );
+
+        // Assert
+        Assert.Equal (expectedJson, json);
+    }
+
+    [Fact]
+    public void TestSerializeColor_BrightRed ()
+    {
+        // Arrange
+        var expectedJson = "\"BrightRed\"";
+
+        // Act
+        string json = JsonSerializer.Serialize (
+                                                new Color (Color.BrightRed),
+                                                new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
+                                               );
+
+        // Assert
+        Assert.Equal (expectedJson, json);
+    }
+}

+ 58 - 0
UnitTests/Configuration/ColorSchemeJsonConverterTests.cs

@@ -0,0 +1,58 @@
+using System.Text.Json;
+
+namespace Terminal.Gui.ConfigurationTests;
+
+public class ColorSchemeJsonConverterTests
+{
+    //string json = @"
+    //	{
+    //	""ColorSchemes"": {
+    //		""Base"": {
+    //			""normal"": {
+    //				""foreground"": ""White"",
+    //				""background"": ""Blue""
+    //   		            },
+    //			""focus"": {
+    //				""foreground"": ""Black"",
+    //				""background"": ""Gray""
+    //			    },
+    //			""hotNormal"": {
+    //				""foreground"": ""BrightCyan"",
+    //				""background"": ""Blue""
+    //			    },
+    //			""hotFocus"": {
+    //				""foreground"": ""BrightBlue"",
+    //				""background"": ""Gray""
+    //			    },
+    //			""disabled"": {
+    //				""foreground"": ""DarkGray"",
+    //				""background"": ""Blue""
+    //			    }
+    //		}
+    //		}
+    //	}";
+    [Fact]
+    [AutoInitShutdown]
+    public void TestColorSchemesSerialization ()
+    {
+        // Arrange
+        var expectedColorScheme = new ColorScheme
+        {
+            Normal = new Attribute (Color.White, Color.Blue),
+            Focus = new Attribute (Color.Black, Color.Gray),
+            HotNormal = new Attribute (Color.BrightCyan, Color.Blue),
+            HotFocus = new Attribute (Color.BrightBlue, Color.Gray),
+            Disabled = new Attribute (Color.DarkGray, Color.Blue)
+        };
+
+        string serializedColorScheme =
+            JsonSerializer.Serialize (expectedColorScheme, ConfigurationManagerTests._jsonOptions);
+
+        // Act
+        var actualColorScheme =
+            JsonSerializer.Deserialize<ColorScheme> (serializedColorScheme, ConfigurationManagerTests._jsonOptions);
+
+        // Assert
+        Assert.Equal (expectedColorScheme, actualColorScheme);
+    }
+}

+ 0 - 355
UnitTests/Configuration/JsonConverterTests.cs

@@ -1,355 +0,0 @@
-using System.Text;
-using System.Text.Encodings.Web;
-using System.Text.Json;
-using System.Text.Unicode;
-
-namespace Terminal.Gui.ConfigurationTests;
-
-public class ColorJsonConverterTests
-{
-    [Theory]
-    [InlineData ("\"#000000\"", 0, 0, 0)]
-    public void DeserializesFromHexCode (string hexCode, int r, int g, int b)
-    {
-        // Arrange
-        var expected = new Color (r, g, b);
-
-        // Act
-        var actual = JsonSerializer.Deserialize<Color> (
-                                                        hexCode,
-                                                        new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
-                                                       );
-
-        //Assert
-        Assert.Equal (expected, actual);
-    }
-
-    [Theory]
-    [InlineData ("\"rgb(0,0,0)\"", 0, 0, 0)]
-    public void DeserializesFromRgb (string rgb, int r, int g, int b)
-    {
-        // Arrange
-        var expected = new Color (r, g, b);
-
-        // Act
-        var actual = JsonSerializer.Deserialize<Color> (
-                                                        rgb,
-                                                        new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
-                                                       );
-
-        //Assert
-        Assert.Equal (expected, actual);
-    }
-
-    [Theory]
-    [InlineData (ColorName16.Black, "Black")]
-    [InlineData (ColorName16.Blue, "Blue")]
-    [InlineData (ColorName16.Green, "Green")]
-    [InlineData (ColorName16.Cyan, "Cyan")]
-    [InlineData (ColorName16.Gray, "Gray")]
-    [InlineData (ColorName16.Red, "Red")]
-    [InlineData (ColorName16.Magenta, "Magenta")]
-    [InlineData (ColorName16.Yellow, "Yellow")]
-    [InlineData (ColorName16.DarkGray, "DarkGray")]
-    [InlineData (ColorName16.BrightBlue, "BrightBlue")]
-    [InlineData (ColorName16.BrightGreen, "BrightGreen")]
-    [InlineData (ColorName16.BrightCyan, "BrightCyan")]
-    [InlineData (ColorName16.BrightRed, "BrightRed")]
-    [InlineData (ColorName16.BrightMagenta, "BrightMagenta")]
-    [InlineData (ColorName16.BrightYellow, "BrightYellow")]
-    [InlineData (ColorName16.White, "White")]
-    public void SerializesEnumValuesAsStrings (ColorName16 colorName, string expectedJson)
-    {
-        var converter = new ColorJsonConverter ();
-        var options = new JsonSerializerOptions { Converters = { converter } };
-
-        string serialized = JsonSerializer.Serialize (new Color (colorName), options);
-
-        Assert.Equal ($"\"{expectedJson}\"", serialized);
-    }
-
-    [Theory (Skip = "Not anymore. If a W3C color matches, that's used")]
-    [InlineData (0, 0, 0, "\"#000000\"")]
-    [InlineData (0, 0, 1, "\"#000001\"")]
-    public void SerializesToHexCode (int r, int g, int b, string expected)
-    {
-        // Arrange
-
-        // Act
-        string actual = JsonSerializer.Serialize (
-                                                  new Color (r, g, b),
-                                                  new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
-                                                 );
-
-        //Assert
-        Assert.Equal (expected, actual);
-    }
-
-    [Theory]
-    [InlineData ("Black", Color.Black)]
-    [InlineData ("Blue", Color.Blue)]
-    [InlineData ("BrightBlue", Color.BrightBlue)]
-    [InlineData ("BrightCyan", Color.BrightCyan)]
-    [InlineData ("BrightGreen", Color.BrightGreen)]
-    [InlineData ("BrightMagenta", Color.BrightMagenta)]
-    [InlineData ("BrightRed", Color.BrightRed)]
-    [InlineData ("BrightYellow", Color.BrightYellow)]
-    [InlineData ("Yellow", Color.Yellow)]
-    [InlineData ("Cyan", Color.Cyan)]
-    [InlineData ("DarkGray", Color.DarkGray)]
-    [InlineData ("Gray", Color.Gray)]
-    [InlineData ("Green", Color.Green)]
-    [InlineData ("Magenta", Color.Magenta)]
-    [InlineData ("Red", Color.Red)]
-    [InlineData ("White", Color.White)]
-    public void TestColorDeserializationFromHumanReadableColorName16 (string colorName, ColorName16 expectedColor)
-    {
-        // Arrange
-        var json = $"\"{colorName}\"";
-
-        // Act
-        var actualColor = JsonSerializer.Deserialize<Color> (json, ConfigurationManagerTests._jsonOptions);
-
-        // Assert
-        Assert.Equal (new Color (expectedColor), actualColor);
-    }
-
-    [Fact]
-    public void TestDeserializeColor_Black ()
-    {
-        // Arrange
-        var json = "\"Black\"";
-        var expectedColor = new Color ("Black");
-
-        // Act
-        var color = JsonSerializer.Deserialize<Color> (
-                                                       json,
-                                                       new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
-                                                      );
-
-        // Assert
-        Assert.Equal (expectedColor, color);
-    }
-
-    [Fact]
-    public void TestDeserializeColor_BrightRed ()
-    {
-        // Arrange
-        var json = "\"BrightRed\"";
-        var expectedColor = Color.BrightRed;
-
-        // Act
-        var color = JsonSerializer.Deserialize<Color> (
-                                                       json,
-                                                       new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
-                                                      );
-
-        // Assert
-        Assert.Equal (expectedColor, color);
-    }
-
-    [Fact]
-    public void TestSerializeColor_Black ()
-    {
-        // Arrange
-        var expectedJson = "\"Black\"";
-
-        // Act
-        string json = JsonSerializer.Serialize (
-                                                new Color (Color.Black),
-                                                new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
-                                               );
-
-        // Assert
-        Assert.Equal (expectedJson, json);
-    }
-
-    [Fact]
-    public void TestSerializeColor_BrightRed ()
-    {
-        // Arrange
-        var expectedJson = "\"BrightRed\"";
-
-        // Act
-        string json = JsonSerializer.Serialize (
-                                                new Color (Color.BrightRed),
-                                                new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
-                                               );
-
-        // Assert
-        Assert.Equal (expectedJson, json);
-    }
-}
-
-public class AttributeJsonConverterTests
-{
-    [Fact]
-    public void TestDeserialize ()
-    {
-        // Test deserializing from human-readable color names
-        var json = "{\"Foreground\":\"Blue\",\"Background\":\"Green\"}";
-        var attribute = JsonSerializer.Deserialize<Attribute> (json, ConfigurationManagerTests._jsonOptions);
-        Assert.Equal (Color.Blue, attribute.Foreground.GetClosestNamedColor16 ());
-        Assert.Equal (Color.Green, attribute.Background.GetClosestNamedColor16 ());
-
-        // Test deserializing from RGB values
-        json = "{\"Foreground\":\"rgb(255,0,0)\",\"Background\":\"rgb(0,255,0)\"}";
-        attribute = JsonSerializer.Deserialize<Attribute> (json, ConfigurationManagerTests._jsonOptions);
-        Assert.Equal (Color.Red, attribute.Foreground.GetClosestNamedColor16 ());
-        Assert.Equal (Color.BrightGreen, attribute.Background.GetClosestNamedColor16 ());
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void TestSerialize ()
-    {
-        // Test serializing to human-readable color names
-        var attribute = new Attribute (Color.Blue, Color.Green);
-        string json = JsonSerializer.Serialize (attribute, ConfigurationManagerTests._jsonOptions);
-        Assert.Equal ("{\"Foreground\":\"Blue\",\"Background\":\"Green\"}", json);
-    }
-}
-
-public class ColorSchemeJsonConverterTests
-{
-    //string json = @"
-    //	{
-    //	""ColorSchemes"": {
-    //		""Base"": {
-    //			""normal"": {
-    //				""foreground"": ""White"",
-    //				""background"": ""Blue""
-    //   		            },
-    //			""focus"": {
-    //				""foreground"": ""Black"",
-    //				""background"": ""Gray""
-    //			    },
-    //			""hotNormal"": {
-    //				""foreground"": ""BrightCyan"",
-    //				""background"": ""Blue""
-    //			    },
-    //			""hotFocus"": {
-    //				""foreground"": ""BrightBlue"",
-    //				""background"": ""Gray""
-    //			    },
-    //			""disabled"": {
-    //				""foreground"": ""DarkGray"",
-    //				""background"": ""Blue""
-    //			    }
-    //		}
-    //		}
-    //	}";
-    [Fact]
-    [AutoInitShutdown]
-    public void TestColorSchemesSerialization ()
-    {
-        // Arrange
-        var expectedColorScheme = new ColorScheme
-        {
-            Normal = new Attribute (Color.White, Color.Blue),
-            Focus = new Attribute (Color.Black, Color.Gray),
-            HotNormal = new Attribute (Color.BrightCyan, Color.Blue),
-            HotFocus = new Attribute (Color.BrightBlue, Color.Gray),
-            Disabled = new Attribute (Color.DarkGray, Color.Blue)
-        };
-
-        string serializedColorScheme =
-            JsonSerializer.Serialize (expectedColorScheme, ConfigurationManagerTests._jsonOptions);
-
-        // Act
-        var actualColorScheme =
-            JsonSerializer.Deserialize<ColorScheme> (serializedColorScheme, ConfigurationManagerTests._jsonOptions);
-
-        // Assert
-        Assert.Equal (expectedColorScheme, actualColorScheme);
-    }
-}
-
-public class KeyCodeJsonConverterTests
-{
-    [Theory]
-    [InlineData (KeyCode.A, "A")]
-    [InlineData (KeyCode.A | KeyCode.ShiftMask, "A, ShiftMask")]
-    [InlineData (KeyCode.A | KeyCode.CtrlMask, "A, CtrlMask")]
-    [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "A, CtrlMask, AltMask")]
-    [InlineData ((KeyCode)'a' | KeyCode.AltMask | KeyCode.CtrlMask, "Space, A, CtrlMask, AltMask")]
-    [InlineData ((KeyCode)'a' | KeyCode.ShiftMask, "Space, A, ShiftMask")]
-    [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "Delete, CtrlMask, AltMask")]
-    [InlineData (KeyCode.D4, "D4")]
-    [InlineData (KeyCode.Esc, "Esc")]
-    public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo)
-    {
-        // Arrange
-        var options = new JsonSerializerOptions ();
-        options.Converters.Add (new KeyCodeJsonConverter ());
-
-        // Act
-        string json = JsonSerializer.Serialize (key, options);
-        var deserializedKey = JsonSerializer.Deserialize<KeyCode> (json, options);
-
-        // Assert
-        Assert.Equal (expectedStringTo, deserializedKey.ToString ());
-    }
-}
-
-public class KeyJsonConverterTests
-{
-    [Theory]
-    [InlineData (KeyCode.A, "\"a\"")]
-    [InlineData ((KeyCode)'â', "\"â\"")]
-    [InlineData (KeyCode.A | KeyCode.ShiftMask, "\"A\"")]
-    [InlineData (KeyCode.A | KeyCode.CtrlMask, "\"Ctrl+A\"")]
-    [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "\"Ctrl+Alt+A\"")]
-    [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "\"Ctrl+Alt+Delete\"")]
-    [InlineData (KeyCode.D4, "\"4\"")]
-    [InlineData (KeyCode.Esc, "\"Esc\"")]
-    public void TestKey_Serialize (KeyCode key, string expected)
-    {
-        // Arrange
-        var options = new JsonSerializerOptions ();
-        options.Converters.Add (new KeyJsonConverter ());
-        options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
-
-        // Act
-        string json = JsonSerializer.Serialize ((Key)key, options);
-
-        // Assert
-        Assert.Equal (expected, json);
-    }
-
-    [Theory]
-    [InlineData (KeyCode.A, "a")]
-    [InlineData (KeyCode.A | KeyCode.ShiftMask, "A")]
-    [InlineData (KeyCode.A | KeyCode.CtrlMask, "Ctrl+A")]
-    [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+A")]
-    [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+Delete")]
-    [InlineData (KeyCode.D4, "4")]
-    [InlineData (KeyCode.Esc, "Esc")]
-    public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo)
-    {
-        // Arrange
-        var options = new JsonSerializerOptions ();
-        options.Converters.Add (new KeyJsonConverter ());
-        var encoderSettings = new TextEncoderSettings ();
-        encoderSettings.AllowCharacters ('+', '-');
-        encoderSettings.AllowRange (UnicodeRanges.BasicLatin);
-        options.Encoder = JavaScriptEncoder.Create (encoderSettings);
-
-        // Act
-        string json = JsonSerializer.Serialize ((Key)key, options);
-        var deserializedKey = JsonSerializer.Deserialize<Key> (json, options);
-
-        // Assert
-        Assert.Equal (expectedStringTo, deserializedKey.ToString ());
-    }
-
-    [Fact]
-    public void Separator_Property_Serializes_As_Glyph ()
-    {
-        // Act
-        string json = JsonSerializer.Serialize (Key.Separator, ConfigurationManager._serializerOptions);
-
-        // Assert
-        Assert.Equal ("\"+\"", json);
-    }
-}

+ 30 - 0
UnitTests/Configuration/KeyCodeJsonConverterTests.cs

@@ -0,0 +1,30 @@
+using System.Text.Json;
+
+namespace Terminal.Gui.ConfigurationTests;
+
+public class KeyCodeJsonConverterTests
+{
+    [Theory]
+    [InlineData (KeyCode.A, "A")]
+    [InlineData (KeyCode.A | KeyCode.ShiftMask, "A, ShiftMask")]
+    [InlineData (KeyCode.A | KeyCode.CtrlMask, "A, CtrlMask")]
+    [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "A, CtrlMask, AltMask")]
+    [InlineData ((KeyCode)'a' | KeyCode.AltMask | KeyCode.CtrlMask, "Space, A, CtrlMask, AltMask")]
+    [InlineData ((KeyCode)'a' | KeyCode.ShiftMask, "Space, A, ShiftMask")]
+    [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "Delete, CtrlMask, AltMask")]
+    [InlineData (KeyCode.D4, "D4")]
+    [InlineData (KeyCode.Esc, "Esc")]
+    public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo)
+    {
+        // Arrange
+        var options = new JsonSerializerOptions ();
+        options.Converters.Add (new KeyCodeJsonConverter ());
+
+        // Act
+        string json = JsonSerializer.Serialize (key, options);
+        var deserializedKey = JsonSerializer.Deserialize<KeyCode> (json, options);
+
+        // Assert
+        Assert.Equal (expectedStringTo, deserializedKey.ToString ());
+    }
+}

+ 127 - 0
UnitTests/Configuration/KeyJsonConverterTests.cs

@@ -0,0 +1,127 @@
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Unicode;
+
+namespace Terminal.Gui.ConfigurationTests;
+
+public class KeyJsonConverterTests
+{
+    [Theory]
+    [InlineData (KeyCode.A, "\"a\"")]
+    [InlineData ((KeyCode)'â', "\"â\"")]
+    [InlineData (KeyCode.A | KeyCode.ShiftMask, "\"A\"")]
+    [InlineData (KeyCode.A | KeyCode.CtrlMask, "\"Ctrl+A\"")]
+    [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "\"Ctrl+Alt+A\"")]
+    [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "\"Ctrl+Alt+Delete\"")]
+    [InlineData (KeyCode.D4, "\"4\"")]
+    [InlineData (KeyCode.Esc, "\"Esc\"")]
+    [InlineData ((KeyCode)'+' | KeyCode.AltMask | KeyCode.CtrlMask, "\"Ctrl+Alt++\"")]
+    public void TestKey_Serialize (KeyCode key, string expected)
+    {
+        // Arrange
+        var options = new JsonSerializerOptions ();
+        options.Converters.Add (new KeyJsonConverter ());
+        options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
+
+        // Act
+        string json = JsonSerializer.Serialize ((Key)key, options);
+
+        // Assert
+        Assert.Equal (expected, json);
+    }
+
+    [Theory]
+    [InlineData (KeyCode.A, "a")]
+    [InlineData (KeyCode.A | KeyCode.ShiftMask, "A")]
+    [InlineData (KeyCode.A | KeyCode.CtrlMask, "Ctrl+A")]
+    [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+A")]
+    [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+Delete")]
+    [InlineData (KeyCode.D4, "4")]
+    [InlineData (KeyCode.Esc, "Esc")]
+    [InlineData ((KeyCode)'+' | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt++")]
+    [InlineData ((KeyCode)'+' | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt++")]
+    public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo)
+    {
+        // Arrange
+
+        // Act
+        string json = JsonSerializer.Serialize ((Key)key, ConfigurationManager._serializerOptions);
+        var deserializedKey = JsonSerializer.Deserialize<Key> (json, ConfigurationManager._serializerOptions);
+
+        // Assert
+        Assert.Equal (expectedStringTo, deserializedKey.ToString ());
+    }
+
+    [Fact]
+    public void Separator_Property_Serializes_As_Glyph ()
+    {
+        // Act
+        string json = JsonSerializer.Serialize (Key.Separator, ConfigurationManager._serializerOptions);
+
+        // Assert
+        Assert.Equal ($"\"{Key.Separator}\"", json);
+    }
+
+    [Fact]
+    public void Separator_Property_Set_Changes_Serialization_Format ()
+    {
+        Rune savedSeparator = Key.Separator;
+
+        try
+        {
+            // Act
+            Key.Separator = (Rune)'*';
+            string json = JsonSerializer.Serialize (Key.Separator, ConfigurationManager._serializerOptions);
+
+            // Assert
+            Assert.Equal ("\"*\"", json);
+        }
+        finally
+        {
+            Key.Separator = savedSeparator;
+        }
+
+        Key.Separator = savedSeparator;
+    }
+
+    [Theory]
+    [InlineData ('A', '+', "\"Ctrl+Alt+A\"")]
+    [InlineData ('A', '-', "\"Ctrl+Alt+A\"")]
+    [InlineData ('A', '*', "\"Ctrl+Alt+A\"")]
+    [InlineData ('A', '@', "\"Ctrl+Alt+A\"")]
+    [InlineData ('A', '+', "\"Ctrl@Alt@A\"")]
+    [InlineData ('A', '-', "\"Ctrl@Alt@A\"")]
+    [InlineData ('A', '*', "\"Ctrl@Alt@A\"")]
+    [InlineData ('A', '@', "\"Ctrl@Alt@A\"")]
+    [InlineData ('+', '+', "\"Ctrl+Alt++\"")]
+    [InlineData ('+', '-', "\"Ctrl+Alt++\"")]
+    [InlineData ('+', '*', "\"Ctrl+Alt++\"")]
+    [InlineData ('+', '@', "\"Ctrl+Alt++\"")]
+    [InlineData ('+', '+', "\"Ctrl@Alt@+\"")]
+    [InlineData ('+', '-', "\"Ctrl@Alt@+\"")]
+    [InlineData ('+', '*', "\"Ctrl@Alt@+\"")]
+    [InlineData ('+', '@', "\"Ctrl@Alt@+\"")]
+    public void Separator_Property_Set_Deserialization_Works_With_Any (char keyChar, char separator, string json)
+    {
+        Rune savedSeparator = Key.Separator;
+
+        try
+        {
+            // Act
+            Key.Separator = (Rune)separator;
+
+            Key deserializedKey = JsonSerializer.Deserialize<Key> (json, ConfigurationManager._serializerOptions);
+
+            Key expectedKey = new Key ((KeyCode)keyChar).WithCtrl.WithAlt;
+            // Assert
+            Assert.Equal (expectedKey, deserializedKey);
+        }
+        finally
+        {
+            Key.Separator = savedSeparator;
+        }
+
+        Key.Separator = savedSeparator;
+    }
+}

+ 13 - 13
UnitTests/Input/KeyTests.cs

@@ -17,9 +17,9 @@ public class KeyTests
             { "Alt+A", Key.A.WithAlt },
             { "Shift+A", Key.A.WithShift },
             { "A", Key.A.WithShift },
-            { "â", new Key ((KeyCode)'â') },
-            { "Shift+â", new Key ((KeyCode)'â' | KeyCode.ShiftMask) },
-            { "Shift+Â", new Key ((KeyCode)'Â' | KeyCode.ShiftMask) },
+            { "â", new ((KeyCode)'â') },
+            { "Shift+â", new ((KeyCode)'â' | KeyCode.ShiftMask) },
+            { "Shift+Â", new ((KeyCode)'Â' | KeyCode.ShiftMask) },
             { "Ctrl+Shift+CursorUp", Key.CursorUp.WithShift.WithCtrl },
             { "Ctrl+Alt+Shift+CursorUp", Key.CursorUp.WithShift.WithCtrl.WithAlt },
             { "ctrl+alt+shift+cursorup", Key.CursorUp.WithShift.WithCtrl.WithAlt },
@@ -439,7 +439,7 @@ public class KeyTests
     [Fact]
     public void ToString_ShouldReturnReadableString ()
     {
-        var eventArgs = Key.A.WithCtrl;
+        Key eventArgs = Key.A.WithCtrl;
         Assert.Equal ("Ctrl+A", eventArgs.ToString ());
     }
 
@@ -454,8 +454,6 @@ public class KeyTests
     [Theory]
     [InlineData ("aa")]
     [InlineData ("-1")]
-    [InlineData ("Crtl-A")]
-    [InlineData ("Ctrl=A")]
     [InlineData ("Crtl")]
     [InlineData ("99a")]
     [InlineData ("a99")]
@@ -508,6 +506,8 @@ public class KeyTests
     [InlineData ("Alt-A", KeyCode.A | KeyCode.AltMask)]
     [InlineData ("A-Ctrl", KeyCode.A | KeyCode.CtrlMask)]
     [InlineData ("Alt-A-Ctrl", KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask)]
+    [InlineData ("120", KeyCode.X)]
+    [InlineData ("Ctrl@A", KeyCode.A | KeyCode.CtrlMask)]
     public void TryParse_ShouldReturnTrue_WhenValidKey (string keyString, KeyCode expected)
     {
         Assert.True (Key.TryParse (keyString, out Key key));
@@ -518,7 +518,7 @@ public class KeyTests
     [Fact]
     public void WithShift_ShouldReturnCorrectValue ()
     {
-        var a = Key.A;
+        Key a = Key.A;
         Assert.Equal (KeyCode.A | KeyCode.ShiftMask, a.WithShift);
 
         Key CAD = Key.Delete.WithCtrl.WithAlt;
@@ -529,17 +529,17 @@ public class KeyTests
     [Fact]
     public void Equals_ShouldReturnTrue_WhenEqual ()
     {
-        var a = Key.A;
-        var b = Key.A;
+        Key a = Key.A;
+        Key b = Key.A;
         Assert.True (a.Equals (b));
     }
 
     [Fact]
     public void Equals_Handled_Changed_ShouldReturnTrue_WhenEqual ()
     {
-        var a = Key.A;
+        Key a = Key.A;
         a.Handled = true;
-        var b = Key.A;
+        Key b = Key.A;
         b.Handled = true;
         Assert.True (a.Equals (b));
     }
@@ -547,9 +547,9 @@ public class KeyTests
     [Fact]
     public void Equals_Handled_Changed_ShouldReturnFalse_WhenNotEqual ()
     {
-        var a = Key.A;
+        Key a = Key.A;
         a.Handled = true;
-        var b = Key.A;
+        Key b = Key.A;
         Assert.False (a.Equals (b));
     }