using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using static Terminal.Gui.ConfigurationManager;
#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 {
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 = {
// No need to set converters - the ConfigRootConverter uses property attributes apply the correct
// Converter.
},
};
///
/// 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 BorderStyle DefaultBorderStyle {
/// ...
///
[AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class SerializableConfigurationProperty : System.Attribute {
///
/// Specifies the scope of the property.
///
public Type? Scope { 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 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;
}
return PropertyValue;
}
///
/// Retrieves (using reflection) the value of the static property described in
/// into .
///
///
public object? RetrieveValue ()
{
return PropertyValue = PropertyInfo!.GetValue (null);
}
///
/// Applies the to the property described by .
///
///
public bool Apply ()
{
if (PropertyValue != null) {
PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
}
return PropertyValue != null;
}
}
///
/// 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; }
///
/// 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} clases:");
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 = 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
// }
//}
}
}