diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs
index 455697ffc..e709bca8e 100644
--- a/Terminal.Gui/Configuration/ConfigurationManager.cs
+++ b/Terminal.Gui/Configuration/ConfigurationManager.cs
@@ -13,646 +13,645 @@ using System.Text.Json.Serialization;
#nullable enable
-namespace Terminal.Gui {
- ///
- /// Provides settings and configuration management for Terminal.Gui applications.
- ///
- /// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration files.
- /// The configuration files can be placed in at .tui folder in the user's home directory (e.g. C:/Users/username/.tui,
- /// or /usr/username/.tui),
- /// the folder where the Terminal.Gui application was launched from (e.g. ./.tui), or as a resource
- /// within the Terminal.Gui application's main assembly.
- ///
- ///
- /// Settings are defined in JSON format, according to this schema:
- /// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
- ///
- ///
- /// Settings that will apply to all applications (global settings) reside in files named config.json. Settings
- /// that will apply to a specific Terminal.Gui application reside in files named appname.config.json,
- /// where appname is the assembly name of the application (e.g. UICatalog.config.json).
- ///
- /// Settings are applied using the following precedence (higher precedence settings
- /// overwrite lower precedence settings):
- ///
- /// 1. Application configuration found in the users's home directory (~/.tui/appname.config.json) -- Highest precedence
- ///
- ///
- /// 2. Application configuration found in the directory the app was launched from (./.tui/appname.config.json).
- ///
- ///
- /// 3. Application configuration found in the applications's resources (Resources/config.json).
- ///
- ///
- /// 4. Global configuration found in the user's home directory (~/.tui/config.json).
- ///
- ///
- /// 5. Global configuration found in the directory the app was launched from (./.tui/config.json).
- ///
- ///
- /// 6. Global configuration in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json) -- Lowest Precidence.
- ///
- ///
- public static partial class ConfigurationManager {
+namespace Terminal.Gui;
+///
+/// Provides settings and configuration management for Terminal.Gui applications.
+///
+/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration files.
+/// The configuration files can be placed in at .tui folder in the user's home directory (e.g. C:/Users/username/.tui,
+/// or /usr/username/.tui),
+/// the folder where the Terminal.Gui application was launched from (e.g. ./.tui), or as a resource
+/// within the Terminal.Gui application's main assembly.
+///
+///
+/// Settings are defined in JSON format, according to this schema:
+/// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
+///
+///
+/// Settings that will apply to all applications (global settings) reside in files named config.json. Settings
+/// that will apply to a specific Terminal.Gui application reside in files named appname.config.json,
+/// where appname is the assembly name of the application (e.g. UICatalog.config.json).
+///
+/// Settings are applied using the following precedence (higher precedence settings
+/// overwrite lower precedence settings):
+///
+/// 1. Application configuration found in the users's home directory (~/.tui/appname.config.json) -- Highest precedence
+///
+///
+/// 2. Application configuration found in the directory the app was launched from (./.tui/appname.config.json).
+///
+///
+/// 3. Application configuration found in the applications's resources (Resources/config.json).
+///
+///
+/// 4. Global configuration found in the user's home directory (~/.tui/config.json).
+///
+///
+/// 5. Global configuration found in the directory the app was launched from (./.tui/config.json).
+///
+///
+/// 6. Global configuration in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json) -- Lowest Precidence.
+///
+///
+public static partial class ConfigurationManager {
- private static readonly string _configFilename = "config.json";
+ private static readonly string _configFilename = "config.json";
- private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions {
- ReadCommentHandling = JsonCommentHandling.Skip,
- PropertyNameCaseInsensitive = true,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- WriteIndented = true,
- Converters = {
+ private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions {
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = true,
+ Converters = {
// We override the standard Rune converter to support specifying Glyphs in
// a flexible way
new RuneJsonConverter(),
},
- };
+ };
+
+ ///
+ /// An attribute that can be applied to a property to indicate that it should included in the configuration file.
+ ///
+ ///
+ /// [SerializableConfigurationProperty(Scope = typeof(Configuration.ThemeManager.ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
+ /// public static LineStyle DefaultBorderStyle {
+ /// ...
+ ///
+ [AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+ public class SerializableConfigurationProperty : System.Attribute {
+ ///
+ /// Specifies the scope of the property.
+ ///
+ public Type? Scope { get; set; }
///
- /// An attribute that can be applied to a property to indicate that it should included in the configuration file.
+ /// If , the property will be serialized to the configuration file using only the property name
+ /// as the key. If , the property will be serialized to the configuration file using the
+ /// property name pre-pended with the classname (e.g. Application.UseSystemConsole).
///
- ///
- /// [SerializableConfigurationProperty(Scope = typeof(Configuration.ThemeManager.ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
- /// public static LineStyle DefaultBorderStyle {
- /// ...
- ///
- [AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
- public class SerializableConfigurationProperty : System.Attribute {
- ///
- /// Specifies the scope of the property.
- ///
- public Type? Scope { get; set; }
+ public bool OmitClassName { get; set; }
+ }
- ///
- /// If , the property will be serialized to the configuration file using only the property name
- /// as the key. If , the property will be serialized to the configuration file using the
- /// property name pre-pended with the classname (e.g. Application.UseSystemConsole).
- ///
- public bool OmitClassName { get; set; }
+ ///
+ /// Holds a property's value and the that allows
+ /// to get and set the property's value.
+ ///
+ ///
+ /// Configuration properties must be and
+ /// and have the
+ /// attribute. If the type of the property requires specialized JSON serialization,
+ /// a must be provided using
+ /// the attribute.
+ ///
+ public class ConfigProperty {
+ private object? propertyValue;
+
+ ///
+ /// Describes the property.
+ ///
+ public PropertyInfo? PropertyInfo { get; set; }
+
+ ///
+ /// Helper to get either the Json property named (specified by [JsonPropertyName(name)]
+ /// or the actual property name.
+ ///
+ ///
+ ///
+ public static string GetJsonPropertyName (PropertyInfo pi)
+ {
+ var jpna = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
+ return jpna?.Name ?? pi.Name;
}
///
- /// Holds a property's value and the that allows
- /// to get and set the property's value.
+ /// Holds the property's value as it was either read from the class's implementation or from a config file.
+ /// If the property has not been set (e.g. because no configuration file specified a value),
+ /// this will be .
///
///
- /// Configuration properties must be and
- /// and have the
- /// attribute. If the type of the property requires specialized JSON serialization,
- /// a must be provided using
- /// the attribute.
+ /// On , performs a sparse-copy of the new value to the existing value (only copies elements of
+ /// the object that are non-null).
///
- public class ConfigProperty {
- private object? propertyValue;
-
- ///
- /// Describes the property.
- ///
- public PropertyInfo? PropertyInfo { get; set; }
-
- ///
- /// Helper to get either the Json property named (specified by [JsonPropertyName(name)]
- /// or the actual property name.
- ///
- ///
- ///
- public static string GetJsonPropertyName (PropertyInfo pi)
- {
- var jpna = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
- return jpna?.Name ?? pi.Name;
+ public object? PropertyValue {
+ get => propertyValue;
+ set {
+ propertyValue = value;
}
+ }
- ///
- /// Holds the property's value as it was either read from the class's implementation or from a config file.
- /// If the property has not been set (e.g. because no configuration file specified a value),
- /// this will be .
- ///
- ///
- /// On , performs a sparse-copy of the new value to the existing value (only copies elements of
- /// the object that are non-null).
- ///
- public object? PropertyValue {
- get => propertyValue;
- set {
- propertyValue = value;
- }
- }
-
- internal object? UpdateValueFrom (object source)
- {
- if (source == null) {
- return PropertyValue;
- }
-
- var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
- if (source.GetType () != PropertyInfo!.PropertyType && (ut != null && source.GetType () != ut)) {
- throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}.");
- }
- if (PropertyValue != null && source != null) {
- PropertyValue = DeepMemberwiseCopy (source, PropertyValue);
- } else {
- PropertyValue = source;
- }
-
+ internal object? UpdateValueFrom (object source)
+ {
+ if (source == null) {
return PropertyValue;
}
- ///
- /// Retrieves (using reflection) the value of the static property described in
- /// into .
- ///
- ///
- public object? RetrieveValue ()
- {
- return PropertyValue = PropertyInfo!.GetValue (null);
+ var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
+ if (source.GetType () != PropertyInfo!.PropertyType && (ut != null && source.GetType () != ut)) {
+ throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}.");
+ }
+ if (PropertyValue != null && source != null) {
+ PropertyValue = DeepMemberwiseCopy (source, PropertyValue);
+ } else {
+ PropertyValue = source;
}
- ///
- /// Applies the to the property described by .
- ///
- ///
- public bool Apply ()
- {
- if (PropertyValue != null) {
- PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
- }
- return PropertyValue != null;
- }
+ return PropertyValue;
}
///
- /// 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 class that implements the property (e.g. Application.UseSystemConsole).
- /// The values are instances of which hold the property's value and the
- /// that allows to get and set the property's value.
- ///
- ///
- /// Is until is called.
- ///
- private static Dictionary? _allConfigProperties;
-
- ///
- /// The backing property for .
- ///
- ///
- /// Is until is called. Gets set to a new instance by
- /// deserialization (see ).
- ///
- private static SettingsScope? _settings;
-
- ///
- /// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the
- /// attribute value.
- ///
- public static SettingsScope? Settings {
- get {
- if (_settings == null) {
- throw new InvalidOperationException ("ConfigurationManager has not been initialized. Call ConfigurationManager.Reset() before accessing the Settings property.");
- }
- return _settings;
- }
- set {
- _settings = value!;
- }
- }
-
- ///
- /// The root object of Terminal.Gui themes manager. Contains only properties with the
- /// attribute value.
- ///
- public static ThemeManager? Themes => ThemeManager.Instance;
-
- ///
- /// Application-specific configuration settings scope.
- ///
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("AppSettings")]
- public static AppScope? AppSettings { get; set; }
-
- ///
- /// The set of glyphs used to draw checkboxes, lines, borders, etc...See also .
- ///
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true),
- JsonPropertyName ("Glyphs")]
- public static GlyphDefinitions Glyphs { get; set; } = new GlyphDefinitions ();
-
- ///
- /// Initializes the internal state of ConfigurationManager. Nominally called once as part of application
- /// startup to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
- ///
- internal static void Initialize ()
- {
- _allConfigProperties = new Dictionary ();
- _settings = null;
-
- Dictionary classesWithConfigProps = new Dictionary (StringComparer.InvariantCultureIgnoreCase);
- // Get Terminal.Gui.dll classes
-
- var types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
- from type in assembly.GetTypes ()
- where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
- select type;
-
- foreach (var classWithConfig in types) {
- classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
- }
-
- Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
- classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($" Class: {x.Key}"));
-
- foreach (var p in from c in classesWithConfigProps
- let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
- prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
- let enumerable = props
- from p in enumerable
- select p) {
- if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) {
- if (p.GetGetMethod (true)!.IsStatic) {
- // If the class name is omitted, JsonPropertyName is allowed.
- _allConfigProperties!.Add (scp.OmitClassName ? ConfigProperty.GetJsonPropertyName (p) : $"{p.DeclaringType?.Name}.{p.Name}", new ConfigProperty {
- PropertyInfo = p,
- PropertyValue = null
- });
- } else {
- throw new Exception ($"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static.");
- }
- }
- }
-
- _allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key).ToDictionary (x => x.Key, x => x.Value, StringComparer.InvariantCultureIgnoreCase);
-
- Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
- //_allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($" Property: {x.Key}"));
-
- AppSettings = new AppScope ();
- }
-
- ///
- /// Creates a JSON document with the configuration specified.
+ /// Retrieves (using reflection) the value of the static property described in
+ /// into .
///
///
- internal static string ToJson ()
+ public object? RetrieveValue ()
{
- Debug.WriteLine ($"ConfigurationManager.ToJson()");
- return JsonSerializer.Serialize (Settings!, _serializerOptions);
- }
-
- internal static Stream ToStream ()
- {
- var json = JsonSerializer.Serialize (Settings!, _serializerOptions);
- // turn it into a stream
- var stream = new MemoryStream ();
- var writer = new StreamWriter (stream);
- writer.Write (json);
- writer.Flush ();
- stream.Position = 0;
- return stream;
+ return PropertyValue = PropertyInfo!.GetValue (null);
}
///
- /// Gets or sets whether the should throw an exception if it encounters
- /// an error on deserialization. If (the default), the error is logged and printed to the
- /// console when is called.
- ///
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
- public static bool? ThrowOnJsonErrors { get; set; } = false;
-
- internal static StringBuilder jsonErrors = new StringBuilder ();
-
- private static void AddJsonError (string error)
- {
- Debug.WriteLine ($"ConfigurationManager: {error}");
- jsonErrors.AppendLine (error);
- }
-
- ///
- /// Prints any Json deserialization errors that occurred during deserialization to the console.
- ///
- public static void PrintJsonErrors ()
- {
- if (jsonErrors.Length > 0) {
- Console.WriteLine ($"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
- Console.WriteLine (jsonErrors.ToString ());
- }
- }
-
- private static void ClearJsonErrors ()
- {
- jsonErrors.Clear ();
- }
-
- ///
- /// Called when the configuration has been updated from a configuration file. Invokes the
- /// event.
- ///
- public static void OnUpdated ()
- {
- Debug.WriteLine ($"ConfigurationManager.OnApplied()");
- Updated?.Invoke (null, new ConfigurationManagerEventArgs ());
- }
-
- ///
- /// Event fired when the configuration has been updated from a configuration source.
- /// application.
- ///
- public static event EventHandler? Updated;
-
- ///
- /// Resets the state of . Should be called whenever a new app session
- /// (e.g. in starts. Called by
- /// if the reset parameter is .
- ///
- ///
- ///
- ///
- public static void Reset ()
- {
- Debug.WriteLine ($"ConfigurationManager.Reset()");
- if (_allConfigProperties == null) {
- ConfigurationManager.Initialize ();
- }
-
- ClearJsonErrors ();
-
- Settings = new SettingsScope ();
- ThemeManager.Reset ();
- AppSettings = new AppScope ();
-
- // To enable some unit tests, we only load from resources if the flag is set
- if (Locations.HasFlag (ConfigLocations.DefaultOnly)) {
- Settings.UpdateFromResource (typeof (ConfigurationManager).Assembly, $"Terminal.Gui.Resources.{_configFilename}");
- }
- Apply ();
- ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
- AppSettings?.Apply ();
- }
-
- ///
- /// 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 .
- ///
- ///
- ///
- /// This method is only really useful when using ConfigurationManagerTests
- /// to generate the JSON doc that is embedded into Terminal.Gui (during development).
- ///
- ///
- /// WARNING: The Terminal.Gui.Resources.config.json resource has setting definitions (Themes)
- /// that are NOT generated by this function. If you use this function to regenerate Terminal.Gui.Resources.config.json,
- /// make sure you copy the Theme definitions from the existing Terminal.Gui.Resources.config.json file.
- ///
- ///
- internal static void GetHardCodedDefaults ()
- {
- if (_allConfigProperties == null) {
- throw new InvalidOperationException ("Initialize must be called first.");
- }
- Settings = new SettingsScope ();
- ThemeManager.GetHardCodedDefaults ();
- AppSettings?.RetrieveValues ();
- foreach (var p in Settings!.Where (cp => cp.Value.PropertyInfo != null)) {
- Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
- }
- }
-
- ///
- /// Applies the configuration settings to the running instance.
- ///
- public static void Apply ()
- {
- bool settings = Settings?.Apply () ?? false;
- bool themes = !string.IsNullOrEmpty(ThemeManager.SelectedTheme) && (ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false);
- bool appsettings = AppSettings?.Apply () ?? false;
- if (settings || themes || appsettings) {
- OnApplied ();
- }
- }
-
- ///
- /// Called when an updated configuration has been applied to the
- /// application. Fires the event.
- ///
- public static void OnApplied ()
- {
- Debug.WriteLine ($"ConfigurationManager.OnApplied()");
- Applied?.Invoke (null, new ConfigurationManagerEventArgs ());
- }
-
- ///
- /// Event fired when an updated configuration has been applied to the
- /// application.
- ///
- public static event EventHandler? Applied;
-
- ///
- /// Name of the running application. By default this property is set to the application's assembly name.
- ///
- public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
-
- ///
- /// 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 Precidence.
- ///
- DefaultOnly,
-
- ///
- /// This constant is a combination of all locations
- ///
- All = -1
-
- }
-
- ///
- /// Gets and sets the locations where will look for config files.
- /// The value is .
- ///
- public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
-
- ///
- /// Loads all settings found in the various configuration storage locations to
- /// the . Optionally,
- /// resets all settings attributed with to the defaults.
- ///
- ///
- /// Use to cause the loaded settings to be applied to the running application.
- ///
- /// If the state of will
- /// be reset to the defaults.
- public static void Load (bool reset = false)
- {
- Debug.WriteLine ($"ConfigurationManager.Load()");
-
- if (reset) Reset ();
-
- // LibraryResources is always loaded by Reset
- if (Locations == ConfigLocations.All) {
- var embeddedStylesResourceName = Assembly.GetEntryAssembly ()?
- .GetManifestResourceNames ().FirstOrDefault (x => x.EndsWith (_configFilename));
- if (string.IsNullOrEmpty (embeddedStylesResourceName)) {
- 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}");
- }
- }
-
- ///
- /// Returns an empty Json document with just the $schema tag.
+ /// Applies the to the property described by .
///
///
- public static string GetEmptyJson ()
+ public bool Apply ()
{
- var emptyScope = new SettingsScope ();
- emptyScope.Clear ();
- return JsonSerializer.Serialize (emptyScope, _serializerOptions);
+ if (PropertyValue != null) {
+ PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
+ }
+ return PropertyValue != null;
}
-
- ///
- /// System.Text.Json does not support copying a deserialized object to an existing instance.
- /// To work around this, we implement a 'deep, memberwise copy' method.
- ///
- ///
- /// TOOD: When System.Text.Json implements `PopulateObject` revisit
- /// https://github.com/dotnet/corefx/issues/37627
- ///
- ///
- ///
- /// updated from
- internal static object? DeepMemberwiseCopy (object? source, object? destination)
- {
- if (destination == null) {
- throw new ArgumentNullException (nameof (destination));
- }
-
- if (source == null) {
- return null!;
- }
-
- if (source.GetType () == typeof (SettingsScope)) {
- return ((SettingsScope)destination).Update ((SettingsScope)source);
- }
- if (source.GetType () == typeof (ThemeScope)) {
- return ((ThemeScope)destination).Update ((ThemeScope)source);
- }
- if (source.GetType () == typeof (AppScope)) {
- return ((AppScope)destination).Update ((AppScope)source);
- }
-
- // If value type, just use copy constructor.
- if (source.GetType ().IsValueType || source.GetType () == typeof (string)) {
- return source;
- }
-
- // Dictionary
- if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) {
- foreach (var srcKey in ((IDictionary)source).Keys) {
- if (((IDictionary)destination).Contains (srcKey))
- ((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
- else {
- ((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
- }
- }
- return destination;
- }
-
- // ALl other object types
- var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
- var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
- foreach (var (sourceProp, destProp) in
- from sourceProp in sourceProps
- where destProps.Any (x => x.Name == sourceProp.Name)
- let destProp = destProps.First (x => x.Name == sourceProp.Name)
- where destProp.CanWrite
- select (sourceProp, destProp)) {
-
- var sourceVal = sourceProp.GetValue (source);
- var destVal = destProp.GetValue (destination);
- if (sourceVal != null) {
- if (destVal != null) {
- // Recurse
- destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
- } else {
- destProp.SetValue (destination, sourceVal);
- }
- }
- }
- return destination!;
- }
-
- //public class ConfiguraitonLocation
- //{
- // public string Name { get; set; } = string.Empty;
-
- // public string? Path { get; set; }
-
- // public async Task UpdateAsync (Stream stream)
- // {
- // var scope = await JsonSerializer.DeserializeAsync (stream, serializerOptions);
- // if (scope != null) {
- // ConfigurationManager.Settings?.UpdateFrom (scope);
- // return scope;
- // }
- // return new SettingsScope ();
- // }
-
- //}
-
- //public class StreamConfiguration {
- // private bool _reset;
-
- // public StreamConfiguration (bool reset)
- // {
- // _reset = reset;
- // }
-
- // public StreamConfiguration UpdateAppResources ()
- // {
- // if (Locations.HasFlag (ConfigLocations.AppResources)) LoadAppResources ();
- // return this;
- // }
-
- // public StreamConfiguration UpdateAppDirectory ()
- // {
- // if (Locations.HasFlag (ConfigLocations.AppDirectory)) LoadAppDirectory ();
- // return this;
- // }
-
- // // Additional update methods for each location here
-
- // private void LoadAppResources ()
- // {
- // // Load AppResources logic here
- // }
-
- // private void LoadAppDirectory ()
- // {
- // // Load AppDirectory logic here
- // }
- //}
}
+
+ ///
+ /// 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 class that implements the property (e.g. Application.UseSystemConsole).
+ /// The values are instances of which hold the property's value and the
+ /// that allows to get and set the property's value.
+ ///
+ ///
+ /// Is until is called.
+ ///
+ private static Dictionary? _allConfigProperties;
+
+ ///
+ /// The backing property for .
+ ///
+ ///
+ /// Is until is called. Gets set to a new instance by
+ /// deserialization (see ).
+ ///
+ private static SettingsScope? _settings;
+
+ ///
+ /// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the
+ /// attribute value.
+ ///
+ public static SettingsScope? Settings {
+ get {
+ if (_settings == null) {
+ throw new InvalidOperationException ("ConfigurationManager has not been initialized. Call ConfigurationManager.Reset() before accessing the Settings property.");
+ }
+ return _settings;
+ }
+ set {
+ _settings = value!;
+ }
+ }
+
+ ///
+ /// The root object of Terminal.Gui themes manager. Contains only properties with the
+ /// attribute value.
+ ///
+ public static ThemeManager? Themes => ThemeManager.Instance;
+
+ ///
+ /// Application-specific configuration settings scope.
+ ///
+ [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("AppSettings")]
+ public static AppScope? AppSettings { get; set; }
+
+ ///
+ /// The set of glyphs used to draw checkboxes, lines, borders, etc...See also .
+ ///
+ [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true),
+ JsonPropertyName ("Glyphs")]
+ public static GlyphDefinitions Glyphs { get; set; } = new GlyphDefinitions ();
+
+ ///
+ /// Initializes the internal state of ConfigurationManager. Nominally called once as part of application
+ /// startup to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
+ ///
+ internal static void Initialize ()
+ {
+ _allConfigProperties = new Dictionary ();
+ _settings = null;
+
+ Dictionary classesWithConfigProps = new Dictionary (StringComparer.InvariantCultureIgnoreCase);
+ // Get Terminal.Gui.dll classes
+
+ var types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
+ from type in assembly.GetTypes ()
+ where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
+ select type;
+
+ foreach (var classWithConfig in types) {
+ classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
+ }
+
+ Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
+ classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($" Class: {x.Key}"));
+
+ foreach (var p in from c in classesWithConfigProps
+ let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
+ prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
+ let enumerable = props
+ from p in enumerable
+ select p) {
+ if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) {
+ if (p.GetGetMethod (true)!.IsStatic) {
+ // If the class name is omitted, JsonPropertyName is allowed.
+ _allConfigProperties!.Add (scp.OmitClassName ? ConfigProperty.GetJsonPropertyName (p) : $"{p.DeclaringType?.Name}.{p.Name}", new ConfigProperty {
+ PropertyInfo = p,
+ PropertyValue = null
+ });
+ } else {
+ throw new Exception ($"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static.");
+ }
+ }
+ }
+
+ _allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key).ToDictionary (x => x.Key, x => x.Value, StringComparer.InvariantCultureIgnoreCase);
+
+ Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
+ //_allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($" Property: {x.Key}"));
+
+ AppSettings = new AppScope ();
+ }
+
+ ///
+ /// Creates a JSON document with the configuration specified.
+ ///
+ ///
+ internal static string ToJson ()
+ {
+ Debug.WriteLine ($"ConfigurationManager.ToJson()");
+ return JsonSerializer.Serialize (Settings!, _serializerOptions);
+ }
+
+ internal static Stream ToStream ()
+ {
+ var json = JsonSerializer.Serialize (Settings!, _serializerOptions);
+ // turn it into a stream
+ var stream = new MemoryStream ();
+ var writer = new StreamWriter (stream);
+ writer.Write (json);
+ writer.Flush ();
+ stream.Position = 0;
+ return stream;
+ }
+
+ ///
+ /// Gets or sets whether the should throw an exception if it encounters
+ /// an error on deserialization. If (the default), the error is logged and printed to the
+ /// console when is called.
+ ///
+ [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+ public static bool? ThrowOnJsonErrors { get; set; } = false;
+
+ internal static StringBuilder jsonErrors = new StringBuilder ();
+
+ private static void AddJsonError (string error)
+ {
+ Debug.WriteLine ($"ConfigurationManager: {error}");
+ jsonErrors.AppendLine (error);
+ }
+
+ ///
+ /// Prints any Json deserialization errors that occurred during deserialization to the console.
+ ///
+ public static void PrintJsonErrors ()
+ {
+ if (jsonErrors.Length > 0) {
+ Console.WriteLine ($"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
+ Console.WriteLine (jsonErrors.ToString ());
+ }
+ }
+
+ private static void ClearJsonErrors ()
+ {
+ jsonErrors.Clear ();
+ }
+
+ ///
+ /// Called when the configuration has been updated from a configuration file. Invokes the
+ /// event.
+ ///
+ public static void OnUpdated ()
+ {
+ Debug.WriteLine ($"ConfigurationManager.OnApplied()");
+ Updated?.Invoke (null, new ConfigurationManagerEventArgs ());
+ }
+
+ ///
+ /// Event fired when the configuration has been updated from a configuration source.
+ /// application.
+ ///
+ public static event EventHandler? Updated;
+
+ ///
+ /// Resets the state of . Should be called whenever a new app session
+ /// (e.g. in starts. Called by
+ /// if the reset parameter is .
+ ///
+ ///
+ ///
+ ///
+ public static void Reset ()
+ {
+ Debug.WriteLine ($"ConfigurationManager.Reset()");
+ if (_allConfigProperties == null) {
+ ConfigurationManager.Initialize ();
+ }
+
+ ClearJsonErrors ();
+
+ Settings = new SettingsScope ();
+ ThemeManager.Reset ();
+ AppSettings = new AppScope ();
+
+ // To enable some unit tests, we only load from resources if the flag is set
+ if (Locations.HasFlag (ConfigLocations.DefaultOnly)) {
+ Settings.UpdateFromResource (typeof (ConfigurationManager).Assembly, $"Terminal.Gui.Resources.{_configFilename}");
+ }
+ Apply ();
+ ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
+ AppSettings?.Apply ();
+ }
+
+ ///
+ /// 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 .
+ ///
+ ///
+ ///
+ /// This method is only really useful when using ConfigurationManagerTests
+ /// to generate the JSON doc that is embedded into Terminal.Gui (during development).
+ ///
+ ///
+ /// WARNING: The Terminal.Gui.Resources.config.json resource has setting definitions (Themes)
+ /// that are NOT generated by this function. If you use this function to regenerate Terminal.Gui.Resources.config.json,
+ /// make sure you copy the Theme definitions from the existing Terminal.Gui.Resources.config.json file.
+ ///
+ ///
+ internal static void GetHardCodedDefaults ()
+ {
+ if (_allConfigProperties == null) {
+ throw new InvalidOperationException ("Initialize must be called first.");
+ }
+ Settings = new SettingsScope ();
+ ThemeManager.GetHardCodedDefaults ();
+ AppSettings?.RetrieveValues ();
+ foreach (var p in Settings!.Where (cp => cp.Value.PropertyInfo != null)) {
+ Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
+ }
+ }
+
+ ///
+ /// Applies the configuration settings to the running instance.
+ ///
+ public static void Apply ()
+ {
+ bool settings = Settings?.Apply () ?? false;
+ bool themes = !string.IsNullOrEmpty (ThemeManager.SelectedTheme) && (ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false);
+ bool appsettings = AppSettings?.Apply () ?? false;
+ if (settings || themes || appsettings) {
+ OnApplied ();
+ }
+ }
+
+ ///
+ /// Called when an updated configuration has been applied to the
+ /// application. Fires the event.
+ ///
+ public static void OnApplied ()
+ {
+ Debug.WriteLine ($"ConfigurationManager.OnApplied()");
+ Applied?.Invoke (null, new ConfigurationManagerEventArgs ());
+ }
+
+ ///
+ /// Event fired when an updated configuration has been applied to the
+ /// application.
+ ///
+ public static event EventHandler? Applied;
+
+ ///
+ /// Name of the running application. By default this property is set to the application's assembly name.
+ ///
+ public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
+
+ ///
+ /// 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 Precidence.
+ ///
+ DefaultOnly,
+
+ ///
+ /// This constant is a combination of all locations
+ ///
+ All = -1
+
+ }
+
+ ///
+ /// Gets and sets the locations where will look for config files.
+ /// The value is .
+ ///
+ public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
+
+ ///
+ /// Loads all settings found in the various configuration storage locations to
+ /// the . Optionally,
+ /// resets all settings attributed with to the defaults.
+ ///
+ ///
+ /// Use to cause the loaded settings to be applied to the running application.
+ ///
+ /// If the state of will
+ /// be reset to the defaults.
+ public static void Load (bool reset = false)
+ {
+ Debug.WriteLine ($"ConfigurationManager.Load()");
+
+ if (reset) Reset ();
+
+ // LibraryResources is always loaded by Reset
+ if (Locations == ConfigLocations.All) {
+ var embeddedStylesResourceName = Assembly.GetEntryAssembly ()?
+ .GetManifestResourceNames ().FirstOrDefault (x => x.EndsWith (_configFilename));
+ if (string.IsNullOrEmpty (embeddedStylesResourceName)) {
+ 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}");
+ }
+ }
+
+ ///
+ /// Returns an empty Json document with just the $schema tag.
+ ///
+ ///
+ public static string GetEmptyJson ()
+ {
+ var emptyScope = new SettingsScope ();
+ emptyScope.Clear ();
+ return JsonSerializer.Serialize (emptyScope, _serializerOptions);
+ }
+
+ ///
+ /// System.Text.Json does not support copying a deserialized object to an existing instance.
+ /// To work around this, we implement a 'deep, memberwise copy' method.
+ ///
+ ///
+ /// TOOD: When System.Text.Json implements `PopulateObject` revisit
+ /// https://github.com/dotnet/corefx/issues/37627
+ ///
+ ///
+ ///
+ /// updated from
+ internal static object? DeepMemberwiseCopy (object? source, object? destination)
+ {
+ if (destination == null) {
+ throw new ArgumentNullException (nameof (destination));
+ }
+
+ if (source == null) {
+ return null!;
+ }
+
+ if (source.GetType () == typeof (SettingsScope)) {
+ return ((SettingsScope)destination).Update ((SettingsScope)source);
+ }
+ if (source.GetType () == typeof (ThemeScope)) {
+ return ((ThemeScope)destination).Update ((ThemeScope)source);
+ }
+ if (source.GetType () == typeof (AppScope)) {
+ return ((AppScope)destination).Update ((AppScope)source);
+ }
+
+ // If value type, just use copy constructor.
+ if (source.GetType ().IsValueType || source.GetType () == typeof (string)) {
+ return source;
+ }
+
+ // Dictionary
+ if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) {
+ foreach (var srcKey in ((IDictionary)source).Keys) {
+ if (((IDictionary)destination).Contains (srcKey))
+ ((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
+ else {
+ ((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
+ }
+ }
+ return destination;
+ }
+
+ // ALl other object types
+ var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
+ var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
+ foreach (var (sourceProp, destProp) in
+ from sourceProp in sourceProps
+ where destProps.Any (x => x.Name == sourceProp.Name)
+ let destProp = destProps.First (x => x.Name == sourceProp.Name)
+ where destProp.CanWrite
+ select (sourceProp, destProp)) {
+
+ var sourceVal = sourceProp.GetValue (source);
+ var destVal = destProp.GetValue (destination);
+ if (sourceVal != null) {
+ if (destVal != null) {
+ // Recurse
+ destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
+ } else {
+ destProp.SetValue (destination, sourceVal);
+ }
+ }
+ }
+ return destination!;
+ }
+
+ //public class ConfiguraitonLocation
+ //{
+ // public string Name { get; set; } = string.Empty;
+
+ // public string? Path { get; set; }
+
+ // public async Task UpdateAsync (Stream stream)
+ // {
+ // var scope = await JsonSerializer.DeserializeAsync (stream, serializerOptions);
+ // if (scope != null) {
+ // ConfigurationManager.Settings?.UpdateFrom (scope);
+ // return scope;
+ // }
+ // return new SettingsScope ();
+ // }
+
+ //}
+
+ //public class StreamConfiguration {
+ // private bool _reset;
+
+ // public StreamConfiguration (bool reset)
+ // {
+ // _reset = reset;
+ // }
+
+ // public StreamConfiguration UpdateAppResources ()
+ // {
+ // if (Locations.HasFlag (ConfigLocations.AppResources)) LoadAppResources ();
+ // return this;
+ // }
+
+ // public StreamConfiguration UpdateAppDirectory ()
+ // {
+ // if (Locations.HasFlag (ConfigLocations.AppDirectory)) LoadAppDirectory ();
+ // return this;
+ // }
+
+ // // Additional update methods for each location here
+
+ // private void LoadAppResources ()
+ // {
+ // // Load AppResources logic here
+ // }
+
+ // private void LoadAppDirectory ()
+ // {
+ // // Load AppDirectory logic here
+ // }
+ //}
}
diff --git a/Terminal.Gui/Configuration/DictionaryJsonConverter.cs b/Terminal.Gui/Configuration/DictionaryJsonConverter.cs
index d6187fc14..53ff7f3af 100644
--- a/Terminal.Gui/Configuration/DictionaryJsonConverter.cs
+++ b/Terminal.Gui/Configuration/DictionaryJsonConverter.cs
@@ -7,6 +7,10 @@ namespace Terminal.Gui {
class DictionaryJsonConverter : JsonConverter> {
public override Dictionary Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
+ if (reader.TokenType != JsonTokenType.StartArray) {
+ throw new JsonException ($"Expected a JSON array (\"[ {{ ... }} ]\"), but got \"{reader.TokenType}\".");
+ }
+
var dictionary = new Dictionary ();
while (reader.Read ()) {
if (reader.TokenType == JsonTokenType.StartObject) {
diff --git a/Terminal.Gui/Configuration/Scope.cs b/Terminal.Gui/Configuration/Scope.cs
index d950405ed..245153dd2 100644
--- a/Terminal.Gui/Configuration/Scope.cs
+++ b/Terminal.Gui/Configuration/Scope.cs
@@ -100,7 +100,7 @@ namespace Terminal.Gui {
public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject) {
- throw new JsonException ($"Expected a JSON object, but got \"{reader.TokenType}\".");
+ throw new JsonException ($"Expected a JSON object (\"{{ \"propName\" : ... }}\"), but got \"{reader.TokenType}\".");
}
var scope = (scopeT)Activator.CreateInstance (typeof (scopeT))!;
diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json
index af1932937..10d489107 100644
--- a/Terminal.Gui/Resources/config.json
+++ b/Terminal.Gui/Resources/config.json
@@ -11,6 +11,11 @@
// null).
//
"$schema": "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
+
+ // Set this to true in a .config file to be loaded to cause JSON parsing errors
+ // to throw exceptions.
+ "ConfigurationManager.ThrowOnJsonErrors": false,
+
"Application.AlternateBackwardKey": {
"Key": "PageUp",
"Modifiers": [
diff --git a/UnitTests/Configuration/ConfigurationMangerTests.cs b/UnitTests/Configuration/ConfigurationMangerTests.cs
index 7390d6092..e95433e22 100644
--- a/UnitTests/Configuration/ConfigurationMangerTests.cs
+++ b/UnitTests/Configuration/ConfigurationMangerTests.cs
@@ -544,8 +544,7 @@ namespace Terminal.Gui.ConfigurationTests {
// "yellow" is not a color
string json = @"
{
- ""Themes"" : {
- ""ThemeDefinitions"" : [
+ ""Themes"" : [
{
""Default"" : {
""ColorSchemes"": [
@@ -560,8 +559,7 @@ namespace Terminal.Gui.ConfigurationTests {
]
}
}
- ]
- }
+ ]
}";
JsonException jsonException = Assert.Throws (() => ConfigurationManager.Settings.Update (json, "test"));
@@ -570,8 +568,7 @@ namespace Terminal.Gui.ConfigurationTests {
// AbNormal is not a ColorScheme attribute
json = @"
{
- ""Themes"" : {
- ""ThemeDefinitions"" : [
+ ""Themes"" : [
{
""Default"" : {
""ColorSchemes"": [
@@ -586,8 +583,7 @@ namespace Terminal.Gui.ConfigurationTests {
]
}
}
- ]
- }
+ ]
}";
jsonException = Assert.Throws (() => ConfigurationManager.Settings.Update (json, "test"));
@@ -596,8 +592,7 @@ namespace Terminal.Gui.ConfigurationTests {
// Modify hotNormal background only
json = @"
{
- ""Themes"" : {
- ""ThemeDefinitions"" : [
+ ""Themes"" : [
{
""Default"" : {
""ColorSchemes"": [
@@ -611,8 +606,7 @@ namespace Terminal.Gui.ConfigurationTests {
]
}
}
- ]
- }
+ ]
}";
jsonException = Assert.Throws (() => ConfigurationManager.Settings.Update (json, "test"));
@@ -641,8 +635,7 @@ namespace Terminal.Gui.ConfigurationTests {
// "yellow" is not a color
string json = @"
{
- ""Themes"" : {
- ""ThemeDefinitions"" : [
+ ""Themes"" : [
{
""Default"" : {
""ColorSchemes"": [
@@ -657,7 +650,6 @@ namespace Terminal.Gui.ConfigurationTests {
]
}
}
- ]
}
}";
@@ -666,8 +658,7 @@ namespace Terminal.Gui.ConfigurationTests {
// AbNormal is not a ColorScheme attribute
json = @"
{
- ""Themes"" : {
- ""ThemeDefinitions"" : [
+ ""Themes"" : [
{
""Default"" : {
""ColorSchemes"": [
@@ -682,7 +673,6 @@ namespace Terminal.Gui.ConfigurationTests {
]
}
}
- ]
}
}";
@@ -691,8 +681,7 @@ namespace Terminal.Gui.ConfigurationTests {
// Modify hotNormal background only
json = @"
{
- ""Themes"" : {
- ""ThemeDefinitions"" : [
+ ""Themes"" : [
{
""Default"" : {
""ColorSchemes"": [
@@ -706,7 +695,6 @@ namespace Terminal.Gui.ConfigurationTests {
]
}
}
- ]
}
}";