diff --git a/Example/Example.cs b/Example/Example.cs index 39126f4d92..1d13f47b26 100644 --- a/Example/Example.cs +++ b/Example/Example.cs @@ -6,6 +6,9 @@ using System; using Terminal.Gui; +// Override the default configuraiton for the application to use the Light theme +ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }"""; + Application.Run ().Dispose (); // Before the application exits, reset Terminal.Gui for clean shutdown diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 3d47aeb410..47c07a19f3 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -86,12 +86,14 @@ internal static void InternalInit ( // We're running unit tests. Disable loading config files other than default if (Locations == ConfigLocations.All) { - Locations = ConfigLocations.DefaultOnly; + Locations = ConfigLocations.Default; Reset (); } } } + AddApplicationKeyBindings (); + // Start the process of configuration management. // Note that we end up calling LoadConfigurationFromAllSources // multiple times. We need to do this because some settings are only @@ -106,8 +108,6 @@ internal static void InternalInit ( } Apply (); - AddApplicationKeyBindings (); - // Ignore Configuration for ForceDriver if driverName is specified if (!string.IsNullOrEmpty (driverName)) { diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index a883dea8e5..18881d6c57 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -290,7 +290,15 @@ private static void ReplaceKey (Key oldKey, Key newKey) } else { - KeyBindings.ReplaceKey (oldKey, newKey); + if (KeyBindings.TryGet(oldKey, out KeyBinding binding)) + { + KeyBindings.Remove (oldKey); + KeyBindings.Add (newKey, binding); + } + else + { + KeyBindings.Add (newKey, binding); + } } } diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 898fc935b0..7511f82161 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -1,6 +1,7 @@ #nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Microsoft.CodeAnalysis.Diagnostics; namespace Terminal.Gui; diff --git a/Terminal.Gui/Configuration/ConfigLocations.cs b/Terminal.Gui/Configuration/ConfigLocations.cs new file mode 100644 index 0000000000..c2499c6c38 --- /dev/null +++ b/Terminal.Gui/Configuration/ConfigLocations.cs @@ -0,0 +1,57 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Describes the location of the configuration files. The constants can be combined (bitwise) to specify multiple +/// locations. The more significant the bit, the higher the priority meaning that the last location will override the +/// earlier ones. +/// + +[Flags] +public enum ConfigLocations +{ + /// No configuration will be loaded. + /// + /// Used for development and testing only. For Terminal,Gui to function properly, at least + /// should be set. + /// + None = 0, + + /// + /// Deafult configuration in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json). + /// + Default = 0b_0000_0001, + + /// + /// Global settings in the current directory (e.g. ./.tui/config.json). + /// + GlobalCurrent = 0b_0000_0010, + + /// + /// Global settings in the home directory (e.g. ~/.tui/config.json). + /// + GlobalHome = 0b_0000_0100, + + /// + /// App resources (e.g. MyApp.Resources.config.json). + /// + AppResources = 0b_0000_1000, + + /// + /// App settings in the current directory (e.g. ./.tui/MyApp.config.json). + /// + AppCurrent = 0b_0001_0000, + + /// + /// App settings in the home directory (e.g. ~/.tui/MyApp.config.json). + /// + AppHome = 0b_0010_0000, + + /// + /// Settings in the static property. + /// + Runtime = 0b_0100_0000, + + /// This constant is a combination of all locations + All = 0b_1111_1111 +} diff --git a/Terminal.Gui/Configuration/ConfigProperty.cs b/Terminal.Gui/Configuration/ConfigProperty.cs index 0c4a41682a..5a132b48c0 100644 --- a/Terminal.Gui/Configuration/ConfigProperty.cs +++ b/Terminal.Gui/Configuration/ConfigProperty.cs @@ -31,44 +31,42 @@ public class ConfigProperty /// public object? PropertyValue { get; set; } - /// Applies the to the property described by . + /// Applies the to the static property described by . /// public bool Apply () { - if (PropertyValue is { }) + try { - try + if (PropertyInfo?.GetValue (null) is { }) { - if (PropertyInfo?.GetValue (null) is { }) - { - PropertyInfo?.SetValue (null, DeepMemberWiseCopy (PropertyValue, PropertyInfo?.GetValue (null))); - } + var val = DeepMemberWiseCopy (PropertyValue, PropertyInfo?.GetValue (null)); + PropertyInfo?.SetValue (null, val); } - catch (TargetInvocationException tie) + } + catch (TargetInvocationException tie) + { + // Check if there is an inner exception + if (tie.InnerException is { }) { - // Check if there is an inner exception - if (tie.InnerException is { }) - { - // Handle the inner exception separately without catching the outer exception - Exception? innerException = tie.InnerException; + // Handle the inner exception separately without catching the outer exception + Exception? innerException = tie.InnerException; - // Handle the inner exception here - throw new JsonException ( - $"Error Applying Configuration Change: {innerException.Message}", - innerException - ); - } - - // Handle the outer exception or rethrow it if needed - throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie); - } - catch (ArgumentException ae) - { + // Handle the inner exception here throw new JsonException ( - $"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}", - ae + $"Error Applying Configuration Change: {innerException.Message}", + innerException ); } + + // Handle the outer exception or rethrow it if needed + throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie); + } + catch (ArgumentException ae) + { + throw new JsonException ( + $"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}", + ae + ); } return PropertyValue != null; @@ -94,6 +92,12 @@ public static string GetJsonPropertyName (PropertyInfo pi) /// public object? RetrieveValue () { return PropertyValue = PropertyInfo!.GetValue (null); } + /// + /// Updates (using reflection) with the value in using a deep memberwise copy. + /// + /// + /// + /// internal object? UpdateValueFrom (object source) { if (source is null) diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs index cd5befa16a..dff4abed43 100644 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ b/Terminal.Gui/Configuration/ConfigurationManager.cs @@ -24,7 +24,7 @@ namespace Terminal.Gui; /// /// /// Settings are defined in JSON format, according to this schema: -/// https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json +/// https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json /// /// /// Settings that will apply to all applications (global settings) reside in files named config.json. @@ -53,30 +53,6 @@ namespace Terminal.Gui; [ComponentGuarantees (ComponentGuaranteesOptions.None)] public static class ConfigurationManager { - /// - /// Describes the location of the configuration files. The constants can be combined (bitwise) to specify multiple - /// locations. - /// - [Flags] - public enum ConfigLocations - { - /// No configuration will be loaded. - /// - /// Used for development and testing only. For Terminal,Gui to function properly, at least - /// should be set. - /// - None = 0, - - /// - /// Global configuration in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json) -- - /// Lowest Precedence. - /// - DefaultOnly, - - /// This constant is a combination of all locations - All = -1 - } - /// /// A dictionary of all properties in the Terminal.Gui project that are decorated with the /// attribute. The keys are the property names pre-pended with the @@ -201,6 +177,7 @@ public static void Apply () { // First start. Apply settings first. This ensures if a config sets Theme to something other than "Default", it gets used settings = Settings?.Apply () ?? false; + themes = !string.IsNullOrEmpty (ThemeManager.SelectedTheme) && (ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false); } @@ -210,6 +187,7 @@ public static void Apply () themes = ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false; settings = Settings?.Apply () ?? false; } + appSettings = AppSettings?.Apply () ?? false; } catch (JsonException e) @@ -243,14 +221,23 @@ public static string GetEmptyJson () } /// - /// Loads all settings found in the various configuration storage locations to the - /// . Optionally, resets all settings attributed with + /// Gets or sets the in-memory config.json. See . + /// + public static string? RuntimeConfig { get; set; } + + /// + /// Loads all settings found in the configuration storage locations (). Optionally, resets + /// all settings attributed with /// to the defaults. /// - /// Use to cause the loaded settings to be applied to the running application. + /// + /// + /// Use to cause the loaded settings to be applied to the running application. + /// + /// /// /// If the state of will be reset to the - /// defaults. + /// defaults (). /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] @@ -263,8 +250,17 @@ public static void Load (bool reset = false) Reset (); } - // LibraryResources is always loaded by Reset - if (Locations == ConfigLocations.All) + if (Locations.HasFlag (ConfigLocations.GlobalCurrent)) + { + Settings?.Update ($"./.tui/{_configFilename}"); + } + + if (Locations.HasFlag (ConfigLocations.GlobalHome)) + { + Settings?.Update ($"~/.tui/{_configFilename}"); + } + + if (Locations.HasFlag (ConfigLocations.AppResources)) { string? embeddedStylesResourceName = Assembly.GetEntryAssembly () ? @@ -276,27 +272,25 @@ public static void Load (bool reset = false) embeddedStylesResourceName = _configFilename; } - Settings = Settings? - - // Global current directory - .Update ($"./.tui/{_configFilename}") - ? - - // Global home directory - .Update ($"~/.tui/{_configFilename}") - ? + Settings?.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!); + } - // App resources - .UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!) - ? + if (Locations.HasFlag (ConfigLocations.AppCurrent)) + { + Settings?.Update ($"./.tui/{AppName}.{_configFilename}"); + } - // App current directory - .Update ($"./.tui/{AppName}.{_configFilename}") - ? + if (Locations.HasFlag (ConfigLocations.AppHome)) + { + Settings?.Update ($"~/.tui/{AppName}.{_configFilename}"); + } - // App home directory - .Update ($"~/.tui/{AppName}.{_configFilename}"); + if (Locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig)) + { + Settings?.Update (RuntimeConfig, "ConfigurationManager.Memory"); } + + ThemeManager.SelectedTheme = Settings!["Theme"].PropertyValue as string ?? "Default"; } /// @@ -314,12 +308,13 @@ public static void OnApplied () } /// - /// Called when the configuration has been updated from a configuration file. Invokes the + /// Called when the configuration has been updated from a configuration file or reset. Invokes the + /// /// event. /// public static void OnUpdated () { - Debug.WriteLine (@"ConfigurationManager.OnApplied()"); + Debug.WriteLine (@"ConfigurationManager.OnUpdated()"); Updated?.Invoke (null, new ()); } @@ -359,7 +354,7 @@ public static void Reset () AppSettings = new (); // To enable some unit tests, we only load from resources if the flag is set - if (Locations.HasFlag (ConfigLocations.DefaultOnly)) + if (Locations.HasFlag (ConfigLocations.Default)) { Settings.UpdateFromResource ( typeof (ConfigurationManager).Assembly, @@ -367,12 +362,14 @@ public static void Reset () ); } + OnUpdated (); + Apply (); ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply (); AppSettings?.Apply (); } - /// Event fired when the configuration has been updated from a configuration source. application. + /// Event fired when the configuration has been updated from a configuration source or reset. public static event EventHandler? Updated; internal static void AddJsonError (string error) @@ -414,7 +411,13 @@ internal static void AddJsonError (string error) } // If value type, just use copy constructor. - if (source.GetType ().IsValueType || source.GetType () == typeof (string)) + if (source.GetType ().IsValueType || source is string) + { + return source; + } + + // HACK: Key is a class, but we want to treat it as a value type so just _keyCode gets copied. + if (source.GetType () == typeof (Key)) { return source; } @@ -425,9 +428,6 @@ internal static void AddJsonError (string error) { foreach (object? srcKey in ((IDictionary)source).Keys) { - if (srcKey is string) - { } - if (((IDictionary)destination).Contains (srcKey)) { ((IDictionary)destination) [srcKey] = @@ -477,13 +477,14 @@ where destProp.CanWrite } } - return destination!; + return destination; } + /// - /// Retrieves the hard coded default settings from the Terminal.Gui library implementation. Used in development of - /// the library to generate the default configuration file. Before calling Application.Init, make sure - /// is set to . + /// Retrieves the hard coded default settings (static properites) from the Terminal.Gui library implementation. Used in + /// development of + /// the library to generate the default configuration file. /// /// /// @@ -552,9 +553,12 @@ where type.GetProperties () let props = c.Value .GetProperties ( BindingFlags.Instance - | BindingFlags.Static - | BindingFlags.NonPublic - | BindingFlags.Public + | + BindingFlags.Static + | + BindingFlags.NonPublic + | + BindingFlags.Public ) .Where ( prop => @@ -577,17 +581,13 @@ from p in enumerable scp.OmitClassName ? ConfigProperty.GetJsonPropertyName (p) : $"{p.DeclaringType?.Name}.{p.Name}", - new() { PropertyInfo = p, PropertyValue = null } + new () { PropertyInfo = p, PropertyValue = null } ); } else { throw new ( - $"Property { - p.Name - } in class { - p.DeclaringType?.Name - } is not static. All SerializableConfigurationProperty properties must be static." + $"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static." ); } } diff --git a/Terminal.Gui/Configuration/SettingsScope.cs b/Terminal.Gui/Configuration/SettingsScope.cs index 2ac4bf4c13..983d790a11 100644 --- a/Terminal.Gui/Configuration/SettingsScope.cs +++ b/Terminal.Gui/Configuration/SettingsScope.cs @@ -117,8 +117,13 @@ public class SettingsScope : Scope /// The source (filename/resource name) the Json document was read from. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public SettingsScope? Update (string json, string source) + public SettingsScope? Update (string? json, string source) { + //if (string.IsNullOrEmpty (json)) + //{ + // Debug.WriteLine ($"ConfigurationManager: Configuration file \"{source}\" is empty."); + // return this; + //} var stream = new MemoryStream (); var writer = new StreamWriter (stream); writer.Write (json); diff --git a/Terminal.Gui/Configuration/ThemeManager.cs b/Terminal.Gui/Configuration/ThemeManager.cs index b542c9f2c3..8eb54fd29f 100644 --- a/Terminal.Gui/Configuration/ThemeManager.cs +++ b/Terminal.Gui/Configuration/ThemeManager.cs @@ -110,7 +110,9 @@ internal static string SelectedTheme string oldTheme = _theme; _theme = value; - if ((oldTheme != _theme || oldTheme != Settings! ["Theme"].PropertyValue as string) && Settings! ["Themes"]?.PropertyValue is Dictionary themes && themes.ContainsKey (_theme)) + if ((oldTheme != _theme + || oldTheme != Settings! ["Theme"].PropertyValue as string) + && Settings! ["Themes"]?.PropertyValue is Dictionary themes && themes.ContainsKey (_theme)) { Settings! ["Theme"].PropertyValue = _theme; Instance.OnThemeChanged (oldTheme); diff --git a/Terminal.Gui/Input/Key.cs b/Terminal.Gui/Input/Key.cs index 11f8d938e2..70d078e0ca 100644 --- a/Terminal.Gui/Input/Key.cs +++ b/Terminal.Gui/Input/Key.cs @@ -393,24 +393,31 @@ public static Rune ToRune (KeyCode key) public static implicit operator string (Key key) { return key.ToString (); } /// - public override bool Equals (object obj) { return obj is Key k && k.KeyCode == KeyCode && k.Handled == Handled; } + public override bool Equals (object obj) + { + if (obj is Key other) + { + return other._keyCode == _keyCode && other.Handled == Handled; + } + return false; + } bool IEquatable.Equals (Key other) { return Equals (other); } /// - public override int GetHashCode () { return (int)KeyCode; } + public override int GetHashCode () { return _keyCode.GetHashCode (); } /// Compares two s for equality. /// /// /// - public static bool operator == (Key a, Key b) { return a?.KeyCode == b?.KeyCode; } + public static bool operator == (Key a, Key b) { return a!.Equals (b); } /// Compares two s for not equality. /// /// /// - public static bool operator != (Key a, Key b) { return a?.KeyCode != b?.KeyCode; } + public static bool operator != (Key a, Key b) { return !a!.Equals (b); } /// Compares two s for less-than. /// diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs index e2f0ed8beb..59a97bfd69 100644 --- a/Terminal.Gui/Input/KeyBindings.cs +++ b/Terminal.Gui/Input/KeyBindings.cs @@ -46,7 +46,11 @@ public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null) binding.BoundView = boundViewForAppScope; } - Bindings.Add (key, binding); + // IMPORTANT: Add a COPY of the key. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy + // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus + // IMPORTANT: Apply will update the Dictionary with the new key, but the old key will still be in the dictionary. + // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. + Bindings.Add (new (key), binding); } /// @@ -213,7 +217,7 @@ public void Add (Key key, params Command [] commands) // TODO: Add a dictionary comparer that ignores Scope // TODO: This should not be public! /// The collection of objects. - public Dictionary Bindings { get; } = new (); + public Dictionary Bindings { get; } = new (new KeyEqualityComparer ()); /// /// The view that the are bound to. @@ -388,15 +392,23 @@ public bool TryGet (Key key, out KeyBinding binding) /// if the Key is bound; otherwise . public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) { - binding = new (Array.Empty (), KeyBindingScope.Disabled, null); + if (!key.IsValid) + { + binding = new (Array.Empty (), KeyBindingScope.Disabled, null); + return false; + } - if (key.IsValid && Bindings.TryGetValue (key, out binding)) + if (Bindings.TryGetValue (key, out binding)) { if (scope.HasFlag (binding.Scope)) { return true; } } + else + { + binding = new (Array.Empty (), KeyBindingScope.Disabled, null); + } return false; } diff --git a/Terminal.Gui/Input/KeyEqualityComparer.cs b/Terminal.Gui/Input/KeyEqualityComparer.cs new file mode 100644 index 0000000000..fe02f13dc6 --- /dev/null +++ b/Terminal.Gui/Input/KeyEqualityComparer.cs @@ -0,0 +1,35 @@ +#nullable enable +using Terminal.Gui; + +/// +/// +/// +public class KeyEqualityComparer : IEqualityComparer +{ + /// + public bool Equals (Key? x, Key? y) + { + if (ReferenceEquals (x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return x.KeyCode == y.KeyCode; + } + + /// + public int GetHashCode (Key? obj) + { + if (obj is null) + { + return 0; + } + + return obj.KeyCode.GetHashCode (); + } +} diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 3428fcd15b..38400d8a2a 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -1,4 +1,5 @@ using Xunit.Abstractions; +using static Terminal.Gui.ConfigurationManager; // Alias Console to MockConsole so we don't accidentally use Console @@ -10,7 +11,7 @@ public ApplicationTests (ITestOutputHelper output) { _output = output; ConsoleDriver.RunningUnitTests = true; - ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.None; + Locations = ConfigLocations.Default; #if DEBUG_IDISPOSABLE View.Instances.Clear (); @@ -272,14 +273,15 @@ public void Init_Null_Driver_Should_Pick_A_Driver () [InlineData (typeof (CursesDriver))] public void Init_ResetState_Resets_Properties (Type driverType) { - ConfigurationManager.ThrowOnJsonErrors = true; + ThrowOnJsonErrors = true; // For all the fields/properties of Application, check that they are reset to their default values // Set some values Application.Init (driverName: driverType.Name); - // Application.IsInitialized = true; + + // Application.IsInitialized = true; // Reset Application.ResetState (); @@ -370,7 +372,7 @@ void CheckReset () Application.ResetState (); CheckReset (); - ConfigurationManager.ThrowOnJsonErrors = false; + ThrowOnJsonErrors = false; } [Fact] @@ -398,10 +400,7 @@ public void Init_Shutdown_Cleans_Up () } [Fact] - public void Shutdown_Alone_Does_Nothing () - { - Application.Shutdown (); - } + public void Shutdown_Alone_Does_Nothing () { Application.Shutdown (); } [Theory] [InlineData (typeof (FakeDriver))] @@ -520,6 +519,48 @@ public void Init_NoParam_ForceDriver_Works () Application.ResetState (); } + [Fact] + public void Init_KeyBindings_Set_To_Defaults () + { + // arrange + Locations = ConfigLocations.All; + ThrowOnJsonErrors = true; + + Application.QuitKey = Key.Q; + + Application.Init (new FakeDriver ()); + + Assert.Equal (Key.Esc, Application.QuitKey); + + Application.Shutdown (); + } + + [Fact] + public void Init_KeyBindings_Set_To_Custom () + { + // arrange + Locations = ConfigLocations.Runtime; + ThrowOnJsonErrors = true; + + RuntimeConfig = """ + { + "Application.QuitKey": "Ctrl-Q" + } + """; + + Assert.Equal (Key.Esc, Application.QuitKey); + + // Act + Application.Init (new FakeDriver ()); + + Assert.Equal (Key.Q.WithCtrl, Application.QuitKey); + + Assert.Contains (Key.Q.WithCtrl, Application.KeyBindings.Bindings); + + Application.Shutdown (); + Locations = ConfigLocations.Default; + } + [Fact] [AutoInitShutdown (verifyShutdown: true)] public void Internal_Properties_Correct () diff --git a/UnitTests/Configuration/AppScopeTests.cs b/UnitTests/Configuration/AppScopeTests.cs index 059f724d21..d09e74f1d4 100644 --- a/UnitTests/Configuration/AppScopeTests.cs +++ b/UnitTests/Configuration/AppScopeTests.cs @@ -15,7 +15,7 @@ public class AppScopeTests }; [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void Apply_ShouldApplyUpdatedProperties () { Reset (); diff --git a/UnitTests/Configuration/ConfigPropertyTests.cs b/UnitTests/Configuration/ConfigPropertyTests.cs new file mode 100644 index 0000000000..0bf96dc6e3 --- /dev/null +++ b/UnitTests/Configuration/ConfigPropertyTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using Terminal.Gui; +using Xunit; + +public class ConfigPropertyTests +{ + [Fact] + public void Apply_PropertyValueIsAppliedToStatic_String_Property() + { + // Arrange + TestConfiguration.Reset (); + var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty)); + var configProperty = new ConfigProperty + { + PropertyInfo = propertyInfo, + PropertyValue = "UpdatedValue" + }; + + // Act + var result = configProperty.Apply(); + + // Assert + Assert.Equal (1, TestConfiguration.TestStringPropertySetCount); + Assert.True(result); + Assert.Equal("UpdatedValue", TestConfiguration.TestStringProperty); + TestConfiguration.Reset (); + } + + [Fact] + public void Apply_PropertyValueIsAppliedToStatic_Key_Property () + { + // Arrange + TestConfiguration.Reset (); + var propertyInfo = typeof (TestConfiguration).GetProperty (nameof (TestConfiguration.TestKeyProperty)); + var configProperty = new ConfigProperty + { + PropertyInfo = propertyInfo, + PropertyValue = Key.Q.WithCtrl + }; + + // Act + var result = configProperty.Apply (); + + // Assert + Assert.Equal(1, TestConfiguration.TestKeyPropertySetCount); + Assert.True (result); + Assert.Equal (Key.Q.WithCtrl, TestConfiguration.TestKeyProperty); + TestConfiguration.Reset (); + } + + [Fact] + public void RetrieveValue_GetsCurrentValueOfStaticProperty() + { + // Arrange + TestConfiguration.TestStringProperty = "CurrentValue"; + var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty)); + var configProperty = new ConfigProperty + { + PropertyInfo = propertyInfo + }; + + // Act + var value = configProperty.RetrieveValue(); + + // Assert + Assert.Equal("CurrentValue", value); + Assert.Equal("CurrentValue", configProperty.PropertyValue); + } + + [Fact] + public void UpdateValueFrom_Updates_String_Property_Value () + { + // Arrange + TestConfiguration.Reset (); + var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty)); + var configProperty = new ConfigProperty + { + PropertyInfo = propertyInfo, + PropertyValue = "InitialValue" + }; + + // Act + var updatedValue = configProperty.UpdateValueFrom("NewValue"); + + // Assert + Assert.Equal (0, TestConfiguration.TestStringPropertySetCount); + Assert.Equal("NewValue", updatedValue); + Assert.Equal("NewValue", configProperty.PropertyValue); + TestConfiguration.Reset (); + } + + //[Fact] + //public void UpdateValueFrom_InvalidType_ThrowsArgumentException() + //{ + // // Arrange + // var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty)); + // var configProperty = new ConfigProperty + // { + // PropertyInfo = propertyInfo + // }; + + // // Act & Assert + // Assert.Throws(() => configProperty.UpdateValueFrom(123)); + //} + + [Fact] + public void Apply_TargetInvocationException_ThrowsJsonException() + { + // Arrange + var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty)); + var configProperty = new ConfigProperty + { + PropertyInfo = propertyInfo, + PropertyValue = null // This will cause ArgumentNullException in the set accessor + }; + + // Act & Assert + var exception = Assert.Throws (() => configProperty.Apply()); + } + + [Fact] + public void GetJsonPropertyName_ReturnsJsonPropertyNameAttributeValue() + { + // Arrange + var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty)); + + // Act + var jsonPropertyName = ConfigProperty.GetJsonPropertyName(propertyInfo); + + // Assert + Assert.Equal("TestStringProperty", jsonPropertyName); + } +} + +public class TestConfiguration +{ + private static string _testStringProperty = "Default"; + public static int TestStringPropertySetCount { get; set; } + + [SerializableConfigurationProperty] + public static string TestStringProperty + { + get => _testStringProperty; + set + { + TestStringPropertySetCount++; + _testStringProperty = value ?? throw new ArgumentNullException (nameof (value)); + } + } + + private static Key _testKeyProperty = Key.Esc; + + public static int TestKeyPropertySetCount { get; set; } + + [SerializableConfigurationProperty] + public static Key TestKeyProperty + { + get => _testKeyProperty; + set + { + TestKeyPropertySetCount++; + _testKeyProperty = value ?? throw new ArgumentNullException (nameof (value)); + } + } + + public static void Reset () + { + TestStringPropertySetCount = 0; + TestKeyPropertySetCount = 0; + } +} diff --git a/UnitTests/Configuration/ConfigurationMangerTests.cs b/UnitTests/Configuration/ConfigurationMangerTests.cs index 5f8e767c01..3c6c501744 100644 --- a/UnitTests/Configuration/ConfigurationMangerTests.cs +++ b/UnitTests/Configuration/ConfigurationMangerTests.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics; +using System.Reflection; using System.Text.Json; using Xunit.Abstractions; using static Terminal.Gui.ConfigurationManager; @@ -21,7 +22,7 @@ public ConfigurationManagerTests (ITestOutputHelper output) }; [Fact] - public void Apply_FiresApplied () + public void Apply_Raises_Applied () { Reset (); Applied += ConfigurationManager_Applied; @@ -146,46 +147,93 @@ public void DeepMemberWiseCopyTest () Assert.Equal (dictDest ["Normal"], dictCopy ["Normal"]); } + public class DeepCopyTest () + { + public static Key key = Key.Esc; + } + [Fact] - public void Load_FiresUpdated () + public void Illustrate_DeepMemberWiseCopy_Breaks_Dictionary () { - ConfigLocations savedLocations = Locations; + Assert.Equal (Key.Esc, DeepCopyTest.key); + + Dictionary dict = new Dictionary (new KeyEqualityComparer ()); + dict.Add (new (DeepCopyTest.key), "Esc"); + Assert.Contains (Key.Esc, dict); + + DeepCopyTest.key = (Key)DeepMemberWiseCopy (Key.Q.WithCtrl, DeepCopyTest.key); + + Assert.Equal (Key.Q.WithCtrl, DeepCopyTest.key); + Assert.Equal (Key.Esc, dict.Keys.ToArray () [0]); + + var eq = new KeyEqualityComparer (); + Assert.True (eq.Equals (Key.Q.WithCtrl, DeepCopyTest.key)); + Assert.Equal (Key.Q.WithCtrl.GetHashCode (), DeepCopyTest.key.GetHashCode ()); + Assert.Equal (eq.GetHashCode (Key.Q.WithCtrl), eq.GetHashCode (DeepCopyTest.key)); + Assert.Equal (Key.Q.WithCtrl.GetHashCode (), eq.GetHashCode (DeepCopyTest.key)); + Assert.True (dict.ContainsKey (Key.Esc)); + + dict.Remove (Key.Esc); + dict.Add (new (DeepCopyTest.key), "Ctrl+Q"); + Assert.True (dict.ContainsKey (Key.Q.WithCtrl)); + } + + [Fact] + public void Load_Raises_Updated () + { + ThrowOnJsonErrors = true; Locations = ConfigLocations.All; Reset (); - - Settings! ["Application.QuitKey"].PropertyValue = Key.Q; - Settings ["Application.NextTabGroupKey"].PropertyValue = Key.F; - Settings ["Application.PrevTabGroupKey"].PropertyValue = Key.B; + Assert.Equal (Key.Esc, (((Key)Settings! ["Application.QuitKey"].PropertyValue)!).KeyCode); Updated += ConfigurationManager_Updated; var fired = false; - void ConfigurationManager_Updated (object sender, ConfigurationManagerEventArgs obj) + void ConfigurationManager_Updated (object? sender, ConfigurationManagerEventArgs obj) { fired = true; - - // assert - Assert.Equal (Key.Esc, (((Key)Settings! ["Application.QuitKey"].PropertyValue)!).KeyCode); - - Assert.Equal ( - KeyCode.F6, - (((Key)Settings ["Application.NextTabGroupKey"].PropertyValue)!).KeyCode - ); - - Assert.Equal ( - KeyCode.F6 | KeyCode.ShiftMask, - (((Key)Settings ["Application.PrevTabGroupKey"].PropertyValue)!).KeyCode - ); } + // Act + // Reset to cause load to raise event Load (true); // assert Assert.True (fired); Updated -= ConfigurationManager_Updated; + + // clean up + Locations = ConfigLocations.Default; + Reset (); + } + + + [Fact] + public void Load_Loads_Custom_Json () + { + // arrange + Locations = ConfigLocations.All; + Reset (); + ThrowOnJsonErrors = true; + + Assert.Equal (Key.Esc, (Key)Settings! ["Application.QuitKey"].PropertyValue); + + // act + RuntimeConfig = """ + + { + "Application.QuitKey": "Ctrl-Q" + } + """; + Load (false); + + // assert + Assert.Equal (Key.Q.WithCtrl, (Key)Settings ["Application.QuitKey"].PropertyValue); + + // clean up + Locations = ConfigLocations.Default; Reset (); - Locations = savedLocations; } [Fact] @@ -224,10 +272,40 @@ public void LoadConfigurationFromAllSources_ShouldLoadSettingsFromAllSources () //Assert.Equal ("AppSpecific", ConfigurationManager.Config.Settings.TestSetting); } + + [Fact] + public void Reset_Raises_Updated () + { + ConfigLocations savedLocations = Locations; + Locations = ConfigLocations.All; + Reset (); + + Settings! ["Application.QuitKey"].PropertyValue = Key.Q; + + Updated += ConfigurationManager_Updated; + var fired = false; + + void ConfigurationManager_Updated (object? sender, ConfigurationManagerEventArgs obj) + { + fired = true; + } + + // Act + Reset (); + + // assert + Assert.True (fired); + + Updated -= ConfigurationManager_Updated; + Reset (); + Locations = savedLocations; + } + + [Fact] public void Reset_and_ResetLoadWithLibraryResourcesOnly_are_same () { - Locations = ConfigLocations.DefaultOnly; + Locations = ConfigLocations.Default; // arrange Reset (); @@ -257,7 +335,7 @@ public void Reset_and_ResetLoadWithLibraryResourcesOnly_are_same () Settings ["Application.PrevTabGroupKey"].PropertyValue = Key.B; Settings.Apply (); - Locations = ConfigLocations.DefaultOnly; + Locations = ConfigLocations.Default; // act Reset (); @@ -275,7 +353,7 @@ public void Reset_and_ResetLoadWithLibraryResourcesOnly_are_same () [Fact] public void Reset_Resets () { - Locations = ConfigLocations.DefaultOnly; + Locations = ConfigLocations.Default; Reset (); Assert.NotEmpty (Themes!); Assert.Equal ("Default", Themes.Theme); @@ -433,7 +511,7 @@ public void TestConfigPropertyOmitClassName () } [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void TestConfigurationManagerInitDriver () { Assert.Equal ("Default", Themes!.Theme); @@ -469,7 +547,10 @@ public void TestConfigurationManagerInitDriver () [Fact] [AutoInitShutdown (configLocation: ConfigLocations.None)] - public void TestConfigurationManagerInitDriver_NoLocations () { } + public void TestConfigurationManagerInitDriver_NoLocations () + { + // TODO: Write this test + } [Fact] public void TestConfigurationManagerInvalidJsonLogs () diff --git a/UnitTests/Configuration/KeyJsonConverterTests.cs b/UnitTests/Configuration/KeyJsonConverterTests.cs index de9a5553ab..b30b5a30a7 100644 --- a/UnitTests/Configuration/KeyJsonConverterTests.cs +++ b/UnitTests/Configuration/KeyJsonConverterTests.cs @@ -52,6 +52,20 @@ public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo) Assert.Equal (expectedStringTo, deserializedKey.ToString ()); } + [Fact] + public void Deserialized_Key_Equals () + { + // Arrange + Key key = Key.Q.WithCtrl; + + // Act + string json = "\"Ctrl+Q\""; + Key deserializedKey = JsonSerializer.Deserialize (json, ConfigurationManager._serializerOptions); + + // Assert + Assert.Equal (key, deserializedKey); + + } [Fact] public void Separator_Property_Serializes_As_Glyph () { diff --git a/UnitTests/Configuration/SettingsScopeTests.cs b/UnitTests/Configuration/SettingsScopeTests.cs index d743b977f9..746bfba6cf 100644 --- a/UnitTests/Configuration/SettingsScopeTests.cs +++ b/UnitTests/Configuration/SettingsScopeTests.cs @@ -5,9 +5,39 @@ namespace Terminal.Gui.ConfigurationTests; public class SettingsScopeTests { [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + public void Update_Overrides_Defaults () + { + // arrange + Locations = ConfigLocations.Default; + Load (true); + + Assert.Equal (Key.Esc, (Key)Settings ["Application.QuitKey"].PropertyValue); + + ThrowOnJsonErrors = true; + + // act + var json = """ + + { + "Application.QuitKey": "Ctrl-Q" + } + """; + + Settings!.Update (json, "test"); + + // assert + Assert.Equal (Key.Q.WithCtrl, (Key)Settings ["Application.QuitKey"].PropertyValue); + + // clean up + Locations = ConfigLocations.All; + } + + [Fact] public void Apply_ShouldApplyProperties () { + Locations = ConfigLocations.Default; + Reset(); + // arrange Assert.Equal (Key.Esc, (Key)Settings ["Application.QuitKey"].PropertyValue); @@ -18,7 +48,7 @@ public void Apply_ShouldApplyProperties () Assert.Equal ( Key.F6.WithShift, - (Key)Settings["Application.PrevTabGroupKey"].PropertyValue + (Key)Settings ["Application.PrevTabGroupKey"].PropertyValue ); // act @@ -32,6 +62,10 @@ public void Apply_ShouldApplyProperties () Assert.Equal (Key.Q, Application.QuitKey); Assert.Equal (Key.F, Application.NextTabGroupKey); Assert.Equal (Key.B, Application.PrevTabGroupKey); + + Locations = ConfigLocations.Default; + Reset (); + } [Fact] @@ -56,7 +90,7 @@ public void CopyUpdatedPropertiesFrom_ShouldCopyChangedPropertiesOnly () public void GetHardCodedDefaults_ShouldSetProperties () { ConfigLocations savedLocations = Locations; - Locations = ConfigLocations.DefaultOnly; + Locations = ConfigLocations.Default; Reset (); Assert.Equal (5, ((Dictionary)Settings ["Themes"].PropertyValue).Count); diff --git a/UnitTests/Configuration/ThemeScopeTests.cs b/UnitTests/Configuration/ThemeScopeTests.cs index 64d13e0b44..28477f4238 100644 --- a/UnitTests/Configuration/ThemeScopeTests.cs +++ b/UnitTests/Configuration/ThemeScopeTests.cs @@ -15,7 +15,7 @@ public class ThemeScopeTests }; [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void AllThemesPresent () { Reset (); @@ -25,7 +25,7 @@ public void AllThemesPresent () } [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void Apply_ShouldApplyUpdatedProperties () { Reset (); @@ -54,7 +54,7 @@ public void GetHardCodedDefaults_ShouldSetProperties () } [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void TestSerialize_RoundTrip () { Reset (); @@ -71,7 +71,7 @@ public void TestSerialize_RoundTrip () } [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void ThemeManager_ClassMethodsWork () { Reset (); diff --git a/UnitTests/Configuration/ThemeTests.cs b/UnitTests/Configuration/ThemeTests.cs index e7d023d100..70a1b393be 100644 --- a/UnitTests/Configuration/ThemeTests.cs +++ b/UnitTests/Configuration/ThemeTests.cs @@ -11,7 +11,7 @@ public class ThemeTests }; [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void TestApply () { Reset (); @@ -33,7 +33,7 @@ public void TestApply () } [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void TestApply_UpdatesColors () { // Arrange diff --git a/UnitTests/Input/KeyBindingTests.cs b/UnitTests/Input/KeyBindingTests.cs index e5628da5ad..af95eff470 100644 --- a/UnitTests/Input/KeyBindingTests.cs +++ b/UnitTests/Input/KeyBindingTests.cs @@ -331,6 +331,19 @@ public void Scope_TryGet_Filters (KeyBindingScope scope) } // TryGet + [Fact] + public void TryGet_Succeeds () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.Q.WithCtrl, KeyBindingScope.Application, Command.HotKey); + var key = new Key (Key.Q.WithCtrl); + bool result = keyBindings.TryGet (key, out KeyBinding _); + Assert.True (result);; + + result = keyBindings.Bindings.TryGetValue (key, out KeyBinding _); + Assert.True (result); + } + [Fact] public void TryGet_Unknown_ReturnsFalse () { diff --git a/UnitTests/Input/KeyTests.cs b/UnitTests/Input/KeyTests.cs index fa2695e5f9..8493e09cbe 100644 --- a/UnitTests/Input/KeyTests.cs +++ b/UnitTests/Input/KeyTests.cs @@ -532,6 +532,10 @@ public void Equals_ShouldReturnTrue_WhenEqual () Key a = Key.A; Key b = Key.A; Assert.True (a.Equals (b)); + + b.Handled = true; + Assert.False (a.Equals (b)); + } [Fact] diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index b8d3bb98da..85bbc7d1d5 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -49,7 +49,7 @@ public AutoInitShutdownAttribute ( bool useFakeClipboard = true, bool fakeClipboardAlwaysThrowsNotSupportedException = false, bool fakeClipboardIsSupportedAlwaysTrue = false, - ConfigLocations configLocation = ConfigLocations.None, + ConfigLocations configLocation = ConfigLocations.Default, // DefaultOnly is the default for tests bool verifyShutdown = false ) { @@ -110,7 +110,7 @@ public override void After (MethodInfo methodUnderTest) } // Reset to defaults - Locations = ConfigLocations.DefaultOnly; + Locations = ConfigLocations.Default; Reset (); // Enable subsequent tests that call Init to get all config files (the default). diff --git a/UnitTests/UICatalog/ScenarioTests.cs b/UnitTests/UICatalog/ScenarioTests.cs index 27cc5d6612..aeac22357a 100644 --- a/UnitTests/UICatalog/ScenarioTests.cs +++ b/UnitTests/UICatalog/ScenarioTests.cs @@ -31,8 +31,8 @@ public void All_Scenarios_Quit_And_Init_Shutdown_Properly (Type scenarioType) _timeoutLock = new (); // Disable any UIConfig settings - ConfigurationManager.ConfigLocations savedConfigLocations = ConfigurationManager.Locations; - ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly; + ConfigLocations savedConfigLocations = ConfigurationManager.Locations; + ConfigurationManager.Locations = ConfigLocations.Default; // If a previous test failed, this will ensure that the Application is in a clean state Application.ResetState (true); @@ -148,8 +148,8 @@ public void All_Scenarios_Benchmark (Type scenarioType) _timeoutLock = new (); // Disable any UIConfig settings - ConfigurationManager.ConfigLocations savedConfigLocations = ConfigurationManager.Locations; - ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly; + ConfigLocations savedConfigLocations = ConfigurationManager.Locations; + ConfigurationManager.Locations = ConfigLocations.Default; // If a previous test failed, this will ensure that the Application is in a clean state Application.ResetState (true); @@ -305,8 +305,8 @@ bool ForceCloseCallback () public void Run_All_Views_Tester_Scenario () { // Disable any UIConfig settings - ConfigurationManager.ConfigLocations savedConfigLocations = ConfigurationManager.Locations; - ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly; + ConfigLocations savedConfigLocations = ConfigurationManager.Locations; + ConfigurationManager.Locations = ConfigLocations.Default; Window _leftPane; ListView _classListView; @@ -764,8 +764,8 @@ View CreateClass (Type type) public void Run_Generic () { // Disable any UIConfig settings - ConfigurationManager.ConfigLocations savedConfigLocations = ConfigurationManager.Locations; - ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly; + ConfigLocations savedConfigLocations = ConfigurationManager.Locations; + ConfigurationManager.Locations = ConfigLocations.Default; ObservableCollection scenarios = Scenario.GetScenarios (); Assert.NotEmpty (scenarios); diff --git a/UnitTests/View/Draw/AllViewsDrawTests.cs b/UnitTests/View/Draw/AllViewsDrawTests.cs index 0be06a6f28..376f53d822 100644 --- a/UnitTests/View/Draw/AllViewsDrawTests.cs +++ b/UnitTests/View/Draw/AllViewsDrawTests.cs @@ -8,6 +8,8 @@ public class AllViewsDrawTests (ITestOutputHelper _output) : TestsAllViews [MemberData (nameof (AllViewTypes))] public void AllViews_Draw_Does_Not_Layout (Type viewType) { + Application.ResetState (true); + var view = (View)CreateInstanceIfNotGeneric (viewType); if (view == null) diff --git a/UnitTests/View/ViewTests.cs b/UnitTests/View/ViewTests.cs index 4bac57b602..c169013e99 100644 --- a/UnitTests/View/ViewTests.cs +++ b/UnitTests/View/ViewTests.cs @@ -283,7 +283,7 @@ public void Clear_Can_Use_Driver_AddRune_Or_AddStr_Methods () } [Theory] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] [InlineData (true)] [InlineData (false)] public void Clear_Does_Not_Spillover_Its_Parent (bool label) diff --git a/UnitTests/Views/ComboBoxTests.cs b/UnitTests/Views/ComboBoxTests.cs index ecc4a5cbc5..839b3ece72 100644 --- a/UnitTests/Views/ComboBoxTests.cs +++ b/UnitTests/Views/ComboBoxTests.cs @@ -494,7 +494,7 @@ cb.Subviews [1] } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void HideDropdownListOnClick_True_Highlight_Current_Item () { var selected = ""; diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index f3a49a66d7..534ac5085e 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -315,7 +315,7 @@ public void Constructors_Defaults () } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void Disabled_MenuBar_Is_Never_Opened () { Toplevel top = new (); @@ -341,7 +341,7 @@ public void Disabled_MenuBar_Is_Never_Opened () } [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void Disabled_MenuItem_Is_Never_Selected () { var menu = new MenuBar diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 1ab880b7f9..15896267ea 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -1480,7 +1480,7 @@ private TabView GetTabView (out Tab tab1, out Tab tab2, bool initFakeDriver = tr private void InitFakeDriver () { - ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly; + ConfigurationManager.Locations = ConfigLocations.Default; ConfigurationManager.Reset (); var driver = new FakeDriver (); diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index dd099553f8..6cceed034c 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -54,7 +54,7 @@ public static DataTableSource BuildTable (int cols, int rows, out DataTable dt) } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void CellEventsBackgroundFill () { var tv = new TableView { Width = 20, Height = 4 }; @@ -412,7 +412,7 @@ public void IsSelected_MultiSelectionOn_Vertical () } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void LongColumnTest () { var tableView = new TableView (); @@ -593,7 +593,7 @@ public void LongColumnTest () top.Dispose (); } - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] [Fact] public void PageDown_ExcludesHeaders () { @@ -993,7 +993,7 @@ public void ShowHorizontalBottomLine_WithVerticalCellLines () } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void TableView_Activate () { string activatedValue = null; @@ -1033,7 +1033,7 @@ public void TableView_Activate () } [Theory] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] [InlineData (false)] [InlineData (true)] public void TableView_ColorsTest_ColorGetter (bool focused) @@ -1134,7 +1134,7 @@ public void TableView_ColorsTest_ColorGetter (bool focused) } [Theory] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] [InlineData (false)] [InlineData (true)] public void TableView_ColorsTest_RowColorGetter (bool focused) @@ -1228,7 +1228,7 @@ public void TableView_ColorsTest_RowColorGetter (bool focused) } [Theory] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] [InlineData (false)] [InlineData (true)] public void TableView_ColorTests_FocusedOrNot (bool focused) @@ -1566,7 +1566,7 @@ public void TableViewMultiSelect_CannotFallOffTop () } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void Test_CollectionNavigator () { var tv = new TableView (); @@ -2572,7 +2572,7 @@ public void TestShiftClick_MultiSelect_TwoRowTable_FullRowSelect () [SetupFakeDriver] public void TestTableViewCheckboxes_ByObject () { - ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly; + ConfigurationManager.Locations = ConfigLocations.Default; ConfigurationManager.Reset(); TableView tv = GetPetTable (out EnumerableTableSource source); diff --git a/UnitTests/Views/TextViewTests.cs b/UnitTests/Views/TextViewTests.cs index ced1943fec..28e867c18d 100644 --- a/UnitTests/Views/TextViewTests.cs +++ b/UnitTests/Views/TextViewTests.cs @@ -8525,7 +8525,7 @@ public class TextViewTestsAutoInitShutdown : AutoInitShutdownAttribute { public static string Txt = "TAB to jump between text fields."; - public TextViewTestsAutoInitShutdown () : base (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly) { } + public TextViewTestsAutoInitShutdown () : base (configLocation: ConfigLocations.Default) { } public override void After (MethodInfo methodUnderTest) { @@ -8947,7 +8947,7 @@ This is } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void Cell_LoadCells_InheritsPreviousAttribute () { List cells = []; diff --git a/UnitTests/Views/TreeTableSourceTests.cs b/UnitTests/Views/TreeTableSourceTests.cs index c22bfcc86f..bf8945f610 100644 --- a/UnitTests/Views/TreeTableSourceTests.cs +++ b/UnitTests/Views/TreeTableSourceTests.cs @@ -155,7 +155,7 @@ public void TestTreeTableSource_BasicExpanding_WithMouse () } [Fact] - [AutoInitShutdown (configLocation:ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation:ConfigLocations.Default)] public void TestTreeTableSource_CombinedWithCheckboxes () { Toplevel top = new (); diff --git a/docfx/docs/config.md b/docfx/docs/config.md index cb1754343d..e593f921f2 100644 --- a/docfx/docs/config.md +++ b/docfx/docs/config.md @@ -12,17 +12,19 @@ Settings that will apply to all applications (global settings) reside in files n Settings are applied using the following precedence (higher precedence settings overwrite lower precedence settings): -1. App-specific settings in the users's home directory (`~/.tui/appname.config.json`). -- Highest precedence. +1. @Terminal.Gui.ConfigLocations.Runtime - Settings stored in the @Terminal.Gui.ConfigurationManager.RuntimeConfig static property --- Hightest precedence. -2. App-specific settings in the directory the app was launched from (`./.tui/appname.config.json`). +2. @Terminal.Gui.ConfigLocations.AppHome - App-specific settings in the users's home directory (`~/.tui/appname.config.json`). -3. App settings in app resources (`Resources/config.json`). +3. @Terminal.Gui.ConfigLocations.AppCurrent - App-specific settings in the directory the app was launched from (`./.tui/appname.config.json`). -4. Global settings in the the user's home directory (`~/.tui/config.json`). +4. @Terminal.Gui.ConfigLocations.AppResources - App settings in app resources (`Resources/config.json`). -5. Global settings in the directory the app was launched from (`./.tui/config.json`). +5. @Terminal.Gui.ConfigLocations.GlobalHome - Global settings in the the user's home directory (`~/.tui/config.json`). -6. Default settings in the Terminal.Gui assembly -- Lowest precedence. +6. @Terminal.Gui.ConfigLocations.GlobalCurrent - Global settings in the directory the app was launched from (`./.tui/config.json`). + +7. @Terminal.Gui.ConfigLocations.Default - Default settings in the Terminal.Gui assembly -- Lowest precedence. The `UI Catalog` application provides an example of how to use the [`ConfigurationManager`](~/api/Terminal.Gui.ConfigurationManager.yml) class to load and save configuration files. The `Configuration Editor` scenario provides an editor that allows users to edit the configuration files. UI Catalog also uses a file system watcher to detect changes to the configuration files to tell [`ConfigurationManager`](~/api/Terminal.Gui.ConfigurationManager.yml) to reload them; allowing users to change settings without having to restart the application. @@ -67,71 +69,25 @@ A Theme is a named collection of settings that impact the visual style of Termin Themes support defining ColorSchemes as well as various default settings for Views. Both the default color schemes and user-defined color schemes can be configured. See [ColorSchemes](~/api/Terminal.Gui.Colors.yml) for more information. -# Example Configuration File - -```json -{ - "$schema": "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", - "Application.QuitKey": { - "Key": "Esc" - }, - "AppSettings": { - "UICatalog.StatusBar": false - }, - "Theme": "UI Catalog Theme", - "Themes": [ - { - "UI Catalog Theme": { - "ColorSchemes": [ - { - "UI Catalog Scheme": { - "Normal": { - "Foreground": "White", - "Background": "Green" - }, - "Focus": { - "Foreground": "Green", - "Background": "White" - }, - "HotNormal": { - "Foreground": "Blue", - "Background": "Green" - }, - "HotFocus": { - "Foreground": "BrightRed", - "Background": "White" - }, - "Disabled": { - "Foreground": "BrightGreen", - "Background": "Gray" - } - } - }, - { - "TopLevel": { - "Normal": { - "Foreground": "DarkGray", - "Background": "White" - ... - } - } - } - ], - "Dialog.DefaultEffect3D": false - } - } - ] -} -``` # Key Bindings Key bindings are defined in the `KeyBindings` property of the configuration file. The value is an array of objects, each object defining a key binding. The key binding object has the following properties: -- `Key`: The key to bind to. The format is a string describing the key (e.g. "q", "Q, "Ctrl-Q"). Function keys are specified as "F1", "F2", etc. +- `Key`: The key to bind to. The format is a string describing the key (e.g. "q", "Q, "Ctrl+Q"). Function keys are specified as "F1", "F2", etc. # Configuration File Schema -Settings are defined in JSON format, according to the schema found here: +Settings are defined in JSON format, according to the schema found here: https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json + +## Schema + +[!code-json[tui-config-schema.json](../schemas/tui-config-schema.json)] + +# The Default Config File + +To illustrate the syntax, the below is the `config.json` file found in `Terminal.Gui.dll`: + +[!code-json[config.json](../../Terminal.Gui/Resources/config.json)] \ No newline at end of file diff --git a/local_packages/Terminal.Gui.2.0.0.nupkg b/local_packages/Terminal.Gui.2.0.0.nupkg index 009ca8827e..a24644f0ef 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.nupkg and b/local_packages/Terminal.Gui.2.0.0.nupkg differ diff --git a/local_packages/Terminal.Gui.2.0.0.snupkg b/local_packages/Terminal.Gui.2.0.0.snupkg index 62039da43b..cb57cf6eab 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.snupkg and b/local_packages/Terminal.Gui.2.0.0.snupkg differ