| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829 |
- using System.Collections.Frozen;
- using System.Collections.Immutable;
- using System.Diagnostics;
- using System.Diagnostics.CodeAnalysis;
- using System.Reflection;
- using System.Text.Encodings.Web;
- using System.Text.Json;
- using System.Text.Json.Serialization;
- namespace Terminal.Gui.Configuration;
- /// <summary>
- /// Provides settings and configuration management for Terminal.Gui applications. See the Configuration Deep Dive for
- /// more information: <see href="https://gui-cs.github.io/Terminal.Gui/docs/config.html"/>.
- /// <para>
- /// 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 <c>.tui</c> folder in the user's home
- /// directory (e.g. <c>C:/Users/username/.tui</c>, or <c>/usr/username/.tui</c>), the folder where the Terminal.Gui
- /// application was launched from (e.g. <c>./.tui</c> ), or as a resource within the Terminal.Gui application's
- /// main assembly.
- /// </para>
- /// <para>
- /// Settings are defined in JSON format, according to this schema:
- /// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
- /// </para>
- /// <para>
- /// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>.
- /// Settings that will apply to a specific Terminal.Gui application reside in files named
- /// <c>appname.config.json</c>, where <c>appname</c> is the assembly name of the application (e.g.
- /// <c>UICatalog.config.json</c>).
- /// </para>
- /// <para>
- /// Settings are applied using the precedence defined in <see cref="ConfigLocations"/>.
- /// </para>
- /// <para>
- /// Configuration Management is based on static properties decorated with the
- /// <see cref="ConfigurationPropertyAttribute"/>. Since these properties are static, changes to
- /// configuration settings are applied process-wide.
- /// </para>
- /// <para>
- /// Configuration Management is disabled by default and can be enabled by setting calling
- /// <see cref="ConfigurationManager.Enable"/>.
- /// </para>
- /// <para>
- /// See the UICatalog example for a complete example of how to use ConfigurationManager.
- /// </para>
- /// </summary>
- public static class ConfigurationManager
- {
- /// <summary>The backing property for <see cref="Settings"/> (config settings of <see cref="SettingsScope"/>).</summary>
- /// <remarks>
- /// Is <see langword="null"/> until <see cref="UpdateToCurrentValues"/> is called. Gets set to a new instance by
- /// deserialization
- /// (see <see cref="Load"/>).
- /// </remarks>
- private static SettingsScope? _settings;
- #pragma warning disable IDE1006 // Naming Styles
- private static readonly ReaderWriterLockSlim _settingsLockSlim = new ();
- #pragma warning restore IDE1006 // Naming Styles
- /// <summary>
- /// The root object of Terminal.Gui configuration settings / JSON schema.
- /// </summary>
- public static SettingsScope? Settings
- {
- get
- {
- _settingsLockSlim.EnterReadLock ();
- try
- {
- return _settings;
- }
- finally
- {
- _settingsLockSlim.ExitReadLock ();
- }
- }
- set
- {
- _settingsLockSlim.EnterWriteLock ();
- try
- {
- _settings = value;
- }
- finally
- {
- _settingsLockSlim.ExitWriteLock ();
- }
- }
- }
- #region Initialization
- // ConfigurationManager is initialized when the module is loaded, via ModuleInitializers.InitializeConfigurationManager
- // Once initialized, the ConfigurationManager is never un-initialized.
- // The _initialized field is set to true when the module is loaded and the ConfigurationManager is initialized.
- private static bool _initialized;
- #pragma warning disable IDE1006 // Naming Styles
- private static readonly object _initializedLock = new ();
- #pragma warning restore IDE1006 // Naming Styles
- /// <summary>
- /// INTERNAL: For Testing - Indicates whether the <see cref="ConfigurationManager"/> has been initialized.
- /// </summary>
- internal static bool IsInitialized ()
- {
- lock (_initializedLock)
- {
- {
- return _initialized;
- }
- }
- }
- // TODO: Find a way to make this cache truly read-only at the leaf node level.
- // TODO: Right now, the dictionary is frozen, but the ConfigProperty instances can still be modified
- // TODO: if the PropertyValue is a reference type.
- // TODO: See https://github.com/gui-cs/Terminal.Gui/issues/4288
- /// <summary>
- /// A cache of all<see cref="ConfigurationPropertyAttribute"/> properties and their hard coded values.
- /// </summary>
- /// <remarks>Is <see langword="null"/> until <see cref="Initialize"/> is called.</remarks>
- #pragma warning disable IDE1006 // Naming Styles
- internal static FrozenDictionary<string, ConfigProperty>? _hardCodedConfigPropertyCache;
- private static readonly object _hardCodedConfigPropertyCacheLock = new ();
- #pragma warning restore IDE1006 // Naming Styles
- internal static FrozenDictionary<string, ConfigProperty>? GetHardCodedConfigPropertyCache ()
- {
- lock (_hardCodedConfigPropertyCacheLock)
- {
- if (_hardCodedConfigPropertyCache is null)
- {
- throw new InvalidOperationException ("_hardCodedConfigPropertyCache has not been set.");
- }
- return _hardCodedConfigPropertyCache;
- }
- }
- /// <summary>
- /// An immutable cache of all <see cref="ConfigProperty"/>s in module decorated with the
- /// <see cref="ConfigurationPropertyAttribute"/> attribute. Both the dictionary and the contained
- /// <see cref="ConfigProperty"/>s
- /// are immutable.
- /// </summary>
- /// <remarks>Is <see langword="null"/> until <see cref="Initialize"/> is called.</remarks>
- private static ImmutableSortedDictionary<string, ConfigProperty>? _uninitializedConfigPropertiesCache;
- #pragma warning disable IDE1006 // Naming Styles
- private static readonly object _uninitializedConfigPropertiesCacheCacheLock = new ();
- #pragma warning restore IDE1006 // Naming Styles
- /// <summary>
- /// INTERNAL: Initializes the <see cref="ConfigurationManager"/>.
- /// This method is called when the module is loaded,
- /// via <see cref="ModuleInitializers.InitializeConfigurationManager"/>.
- /// For ConfigurationManager to access config resources, <see cref="IsEnabled"/> needs to be
- /// set to <see langword="true"/> after this method has been called.
- /// </summary>
- [RequiresDynamicCode (
- "Uses reflection to scan assemblies for configuration properties. "
- + "Only called during initialization and not needed during normal operation. "
- + "In AOT environments, ensure all types with ConfigurationPropertyAttribute are preserved.")]
- [RequiresUnreferencedCode (
- "Reflection requires all types with ConfigurationPropertyAttribute to be preserved in AOT. "
- + "Use the SourceGenerationContext to register all configuration property types.")]
- internal static void Initialize ()
- {
- lock (_initializedLock)
- {
- if (_initialized)
- {
- throw new InvalidOperationException ("ConfigurationManager is already initialized.");
- }
- }
- // Ensure ConfigProperty has cached the list of all the classes with config properties.
- ConfigProperty.Initialize ();
- // Cache all configuration properties
- lock (_uninitializedConfigPropertiesCacheCacheLock)
- {
- // _allConfigProperties: for ordered, iterable access (LINQ-friendly)
- // _hardCodedConfigPropertyCache: for high-speed key lookup (frozen)
- // Note GetAllConfigProperties returns a new instance and all the properties !HasValue and Immutable.
- _uninitializedConfigPropertiesCache = ConfigProperty.GetAllConfigProperties ();
- }
- // Create a COPY of the _allConfigPropertiesCache to ensure that the original is not modified.
- lock (_hardCodedConfigPropertyCacheLock)
- {
- _hardCodedConfigPropertyCache = ConfigProperty.GetAllConfigProperties ().ToFrozenDictionary ();
- foreach (KeyValuePair<string, ConfigProperty> hardCodedProperty in _hardCodedConfigPropertyCache)
- {
- // Set the PropertyValue to the hard coded value
- hardCodedProperty.Value.Immutable = false;
- hardCodedProperty.Value.UpdateToCurrentValue ();
- hardCodedProperty.Value.Immutable = true;
- }
- }
- lock (_initializedLock)
- {
- _initialized = true;
- }
- LoadHardCodedDefaults ();
- // BUGBUG: ThemeScope is broken and needs to be fixed to not have the hard coded schemes get overwritten.
- // BUGBUG: This a partial workaround.
- // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/4288
- ThemeManager.Themes? [ThemeManager.Theme]?.Apply ();
- }
- #endregion Initialization
- #region Enable/Disable
- private static bool _enabled;
- #pragma warning disable IDE1006 // Naming Styles
- private static readonly object _enabledLock = new ();
- #pragma warning restore IDE1006 // Naming Styles
- /// <summary>
- /// Gets whether <see cref="ConfigurationManager"/> is enabled or not.
- /// If <see langword="false"/>, only the hard coded defaults will be loaded. See <see cref="Enable"/> and
- /// <see cref="Disable"/>
- /// </summary>
- public static bool IsEnabled
- {
- get
- {
- lock (_enabledLock)
- {
- return _enabled;
- }
- }
- }
- /// <summary>
- /// Enables <see cref="ConfigurationManager"/>. If <paramref name="locations"/> is <see cref="ConfigLocations.None"/>,
- /// ConfigurationManager will be enabled as-is; no configuration will be loaded or applied. If
- /// <paramref name="locations"/> is <see cref="ConfigLocations.HardCoded"/>,
- /// ConfigurationManager will be enabled and reset to hard-coded defaults.
- /// For any other value,
- /// ConfigurationManager will be enabled and the configuration will be loaded from the specified locations and applied.
- /// </summary>
- /// <param name="locations"></param>
- [RequiresUnreferencedCode ("AOT")]
- [RequiresDynamicCode ("AOT")]
- public static void Enable (ConfigLocations locations)
- {
- if (IsEnabled)
- {
- return;
- }
- lock (_enabledLock)
- {
- _enabled = true;
- }
- ClearJsonErrors ();
- if (locations == ConfigLocations.None)
- {
- return;
- }
- Load (locations);
- // Works even if ConfigurationManager is not enabled.
- InternalApply ();
- }
- /// <summary>
- /// Disables <see cref="ConfigurationManager"/>.
- /// </summary>
- /// <param name="resetToHardCodedDefaults">
- /// If <see langword="true"/> all static <see cref="ConfigurationPropertyAttribute"/> properties will be reset to their
- /// initial, hard-coded
- /// defaults.
- /// </param>
- [RequiresUnreferencedCode ("Calls ResetToHardCodedDefaults")]
- [RequiresDynamicCode ("Calls ResetToHardCodedDefaults")]
- public static void Disable (bool resetToHardCodedDefaults = false)
- {
- lock (_enabledLock)
- {
- _enabled = false;
- }
- if (resetToHardCodedDefaults)
- {
- // Calls Apply
- ResetToHardCodedDefaults ();
- }
- }
- #endregion Enable/Disable
- #region Reset
- // `Update` - Updates the configuration from either the current values or the hard-coded defaults.
- // Updating does not load the configuration; it only updates the configuration to the values currently
- // in the static ConfigProperties.
- /// <summary>
- /// INTERNAL: Updates <see cref="ConfigurationManager"/> to the settings from the current
- /// values of the static <see cref="ConfigurationPropertyAttribute"/> properties.
- /// </summary>
- [RequiresUnreferencedCode ("AOT")]
- [RequiresDynamicCode ("AOT")]
- internal static void UpdateToCurrentValues ()
- {
- if (!IsInitialized ())
- {
- throw new InvalidOperationException ("Initialize must be called first.");
- }
- _settingsLockSlim.EnterWriteLock ();
- try
- {
- _settings = new ();
- _settings.LoadHardCodedDefaults ();
- }
- finally
- {
- _settingsLockSlim.ExitWriteLock ();
- }
- Settings!.UpdateToCurrentValues ();
- ThemeManager.UpdateToCurrentValues ();
- AppSettings!.UpdateToCurrentValues ();
- }
- /// <summary>
- /// INTERNAL: Loads the hard-coded values of the
- /// <see cref="ConfigurationPropertyAttribute"/> properties and applies them.
- /// </summary>
- [RequiresUnreferencedCode ("AOT")]
- [RequiresDynamicCode ("AOT")]
- internal static void ResetToHardCodedDefaults ()
- {
- LoadHardCodedDefaults ();
- Applied = null;
- Updated = null;
- // Works even if ConfigurationManager is not enabled.
- InternalApply ();
- }
- #endregion Reset
- #region Load
- // `Load` - Load configuration from the given location(s), updating the configuration with any new values.
- // Loading does not apply the settings to the application; that happens when the `Apply` method is called.
- /// <summary>
- /// INTERNAL: Loads all hard-coded configuration properties. Use <see cref="Apply"/> to cause the loaded settings to be
- /// applied to the running application.
- /// </summary>
- [RequiresUnreferencedCode ("AOT")]
- [RequiresDynamicCode ("AOT")]
- internal static void LoadHardCodedDefaults ()
- {
- if (!IsInitialized ())
- {
- throw new InvalidOperationException ("Initialize must be called first.");
- }
- RuntimeConfig = null;
- SourcesManager!.Sources.Clear ();
- SourcesManager.AddSource (ConfigLocations.HardCoded, "HardCoded");
- Settings = new ();
- Settings!.LoadHardCodedDefaults ();
- ThemeManager.LoadHardCodedDefaults ();
- AppSettings!.LoadHardCodedDefaults ();
- }
- /// <summary>
- /// Loads all settings found in <paramref name="locations"/>. Use <see cref="Apply"/> to cause the loaded settings to
- /// be applied to the running application.
- /// </summary>
- /// <exception cref="ConfigurationManagerNotEnabledException">Configuration manager is not enabled.</exception>
- [RequiresUnreferencedCode ("AOT")]
- [RequiresDynamicCode ("AOT")]
- public static void Load (ConfigLocations locations)
- {
- if (!IsEnabled)
- {
- throw new ConfigurationManagerNotEnabledException ();
- }
- // Only load the hard-coded defaults if the user has not specified any locations.
- if (locations == ConfigLocations.HardCoded)
- {
- LoadHardCodedDefaults ();
- }
- if (locations.HasFlag (ConfigLocations.LibraryResources))
- {
- SourcesManager?.Load (
- Settings,
- typeof (ConfigurationManager).Assembly,
- $"Terminal.Gui.Resources.{_configFilename}",
- ConfigLocations.LibraryResources);
- }
- if (locations.HasFlag (ConfigLocations.AppResources))
- {
- string? embeddedStylesResourceName = Assembly.GetEntryAssembly ()
- ?
- .GetManifestResourceNames ()
- .FirstOrDefault (x => x.EndsWith (_configFilename));
- if (string.IsNullOrEmpty (embeddedStylesResourceName))
- {
- embeddedStylesResourceName = _configFilename;
- }
- SourcesManager?.Load (Settings, Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!, ConfigLocations.AppResources);
- }
- // TODO: Determine if Runtime should be applied last.
- if (locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig))
- {
- SourcesManager?.Load (Settings, RuntimeConfig, "ConfigurationManager.RuntimeConfig", ConfigLocations.Runtime);
- }
- if (locations.HasFlag (ConfigLocations.GlobalCurrent))
- {
- SourcesManager?.Load (Settings, $"./.tui/{_configFilename}", ConfigLocations.GlobalCurrent);
- }
- if (locations.HasFlag (ConfigLocations.GlobalHome))
- {
- SourcesManager?.Load (Settings, $"~/.tui/{_configFilename}", ConfigLocations.GlobalHome);
- }
- if (locations.HasFlag (ConfigLocations.AppCurrent))
- {
- SourcesManager?.Load (Settings, $"./.tui/{AppName}.{_configFilename}", ConfigLocations.AppCurrent);
- }
- if (locations.HasFlag (ConfigLocations.AppHome))
- {
- SourcesManager?.Load (Settings, $"~/.tui/{AppName}.{_configFilename}", ConfigLocations.AppHome);
- }
- }
- // TODO: Rename to Loaded?
- /// <summary>
- /// Called when the configuration has been updated from a configuration file or reset. Invokes the
- /// <see cref="Updated"/>
- /// event.
- /// </summary>
- public static void OnUpdated ()
- {
- //Logging.Trace (@"");
- if (!IsEnabled)
- {
- return;
- }
- // Use a local copy of the event delegate when invoking it to avoid race conditions.
- EventHandler<ConfigurationManagerEventArgs>? handler = Updated;
- handler?.Invoke (null, new ());
- }
- /// <summary>Event fired when the configuration has been updated from a configuration source or reset.</summary>
- public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
- #endregion Load
- #region Apply
- // `Apply` - Apply the configuration to the application; this means the settings are copied from the
- // configuration properties to the corresponding `static` `[ConfigurationProperty]` properties.
- /// <summary>
- /// Applies the configuration settings to static <see cref="ConfigurationPropertyAttribute"/> properties.
- /// ConfigurationManager must be Enabled.
- /// </summary>
- /// <exception cref="ConfigurationManagerNotEnabledException">Configuration Manager is not enabled.</exception>
- [RequiresUnreferencedCode ("AOT")]
- [RequiresDynamicCode ("AOT")]
- public static void Apply ()
- {
- if (!IsEnabled)
- {
- throw new ConfigurationManagerNotEnabledException ();
- }
- InternalApply ();
- }
- [RequiresUnreferencedCode ("Calls Terminal.Gui.Scope<T>.Apply()")]
- [RequiresDynamicCode ("Calls Terminal.Gui.Scope<T>.Apply()")]
- private static void InternalApply ()
- {
- var settings = false;
- var themes = false;
- var appSettings = false;
- try
- {
- settings = Settings?.Apply () ?? false;
- themes = ThemeManager.Themes? [ThemeManager.Theme]?.Apply () ?? false;
- appSettings = AppSettings?.Apply () ?? false;
- }
- catch (JsonException e)
- {
- if (ThrowOnJsonErrors ?? false)
- {
- throw;
- }
- else
- {
- AddJsonError ($"Error applying Configuration Change: {e.Message}");
- }
- }
- finally
- {
- if (settings || themes || appSettings)
- {
- OnApplied ();
- }
- }
- }
- /// <summary>
- /// Called when an updated configuration has been applied to the application. Fires the <see cref="Applied"/>
- /// event.
- /// </summary>
- /// <exception cref="ConfigurationManagerNotEnabledException">Configuration manager is not enabled.</exception>
- private static void OnApplied ()
- {
- if (!IsEnabled)
- {
- return;
- }
- // Use a local copy of the event delegate when invoking it to avoid race conditions.
- EventHandler<ConfigurationManagerEventArgs>? handler = Applied;
- handler?.Invoke (null, new ());
- // TODO: Refactor ConfigurationManager to not use an event handler for this.
- // Instead, have it call a method on any class appropriately attributed
- // to update the cached values. See Issue #2871
- }
- /// <summary>Event fired when an updated configuration has been applied to the application.</summary>
- public static event EventHandler<ConfigurationManagerEventArgs>? Applied;
- #endregion Apply
- #region Sources
- // `Sources` - A source is a location where a configuration can be stored. Sources are defined in the `ConfigLocations` enum.
- [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
- internal static readonly SourceGenerationContext SerializerContext = new (
- new()
- {
- // Be relaxed
- ReadCommentHandling = JsonCommentHandling.Skip,
- PropertyNameCaseInsensitive = true,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- WriteIndented = true,
- AllowTrailingCommas = true,
- Converters =
- {
- // We override the standard Rune converter to support specifying Glyphs in
- // a flexible way
- new RuneJsonConverter (),
- // Override Key to support "Ctrl+Q" format.
- new KeyJsonConverter ()
- },
- // Enables Key to be "Ctrl+Q" vs "Ctrl\u002BQ"
- Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
- TypeInfoResolver = SourceGenerationContext.Default
- });
- /// <summary>
- /// Gets the Sources Manager - manages the loading of configuration sources from files and resources.
- /// </summary>
- public static SourcesManager? SourcesManager { get; internal set; } = new ();
- /// <summary>
- /// Gets or sets the in-memory config.json. See <see cref="ConfigLocations.Runtime"/>.
- /// </summary>
- public static string? RuntimeConfig { get; set; } = """{ }""";
- [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
- private static readonly string _configFilename = "config.json";
- #endregion Sources
- #region AppSettings
- /// <summary>
- /// Gets or sets the application-specific configuration settings (config properties with the
- /// <see cref="AppSettingsScope"/> scope.
- /// </summary>
- [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
- [JsonPropertyName ("AppSettings")]
- public static AppSettingsScope? AppSettings
- {
- [RequiresUnreferencedCode ("AOT")]
- [RequiresDynamicCode ("AOT")]
- get
- {
- if (!IsInitialized ())
- {
- // We're being called from the module initializer.
- // Hard coded default value is an empty AppSettingsScope
- var appSettings = new AppSettingsScope ();
- appSettings.LoadHardCodedDefaults ();
- return appSettings;
- }
- if (Settings is null || !Settings.TryGetValue ("AppSettings", out ConfigProperty? appSettingsConfigProperty))
- {
- throw new InvalidOperationException ("Settings is null.");
- }
- {
- if (!appSettingsConfigProperty.HasValue)
- {
- var appSettings = new AppSettingsScope ();
- appSettings.UpdateToCurrentValues ();
- return appSettings;
- }
- return (appSettingsConfigProperty.PropertyValue as AppSettingsScope)!;
- }
- }
- [RequiresUnreferencedCode ("AOT")]
- [RequiresDynamicCode ("AOT")]
- set
- {
- if (!IsInitialized ())
- {
- throw new InvalidOperationException ("AppSettings cannot be set before ConfigurationManager is initialized.");
- }
- // Check if the AppSettings is the same as the previous one
- if (value != Settings! ["AppSettings"].PropertyValue)
- {
- // Update the backing store
- Settings! ["AppSettings"].PropertyValue = value;
- //Instance.OnThemeChanged (previousThemeValue);
- }
- }
- }
- #endregion AppSettings
- #region Error Logging
- [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
- internal static StringBuilder _jsonErrors = new ();
- /// <summary>
- /// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters an
- /// error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the console
- /// when <see cref="Application.Shutdown"/> is called.
- /// </summary>
- [ConfigurationProperty (Scope = typeof (SettingsScope))]
- public static bool? ThrowOnJsonErrors { get; set; } = false;
- #pragma warning disable IDE1006 // Naming Styles
- private static readonly object _jsonErrorsLock = new ();
- #pragma warning restore IDE1006 // Naming Styles
- internal static void AddJsonError (string error)
- {
- Logging.Error ($"{error}");
- lock (_jsonErrorsLock)
- {
- _jsonErrors.AppendLine (@$" {error}");
- }
- }
- private static void ClearJsonErrors ()
- {
- lock (_jsonErrorsLock)
- {
- _jsonErrors.Clear ();
- }
- }
- /// <summary>Prints any Json deserialization errors that occurred during deserialization to the console.</summary>
- public static void PrintJsonErrors ()
- {
- lock (_jsonErrorsLock)
- {
- if (_jsonErrors.Length > 0)
- {
- Console.WriteLine (
- @"Terminal.Gui ConfigurationManager encountered these errors while reading configuration files"
- + @"(set ThrowOnJsonErrors to have these caught during execution):");
- Console.WriteLine (_jsonErrors.ToString ());
- }
- }
- }
- #endregion Error Logging
- /// <summary>Returns an empty Json document with just the $schema tag.</summary>
- /// <returns></returns>
- public static string GetEmptyConfig ()
- {
- var emptyScope = new SettingsScope ();
- emptyScope.Clear ();
- return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext!);
- }
- /// <summary>Returns a Json document containing the hard-coded config.</summary>
- /// <returns></returns>
- public static string GetHardCodedConfig ()
- {
- var emptyScope = new SettingsScope ();
- emptyScope.LoadHardCodedDefaults ();
- IEnumerable<KeyValuePair<string, ConfigProperty>>? settings = GetHardCodedConfigPropertiesByScope ("SettingsScope");
- if (settings is null)
- {
- throw new InvalidOperationException ("GetHardCodedConfigPropertiesByScope returned null.");
- }
- Dictionary<string, ConfigProperty> settingsDict = settings.ToDictionary ();
- foreach (KeyValuePair<string, ConfigProperty> p in Settings!.Where (cp => cp.Value.PropertyInfo is { }))
- {
- emptyScope [p.Key].PropertyValue = settingsDict [p.Key].PropertyValue;
- }
- return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext!);
- }
- /// <summary>Name of the running application. By default, this property is set to the application's assembly name.</summary>
- public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
- /// <summary>
- /// INTERNAL: Retrieves all uninitialized configuration properties that belong to a specific scope from the cache.
- /// The items in the collection are references to the original <see cref="ConfigProperty"/> objects in the
- /// cache. They do not have values and have <see cref="ConfigProperty.Immutable"/> set.
- /// </summary>
- internal static IEnumerable<KeyValuePair<string, ConfigProperty>>? GetUninitializedConfigPropertiesByScope (string scopeType)
- {
- // AOT Note: This method does NOT need the RequiresUnreferencedCode attribute as it is not using reflection
- // and is not using any dynamic code. _allConfigProperties is a static property that is set in the module initializer
- // and is not using any dynamic code. In addition, ScopeType are registered in SourceGenerationContext.
- if (_uninitializedConfigPropertiesCache is null)
- {
- throw new InvalidOperationException ("_allConfigPropertiesCache has not been set.");
- }
- if (string.IsNullOrEmpty (scopeType))
- {
- return _uninitializedConfigPropertiesCache;
- }
- lock (_uninitializedConfigPropertiesCacheCacheLock)
- {
- // Filter properties by scope using the cached ScopeType property instead of reflection
- IEnumerable<KeyValuePair<string, ConfigProperty>>? filtered = _uninitializedConfigPropertiesCache?.Where (cp => cp.Value.ScopeType == scopeType);
- Debug.Assert (filtered is { });
- IEnumerable<KeyValuePair<string, ConfigProperty>> configPropertiesByScope =
- filtered as KeyValuePair<string, ConfigProperty> [] ?? filtered.ToArray ();
- Debug.Assert (configPropertiesByScope.All (v => !v.Value.HasValue));
- return configPropertiesByScope;
- }
- }
- /// <summary>
- /// INTERNAL: Retrieves all configuration properties that belong to a specific scope from the hard coded value cache.
- /// The items in the collection are references to the original <see cref="ConfigProperty"/> objects in the
- /// cache. They contain the hard coded values and have <see cref="ConfigProperty.Immutable"/> set.
- /// </summary>
- internal static IEnumerable<KeyValuePair<string, ConfigProperty>>? GetHardCodedConfigPropertiesByScope (string scopeType)
- {
- // AOT Note: This method does NOT need the RequiresUnreferencedCode attribute as it is not using reflection
- // and is not using any dynamic code. _allConfigProperties is a static property that is set in the module initializer
- // and is not using any dynamic code. In addition, ScopeType are registered in SourceGenerationContext.
- // Filter properties by scope
- IEnumerable<KeyValuePair<string, ConfigProperty>>? cache = GetHardCodedConfigPropertyCache ();
- if (cache is null)
- {
- throw new InvalidOperationException ("GetHardCodedConfigPropertyCache returned null");
- }
- if (string.IsNullOrEmpty (scopeType))
- {
- return cache;
- }
- // Use the cached ScopeType property instead of reflection
- IEnumerable<KeyValuePair<string, ConfigProperty>>? scopedCache = cache?.Where (cp => cp.Value.ScopeType == scopeType);
- return scopedCache!;
- }
- }
|