ConfigurationManager.cs 32 KB

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