ConfigurationManager.cs 33 KB

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