using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace Terminal.Gui.Configuration;
/// Manages Themes.
///
/// A Theme is a collection of settings that are named. The default theme is named "Default".
/// The property is used to determine the currently active theme.
/// The property is a dictionary of themes.
///
public static class ThemeManager
{
///
/// Convenience method to get the current theme. The current theme is the item in the dictionary,
/// with the key of .
///
///
public static ThemeScope GetCurrentTheme () { return Themes! [Theme]; }
///
/// INTERNAL: Getter for .
/// Convenience method to get the themes dictionary. The themes dictionary is a dictionary of
/// objects, with the key being the name of the theme.
///
///
///
private static ConcurrentDictionary GetThemes ()
{
if (!ConfigurationManager.IsInitialized ())
{
// We're being called from the module initializer.
// We need to provide a dictionary of themes containing the hard-coded theme.
return GetHardCodedThemes ()!;
}
if (ConfigurationManager.Settings is null)
{
throw new InvalidOperationException ("Settings is null.");
}
if (ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
{
if (themes.HasValue)
{
return (themes.PropertyValue as ConcurrentDictionary)!;
}
return GetHardCodedThemes ()!;
}
throw new InvalidOperationException ("Settings has no Themes property.");
}
///
/// INTERNAL: Convenience method to get a list of theme names.
///
///
///
public static ImmutableList GetThemeNames ()
{
if (!ConfigurationManager.IsInitialized ())
{
// We're being called from the module initializer.
// We need to provide a dictionary of themes containing the hard-coded theme.
return GetHardCodedThemes ()!.Keys.ToImmutableList ();
}
if (ConfigurationManager.Settings is null)
{
throw new InvalidOperationException ("Settings is null.");
}
if (!ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
{
throw new InvalidOperationException ("Settings has no Themes property.");
}
ConcurrentDictionary? returnConcurrentDictionary;
if (themes.HasValue)
{
returnConcurrentDictionary = themes.PropertyValue as ConcurrentDictionary;
}
else
{
returnConcurrentDictionary = GetHardCodedThemes ();
}
return returnConcurrentDictionary!.Keys
.OrderBy (key => key == DEFAULT_THEME_NAME ? string.Empty : key) // Ensure DEFAULT_THEME_NAME is first
.ToImmutableList ();
}
///
/// Convenience method to get the current theme name. The current theme name is the value of .
///
///
public static string GetCurrentThemeName () { return Theme!; }
// TODO: Add a lock around Theme and Themes
// TODO: For now, this test can't run in parallel with other tests that access Theme or Themes.
// TODO: ThemeScopeList_WithThemes_ClonesSuccessfully
///
/// Gets the Themes dictionary. is preferred.
/// The backing store is ["Themes"].
/// However, if is false, this property will return the
/// hard-coded themes.
///
///
[JsonConverter (typeof (ConcurrentDictionaryJsonConverter))]
[ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
public static ConcurrentDictionary? Themes
{
// Note: This property getter must be public; DeepClone depends on it.
get => GetThemes ();
internal set => SetThemes (value);
}
///
/// INTERNAL: Setter for .
///
///
///
private static void SetThemes (ConcurrentDictionary? dictionary)
{
if (dictionary is { } && !dictionary.ContainsKey (DEFAULT_THEME_NAME))
{
throw new InvalidOperationException ($"Themes must include an item named {DEFAULT_THEME_NAME}");
}
if (ConfigurationManager.Settings is { } && ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
{
ConfigurationManager.Settings ["Themes"].PropertyValue = dictionary;
return;
}
throw new InvalidOperationException ("Settings is null.");
}
///
/// INTERNAL: Returns the hard-coded Themes dictionary.
///
///
///
private static ConcurrentDictionary? GetHardCodedThemes ()
{
ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope ();
if (hardCodedThemeScope is null)
{
throw new InvalidOperationException ("Hard coded theme scope is null.");
}
return new (new Dictionary { { DEFAULT_THEME_NAME, hardCodedThemeScope } }, StringComparer.InvariantCultureIgnoreCase);
}
///
/// INTERNAL: Returns the ThemeScope containing the hard-coded Themes.
///
///
private static ThemeScope GetHardCodedThemeScope ()
{
IEnumerable>? hardCodedThemeProperties = ConfigurationManager.GetHardCodedConfigPropertiesByScope ("ThemeScope");
if (hardCodedThemeProperties is null)
{
throw new InvalidOperationException ("Hard coded theme properties are null.");
}
var hardCodedThemeScope = new ThemeScope ();
foreach (KeyValuePair p in hardCodedThemeProperties)
{
hardCodedThemeScope.AddValue (p.Key, p.Value.PropertyValue);
}
return hardCodedThemeScope;
}
///
/// The name of the default theme ("Default").
///
public const string DEFAULT_THEME_NAME = "Default";
///
/// The currently selected theme. The backing store is ["Theme"].
///
[JsonInclude]
[ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
[JsonPropertyName ("Theme")]
public static string Theme
{
get
{
if (!ConfigurationManager.IsInitialized ())
{
// We're being called from the module initializer.
// Hard coded default value
return DEFAULT_THEME_NAME;
}
if (ConfigurationManager.Settings is { } && ConfigurationManager.Settings.TryGetValue ("Theme", out ConfigProperty? themeCp))
{
if (themeCp.HasValue)
{
return (themeCp.PropertyValue as string)!;
}
return DEFAULT_THEME_NAME;
}
throw new InvalidOperationException ("Settings is null.");
}
[RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigurationManager.Settings")]
[RequiresDynamicCode ("Calls Terminal.Gui.ConfigurationManager.Settings")]
set
{
if (!ConfigurationManager.IsInitialized ())
{
throw new InvalidOperationException ("Theme cannot be set before ConfigurationManager is initialized.");
}
if (ConfigurationManager.Settings is null || !ConfigurationManager.Settings.TryGetValue ("Theme", out ConfigProperty? themeCp))
{
throw new InvalidOperationException ("Settings is null.");
}
if (themeCp is null || !themeCp.HasValue)
{
throw new InvalidOperationException ("Theme has no value.");
}
if (!ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themesCp))
{
throw new InvalidOperationException ("Settings has no Themes property.");
}
string previousThemeValue = GetCurrentThemeName ();
if (value == previousThemeValue)
{
return;
}
if (!Themes!.ContainsKey (value))
{
Logging.Warning ($"{value} is not a valid theme name.");
}
// Update the backing store
ConfigurationManager.Settings! ["Theme"].PropertyValue = value;
OnThemeChanged (previousThemeValue, value);
}
}
///
/// INTERNAL: Updates to the current values of the static
/// properties.
///
[RequiresUnreferencedCode ("Calls Terminal.Gui.ThemeManager.Themes")]
[RequiresDynamicCode ("Calls Terminal.Gui.ThemeManager.Themes")]
internal static void UpdateToCurrentValues ()
{
// BUGBUG: This corrupts _hardCodedDefaults. See #4288
Themes! [Theme].UpdateToCurrentValues ();
}
///
/// INTERNAL: Loads all Themes to their hard-coded default values.
///
[RequiresUnreferencedCode ("Calls SchemeManager.LoadToHardCodedDefaults")]
[RequiresDynamicCode ("Calls SchemeManager.LoadToHardCodedDefaults")]
internal static void LoadHardCodedDefaults ()
{
if (!ConfigurationManager.IsInitialized ())
{
throw new InvalidOperationException ("ThemeManager is not initialized.");
}
if (ConfigurationManager.Settings is null)
{
return;
}
ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope ();
if (hardCodedThemeScope is null)
{
throw new InvalidOperationException ("Hard coded theme scope is null.");
}
ConcurrentDictionary hardCodedThemes = new (
new Dictionary
{
{ Theme, hardCodedThemeScope }
},
StringComparer.InvariantCultureIgnoreCase);
// BUGBUG: SchemeManager is broken and needs to be fixed to not have the hard coded schemes get overwritten.
// BUGBUG: This is a partial workaround
// BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/4288
SchemeManager.LoadToHardCodedDefaults ();
ConfigurationManager.Settings ["Themes"].PropertyValue = hardCodedThemes;
ConfigurationManager.Settings ["Theme"].PropertyValue = DEFAULT_THEME_NAME;
}
/// Called when the selected theme has changed. Fires the event.
internal static void OnThemeChanged (string previousThemeName, string newThemeName)
{
Logging.Debug ($"Themes.OnThemeChanged({previousThemeName}) -> {Theme}");
EventArgs args = new (newThemeName);
ThemeChanged?.Invoke (null, args);
}
/// Raised when the selected theme has changed.
public static event EventHandler>? ThemeChanged;
}