diff --git a/Example/Example.cs b/Example/Example.cs index 39126f4d9..ede120378 100644 --- a/Example/Example.cs +++ b/Example/Example.cs @@ -6,6 +6,9 @@ using System; using Terminal.Gui; +// Override the default configuration 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/Showcase.md b/Showcase.md index 03b664874..8b528b4ad 100644 --- a/Showcase.md +++ b/Showcase.md @@ -21,6 +21,18 @@ * **[Capital and Cargo](https://github.com/dhorions/Capital-and-Cargo)** - A retro console game where you buy, sell, produce and transport goods built with Terminal.Gui ![image](https://github.com/gui-cs/Terminal.Gui/assets/1682004/ed89f3d6-020f-4a8a-ae18-e057514f4c43) +- **[Falcon](https://github.com/MaciekWin3/Falcon)** - Terminal chat application that uses SignalR and Terminal.Gui. + ![Falcon](https://github.com/user-attachments/assets/d505cba3-75d3-43ea-b270-924dfd257a65) + +- **[Muse](https://github.com/MaciekWin3/Muse)** - Muse is terminal music player built with Terminal.Gui and NAudio on .NET platform. + ![Muse](https://github.com/user-attachments/assets/94aeb559-a889-4b52-bb0d-453b3e19b290) +z +- **[Whale](https://github.com/MaciekWin3/Whale)** - Lightweight terminal user interface application that helps software engineers manage Docker containers. + ![Whale](https://github.com/user-attachments/assets/7ef6e348-c36b-4aee-a63c-4e5c60c3aad2) + +- **[TermKeyVault](https://github.com/MaciekWin3/TermKeyVault)** - Terminal based password manager built with F# and Terminal.Gui. + ![TermKeyVault](https://github.com/user-attachments/assets/c40e17ed-2614-4ad4-8547-e93c1b1d8937) + # Examples # diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 3d47aeb41..47c07a19f 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -86,12 +86,14 @@ public static partial class Application // Initialization (Init/Shutdown) // 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 @@ public static partial class Application // Initialization (Init/Shutdown) } 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 a883dea8e..18881d6c5 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -290,7 +290,15 @@ public static partial class Application // Keyboard handling } 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 898fc935b..7511f8216 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 000000000..b5469c1c9 --- /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, + + /// + /// App resources (e.g. MyApp.Resources.config.json). + /// + AppResources = 0b_0000_0010, + + /// + /// Settings in the static property. + /// + Runtime = 0b_0000_0100, + + /// + /// Global settings in the current directory (e.g. ./.tui/config.json). + /// + GlobalCurrent = 0b_0000_1000, + + /// + /// Global settings in the home directory (e.g. ~/.tui/config.json). + /// + GlobalHome = 0b_0001_0000, + + /// + /// App settings in the current directory (e.g. ./.tui/MyApp.config.json). + /// + AppCurrent = 0b_0010_0000, + + /// + /// App settings in the home directory (e.g. ~/.tui/MyApp.config.json). + /// + AppHome = 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 0c4a41682..5a132b48c 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 class ConfigProperty /// 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 cd5befa16..473f1bf65 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 class ConfigurationManager { // 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 class ConfigurationManager themes = ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false; settings = Settings?.Apply () ?? false; } + appSettings = AppSettings?.Apply () ?? false; } catch (JsonException e) @@ -243,14 +221,23 @@ public static class ConfigurationManager } /// - /// 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,7 @@ public static class ConfigurationManager Reset (); } - // LibraryResources is always loaded by Reset - if (Locations == ConfigLocations.All) + if (Locations.HasFlag (ConfigLocations.AppResources)) { string? embeddedStylesResourceName = Assembly.GetEntryAssembly () ? @@ -276,27 +262,36 @@ public static class ConfigurationManager embeddedStylesResourceName = _configFilename; } - Settings = Settings? - - // Global current directory - .Update ($"./.tui/{_configFilename}") - ? - - // Global home directory - .Update ($"~/.tui/{_configFilename}") - ? - - // App resources - .UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!) - ? - - // App current directory - .Update ($"./.tui/{AppName}.{_configFilename}") - ? - - // App home directory - .Update ($"~/.tui/{AppName}.{_configFilename}"); + Settings?.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!, ConfigLocations.AppResources); } + + if (Locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig)) + { + Settings?.Update (RuntimeConfig, "ConfigurationManager.RuntimeConfig", ConfigLocations.Runtime); + } + + if (Locations.HasFlag (ConfigLocations.GlobalCurrent)) + { + Settings?.Update ($"./.tui/{_configFilename}", ConfigLocations.GlobalCurrent); + } + + if (Locations.HasFlag (ConfigLocations.GlobalHome)) + { + Settings?.Update ($"~/.tui/{_configFilename}", ConfigLocations.GlobalHome); + } + + + if (Locations.HasFlag (ConfigLocations.AppCurrent)) + { + Settings?.Update ($"./.tui/{AppName}.{_configFilename}", ConfigLocations.AppCurrent); + } + + if (Locations.HasFlag (ConfigLocations.AppHome)) + { + Settings?.Update ($"~/.tui/{AppName}.{_configFilename}", ConfigLocations.AppHome); + } + + ThemeManager.SelectedTheme = Settings!["Theme"].PropertyValue as string ?? "Default"; } /// @@ -314,12 +309,13 @@ public static class ConfigurationManager } /// - /// 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,20 +355,23 @@ public static class ConfigurationManager 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, - $"Terminal.Gui.Resources.{_configFilename}" + $"Terminal.Gui.Resources.{_configFilename}", + ConfigLocations.Default ); } + 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 +413,13 @@ public static class ConfigurationManager } // 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 +430,6 @@ public static class ConfigurationManager { foreach (object? srcKey in ((IDictionary)source).Keys) { - if (srcKey is string) - { } - if (((IDictionary)destination).Contains (srcKey)) { ((IDictionary)destination) [srcKey] = @@ -477,13 +479,14 @@ public static class ConfigurationManager } } - 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 +555,12 @@ public static class ConfigurationManager let props = c.Value .GetProperties ( BindingFlags.Instance - | BindingFlags.Static - | BindingFlags.NonPublic - | BindingFlags.Public + | + BindingFlags.Static + | + BindingFlags.NonPublic + | + BindingFlags.Public ) .Where ( prop => @@ -577,17 +583,13 @@ public static class ConfigurationManager 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 2ac4bf4c1..25936ca86 100644 --- a/Terminal.Gui/Configuration/SettingsScope.cs +++ b/Terminal.Gui/Configuration/SettingsScope.cs @@ -27,7 +27,7 @@ namespace Terminal.Gui; public class SettingsScope : Scope { /// The list of paths to the configuration files. - public List Sources = new (); + public Dictionary Sources { get; } = new (); /// Points to our JSON schema. [JsonInclude] @@ -37,9 +37,10 @@ public class SettingsScope : Scope /// Updates the with the settings in a JSON string. /// Json document to update the settings with. /// The source (filename/resource name) the Json document was read from. + /// Location [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public SettingsScope? Update (Stream stream, string source) + public SettingsScope? Update (Stream stream, string source, ConfigLocations location) { // Update the existing settings with the new settings. try @@ -47,9 +48,9 @@ public class SettingsScope : Scope Update ((SettingsScope)JsonSerializer.Deserialize (stream, typeof (SettingsScope), _serializerOptions)!); OnUpdated (); Debug.WriteLine ($"ConfigurationManager: Read configuration from \"{source}\""); - if (!Sources.Contains (source)) + if (!Sources.ContainsValue (source)) { - Sources.Add (source); + Sources.Add (location, source); } return this; @@ -68,19 +69,20 @@ public class SettingsScope : Scope } /// Updates the with the settings in a JSON file. - /// + /// Path to the file. + /// The location [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public SettingsScope? Update (string filePath) + public SettingsScope? Update (string filePath, ConfigLocations location) { string realPath = filePath.Replace ("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)); if (!File.Exists (realPath)) { Debug.WriteLine ($"ConfigurationManager: Configuration file \"{realPath}\" does not exist."); - if (!Sources.Contains (filePath)) + if (!Sources.ContainsValue (filePath)) { - Sources.Add (filePath); + Sources.Add (location, filePath); } return this; @@ -95,7 +97,7 @@ public class SettingsScope : Scope try { FileStream? stream = File.OpenRead (realPath); - SettingsScope? s = Update (stream, filePath); + SettingsScope? s = Update (stream, filePath, location); stream.Close (); stream.Dispose (); @@ -103,7 +105,7 @@ public class SettingsScope : Scope } catch (IOException ioe) { - Debug.WriteLine($"Couldn't open {filePath}. Retrying...: {ioe}"); + Debug.WriteLine ($"Couldn't open {filePath}. Retrying...: {ioe}"); Task.Delay (100); retryCount++; } @@ -115,27 +117,33 @@ public class SettingsScope : Scope /// Updates the with the settings in a JSON string. /// Json document to update the settings with. /// The source (filename/resource name) the Json document was read from. + /// The location. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public SettingsScope? Update (string json, string source) + public SettingsScope? Update (string? json, string source, ConfigLocations location) { + if (string.IsNullOrEmpty (json)) + { + return null; + } var stream = new MemoryStream (); var writer = new StreamWriter (stream); writer.Write (json); writer.Flush (); stream.Position = 0; - return Update (stream, source); + return Update (stream, source, location); } /// Updates the with the settings from a Json resource. /// /// + /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName) + public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName, ConfigLocations location) { - if (resourceName is null || string.IsNullOrEmpty (resourceName)) + if (string.IsNullOrEmpty (resourceName)) { Debug.WriteLine ( $"ConfigurationManager: Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\"." @@ -144,20 +152,13 @@ public class SettingsScope : Scope return this; } - // BUG: Not trim-compatible - // Not a bug, per se, but it's easily fixable by just loading the file. - // Defaults can just be field initializers for involved types. - using Stream? stream = assembly.GetManifestResourceStream (resourceName)!; + using Stream? stream = assembly.GetManifestResourceStream (resourceName); if (stream is null) { - Debug.WriteLine ( - $"ConfigurationManager: Failed to read resource \"{resourceName}\" from \"{assembly.GetName ().Name}\"." - ); - - return this; + return null; } - return Update (stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}"); + return Update (stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}", location); } } diff --git a/Terminal.Gui/Configuration/ThemeManager.cs b/Terminal.Gui/Configuration/ThemeManager.cs index b542c9f2c..8eb54fd29 100644 --- a/Terminal.Gui/Configuration/ThemeManager.cs +++ b/Terminal.Gui/Configuration/ThemeManager.cs @@ -110,7 +110,9 @@ public class ThemeManager : IDictionary 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 11f8d938e..70d078e0c 100644 --- a/Terminal.Gui/Input/Key.cs +++ b/Terminal.Gui/Input/Key.cs @@ -393,24 +393,31 @@ public class Key : EventArgs, IEquatable 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 e2f0ed8be..59a97bfd6 100644 --- a/Terminal.Gui/Input/KeyBindings.cs +++ b/Terminal.Gui/Input/KeyBindings.cs @@ -46,7 +46,11 @@ public class KeyBindings 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 class KeyBindings // 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 class KeyBindings /// 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 000000000..fe02f13dc --- /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/UICatalog/Scenarios/Editors/Resources/config.json b/UICatalog/Resources/config.json similarity index 100% rename from UICatalog/Scenarios/Editors/Resources/config.json rename to UICatalog/Resources/config.json diff --git a/UICatalog/Scenarios/ConfigurationEditor.cs b/UICatalog/Scenarios/ConfigurationEditor.cs index 037acceec..5f30416bf 100644 --- a/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/UICatalog/Scenarios/ConfigurationEditor.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.IO; using System.Linq; using System.Reflection; @@ -21,9 +22,9 @@ public class ConfigurationEditor : Scenario HotNormal = new Attribute (Color.Magenta, Color.White) }; - private static Action _editorColorSchemeChanged; - private Shortcut _lenShortcut; - private TileView _tileView; + private static Action? _editorColorSchemeChanged; + private TabView? _tabView; + private Shortcut? _lenShortcut; [SerializableConfigurationProperty (Scope = typeof (AppScope))] public static ColorScheme EditorColorScheme @@ -42,26 +43,15 @@ public class ConfigurationEditor : Scenario Toplevel top = new (); - _tileView = new TileView (0) - { - Width = Dim.Fill (), - Height = Dim.Fill (1), - Orientation = Orientation.Vertical, - LineStyle = LineStyle.Single, - TabStop = TabBehavior.TabGroup - }; - - top.Add (_tileView); - _lenShortcut = new Shortcut () { - Title = "Len: ", + Title = "", }; var quitShortcut = new Shortcut () { Key = Application.QuitKey, - Title = $"{Application.QuitKey} Quit", + Title = $"Quit", Action = Quit }; @@ -81,35 +71,44 @@ public class ConfigurationEditor : Scenario var statusBar = new StatusBar ([quitShortcut, reloadShortcut, saveShortcut, _lenShortcut]); - top.Add (statusBar); + _tabView = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (Dim.Func (() => statusBar.Frame.Height)) + }; + + top.Add (_tabView, statusBar); top.Loaded += (s, a) => { Open (); - //_tileView.AdvanceFocus (NavigationDirection.Forward, null); + _editorColorSchemeChanged?.Invoke (); }; - _editorColorSchemeChanged += () => - { - foreach (Tile t in _tileView.Tiles) - { - t.ContentView.ColorScheme = EditorColorScheme; - t.ContentView.SetNeedsDraw (); - } + void OnEditorColorSchemeChanged () + { + if (Application.Top is { }) + { + return; + } - ; - }; + foreach (ConfigTextView t in _tabView.Subviews.Where (v => v is ConfigTextView).Cast ()) + { + t.ColorScheme = EditorColorScheme; + } + } - _editorColorSchemeChanged.Invoke (); + _editorColorSchemeChanged += OnEditorColorSchemeChanged; Application.Run (top); + _editorColorSchemeChanged -= OnEditorColorSchemeChanged; top.Dispose (); Application.Shutdown (); } public void Save () { - if (_tileView.MostFocused is ConfigTextView editor) + if (Application.Navigation?.GetFocused () is ConfigTextView editor) { editor.Save (); } @@ -117,56 +116,65 @@ public class ConfigurationEditor : Scenario private void Open () { - var subMenu = new MenuBarItem { Title = "_View" }; - - foreach (string configFile in ConfigurationManager.Settings.Sources) + foreach (var config in ConfigurationManager.Settings!.Sources) { var homeDir = $"{Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)}"; - var fileInfo = new FileInfo (configFile.Replace ("~", homeDir)); + var fileInfo = new FileInfo (config.Value.Replace ("~", homeDir)); - Tile tile = _tileView.InsertTile (_tileView.Tiles.Count); - tile.Title = configFile.StartsWith ("resource://") ? fileInfo.Name : configFile; - - var textView = new ConfigTextView + var editor = new ConfigTextView { - X = 0, - Y = 0, + Title = config.Value.StartsWith ("resource://") ? fileInfo.Name : config.Value, Width = Dim.Fill (), - Height = Dim.Fill (), + Height = Dim.Fill(), FileInfo = fileInfo, - Tile = tile }; - tile.ContentView.Add (textView); + Tab tab = new Tab () + { + View = editor, + DisplayText = config.Key.ToString () + }; - textView.Read (); + _tabView!.AddTab (tab, false); - textView.HasFocusChanged += (s, e) => - { - if (e.NewValue) - { - _lenShortcut.Title = $"Len:{textView.Text.Length}"; - } - }; + editor.Read (); + + editor.ContentsChanged += (sender, args) => + { + _lenShortcut!.Title = _lenShortcut!.Title.Replace ("*", ""); + if (editor.IsDirty) + { + _lenShortcut!.Title += "*"; + } + }; + + _lenShortcut!.Title = $"{editor.Title}"; } - if (_tileView.Tiles.Count > 2) - { - _tileView.Tiles.ToArray () [1].ContentView.SetFocus (); - } + _tabView!.SelectedTabChanged += (sender, args) => + { + _lenShortcut!.Title = $"{args.NewTab.View!.Title}"; + }; + } private void Quit () { - foreach (Tile tile in _tileView.Tiles) - { - var editor = tile.ContentView.Subviews [0] as ConfigTextView; + foreach (ConfigTextView editor in _tabView!.Tabs.Select(v => + { + if (v.View is ConfigTextView ctv) + { + return ctv; + } + return null; + }).Cast ()) + { if (editor.IsDirty) { int result = MessageBox.Query ( "Save Changes", - $"Save changes to {editor.FileInfo.FullName}", + $"Save changes to {editor.FileInfo!.Name}", "_Yes", "_No", "_Cancel" @@ -189,7 +197,7 @@ public class ConfigurationEditor : Scenario private void Reload () { - if (_tileView.MostFocused is ConfigTextView editor) + if (Application.Navigation?.GetFocused () is ConfigTextView editor) { editor.Read (); } @@ -199,32 +207,16 @@ public class ConfigurationEditor : Scenario { internal ConfigTextView () { - ContentsChanged += (s, obj) => - { - if (IsDirty) - { - if (!Tile.Title.EndsWith ('*')) - { - Tile.Title += '*'; - } - else - { - Tile.Title = Tile.Title.TrimEnd ('*'); - } - } - }; TabStop = TabBehavior.TabGroup; - } - internal FileInfo FileInfo { get; set; } - internal Tile Tile { get; set; } + internal FileInfo? FileInfo { get; set; } internal void Read () { - Assembly assembly = null; + Assembly? assembly = null; - if (FileInfo.FullName.Contains ("[Terminal.Gui]")) + if (FileInfo!.FullName.Contains ("[Terminal.Gui]")) { // Library resources assembly = typeof (ConfigurationManager).Assembly; @@ -236,19 +228,27 @@ public class ConfigurationEditor : Scenario if (assembly != null) { - string name = assembly - .GetManifestResourceNames () - .FirstOrDefault (x => x.EndsWith ("config.json")); - using Stream stream = assembly.GetManifestResourceStream (name); - using var reader = new StreamReader (stream); - Text = reader.ReadToEnd (); - ReadOnly = true; - Enabled = true; + string? name = assembly + .GetManifestResourceNames () + .FirstOrDefault (x => x.EndsWith ("config.json")); + if (!string.IsNullOrEmpty (name)) + { + + using Stream? stream = assembly.GetManifestResourceStream (name); + using var reader = new StreamReader (stream!); + Text = reader.ReadToEnd (); + ReadOnly = true; + Enabled = true; + } return; } - if (!FileInfo.Exists) + if (FileInfo!.FullName.Contains ("RuntimeConfig")) + { + Text = ConfigurationManager.RuntimeConfig!; + + } else if (!FileInfo.Exists) { // Create empty config file Text = ConfigurationManager.GetEmptyJson (); @@ -257,12 +257,17 @@ public class ConfigurationEditor : Scenario { Text = File.ReadAllText (FileInfo.FullName); } - - Tile.Title = Tile.Title.TrimEnd ('*'); } internal void Save () { + if (FileInfo!.FullName.Contains ("RuntimeConfig")) + { + ConfigurationManager.RuntimeConfig = Text; + IsDirty = false; + return; + } + if (!Directory.Exists (FileInfo.DirectoryName)) { // Create dir @@ -272,7 +277,6 @@ public class ConfigurationEditor : Scenario using StreamWriter writer = File.CreateText (FileInfo.FullName); writer.Write (Text); writer.Close (); - Tile.Title = Tile.Title.TrimEnd ('*'); IsDirty = false; } } diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index a8ae5aa19..fb1247886 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -20,10 +20,10 @@ TRACE;DEBUG_IDISPOSABLE - + - + diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 3428fcd15..38400d8a2 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 class ApplicationTests { _output = output; ConsoleDriver.RunningUnitTests = true; - ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.None; + Locations = ConfigLocations.Default; #if DEBUG_IDISPOSABLE View.Instances.Clear (); @@ -272,14 +273,15 @@ public class ApplicationTests [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 @@ public class ApplicationTests Application.ResetState (); CheckReset (); - ConfigurationManager.ThrowOnJsonErrors = false; + ThrowOnJsonErrors = false; } [Fact] @@ -398,10 +400,7 @@ public class ApplicationTests } [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 class ApplicationTests 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 059f724d2..d09e74f1d 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 000000000..0bf96dc6e --- /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 5f8e767c0..00b6445f8 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 class ConfigurationManagerTests }; [Fact] - public void Apply_FiresApplied () + public void Apply_Raises_Applied () { Reset (); Applied += ConfigurationManager_Applied; @@ -146,16 +147,44 @@ public class ConfigurationManagerTests Assert.Equal (dictDest ["Normal"], dictCopy ["Normal"]); } - [Fact] - public void Load_FiresUpdated () + public class DeepCopyTest () { - ConfigLocations savedLocations = Locations; + public static Key key = Key.Esc; + } + + [Fact] + public void Illustrate_DeepMemberWiseCopy_Breaks_Dictionary () + { + 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; @@ -163,29 +192,48 @@ public class ConfigurationManagerTests 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 class ConfigurationManagerTests //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 class ConfigurationManagerTests Settings ["Application.PrevTabGroupKey"].PropertyValue = Key.B; Settings.Apply (); - Locations = ConfigLocations.DefaultOnly; + Locations = ConfigLocations.Default; // act Reset (); @@ -275,7 +353,7 @@ public class ConfigurationManagerTests [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 class ConfigurationManagerTests } [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void TestConfigurationManagerInitDriver () { Assert.Equal ("Default", Themes!.Theme); @@ -444,7 +522,7 @@ public class ConfigurationManagerTests // Change Base Stream json = ToStream (); - Settings!.Update (json, "TestConfigurationManagerInitDriver"); + Settings!.Update (json, "TestConfigurationManagerInitDriver", ConfigLocations.Runtime); Dictionary colorSchemes = (Dictionary)Themes [Themes.Theme] ["ColorSchemes"].PropertyValue; @@ -469,7 +547,10 @@ public class ConfigurationManagerTests [Fact] [AutoInitShutdown (configLocation: ConfigLocations.None)] - public void TestConfigurationManagerInitDriver_NoLocations () { } + public void TestConfigurationManagerInitDriver_NoLocations () + { + // TODO: Write this test + } [Fact] public void TestConfigurationManagerInvalidJsonLogs () @@ -499,7 +580,7 @@ public class ConfigurationManagerTests } }"; - Settings!.Update (json, "test"); + Settings!.Update (json, "test", ConfigLocations.Runtime); // AbNormal is not a ColorScheme attribute json = @" @@ -522,7 +603,7 @@ public class ConfigurationManagerTests } }"; - Settings.Update (json, "test"); + Settings.Update (json, "test", ConfigLocations.Runtime); // Modify hotNormal background only json = @" @@ -544,9 +625,9 @@ public class ConfigurationManagerTests } }"; - Settings.Update (json, "test"); + Settings.Update (json, "test", ConfigLocations.Runtime); - Settings.Update ("{}}", "test"); + Settings.Update ("{}}", "test", ConfigLocations.Runtime); Assert.NotEqual (0, _jsonErrors.Length); @@ -582,7 +663,7 @@ public class ConfigurationManagerTests ] }"; - var jsonException = Assert.Throws (() => Settings!.Update (json, "test")); + var jsonException = Assert.Throws (() => Settings!.Update (json, "test", ConfigLocations.Runtime)); Assert.Equal ("Unexpected color name: brownish.", jsonException.Message); // AbNormal is not a ColorScheme attribute @@ -606,7 +687,7 @@ public class ConfigurationManagerTests ] }"; - jsonException = Assert.Throws (() => Settings!.Update (json, "test")); + jsonException = Assert.Throws (() => Settings!.Update (json, "test", ConfigLocations.Runtime)); Assert.Equal ("Unrecognized ColorScheme Attribute name: AbNormal.", jsonException.Message); // Modify hotNormal background only @@ -629,7 +710,7 @@ public class ConfigurationManagerTests ] }"; - jsonException = Assert.Throws (() => Settings!.Update (json, "test")); + jsonException = Assert.Throws (() => Settings!.Update (json, "test", ConfigLocations.Runtime)); Assert.Equal ("Both Foreground and Background colors must be provided.", jsonException.Message); // Unknown property @@ -638,7 +719,7 @@ public class ConfigurationManagerTests ""Unknown"" : ""Not known"" }"; - jsonException = Assert.Throws (() => Settings!.Update (json, "test")); + jsonException = Assert.Throws (() => Settings!.Update (json, "test", ConfigLocations.Runtime)); Assert.StartsWith ("Unknown property", jsonException.Message); Assert.Equal (0, _jsonErrors.Length); @@ -654,7 +735,7 @@ public class ConfigurationManagerTests GetHardCodedDefaults (); Stream stream = ToStream (); - Settings!.Update (stream, "TestConfigurationManagerToJson"); + Settings!.Update (stream, "TestConfigurationManagerToJson", ConfigLocations.Runtime); } [Fact] @@ -803,7 +884,7 @@ public class ConfigurationManagerTests Reset (); ThrowOnJsonErrors = true; - Settings!.Update (json, "TestConfigurationManagerUpdateFromJson"); + Settings!.Update (json, "TestConfigurationManagerUpdateFromJson", ConfigLocations.Runtime); Assert.Equal (KeyCode.Esc, Application.QuitKey.KeyCode); Assert.Equal (KeyCode.Z | KeyCode.AltMask, ((Key)Settings ["Application.QuitKey"].PropertyValue)!.KeyCode); diff --git a/UnitTests/Configuration/KeyJsonConverterTests.cs b/UnitTests/Configuration/KeyJsonConverterTests.cs index de9a5553a..b30b5a30a 100644 --- a/UnitTests/Configuration/KeyJsonConverterTests.cs +++ b/UnitTests/Configuration/KeyJsonConverterTests.cs @@ -52,6 +52,20 @@ public class KeyJsonConverterTests 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 d743b977f..13e333a0c 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", ConfigLocations.Runtime); + + // 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 class SettingsScopeTests Assert.Equal ( Key.F6.WithShift, - (Key)Settings["Application.PrevTabGroupKey"].PropertyValue + (Key)Settings ["Application.PrevTabGroupKey"].PropertyValue ); // act @@ -32,6 +62,10 @@ public class SettingsScopeTests 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 class SettingsScopeTests 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 64d13e0b4..28477f423 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 class ThemeScopeTests } [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void Apply_ShouldApplyUpdatedProperties () { Reset (); @@ -54,7 +54,7 @@ public class ThemeScopeTests } [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void TestSerialize_RoundTrip () { Reset (); @@ -71,7 +71,7 @@ public class ThemeScopeTests } [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 e7d023d10..70a1b393b 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 class ThemeTests } [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 e5628da5a..af95eff47 100644 --- a/UnitTests/Input/KeyBindingTests.cs +++ b/UnitTests/Input/KeyBindingTests.cs @@ -331,6 +331,19 @@ public class KeyBindingTests } // 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 fa2695e5f..8493e09cb 100644 --- a/UnitTests/Input/KeyTests.cs +++ b/UnitTests/Input/KeyTests.cs @@ -532,6 +532,10 @@ public class KeyTests 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 b8d3bb98d..85bbc7d1d 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -49,7 +49,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute 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 class AutoInitShutdownAttribute : BeforeAfterTestAttribute } // 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 27cc5d661..aeac22357 100644 --- a/UnitTests/UICatalog/ScenarioTests.cs +++ b/UnitTests/UICatalog/ScenarioTests.cs @@ -31,8 +31,8 @@ public class ScenarioTests : TestsAllViews _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 class ScenarioTests : TestsAllViews _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 @@ public class ScenarioTests : TestsAllViews 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 @@ public class ScenarioTests : TestsAllViews 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 0be06a6f2..376f53d82 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 4bac57b60..c169013e9 100644 --- a/UnitTests/View/ViewTests.cs +++ b/UnitTests/View/ViewTests.cs @@ -283,7 +283,7 @@ public class ViewTests (ITestOutputHelper output) } [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 ecc4a5cbc..839b3ece7 100644 --- a/UnitTests/Views/ComboBoxTests.cs +++ b/UnitTests/Views/ComboBoxTests.cs @@ -494,7 +494,7 @@ public class ComboBoxTests (ITestOutputHelper output) } [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 f3a49a66d..534ac5085 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -315,7 +315,7 @@ public class MenuBarTests (ITestOutputHelper output) } [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 class MenuBarTests (ITestOutputHelper output) } [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 1ab880b7f..15896267e 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -1480,7 +1480,7 @@ public class TabViewTests (ITestOutputHelper output) 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 dd099553f..6cceed034 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -54,7 +54,7 @@ public class TableViewTests (ITestOutputHelper output) } [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 class TableViewTests (ITestOutputHelper output) } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void LongColumnTest () { var tableView = new TableView (); @@ -593,7 +593,7 @@ public class TableViewTests (ITestOutputHelper output) top.Dispose (); } - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] [Fact] public void PageDown_ExcludesHeaders () { @@ -993,7 +993,7 @@ public class TableViewTests (ITestOutputHelper output) } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void TableView_Activate () { string activatedValue = null; @@ -1033,7 +1033,7 @@ public class TableViewTests (ITestOutputHelper output) } [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 class TableViewTests (ITestOutputHelper output) } [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 class TableViewTests (ITestOutputHelper output) } [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 class TableViewTests (ITestOutputHelper output) } [Fact] - [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] public void Test_CollectionNavigator () { var tv = new TableView (); @@ -2572,7 +2572,7 @@ A B C [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 ced1943fe..28e867c18 100644 --- a/UnitTests/Views/TextViewTests.cs +++ b/UnitTests/Views/TextViewTests.cs @@ -8525,7 +8525,7 @@ line. { 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 @@ line. ", } [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 c22bfcc86..bf8945f61 100644 --- a/UnitTests/Views/TreeTableSourceTests.cs +++ b/UnitTests/Views/TreeTableSourceTests.cs @@ -155,7 +155,7 @@ public class TreeTableSourceTests : IDisposable } [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 cb1754343..d806e7f3e 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.Default - Default settings in the Terminal.Gui assembly -- Lowest precedence. -2. App-specific settings in the directory the app was launched from (`./.tui/appname.config.json`). +2. @Terminal.Gui.ConfigLocations.Runtime - Settings stored in the @Terminal.Gui.ConfigurationManager.RuntimeConfig static property. -3. App settings in app resources (`Resources/config.json`). +3. @Terminal.Gui.ConfigLocations.AppResources - App settings in app resources (`Resources/config.json`). -4. Global settings in the the user's home directory (`~/.tui/config.json`). +4. @Terminal.Gui.ConfigLocations.AppHome - App-specific settings in the users's home directory (`~/.tui/appname.config.json`). -5. Global settings in the directory the app was launched from (`./.tui/config.json`). +5. @Terminal.Gui.ConfigLocations.AppCurrent - App-specific settings in the directory the app was launched from (`./.tui/appname.config.json`). -6. Default settings in the Terminal.Gui assembly -- Lowest precedence. +6. @Terminal.Gui.ConfigLocations.GlobalHome - Global settings in the the user's home directory (`~/.tui/config.json`). + +7. @Terminal.Gui.ConfigLocations.GlobalCurrent - Global settings in the directory the app was launched from (`./.tui/config.json`) --- Hightest 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 009ca8827..a24644f0e 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 62039da43..cb57cf6ea 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.snupkg and b/local_packages/Terminal.Gui.2.0.0.snupkg differ