ConfigurationManager.cs 34 KB

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