ConfigurationManager.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. using System.Collections.Frozen;
  2. using System.Collections.Immutable;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Reflection;
  6. using System.Text.Encodings.Web;
  7. using System.Text.Json;
  8. using System.Text.Json.Serialization;
  9. namespace Terminal.Gui.Configuration;
  10. /// <summary>
  11. /// Provides settings and configuration management for Terminal.Gui applications. See the Configuration Deep Dive for
  12. /// more information: <see href="https://gui-cs.github.io/Terminal.Gui/docs/config.html"/>.
  13. /// <para>
  14. /// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted
  15. /// configuration files. The configuration files can be placed in at <c>.tui</c> folder in the user's home
  16. /// directory (e.g. <c>C:/Users/username/.tui</c>, or <c>/usr/username/.tui</c>), the folder where the Terminal.Gui
  17. /// application was launched from (e.g. <c>./.tui</c> ), or as a resource within the Terminal.Gui application's
  18. /// main assembly.
  19. /// </para>
  20. /// <para>
  21. /// Settings are defined in JSON format, according to this schema:
  22. /// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
  23. /// </para>
  24. /// <para>
  25. /// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>.
  26. /// Settings that will apply to a specific Terminal.Gui application reside in files named
  27. /// <c>appname.config.json</c>, where <c>appname</c> is the assembly name of the application (e.g.
  28. /// <c>UICatalog.config.json</c>).
  29. /// </para>
  30. /// <para>
  31. /// Settings are applied using the precedence defined in <see cref="ConfigLocations"/>.
  32. /// </para>
  33. /// <para>
  34. /// Configuration Management is based on static properties decorated with the
  35. /// <see cref="ConfigurationPropertyAttribute"/>. Since these properties are static, changes to
  36. /// configuration settings are applied process-wide.
  37. /// </para>
  38. /// <para>
  39. /// Configuration Management is disabled by default and can be enabled by setting calling
  40. /// <see cref="ConfigurationManager.Enable"/>.
  41. /// </para>
  42. /// <para>
  43. /// See the UICatalog example for a complete example of how to use ConfigurationManager.
  44. /// </para>
  45. /// </summary>
  46. public static class ConfigurationManager
  47. {
  48. /// <summary>The backing property for <see cref="Settings"/> (config settings of <see cref="SettingsScope"/>).</summary>
  49. /// <remarks>
  50. /// Is <see langword="null"/> until <see cref="UpdateToCurrentValues"/> is called. Gets set to a new instance by
  51. /// deserialization
  52. /// (see <see cref="Load"/>).
  53. /// </remarks>
  54. private static SettingsScope? _settings;
  55. #pragma warning disable IDE1006 // Naming Styles
  56. private static readonly ReaderWriterLockSlim _settingsLockSlim = new ();
  57. #pragma warning restore IDE1006 // Naming Styles
  58. /// <summary>
  59. /// The root object of Terminal.Gui configuration settings / JSON schema.
  60. /// </summary>
  61. public static SettingsScope? Settings
  62. {
  63. get
  64. {
  65. _settingsLockSlim.EnterReadLock ();
  66. try
  67. {
  68. return _settings;
  69. }
  70. finally
  71. {
  72. _settingsLockSlim.ExitReadLock ();
  73. }
  74. }
  75. set
  76. {
  77. _settingsLockSlim.EnterWriteLock ();
  78. try
  79. {
  80. _settings = value;
  81. }
  82. finally
  83. {
  84. _settingsLockSlim.ExitWriteLock ();
  85. }
  86. }
  87. }
  88. #region Initialization
  89. // ConfigurationManager is initialized when the module is loaded, via ModuleInitializers.InitializeConfigurationManager
  90. // Once initialized, the ConfigurationManager is never un-initialized.
  91. // The _initialized field is set to true when the module is loaded and the ConfigurationManager is initialized.
  92. private static bool _initialized;
  93. #pragma warning disable IDE1006 // Naming Styles
  94. private static readonly object _initializedLock = new ();
  95. #pragma warning restore IDE1006 // Naming Styles
  96. /// <summary>
  97. /// INTERNAL: For Testing - Indicates whether the <see cref="ConfigurationManager"/> has been initialized.
  98. /// </summary>
  99. internal static bool IsInitialized ()
  100. {
  101. lock (_initializedLock)
  102. {
  103. {
  104. return _initialized;
  105. }
  106. }
  107. }
  108. // TODO: Find a way to make this cache truly read-only at the leaf node level.
  109. // TODO: Right now, the dictionary is frozen, but the ConfigProperty instances can still be modified
  110. // TODO: if the PropertyValue is a reference type.
  111. // TODO: See https://github.com/gui-cs/Terminal.Gui/issues/4288
  112. /// <summary>
  113. /// A cache of all<see cref="ConfigurationPropertyAttribute"/> properties and their hard coded values.
  114. /// </summary>
  115. /// <remarks>Is <see langword="null"/> until <see cref="Initialize"/> is called.</remarks>
  116. #pragma warning disable IDE1006 // Naming Styles
  117. internal static FrozenDictionary<string, ConfigProperty>? _hardCodedConfigPropertyCache;
  118. private static readonly object _hardCodedConfigPropertyCacheLock = new ();
  119. #pragma warning restore IDE1006 // Naming Styles
  120. internal static FrozenDictionary<string, ConfigProperty>? GetHardCodedConfigPropertyCache ()
  121. {
  122. lock (_hardCodedConfigPropertyCacheLock)
  123. {
  124. if (_hardCodedConfigPropertyCache is null)
  125. {
  126. throw new InvalidOperationException ("_hardCodedConfigPropertyCache has not been set.");
  127. }
  128. return _hardCodedConfigPropertyCache;
  129. }
  130. }
  131. /// <summary>
  132. /// An immutable cache of all <see cref="ConfigProperty"/>s in module decorated with the
  133. /// <see cref="ConfigurationPropertyAttribute"/> attribute. Both the dictionary and the contained
  134. /// <see cref="ConfigProperty"/>s
  135. /// are immutable.
  136. /// </summary>
  137. /// <remarks>Is <see langword="null"/> until <see cref="Initialize"/> is called.</remarks>
  138. private static ImmutableSortedDictionary<string, ConfigProperty>? _uninitializedConfigPropertiesCache;
  139. #pragma warning disable IDE1006 // Naming Styles
  140. private static readonly object _uninitializedConfigPropertiesCacheCacheLock = new ();
  141. #pragma warning restore IDE1006 // Naming Styles
  142. /// <summary>
  143. /// INTERNAL: Initializes the <see cref="ConfigurationManager"/>.
  144. /// This method is called when the module is loaded,
  145. /// via <see cref="ModuleInitializers.InitializeConfigurationManager"/>.
  146. /// For ConfigurationManager to access config resources, <see cref="IsEnabled"/> needs to be
  147. /// set to <see langword="true"/> after this method has been called.
  148. /// </summary>
  149. [RequiresDynamicCode (
  150. "Uses reflection to scan assemblies for configuration properties. "
  151. + "Only called during initialization and not needed during normal operation. "
  152. + "In AOT environments, ensure all types with ConfigurationPropertyAttribute are preserved.")]
  153. [RequiresUnreferencedCode (
  154. "Reflection requires all types with ConfigurationPropertyAttribute to be preserved in AOT. "
  155. + "Use the SourceGenerationContext to register all configuration property types.")]
  156. internal static void Initialize ()
  157. {
  158. lock (_initializedLock)
  159. {
  160. if (_initialized)
  161. {
  162. throw new InvalidOperationException ("ConfigurationManager is already initialized.");
  163. }
  164. }
  165. // Ensure ConfigProperty has cached the list of all the classes with config properties.
  166. ConfigProperty.Initialize ();
  167. // Cache all configuration properties
  168. lock (_uninitializedConfigPropertiesCacheCacheLock)
  169. {
  170. // _allConfigProperties: for ordered, iterable access (LINQ-friendly)
  171. // _hardCodedConfigPropertyCache: for high-speed key lookup (frozen)
  172. // Note GetAllConfigProperties returns a new instance and all the properties !HasValue and Immutable.
  173. _uninitializedConfigPropertiesCache = ConfigProperty.GetAllConfigProperties ();
  174. }
  175. // Create a COPY of the _allConfigPropertiesCache to ensure that the original is not modified.
  176. lock (_hardCodedConfigPropertyCacheLock)
  177. {
  178. _hardCodedConfigPropertyCache = ConfigProperty.GetAllConfigProperties ().ToFrozenDictionary ();
  179. foreach (KeyValuePair<string, ConfigProperty> hardCodedProperty in _hardCodedConfigPropertyCache)
  180. {
  181. // Set the PropertyValue to the hard coded value
  182. hardCodedProperty.Value.Immutable = false;
  183. hardCodedProperty.Value.UpdateToCurrentValue ();
  184. hardCodedProperty.Value.Immutable = true;
  185. }
  186. }
  187. lock (_initializedLock)
  188. {
  189. _initialized = true;
  190. }
  191. LoadHardCodedDefaults ();
  192. // BUGBUG: ThemeScope is broken and needs to be fixed to not have the hard coded schemes get overwritten.
  193. // BUGBUG: This a partial workaround.
  194. // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/4288
  195. ThemeManager.Themes? [ThemeManager.Theme]?.Apply ();
  196. }
  197. #endregion Initialization
  198. #region Enable/Disable
  199. private static bool _enabled;
  200. #pragma warning disable IDE1006 // Naming Styles
  201. private static readonly object _enabledLock = new ();
  202. #pragma warning restore IDE1006 // Naming Styles
  203. /// <summary>
  204. /// Gets whether <see cref="ConfigurationManager"/> is enabled or not.
  205. /// If <see langword="false"/>, only the hard coded defaults will be loaded. See <see cref="Enable"/> and
  206. /// <see cref="Disable"/>
  207. /// </summary>
  208. public static bool IsEnabled
  209. {
  210. get
  211. {
  212. lock (_enabledLock)
  213. {
  214. return _enabled;
  215. }
  216. }
  217. }
  218. /// <summary>
  219. /// Enables <see cref="ConfigurationManager"/>. If <paramref name="locations"/> is <see cref="ConfigLocations.None"/>,
  220. /// ConfigurationManager will be enabled as-is; no configuration will be loaded or applied. If
  221. /// <paramref name="locations"/> is <see cref="ConfigLocations.HardCoded"/>,
  222. /// ConfigurationManager will be enabled and reset to hard-coded defaults.
  223. /// For any other value,
  224. /// ConfigurationManager will be enabled and the configuration will be loaded from the specified locations and applied.
  225. /// </summary>
  226. /// <param name="locations"></param>
  227. [RequiresUnreferencedCode ("AOT")]
  228. [RequiresDynamicCode ("AOT")]
  229. public static void Enable (ConfigLocations locations)
  230. {
  231. if (IsEnabled)
  232. {
  233. return;
  234. }
  235. lock (_enabledLock)
  236. {
  237. _enabled = true;
  238. }
  239. ClearJsonErrors ();
  240. if (locations == ConfigLocations.None)
  241. {
  242. return;
  243. }
  244. Load (locations);
  245. // Works even if ConfigurationManager is not enabled.
  246. InternalApply ();
  247. }
  248. /// <summary>
  249. /// Disables <see cref="ConfigurationManager"/>.
  250. /// </summary>
  251. /// <param name="resetToHardCodedDefaults">
  252. /// If <see langword="true"/> all static <see cref="ConfigurationPropertyAttribute"/> properties will be reset to their
  253. /// initial, hard-coded
  254. /// defaults.
  255. /// </param>
  256. [RequiresUnreferencedCode ("Calls ResetToHardCodedDefaults")]
  257. [RequiresDynamicCode ("Calls ResetToHardCodedDefaults")]
  258. public static void Disable (bool resetToHardCodedDefaults = false)
  259. {
  260. lock (_enabledLock)
  261. {
  262. _enabled = false;
  263. }
  264. if (resetToHardCodedDefaults)
  265. {
  266. // Calls Apply
  267. ResetToHardCodedDefaults ();
  268. }
  269. }
  270. #endregion Enable/Disable
  271. #region Reset
  272. // `Update` - Updates the configuration from either the current values or the hard-coded defaults.
  273. // Updating does not load the configuration; it only updates the configuration to the values currently
  274. // in the static ConfigProperties.
  275. /// <summary>
  276. /// INTERNAL: Updates <see cref="ConfigurationManager"/> to the settings from the current
  277. /// values of the static <see cref="ConfigurationPropertyAttribute"/> properties.
  278. /// </summary>
  279. [RequiresUnreferencedCode ("AOT")]
  280. [RequiresDynamicCode ("AOT")]
  281. internal static void UpdateToCurrentValues ()
  282. {
  283. if (!IsInitialized ())
  284. {
  285. throw new InvalidOperationException ("Initialize must be called first.");
  286. }
  287. _settingsLockSlim.EnterWriteLock ();
  288. try
  289. {
  290. _settings = new ();
  291. _settings.LoadHardCodedDefaults ();
  292. }
  293. finally
  294. {
  295. _settingsLockSlim.ExitWriteLock ();
  296. }
  297. Settings!.UpdateToCurrentValues ();
  298. ThemeManager.UpdateToCurrentValues ();
  299. AppSettings!.UpdateToCurrentValues ();
  300. }
  301. /// <summary>
  302. /// INTERNAL: Loads the hard-coded values of the
  303. /// <see cref="ConfigurationPropertyAttribute"/> properties and applies them.
  304. /// </summary>
  305. [RequiresUnreferencedCode ("AOT")]
  306. [RequiresDynamicCode ("AOT")]
  307. internal static void ResetToHardCodedDefaults ()
  308. {
  309. LoadHardCodedDefaults ();
  310. Applied = null;
  311. Updated = null;
  312. // Works even if ConfigurationManager is not enabled.
  313. InternalApply ();
  314. }
  315. #endregion Reset
  316. #region Load
  317. // `Load` - Load configuration from the given location(s), updating the configuration with any new values.
  318. // Loading does not apply the settings to the application; that happens when the `Apply` method is called.
  319. /// <summary>
  320. /// INTERNAL: Loads all hard-coded configuration properties. Use <see cref="Apply"/> to cause the loaded settings to be
  321. /// applied to the running application.
  322. /// </summary>
  323. [RequiresUnreferencedCode ("AOT")]
  324. [RequiresDynamicCode ("AOT")]
  325. internal static void LoadHardCodedDefaults ()
  326. {
  327. if (!IsInitialized ())
  328. {
  329. throw new InvalidOperationException ("Initialize must be called first.");
  330. }
  331. RuntimeConfig = null;
  332. SourcesManager!.Sources.Clear ();
  333. SourcesManager.AddSource (ConfigLocations.HardCoded, "HardCoded");
  334. Settings = new ();
  335. Settings!.LoadHardCodedDefaults ();
  336. ThemeManager.LoadHardCodedDefaults ();
  337. AppSettings!.LoadHardCodedDefaults ();
  338. }
  339. /// <summary>
  340. /// Loads all settings found in <paramref name="locations"/>. Use <see cref="Apply"/> to cause the loaded settings to
  341. /// be applied to the running application.
  342. /// </summary>
  343. /// <exception cref="ConfigurationManagerNotEnabledException">Configuration manager is not enabled.</exception>
  344. [RequiresUnreferencedCode ("AOT")]
  345. [RequiresDynamicCode ("AOT")]
  346. public static void Load (ConfigLocations locations)
  347. {
  348. if (!IsEnabled)
  349. {
  350. throw new ConfigurationManagerNotEnabledException ();
  351. }
  352. // Only load the hard-coded defaults if the user has not specified any locations.
  353. if (locations == ConfigLocations.HardCoded)
  354. {
  355. LoadHardCodedDefaults ();
  356. }
  357. if (locations.HasFlag (ConfigLocations.LibraryResources))
  358. {
  359. SourcesManager?.Load (
  360. Settings,
  361. typeof (ConfigurationManager).Assembly,
  362. $"Terminal.Gui.Resources.{_configFilename}",
  363. ConfigLocations.LibraryResources);
  364. }
  365. if (locations.HasFlag (ConfigLocations.AppResources))
  366. {
  367. string? embeddedStylesResourceName = Assembly.GetEntryAssembly ()
  368. ?
  369. .GetManifestResourceNames ()
  370. .FirstOrDefault (x => x.EndsWith (_configFilename));
  371. if (string.IsNullOrEmpty (embeddedStylesResourceName))
  372. {
  373. embeddedStylesResourceName = _configFilename;
  374. }
  375. SourcesManager?.Load (Settings, Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!, ConfigLocations.AppResources);
  376. }
  377. // TODO: Determine if Runtime should be applied last.
  378. if (locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig))
  379. {
  380. SourcesManager?.Load (Settings, RuntimeConfig, "ConfigurationManager.RuntimeConfig", ConfigLocations.Runtime);
  381. }
  382. if (locations.HasFlag (ConfigLocations.GlobalCurrent))
  383. {
  384. SourcesManager?.Load (Settings, $"./.tui/{_configFilename}", ConfigLocations.GlobalCurrent);
  385. }
  386. if (locations.HasFlag (ConfigLocations.GlobalHome))
  387. {
  388. SourcesManager?.Load (Settings, $"~/.tui/{_configFilename}", ConfigLocations.GlobalHome);
  389. }
  390. if (locations.HasFlag (ConfigLocations.AppCurrent))
  391. {
  392. SourcesManager?.Load (Settings, $"./.tui/{AppName}.{_configFilename}", ConfigLocations.AppCurrent);
  393. }
  394. if (locations.HasFlag (ConfigLocations.AppHome))
  395. {
  396. SourcesManager?.Load (Settings, $"~/.tui/{AppName}.{_configFilename}", ConfigLocations.AppHome);
  397. }
  398. }
  399. // TODO: Rename to Loaded?
  400. /// <summary>
  401. /// Called when the configuration has been updated from a configuration file or reset. Invokes the
  402. /// <see cref="Updated"/>
  403. /// event.
  404. /// </summary>
  405. public static void OnUpdated ()
  406. {
  407. //Logging.Trace (@"");
  408. if (!IsEnabled)
  409. {
  410. return;
  411. }
  412. // Use a local copy of the event delegate when invoking it to avoid race conditions.
  413. EventHandler<ConfigurationManagerEventArgs>? handler = Updated;
  414. handler?.Invoke (null, new ());
  415. }
  416. /// <summary>Event fired when the configuration has been updated from a configuration source or reset.</summary>
  417. public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
  418. #endregion Load
  419. #region Apply
  420. // `Apply` - Apply the configuration to the application; this means the settings are copied from the
  421. // configuration properties to the corresponding `static` `[ConfigurationProperty]` properties.
  422. /// <summary>
  423. /// Applies the configuration settings to static <see cref="ConfigurationPropertyAttribute"/> properties.
  424. /// ConfigurationManager must be Enabled.
  425. /// </summary>
  426. /// <exception cref="ConfigurationManagerNotEnabledException">Configuration Manager is not enabled.</exception>
  427. [RequiresUnreferencedCode ("AOT")]
  428. [RequiresDynamicCode ("AOT")]
  429. public static void Apply ()
  430. {
  431. if (!IsEnabled)
  432. {
  433. throw new ConfigurationManagerNotEnabledException ();
  434. }
  435. InternalApply ();
  436. }
  437. [RequiresUnreferencedCode ("Calls Terminal.Gui.Scope<T>.Apply()")]
  438. [RequiresDynamicCode ("Calls Terminal.Gui.Scope<T>.Apply()")]
  439. private static void InternalApply ()
  440. {
  441. var settings = false;
  442. var themes = false;
  443. var appSettings = false;
  444. try
  445. {
  446. settings = Settings?.Apply () ?? false;
  447. themes = ThemeManager.Themes? [ThemeManager.Theme]?.Apply () ?? false;
  448. appSettings = AppSettings?.Apply () ?? false;
  449. }
  450. catch (JsonException e)
  451. {
  452. if (ThrowOnJsonErrors ?? false)
  453. {
  454. throw;
  455. }
  456. else
  457. {
  458. AddJsonError ($"Error applying Configuration Change: {e.Message}");
  459. }
  460. }
  461. finally
  462. {
  463. if (settings || themes || appSettings)
  464. {
  465. OnApplied ();
  466. }
  467. }
  468. }
  469. /// <summary>
  470. /// Called when an updated configuration has been applied to the application. Fires the <see cref="Applied"/>
  471. /// event.
  472. /// </summary>
  473. /// <exception cref="ConfigurationManagerNotEnabledException">Configuration manager is not enabled.</exception>
  474. private static void OnApplied ()
  475. {
  476. if (!IsEnabled)
  477. {
  478. return;
  479. }
  480. // Use a local copy of the event delegate when invoking it to avoid race conditions.
  481. EventHandler<ConfigurationManagerEventArgs>? handler = Applied;
  482. handler?.Invoke (null, new ());
  483. // TODO: Refactor ConfigurationManager to not use an event handler for this.
  484. // Instead, have it call a method on any class appropriately attributed
  485. // to update the cached values. See Issue #2871
  486. }
  487. /// <summary>Event fired when an updated configuration has been applied to the application.</summary>
  488. public static event EventHandler<ConfigurationManagerEventArgs>? Applied;
  489. #endregion Apply
  490. #region Sources
  491. // `Sources` - A source is a location where a configuration can be stored. Sources are defined in the `ConfigLocations` enum.
  492. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  493. internal static readonly SourceGenerationContext SerializerContext = new (
  494. new()
  495. {
  496. // Be relaxed
  497. ReadCommentHandling = JsonCommentHandling.Skip,
  498. PropertyNameCaseInsensitive = true,
  499. DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
  500. WriteIndented = true,
  501. AllowTrailingCommas = true,
  502. Converters =
  503. {
  504. // We override the standard Rune converter to support specifying Glyphs in
  505. // a flexible way
  506. new RuneJsonConverter (),
  507. // Override Key to support "Ctrl+Q" format.
  508. new KeyJsonConverter ()
  509. },
  510. // Enables Key to be "Ctrl+Q" vs "Ctrl\u002BQ"
  511. Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
  512. TypeInfoResolver = SourceGenerationContext.Default
  513. });
  514. /// <summary>
  515. /// Gets the Sources Manager - manages the loading of configuration sources from files and resources.
  516. /// </summary>
  517. public static SourcesManager? SourcesManager { get; internal set; } = new ();
  518. /// <summary>
  519. /// Gets or sets the in-memory config.json. See <see cref="ConfigLocations.Runtime"/>.
  520. /// </summary>
  521. public static string? RuntimeConfig { get; set; } = """{ }""";
  522. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  523. private static readonly string _configFilename = "config.json";
  524. #endregion Sources
  525. #region AppSettings
  526. /// <summary>
  527. /// Gets or sets the application-specific configuration settings (config properties with the
  528. /// <see cref="AppSettingsScope"/> scope.
  529. /// </summary>
  530. [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
  531. [JsonPropertyName ("AppSettings")]
  532. public static AppSettingsScope? AppSettings
  533. {
  534. [RequiresUnreferencedCode ("AOT")]
  535. [RequiresDynamicCode ("AOT")]
  536. get
  537. {
  538. if (!IsInitialized ())
  539. {
  540. // We're being called from the module initializer.
  541. // Hard coded default value is an empty AppSettingsScope
  542. var appSettings = new AppSettingsScope ();
  543. appSettings.LoadHardCodedDefaults ();
  544. return appSettings;
  545. }
  546. if (Settings is null || !Settings.TryGetValue ("AppSettings", out ConfigProperty? appSettingsConfigProperty))
  547. {
  548. throw new InvalidOperationException ("Settings is null.");
  549. }
  550. {
  551. if (!appSettingsConfigProperty.HasValue)
  552. {
  553. var appSettings = new AppSettingsScope ();
  554. appSettings.UpdateToCurrentValues ();
  555. return appSettings;
  556. }
  557. return (appSettingsConfigProperty.PropertyValue as AppSettingsScope)!;
  558. }
  559. }
  560. [RequiresUnreferencedCode ("AOT")]
  561. [RequiresDynamicCode ("AOT")]
  562. set
  563. {
  564. if (!IsInitialized ())
  565. {
  566. throw new InvalidOperationException ("AppSettings cannot be set before ConfigurationManager is initialized.");
  567. }
  568. // Check if the AppSettings is the same as the previous one
  569. if (value != Settings! ["AppSettings"].PropertyValue)
  570. {
  571. // Update the backing store
  572. Settings! ["AppSettings"].PropertyValue = value;
  573. //Instance.OnThemeChanged (previousThemeValue);
  574. }
  575. }
  576. }
  577. #endregion AppSettings
  578. #region Error Logging
  579. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  580. internal static StringBuilder _jsonErrors = new ();
  581. /// <summary>
  582. /// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters an
  583. /// error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the console
  584. /// when <see cref="Application.Shutdown"/> is called.
  585. /// </summary>
  586. [ConfigurationProperty (Scope = typeof (SettingsScope))]
  587. public static bool? ThrowOnJsonErrors { get; set; } = false;
  588. #pragma warning disable IDE1006 // Naming Styles
  589. private static readonly object _jsonErrorsLock = new ();
  590. #pragma warning restore IDE1006 // Naming Styles
  591. internal static void AddJsonError (string error)
  592. {
  593. Logging.Error ($"{error}");
  594. lock (_jsonErrorsLock)
  595. {
  596. _jsonErrors.AppendLine (@$" {error}");
  597. }
  598. }
  599. private static void ClearJsonErrors ()
  600. {
  601. lock (_jsonErrorsLock)
  602. {
  603. _jsonErrors.Clear ();
  604. }
  605. }
  606. /// <summary>Prints any Json deserialization errors that occurred during deserialization to the console.</summary>
  607. public static void PrintJsonErrors ()
  608. {
  609. lock (_jsonErrorsLock)
  610. {
  611. if (_jsonErrors.Length > 0)
  612. {
  613. Console.WriteLine (
  614. @"Terminal.Gui ConfigurationManager encountered these errors while reading configuration files"
  615. + @"(set ThrowOnJsonErrors to have these caught during execution):");
  616. Console.WriteLine (_jsonErrors.ToString ());
  617. }
  618. }
  619. }
  620. #endregion Error Logging
  621. /// <summary>Returns an empty Json document with just the $schema tag.</summary>
  622. /// <returns></returns>
  623. public static string GetEmptyConfig ()
  624. {
  625. var emptyScope = new SettingsScope ();
  626. emptyScope.Clear ();
  627. return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext!);
  628. }
  629. /// <summary>Returns a Json document containing the hard-coded config.</summary>
  630. /// <returns></returns>
  631. public static string GetHardCodedConfig ()
  632. {
  633. var emptyScope = new SettingsScope ();
  634. emptyScope.LoadHardCodedDefaults ();
  635. IEnumerable<KeyValuePair<string, ConfigProperty>>? settings = GetHardCodedConfigPropertiesByScope ("SettingsScope");
  636. if (settings is null)
  637. {
  638. throw new InvalidOperationException ("GetHardCodedConfigPropertiesByScope returned null.");
  639. }
  640. Dictionary<string, ConfigProperty> settingsDict = settings.ToDictionary ();
  641. foreach (KeyValuePair<string, ConfigProperty> p in Settings!.Where (cp => cp.Value.PropertyInfo is { }))
  642. {
  643. emptyScope [p.Key].PropertyValue = settingsDict [p.Key].PropertyValue;
  644. }
  645. return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext!);
  646. }
  647. /// <summary>Name of the running application. By default, this property is set to the application's assembly name.</summary>
  648. public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
  649. /// <summary>
  650. /// INTERNAL: Retrieves all uninitialized configuration properties that belong to a specific scope from the cache.
  651. /// The items in the collection are references to the original <see cref="ConfigProperty"/> objects in the
  652. /// cache. They do not have values and have <see cref="ConfigProperty.Immutable"/> set.
  653. /// </summary>
  654. internal static IEnumerable<KeyValuePair<string, ConfigProperty>>? GetUninitializedConfigPropertiesByScope (string scopeType)
  655. {
  656. // AOT Note: This method does NOT need the RequiresUnreferencedCode attribute as it is not using reflection
  657. // and is not using any dynamic code. _allConfigProperties is a static property that is set in the module initializer
  658. // and is not using any dynamic code. In addition, ScopeType are registered in SourceGenerationContext.
  659. if (_uninitializedConfigPropertiesCache is null)
  660. {
  661. throw new InvalidOperationException ("_allConfigPropertiesCache has not been set.");
  662. }
  663. if (string.IsNullOrEmpty (scopeType))
  664. {
  665. return _uninitializedConfigPropertiesCache;
  666. }
  667. lock (_uninitializedConfigPropertiesCacheCacheLock)
  668. {
  669. // Filter properties by scope using the cached ScopeType property instead of reflection
  670. IEnumerable<KeyValuePair<string, ConfigProperty>>? filtered = _uninitializedConfigPropertiesCache?.Where (cp => cp.Value.ScopeType == scopeType);
  671. Debug.Assert (filtered is { });
  672. IEnumerable<KeyValuePair<string, ConfigProperty>> configPropertiesByScope =
  673. filtered as KeyValuePair<string, ConfigProperty> [] ?? filtered.ToArray ();
  674. Debug.Assert (configPropertiesByScope.All (v => !v.Value.HasValue));
  675. return configPropertiesByScope;
  676. }
  677. }
  678. /// <summary>
  679. /// INTERNAL: Retrieves all configuration properties that belong to a specific scope from the hard coded value cache.
  680. /// The items in the collection are references to the original <see cref="ConfigProperty"/> objects in the
  681. /// cache. They contain the hard coded values and have <see cref="ConfigProperty.Immutable"/> set.
  682. /// </summary>
  683. internal static IEnumerable<KeyValuePair<string, ConfigProperty>>? GetHardCodedConfigPropertiesByScope (string scopeType)
  684. {
  685. // AOT Note: This method does NOT need the RequiresUnreferencedCode attribute as it is not using reflection
  686. // and is not using any dynamic code. _allConfigProperties is a static property that is set in the module initializer
  687. // and is not using any dynamic code. In addition, ScopeType are registered in SourceGenerationContext.
  688. // Filter properties by scope
  689. IEnumerable<KeyValuePair<string, ConfigProperty>>? cache = GetHardCodedConfigPropertyCache ();
  690. if (cache is null)
  691. {
  692. throw new InvalidOperationException ("GetHardCodedConfigPropertyCache returned null");
  693. }
  694. if (string.IsNullOrEmpty (scopeType))
  695. {
  696. return cache;
  697. }
  698. // Use the cached ScopeType property instead of reflection
  699. IEnumerable<KeyValuePair<string, ConfigProperty>>? scopedCache = cache?.Where (cp => cp.Value.ScopeType == scopeType);
  700. return scopedCache!;
  701. }
  702. }