ConfigurationManager.cs 33 KB

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