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;
///
/// Provides settings and configuration management for Terminal.Gui applications. See the Configuration Deep Dive for
/// more information: .
///
/// 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 precedence defined in .
///
///
/// Configuration Management is based on static properties decorated with the
/// . Since these properties are static, changes to
/// configuration settings are applied process-wide.
///
///
/// Configuration Management is disabled by default and can be enabled by setting calling
/// .
///
///
/// See the UICatalog example for a complete example of how to use ConfigurationManager.
///
///
public static class ConfigurationManager
{
/// The backing property for (config settings of ).
///
/// Is until is called. Gets set to a new instance by
/// deserialization
/// (see ).
///
private static SettingsScope? _settings;
#pragma warning disable IDE1006 // Naming Styles
private static readonly ReaderWriterLockSlim _settingsLockSlim = new ();
#pragma warning restore IDE1006 // Naming Styles
///
/// The root object of Terminal.Gui configuration settings / JSON schema.
///
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
///
/// INTERNAL: For Testing - Indicates whether the has been initialized.
///
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
///
/// A cache of all properties and their hard coded values.
///
/// Is until is called.
#pragma warning disable IDE1006 // Naming Styles
internal static FrozenDictionary? _hardCodedConfigPropertyCache;
private static readonly object _hardCodedConfigPropertyCacheLock = new ();
#pragma warning restore IDE1006 // Naming Styles
internal static FrozenDictionary? GetHardCodedConfigPropertyCache ()
{
lock (_hardCodedConfigPropertyCacheLock)
{
if (_hardCodedConfigPropertyCache is null)
{
throw new InvalidOperationException ("_hardCodedConfigPropertyCache has not been set.");
}
return _hardCodedConfigPropertyCache;
}
}
///
/// An immutable cache of all s in module decorated with the
/// attribute. Both the dictionary and the contained
/// s
/// are immutable.
///
/// Is until is called.
private static ImmutableSortedDictionary? _uninitializedConfigPropertiesCache;
#pragma warning disable IDE1006 // Naming Styles
private static readonly object _uninitializedConfigPropertiesCacheCacheLock = new ();
#pragma warning restore IDE1006 // Naming Styles
///
/// INTERNAL: Initializes the .
/// This method is called when the module is loaded,
/// via .
/// For ConfigurationManager to access config resources, needs to be
/// set to after this method has been called.
///
[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 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
///
/// Gets whether is enabled or not.
/// If , only the hard coded defaults will be loaded. See and
///
///
public static bool IsEnabled
{
get
{
lock (_enabledLock)
{
return _enabled;
}
}
}
///
/// Enables . If is ,
/// ConfigurationManager will be enabled as-is; no configuration will be loaded or applied. If
/// is ,
/// 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.
///
///
[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 ();
}
///
/// Disables .
///
///
/// If all static properties will be reset to their
/// initial, hard-coded
/// defaults.
///
[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.
///
/// INTERNAL: Updates to the settings from the current
/// values of the static properties.
///
[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 ();
}
///
/// INTERNAL: Loads the hard-coded values of the
/// properties and applies them.
///
[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.
///
/// INTERNAL: Loads all hard-coded configuration properties. Use to cause the loaded settings to be
/// applied to the running application.
///
[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 ();
}
///
/// Loads all settings found in . Use to cause the loaded settings to
/// be applied to the running application.
///
/// Configuration manager is not enabled.
[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?
///
/// Called when the configuration has been updated from a configuration file or reset. Invokes the
///
/// event.
///
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? handler = Updated;
handler?.Invoke (null, new ());
}
/// Event fired when the configuration has been updated from a configuration source or reset.
public static event EventHandler? 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.
///
/// Applies the configuration settings to static properties.
/// ConfigurationManager must be Enabled.
///
/// Configuration Manager is not enabled.
[RequiresUnreferencedCode ("AOT")]
[RequiresDynamicCode ("AOT")]
public static void Apply ()
{
if (!IsEnabled)
{
throw new ConfigurationManagerNotEnabledException ();
}
InternalApply ();
}
[RequiresUnreferencedCode ("Calls Terminal.Gui.Scope.Apply()")]
[RequiresDynamicCode ("Calls Terminal.Gui.Scope.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 ();
}
}
}
///
/// Called when an updated configuration has been applied to the application. Fires the
/// event.
///
/// Configuration manager is not enabled.
private static void OnApplied ()
{
if (!IsEnabled)
{
return;
}
// Use a local copy of the event delegate when invoking it to avoid race conditions.
EventHandler? 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
}
/// Event fired when an updated configuration has been applied to the application.
public static event EventHandler? 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 = "")]
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
});
///
/// Gets the Sources Manager - manages the loading of configuration sources from files and resources.
///
public static SourcesManager? SourcesManager { get; internal set; } = new ();
///
/// Gets or sets the in-memory config.json. See .
///
public static string? RuntimeConfig { get; set; } = """{ }""";
[SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")]
private static readonly string _configFilename = "config.json";
#endregion Sources
#region AppSettings
///
/// Gets or sets the application-specific configuration settings (config properties with the
/// scope.
///
[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 = "")]
internal static StringBuilder _jsonErrors = new ();
///
/// 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.
///
[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 ();
}
}
/// Prints any Json deserialization errors that occurred during deserialization to the console.
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
/// Returns an empty Json document with just the $schema tag.
///
public static string GetEmptyConfig ()
{
var emptyScope = new SettingsScope ();
emptyScope.Clear ();
return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext!);
}
/// Returns a Json document containing the hard-coded config.
///
public static string GetHardCodedConfig ()
{
var emptyScope = new SettingsScope ();
emptyScope.LoadHardCodedDefaults ();
IEnumerable>? settings = GetHardCodedConfigPropertiesByScope ("SettingsScope");
if (settings is null)
{
throw new InvalidOperationException ("GetHardCodedConfigPropertiesByScope returned null.");
}
Dictionary settingsDict = settings.ToDictionary ();
foreach (KeyValuePair p in Settings!.Where (cp => cp.Value.PropertyInfo is { }))
{
emptyScope [p.Key].PropertyValue = settingsDict [p.Key].PropertyValue;
}
return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext!);
}
/// 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 ()!;
///
/// 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 objects in the
/// cache. They do not have values and have set.
///
internal static IEnumerable>? 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>? filtered = _uninitializedConfigPropertiesCache?.Where (cp => cp.Value.ScopeType == scopeType);
Debug.Assert (filtered is { });
IEnumerable> configPropertiesByScope =
filtered as KeyValuePair [] ?? filtered.ToArray ();
Debug.Assert (configPropertiesByScope.All (v => !v.Value.HasValue));
return configPropertiesByScope;
}
}
///
/// 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 objects in the
/// cache. They contain the hard coded values and have set.
///
internal static IEnumerable>? 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>? 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>? scopedCache = cache?.Where (cp => cp.Value.ScopeType == scopeType);
return scopedCache!;
}
}