From f1c69a014707219bac1195639fc8b80084b3f799 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 May 2026 09:59:40 -0600 Subject: [PATCH 01/13] chore: start CM removal follow-up Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From aa3dcf2afc3f3bc9ca948e705a4e1842dc7e85a1 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 May 2026 10:01:04 -0600 Subject: [PATCH 02/13] chore: start CM removal follow-up Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 482c24c5727ae05d9f0349146caa86a125d3b378 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 May 2026 11:20:59 -0600 Subject: [PATCH 03/13] Add spec for legacy ConfigurationManager removal Companion to specs/replace-cm-with-mec.md. Defines the scope, design, and phased execution of fully removing CM after the MEC migration in #5411. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- specs/remove-legacy-cm.md | 424 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 specs/remove-legacy-cm.md diff --git a/specs/remove-legacy-cm.md b/specs/remove-legacy-cm.md new file mode 100644 index 0000000000..9b7f9735dd --- /dev/null +++ b/specs/remove-legacy-cm.md @@ -0,0 +1,424 @@ +# Spec: Remove Legacy ConfigurationManager (Phase 4–6 of CM → MEC) + +> **Status:** Draft — follow-up to PR #5411 (`copilot/replace-cm-with-mec`). +> **Tracking Issue:** [#4943](https://github.com/gui-cs/Terminal.Gui/issues/4943) +> **PR:** [#5416 — `tig/remove-cm-followup`](https://github.com/gui-cs/Terminal.Gui/pull/5416) (stacked on `copilot/replace-cm-with-mec`) +> **Predecessor Spec:** [`specs/replace-cm-with-mec.md`](./replace-cm-with-mec.md) + +--- + +## 1. Purpose + +PR #5411 completed the **functional** CM → MEC migration: + +- Per‑component Settings POCOs with static `Defaults` facades. +- `TuiConfigurationBuilder` + `TuiConfigurationExtensions` providing the MEC-based source chain. +- `IThemeManager` / `ISchemeManager` services (currently thin wrappers over legacy static managers). +- All `[ConfigurationProperty(...)]` attributes removed from production view/option properties (only the attribute *class* still exists). +- `ConfigurationManager`, `SourcesManager`, `ConfigProperty`, `Scope`, `DeepCloner`, `ScopeJsonConverter` marked `[Obsolete]` and only referenced internally during the transition. + +This PR finishes the migration by **deleting** every legacy CM type, the embedded flat-key `Resources/config.json` format, the residual CM-backed wiring inside the MEC managers, and all CM-specific tests. The result is the AOT size reduction and parallel-test unlock that motivated #4943 in the first place. + +This corresponds to **Phases 4, 5, and 6** of the predecessor spec, executed as a single stacked PR. + +--- + +## 2. Goals + +1. **Delete every legacy CM type and attribute** (no `[Obsolete]` shim period — they were already shimmed in #5411). +2. **Replace CM-backed `ThemeManager` / `SchemeManager` internals** with POCO + `IOptionsMonitor` storage so `MecThemeManager` / `MecSchemeManager` are no longer thin wrappers over the static legacy types. +3. **Eliminate CM-related anti-trim patterns**: `ConfigPropertyHostTypes.GetTypes()`, `[DynamicDependency]` on 29 host types, `Assembly.GetTypes()` reflection scan, `DeepCloner` reflection, `MakeGenericType` usage. +4. **Drop the `ConfigurationManager.Applied` event subscription pattern** used by `Menu`, `MenuBar`, `StatusBar`, `LineCanvas` and replace with `IOptionsMonitor.OnChange` (or direct theme-manager events). +5. **Migrate the embedded `Terminal.Gui/Resources/config.json`** from the legacy flat-key `Themes: [ { Name: { "Class.Prop": ... } } ]` layout to the MEC-native nested `Themes:{ Name:{ Class:{ Prop:... } } }` layout (per D-02 Option 3 ⇒ now Option 2 since flat keys are no longer parsed). +6. **Delete all CM-only tests** (`ConfigurationMangerTests`, `ConfigPropertyAssemblyScanTests`, `ConfigPropertyTests`, `ConfigPropertyHostTypesTests`, `DeepClonerTests`, `ScopeJsonConverterTests`, `ScopeTests`, `SettingsScopeTests`, `ThemeScopeTests`, `ConfigurationPropertyAttributeTests`, legacy `SourcesManagerTests`). Keep MEC equivalents. +7. **Measure and record the NativeAOT binary-size delta** in PR #5416 so the stated motivation in #4943 is verifiable. +8. **Update `docfx/docs/config.md`** so the documented contract matches the shipped contract (Constitution: *Documentation Is the Spec*). + +### Non‑goals + +- Generic Host / `IHostBuilder` adoption (deferred, Q‑02). +- View constructor injection of `IOptionsMonitor` (deferred — static `Defaults` facade remains per D‑01). +- Multi-instance `IApplication` scoping of themes (D‑03). +- Lazy driver registration / assembly splitting / TextMate conditional compilation (out of scope per #10 of predecessor spec). + +--- + +## 3. Constitution Alignment + +| Tenet | How this PR aligns | +|-------|--------------------| +| ✅ **Testability First** | Removing CM static state moves every theme/scheme test into `UnitTestsParallelizable`. | +| ✅ **Performance Is a Feature** | Removes the `Assembly.GetTypes()` scan + DeepCloner reflection at module init. Hot path (`Rune` read on a glyph, `ShadowStyles` read on Button) becomes a direct field read on a POCO. | +| ✅ **Users Have Final Control** | All configurable surfaces stay configurable. Only the *internal mechanism* changes; the documented public configuration model is MEC. | +| ✅ **Separation of Concerns** | Views no longer reach into a process-wide static (`ConfigurationManager.Settings["Foo"]`). They observe `IOptionsMonitor` (via the static `Defaults` facade for now). | +| ✅ **Respect What Came Before** | The 9‑level precedence semantics are preserved through the MEC provider chain assembled in `TuiConfigurationExtensions`. The user-facing JSON keeps the same *shape* aside from the flat→nested key migration. | +| 🔴 **Documentation Is the Spec** | `docfx/docs/config.md` is rewritten in this PR; no out-of-date references to `ConfigurationManager.Enable`, `RuntimeConfig`, `Applied` event, etc. | + +--- + +## 4. Current Residual Surface (post‑#5411) + +The exhaustive inventory of what still has to go. Discovered by `grep`ping the worktree against `copilot/replace-cm-with-mec` HEAD. + +### 4.1 Types to **delete** entirely (`Terminal.Gui/Configuration/`) + +| File | Notes | +|------|-------| +| `ConfigurationManager.cs` (~33 KB) | Static facade; obsolete | +| `ConfigurationManagerEventArgs.cs` | `Applied`/`Updated` event args | +| `ConfigurationManagerNotEnabledException.cs` | No "enabled" concept in MEC | +| `ConfigurationPropertyAttribute.cs` | Attribute already unused on production properties | +| `ConfigProperty.cs` (~28 KB) | Reflection-discovered config property descriptor | +| `ConfigPropertyHostTypes.cs` | `[DynamicDependency]` rooting of 29 host types | +| `ConfigLocations.cs` | Replaced by MEC provider ordering in `TuiConfigurationExtensions` | +| `DeepCloner.cs` (~19 KB) | Reflection-based deep clone; not needed once POCOs replace `Scope` | +| `Scope.cs`, `SettingsScope.cs`, `AppSettingsScope.cs`, `ThemeScope.cs` | Scope abstractions superseded by typed `IOptions` | +| `ScopeJsonConverter.cs` | Flat-key scope deserializer | +| `DictionaryJsonConverter.cs`, `ConcurrentDictionaryJsonConverter.cs` | Only needed by the flat-key scope format | +| `KeyArrayJsonConverter.cs`, `KeyCodeJsonConverter.cs`, `MouseFlagsArrayJsonConverter.cs` | Audit — only delete if unused after migration; otherwise keep and register on the MEC-side `JsonSerializerOptions` | + +### 4.2 Types to **keep** (already MEC-shaped) and harden + +| File | Action | +|------|--------| +| `SourceGenerationContext.cs` | Keep. Re-audit `[JsonSerializable]` entries — remove any that referenced `SettingsScope` / `ThemeScope` / `AppSettingsScope`. Add `ThemeSettings` and the per-component settings POCOs. | +| `AttributeJsonConverter.cs`, `SchemeJsonConverter.cs`, `RuneJsonConverter.cs`, `KeyJsonConverter.cs`, `ColorJsonConverter.cs`, `TraceCategoryJsonConverter.cs` | Keep. Replace internal references to `ConfigurationManager.SerializerContext` with `SourceGenerationContext.Default` (the source‑generated context). Remove the `#pragma warning disable CS0618` blocks. | +| `Settings/*Settings.cs` (all 30+) | Keep. These are the POCOs that became the source of truth in #5411. | +| `Settings/TuiConfigurationBuilder.cs` | Keep. Becomes the **only** configuration entry point. | +| `Settings/TuiConfigurationExtensions.cs` | Keep. | +| `Settings/IThemeManager.cs`, `Settings/IThemeManager.cs`, `Settings/MecThemeManager.cs`, `Settings/MecSchemeManager.cs` | Keep but rewrite internals (see §5.2). | + +### 4.3 Types to **rename** (drop the `Mec` prefix once the legacy peers are gone) + +| Old (post‑#5411) | New | +|------------------|-----| +| `MecSchemeManager` | `SchemeManager` (replaces the deleted legacy static class) | +| `MecThemeManager` | `ThemeManager` (replaces the deleted legacy static class) | +| `Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs` | `SettingsTests.cs` | +| `Tests/UnitTestsParallelizable/Configuration/MecThemeTests.cs` | `ThemeTests.cs` | +| `Tests/UnitTestsParallelizable/Configuration/MecAppSettingsTests.cs` | `AppSettingsTests.cs` | + +The legacy `SchemeManager` / `ThemeManager` are deleted in §4.1; the rename is a single namespace move and does not collide. + +### 4.4 Callers to **rewire** (production source outside `Configuration/`) + +| File | Current dependency | Replacement | +|------|--------------------|-------------| +| `ModuleInitializers.cs` | Calls `ConfigurationManager.Initialize ()` then `mecBuilder.ApplyToStaticFacades ()` | Single call to `TuiConfigurationBuilder.ApplyToStaticFacades ()`. Delete the dual-init suppression. | +| `App/ApplicationImpl.Lifecycle.cs` | `ConfigurationManager.PrintJsonErrors ()` | Delete the call. MEC throws on parse error; STJ errors go through `Logging`. | +| `App/Application.cs` | Several `ConfigurationManager.*` references in xmldoc/method bodies | Replace with `TuiConfigurationBuilder` references or delete. | +| `App/Tracing/Trace.cs` | One CM reference | Replace with `TraceSettings.Defaults.EnabledCategories`. | +| `Views/Menu/Menu.cs`, `Views/Menu/MenuBar.cs`, `Views/StatusBar.cs`, `Drawing/LineCanvas/LineCanvas.cs` | Subscribe/unsubscribe to `ConfigurationManager.Applied` to re-read theme-driven defaults | Subscribe to `IOptionsMonitor.OnChange` (exposed via a small `ThemeChanges.Observed` helper, or via `IThemeManager.ThemeChanged`). Drop the `#pragma warning disable CS0618` suppressions. | +| `Drawing/Scheme.cs` | xmldoc references CM in a comment | Update wording. | +| `Drawing/Glyphs.cs` | (None — already POCO-backed via `GlyphSettings.Defaults`.) | No change. Confirm via grep after removal. | +| `Input/CommandBindingsBase.cs` | Comment refers to `ConfigurationManager.Apply` / `DeepMemberWiseCopy` | Update comment; the dictionary‑copy workaround stays because the underlying race is now in `IOptionsMonitor` callbacks. | +| `Views/Menu/PopoverMenu.cs`, `Views/Selectors/SelectorBase.cs`, `Views/Window.cs`, `Views/Dialog.cs`, `Views/FrameView.cs`, `Views/MessageBox.cs`, `Views/HexView.cs`, `Views/CharMap/CharMap.cs`, `Views/CheckBox.cs`, `Views/Button.cs`, `Views/FileDialogs/FileDialog.cs`, `Views/FileDialogs/FileDialogStyle.cs`, `Views/LinearRange/LinearRangeDefaults.cs`, `Views/LinearRange/LinearRangeViewBase.cs`, `Views/TextInput/TextField/TextField.cs`, `Views/TextInput/TextView/TextView.cs`, `Views/TreeView/TreeViewT.cs`, `ViewBase/View.Keyboard.cs`, `ViewBase/Mouse/View.Mouse.cs`, `ViewBase/Adornment/BorderView.Arrangement.cs`, `Text/NerdFonts.cs`, `FileServices/FileSystemIconProvider.cs`, `Drawing/Color/Color.cs`, `Input/Keyboard/Key.cs`, `Drivers/Driver.cs`, `App/Legacy/Application.Mouse.cs` | Each contains one or two stale doc-comment references to `ConfigurationManager`, `SettingsScope`, or `ThemeScope` (no live code dependency confirmed by grep `[ConfigurationProperty(` returning only the attribute file). | Sweep comments only. | + +### 4.5 Resource files + +| File | Action | +|------|--------| +| `Terminal.Gui/Resources/config.json` (52 KB, ~1,500 lines) | **Rewrite** in nested MEC format. Keys like `"Button.DefaultShadow"` become `Button: { DefaultShadow: ... }`. The `"Themes": [ { "Default": { ... } } ]` array-of-single-key-objects becomes `Themes: { Default: { ... }, Dark: { ... } }`. Schemes stay as a nested map. The `$schema` URL stays; the hosted schema must be updated in tandem (Q‑03 — file a tracking issue). | +| `Examples/Config/example_config.json` | Migrate alongside library `config.json`. | +| `Tests/.../*config.json` (if any) | Audit and migrate. | + +### 4.6 Tests to **delete** (`Tests/`) + +| File | Reason | +|------|--------| +| `UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs` | Tests deleted facade. | +| `UnitTests.NonParallelizable/Configuration/ConfigPropertyAssemblyScanTests.cs` | Tests deleted reflection scan. | +| `UnitTests.NonParallelizable/Configuration/SourcesManagerLoadNullJsonTests.cs` | Replaced by `SourcesManagerTests` in `UnitTestsParallelizable` (which already test the MEC builder). | +| `UnitTests.NonParallelizable/Configuration/GlyphTests.cs` | Move to parallelizable if it doesn't depend on static state; otherwise replace with MEC equivalent. | +| `UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs` | Tests deleted type. | +| `UnitTestsParallelizable/Configuration/ConfigPropertyHostTypesTests.cs` | Tests deleted type. | +| `UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs` | Tests deleted attribute. | +| `UnitTestsParallelizable/Configuration/DeepClonerTests.cs` | Tests deleted utility. | +| `UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs` | Tests deleted converter. | +| `UnitTestsParallelizable/Configuration/ScopeTests.cs` | Tests deleted base class. | +| `UnitTestsParallelizable/Configuration/SettingsScopeTests.cs` | Tests deleted scope. | +| `UnitTestsParallelizable/Configuration/ThemeScopeTests.cs` | Tests deleted scope. | +| `Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs` | Replace with `TuiConfigurationBuilderBuildBenchmark`. | +| `Benchmarks/Configuration/ThemeSwitchBenchmark.cs` | Rewrite against `IThemeManager.SwitchTheme`. | + +### 4.7 Tests to **keep** (and audit) + +- `UnitTestsParallelizable/Configuration/MecAppSettingsTests.cs` +- `UnitTestsParallelizable/Configuration/MecSettingsTests.cs` +- `UnitTestsParallelizable/Configuration/MecThemeTests.cs` +- `UnitTestsParallelizable/Configuration/SourcesManagerTests.cs` (MEC version — confirm not the legacy one) +- `UnitTestsParallelizable/Configuration/KeyJsonConverterTests.cs` +- `UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs` +- `UnitTestsParallelizable/Configuration/SchemeJsonConverterTests.cs` +- All `*DefaultKeyBindingsTests.cs` (these read `View.DefaultKeyBindings`; after CM removal they read it via the bound POCO) + +### 4.8 Examples to **rewrite** + +| File | Action | +|------|--------| +| `Examples/UICatalog/Runner.cs` | Replace `ConfigurationManager.RuntimeConfig = "..."` with `TuiConfigurationBuilder.Runtime(...)` | +| `Examples/UICatalog/UICatalog.cs` and `UICatalogRunnable.cs` | Replace `ConfigurationManager.Enable/Apply/Applied` with the new builder + `IOptionsMonitor.OnChange` | +| `Examples/UICatalog/Scenarios/ConfigurationEditor.cs` | Heavy CM user. Rewrite to enumerate the `Settings/*Settings.cs` POCOs and serialize via `SourceGenerationContext`. | +| `Examples/UICatalog/Scenarios/Themes.cs`, `ThemeFallback.cs` | Use `IThemeManager.ThemeChanged` and `IThemeManager.SwitchTheme`. | +| `Examples/NativeAot/Program.cs`, `Examples/SelfContained/Program.cs`, `Examples/ReactiveExample/Program.cs`, `Examples/CommunityToolkitExample/Program.cs`, `Examples/ScenarioRunner/Program.cs`, `Examples/ShortcutTest/ShortcutTest.cs`, `Examples/PromptExample/Program.cs`, `Examples/Example/Example.cs` | Drop `ConfigurationManager.Enable (...)`; replace with `new TuiConfigurationBuilder ().ApplyToStaticFacades ()` (or remove entirely if defaults are sufficient). | +| `Examples/Config/README.md`, `Examples/NativeAot/README.md`, `Examples/SelfContained/README.md`, `Examples/ReactiveExample/README.md`, `Examples/CommunityToolkitExample/README.md` | Update documentation to reference MEC. | +| All `Examples/UICatalog/Scenarios/*.cs` files listed in §4 of the discovery output | Most contain only stale `using Terminal.Gui.Configuration;` plus an occasional `ConfigurationManager.Applied` subscription. Sweep with a single mechanical pass. | + +--- + +## 5. Detailed Design + +### 5.1 New `IOptionsMonitor` flow + +```text +TuiConfigurationBuilder.Build() + └─ IConfigurationBuilder + ├─ AddTuiLibraryDefaults() (embedded Terminal.Gui.Resources.config.json) + ├─ AddTuiAppDefaults(appName) (entry assembly config.json) + ├─ AddTuiUserFiles(appName) (~/.tui/*.json, ./.tui/*.json) + ├─ AddTuiEnvironmentVariable() (TUI_CONFIG) + └─ AddTuiRuntimeConfig(json) (in-memory string) + └─ IConfiguration root + └─ services.Configure(root.GetSection("Themes")) + └─ services.Configure(root.GetSection("Application")) + └─ services.Configure(root.GetSection("Button")) + └─ ... one Configure per POCO + +TuiConfigurationBuilder.ApplyToStaticFacades() + └─ ButtonSettings.Defaults = options.Get() + └─ DialogSettings.Defaults = options.Get() + └─ ... per POCO + └─ Hook IOptionsMonitor.OnChange(newValue => XxxSettings.Defaults = newValue) +``` + +`ButtonSettings.Defaults` (and peers) remain the **static facade** used by all views per D‑01. The only change versus #5411 is that the facade is now updated *only* by the MEC monitor — never by `ConfigurationManager.Apply`. + +### 5.2 ThemeManager / SchemeManager: from wrapper to owner + +`MecThemeManager` and `MecSchemeManager` currently delegate to the legacy static `ThemeManager` / `SchemeManager`. Once the legacy types are deleted, they become the owners: + +```csharp +public sealed class ThemeManager : IThemeManager +{ + private readonly IOptionsMonitor _monitor; + private readonly object _switchLock = new (); + + public ThemeManager (IOptionsMonitor monitor) + { + _monitor = monitor; + _monitor.OnChange (HandleChanged); + } + + public string ActiveTheme + { + get => _monitor.CurrentValue.ActiveTheme; + set => SwitchTheme (value); + } + + public IReadOnlyList ThemeNames => _monitor.CurrentValue.Themes.Keys.ToImmutableList (); + + public event EventHandler? ThemeChanged; + + public void SwitchTheme (string name) { /* update + raise */ } + + private void HandleChanged (ThemeSettings settings, string? name) => ThemeChanged?.Invoke (this, settings.ActiveTheme); +} +``` + +`SchemeManager` becomes an instance class fronting `ThemeSettings.Schemes`. The static `SchemeManager.GetScheme(string)` API used in many views is kept as a **static convenience facade** that forwards to the registered service (via a static `Application.Services` accessor) or to a process-wide fallback when no app has been created — same shape as `XxxSettings.Defaults`. + +### 5.3 Event replacement table + +| Old subscription | New subscription | File(s) affected | +|------------------|------------------|------------------| +| `ConfigurationManager.Applied += handler` (to re-read theme glyphs/border styles) | `ThemeChanges.Observed += handler` (small static event raised once after MEC apply finishes), OR `IThemeManager.ThemeChanged` | `Menu.cs`, `MenuBar.cs`, `StatusBar.cs`, `LineCanvas.cs`, `UICatalogRunnable.cs` | +| `ConfigurationManager.Updated += handler` | Delete — there is no MEC "loaded but not applied" state. | +| `ThemeManager.ThemeChanged` (legacy) | `IThemeManager.ThemeChanged` | + +### 5.4 JSON config: flat → nested key migration + +**Library `config.json`** is rewritten. Example snippet: + +Before (flat): +```json +{ + "Themes": [ + { + "Default": { + "Button.DefaultShadow": "Opaque", + "Glyphs.CheckStateChecked": "☑", + "Schemes": [ + { "Base": { "Normal": { "Foreground": "White", "Background": "Black" } } } + ] + } + } + ] +} +``` + +After (nested, MEC-native): +```json +{ + "Themes": { + "Default": { + "Button": { "DefaultShadow": "Opaque" }, + "Glyphs": { "CheckStateChecked": "☑" }, + "Schemes": { + "Base": { "Normal": { "Foreground": "White", "Background": "Black" } } + } + } + } +} +``` + +**Breaking change for users with hand-authored `~/.tui/config.json` files.** A small migration helper (`TuiConfigMigrator.MigrateFlatToNested(string json)`) is shipped in `Terminal.Gui` for one release; the UICatalog `ConfigurationEditor` scenario gains a "Migrate" button. After one major version the helper is removed. + +(This supersedes predecessor spec D‑02 Option 3. Continuing to accept flat keys requires keeping `ScopeJsonConverter`, which is the largest AOT-hostile blob in the Configuration namespace. The cost/benefit no longer favors dual format support after #5411 makes the nested format viable.) + +### 5.5 Source generation context cleanup + +`SourceGenerationContext` currently lists `SettingsScope`, `ThemeScope`, `AppSettingsScope`, and various dictionary closures. Replace with the per-component POCOs: + +```csharp +[JsonSerializable (typeof (ThemeSettings))] +[JsonSerializable (typeof (ApplicationSettings))] +[JsonSerializable (typeof (ButtonSettings))] +[JsonSerializable (typeof (DialogSettings))] +[JsonSerializable (typeof (GlyphSettings))] +// ... one entry per Settings/*Settings.cs POCO +[JsonSerializable (typeof (Scheme))] +[JsonSerializable (typeof (Attribute))] +[JsonSerializable (typeof (Color))] +[JsonSerializable (typeof (Dictionary))] +[JsonSerializable (typeof (Dictionary))] +[JsonSourceGenerationOptions (Converters = new[] { + typeof (RuneJsonConverter), typeof (KeyJsonConverter), + typeof (ColorJsonConverter), typeof (AttributeJsonConverter), + typeof (SchemeJsonConverter), typeof (TraceCategoryJsonConverter) +})] +internal partial class SourceGenerationContext : JsonSerializerContext { } +``` + +This is what unlocks the AOT-friendly path: every type STJ touches is statically known, no `MakeGenericType`, no `Assembly.GetTypes()`. + +### 5.6 ModuleInitializer simplification + +```csharp +[ModuleInitializer] +internal static void InitializeTuiConfiguration () +{ + TuiConfigurationBuilder builder = new (); + builder.ApplyToStaticFacades (); +} +``` + +Single call. No `ConfigurationManager.Initialize ()`, no `#pragma warning disable CS0618`. + +--- + +## 6. Implementation Phases (within this PR) + +Each phase is one or more commits. Tests must build and pass at every commit. + +### Phase A — Make `MecThemeManager` / `MecSchemeManager` self-sufficient +- Move theme/scheme storage from the legacy static `ThemeManager` / `SchemeManager` into `ThemeSettings` (already exists; populate it from `IOptionsMonitor`). +- Update `MecThemeManager` / `MecSchemeManager` to read/write the POCO directly. +- Add tests in `MecThemeTests` for switch / add / remove scheme paths that previously delegated to the legacy code. + +### Phase B — Replace `ConfigurationManager.Applied` subscribers +- Introduce `Terminal.Gui.Configuration.ThemeChanges` (or expose `IThemeManager.ThemeChanged` via a static convenience accessor). +- Rewire `Menu`, `MenuBar`, `StatusBar`, `LineCanvas` to the new event. +- Delete the `#pragma warning disable CS0618` suppressions in those files. +- Update tests that assert the subscription chain. + +### Phase C — JSON converter / `SourceGenerationContext` decoupling +- Replace all `ConfigurationManager.SerializerContext` references in `AttributeJsonConverter`, `SchemeJsonConverter`, `Concurrent/DictionaryJsonConverter`, `DeepCloner` (the last one is being deleted anyway) with `SourceGenerationContext.Default`. +- Audit `SourceGenerationContext` entries and replace scope types with the POCOs (§5.5). + +### Phase D — Library `config.json` rewrite + migration helper +- Rewrite `Terminal.Gui/Resources/config.json` in nested format. +- Add `TuiConfigMigrator.MigrateFlatToNested(string)` and tests. +- Update `Examples/Config/example_config.json`. +- Update the JSON schema if hosted (file follow-up issue). + +### Phase E — Delete legacy CM types + tests +- Delete every file listed in §4.1 and every test listed in §4.6. +- Delete `ModuleInitializers.cs`'s `ConfigurationManager.Initialize ()` call (§5.6). +- Delete `ConfigurationManager.PrintJsonErrors ()` from `ApplicationImpl.Lifecycle.cs`. +- Sweep stale doc-comment references (§4.4 last row). + +### Phase F — Rename `MecThemeManager`/`MecSchemeManager` → `ThemeManager`/`SchemeManager` +- Now that the legacy types are gone, the `Mec` prefix is redundant. +- Pure rename — no behavior change. + +### Phase G — Update examples (Runner, UICatalog, NativeAot, etc.) +- §4.8 sweep. + +### Phase H — Update `docfx/docs/config.md` +- Rewrite to reflect the MEC contract. +- Remove every reference to `ConfigurationManager.Enable`, `ConfigLocations`, `ConfigurationProperty`, `SettingsScope`, `ThemeScope`, `AppSettingsScope`. +- Document `TuiConfigurationBuilder`, `IThemeManager`, `ISchemeManager`, the nested JSON schema, the migration helper. + +### Phase I — Verification +- Run full test matrix (`UnitTestsParallelizable`, `UnitTests.NonParallelizable`, `IntegrationTests`). +- Confirm zero new warnings (the `CS0618` suppressions are gone — if anything tries to use a deleted obsolete API the build fails by design). +- Build `Examples/NativeAot` with `PublishAot=true` and record `before` / `after` binary size in the PR description. +- Run `Benchmarks/Configuration/*` and record delta. + +--- + +## 7. Breaking Changes (Public API) + +The following public surface is **removed**. Source-incompatible for any consumer that touched these (the predecessor #5411 marked them all `[Obsolete]`). + +- `Terminal.Gui.Configuration.ConfigurationManager` (class) +- `Terminal.Gui.Configuration.ConfigurationManagerNotEnabledException` +- `Terminal.Gui.Configuration.ConfigurationPropertyAttribute` +- `Terminal.Gui.Configuration.ConfigLocations` +- `Terminal.Gui.Configuration.SettingsScope`, `ThemeScope`, `AppSettingsScope`, `Scope` +- `Terminal.Gui.Configuration.ConfigProperty` +- `Terminal.Gui.Configuration.ConfigPropertyHostTypes` +- `Terminal.Gui.Configuration.SourcesManager` (replaced by `TuiConfigurationBuilder`) +- `Terminal.Gui.Configuration.DeepCloner` +- `Terminal.Gui.Configuration.ScopeJsonConverter` +- Static `Terminal.Gui.Configuration.ThemeManager` — replaced by instance `IThemeManager` +- Static `Terminal.Gui.Configuration.SchemeManager` static class — replaced by instance `ISchemeManager` (with a static convenience facade for `GetScheme(string)` only) +- The `Terminal.Gui.Configuration.ConfigurationManager.Applied` / `Updated` events + +### JSON file breaking change + +User config files in the legacy flat-key format (`"Button.DefaultShadow": "..."`) must be migrated. A migration helper ships for one release. + +### Migration guide + +A separate `docfx/docs/migrate-cm-to-mec.md` is added in Phase H with a side-by-side cheatsheet. The PR description links to it. + +--- + +## 8. Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| Hidden third-party consumers still call obsolete CM API | They received an `[Obsolete]` warning in #5411 with the message pointing at `TuiConfigurationBuilder`. One release window has elapsed by the time this PR ships. | +| Nested JSON breaks every existing user config file | Ship `TuiConfigMigrator` + UICatalog "Migrate" button + clear release-note + `migrate-cm-to-mec.md` guide. | +| Renaming `MecThemeManager` → `ThemeManager` clashes with the deleted legacy `ThemeManager` if a partial revert lands | Phase F runs only after Phase E commits; if reverted, the rename is reverted with it. | +| AOT size delta is smaller than predicted | Acceptable. The parallel-test and decoupling wins still justify the change. Record actual delta in the PR. | +| `IOptionsMonitor.OnChange` is not invoked in NativeAOT due to missing trim roots | Verified in #5411. Re-verify with a smoke test in `Examples/NativeAot` as part of Phase I. | + +--- + +## 9. Out of Scope + +Carried forward from the predecessor spec (§10), unchanged: + +1. Generic Host / `IHostBuilder` adoption. +2. Constructor injection mandate for all Views. +3. View-level DI. +4. Multiple concurrent `IApplication` instances. +5. TextMate / Markdig conditional compilation. +6. Lazy driver registration. +7. Assembly splitting. + +--- + +*Authors: @copilot. Companion to [`specs/replace-cm-with-mec.md`](./replace-cm-with-mec.md). Tracking PR: #5416.* From f122ebe6a6fe382fe99f064d9c979cea990e5884 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 May 2026 11:50:05 -0600 Subject: [PATCH 04/13] Update removal spec to reflect Phase A1/B/C-extract landed in #5411 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes from scope: ConfigurationManager.Applied subscriber rewiring (done via ThemeChanges in #5411), JSON converter SerializerContext sweep (done via TuiSerializerContext in #5411). Adds Phase A2 (Mec managers own runtime theme/scheme data) as the gating prerequisite for ScopeJsonConverter deletion. Sharpens D-02 decision (Option alpha: nested-only + TuiConfigMigrator; Option beta: custom legacy MEC source). Corrects PrintJsonErrors framing — behavior-preserving replacement is possible via JsonConfigurationSource.OnLoadException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- specs/remove-legacy-cm.md | 200 ++++++++++++++++++++++++-------------- 1 file changed, 126 insertions(+), 74 deletions(-) diff --git a/specs/remove-legacy-cm.md b/specs/remove-legacy-cm.md index 9b7f9735dd..296f2911a1 100644 --- a/specs/remove-legacy-cm.md +++ b/specs/remove-legacy-cm.md @@ -9,30 +9,33 @@ ## 1. Purpose -PR #5411 completed the **functional** CM → MEC migration: +PR #5411 completed the **functional** CM → MEC migration plus the cross-cutting cleanup that did not require deleting CM itself: - Per‑component Settings POCOs with static `Defaults` facades. - `TuiConfigurationBuilder` + `TuiConfigurationExtensions` providing the MEC-based source chain. -- `IThemeManager` / `ISchemeManager` services (currently thin wrappers over legacy static managers). +- `IThemeManager` / `ISchemeManager` services. `IThemeManager.ThemeChanged` exists and is wired (A1 done). +- `Terminal.Gui.Configuration.ThemeChanges` static facade — bridges both `ConfigurationManager.Applied` (because `CM.Apply()` writes `ConfigProperty` values directly, bypassing C# setters) and `IThemeManager.ThemeChanged` into a single observer-friendly event. All four internal view subscribers (`Menu`, `MenuBar`, `StatusBar`, `LineCanvas`) have migrated off `ConfigurationManager.Applied`. +- `Terminal.Gui.Configuration.TuiSerializerContext` — internal, non-obsolete holder of the configured `SourceGenerationContext` instance with all custom converters and JSON options. All 7 internal JSON converter consumers reference `TuiSerializerContext.Instance` directly; their `#pragma warning disable CS0618` blocks are gone. `ConfigurationManager.SerializerContext` is now a one-line delegator. - All `[ConfigurationProperty(...)]` attributes removed from production view/option properties (only the attribute *class* still exists). - `ConfigurationManager`, `SourcesManager`, `ConfigProperty`, `Scope`, `DeepCloner`, `ScopeJsonConverter` marked `[Obsolete]` and only referenced internally during the transition. -This PR finishes the migration by **deleting** every legacy CM type, the embedded flat-key `Resources/config.json` format, the residual CM-backed wiring inside the MEC managers, and all CM-specific tests. The result is the AOT size reduction and parallel-test unlock that motivated #4943 in the first place. +This PR finishes the migration by **deleting** every legacy CM type, **migrating** the embedded `Resources/config.json` to a MEC-compatible shape, **promoting** `MecThemeManager`/`MecSchemeManager` from event-bridging wrappers to data owners (A2), and **removing** all CM-specific tests. The result is the AOT size reduction and parallel-test unlock that motivated #4943. -This corresponds to **Phases 4, 5, and 6** of the predecessor spec, executed as a single stacked PR. +This corresponds to **Phases 4, 5, and 6** of the predecessor spec, executed as a single stacked PR. See the `Phase 3A.x — Internal subscriber rewiring (landed in #5411)` subsection of [`specs/replace-cm-with-mec.md`](./replace-cm-with-mec.md) for the authoritative record of what shipped in the parent PR. --- ## 2. Goals -1. **Delete every legacy CM type and attribute** (no `[Obsolete]` shim period — they were already shimmed in #5411). -2. **Replace CM-backed `ThemeManager` / `SchemeManager` internals** with POCO + `IOptionsMonitor` storage so `MecThemeManager` / `MecSchemeManager` are no longer thin wrappers over the static legacy types. +1. **Complete Phase A2** — Migrate runtime theme/scheme data ownership from `ConfigurationManager.Settings["Themes"]` (parsed by `ScopeJsonConverter`) into `IOptionsMonitor`. This is the gating prerequisite for deleting `ScopeJsonConverter` and is tightly coupled to the D-02 resource-shape decision (see §5.4). +2. **Delete every legacy CM type and attribute** (no `[Obsolete]` shim period — they were already shimmed in #5411). 3. **Eliminate CM-related anti-trim patterns**: `ConfigPropertyHostTypes.GetTypes()`, `[DynamicDependency]` on 29 host types, `Assembly.GetTypes()` reflection scan, `DeepCloner` reflection, `MakeGenericType` usage. -4. **Drop the `ConfigurationManager.Applied` event subscription pattern** used by `Menu`, `MenuBar`, `StatusBar`, `LineCanvas` and replace with `IOptionsMonitor.OnChange` (or direct theme-manager events). -5. **Migrate the embedded `Terminal.Gui/Resources/config.json`** from the legacy flat-key `Themes: [ { Name: { "Class.Prop": ... } } ]` layout to the MEC-native nested `Themes:{ Name:{ Class:{ Prop:... } } }` layout (per D-02 Option 3 ⇒ now Option 2 since flat keys are no longer parsed). -6. **Delete all CM-only tests** (`ConfigurationMangerTests`, `ConfigPropertyAssemblyScanTests`, `ConfigPropertyTests`, `ConfigPropertyHostTypesTests`, `DeepClonerTests`, `ScopeJsonConverterTests`, `ScopeTests`, `SettingsScopeTests`, `ThemeScopeTests`, `ConfigurationPropertyAttributeTests`, legacy `SourcesManagerTests`). Keep MEC equivalents. -7. **Measure and record the NativeAOT binary-size delta** in PR #5416 so the stated motivation in #4943 is verifiable. -8. **Update `docfx/docs/config.md`** so the documented contract matches the shipped contract (Constitution: *Documentation Is the Spec*). +4. **Migrate the embedded `Terminal.Gui/Resources/config.json`** from the legacy flat-key `Themes: [ { Name: { "Class.Prop": ... } } ]` layout to a MEC-readable shape. Either nested-only with a one-release migration helper, **or** a custom MEC source that parses the legacy shape directly — see D-02 in §5.4. +5. **Delete the four-line `ConfigurationManager.Applied` bridge** inside `ThemeChanges` once `ConfigurationManager` is gone. Keep `ThemeChanges` itself as the supported public observer facade. +6. **Delete the `ConfigurationManager.SerializerContext` delegator** (one field). All real consumers already reference `TuiSerializerContext.Instance`. +7. **Delete all CM-only tests** (`ConfigurationMangerTests`, `ConfigPropertyAssemblyScanTests`, `ConfigPropertyTests`, `ConfigPropertyHostTypesTests`, `DeepClonerTests`, `ScopeJsonConverterTests`, `ScopeTests`, `SettingsScopeTests`, `ThemeScopeTests`, `ConfigurationPropertyAttributeTests`, legacy `SourcesManagerTests`). Keep MEC equivalents. +8. **Measure and record the NativeAOT binary-size delta** in PR #5416 so the stated motivation in #4943 is verifiable. +9. **Update `docfx/docs/config.md`** so the documented contract matches the shipped contract (Constitution: *Documentation Is the Spec*). ### Non‑goals @@ -81,12 +84,14 @@ The exhaustive inventory of what still has to go. Discovered by `grep`ping the w | File | Action | |------|--------| -| `SourceGenerationContext.cs` | Keep. Re-audit `[JsonSerializable]` entries — remove any that referenced `SettingsScope` / `ThemeScope` / `AppSettingsScope`. Add `ThemeSettings` and the per-component settings POCOs. | -| `AttributeJsonConverter.cs`, `SchemeJsonConverter.cs`, `RuneJsonConverter.cs`, `KeyJsonConverter.cs`, `ColorJsonConverter.cs`, `TraceCategoryJsonConverter.cs` | Keep. Replace internal references to `ConfigurationManager.SerializerContext` with `SourceGenerationContext.Default` (the source‑generated context). Remove the `#pragma warning disable CS0618` blocks. | +| `TuiSerializerContext.cs` | **Already exists post-#5411.** No change. This is the canonical JSON context for the MEC era. | +| `SourceGenerationContext.cs` | Keep. Re-audit `[JsonSerializable]` entries — remove any that reference `SettingsScope` / `ThemeScope` / `AppSettingsScope`. Add `ThemeSettings` and the per-component settings POCOs. | +| `ThemeChanges.cs` | **Already exists post-#5411.** Keep as the supported observer facade. In this PR: remove the `ConfigurationManager.Applied` bridge inside it (one branch of an `OR`) once `ConfigurationManager` is deleted. | +| `AttributeJsonConverter.cs`, `SchemeJsonConverter.cs`, `RuneJsonConverter.cs`, `KeyJsonConverter.cs`, `ColorJsonConverter.cs`, `TraceCategoryJsonConverter.cs`, `DictionaryJsonConverter.cs`, `ConcurrentDictionaryJsonConverter.cs` | Keep. **Already point at `TuiSerializerContext.Instance` post-#5411.** No further work. | | `Settings/*Settings.cs` (all 30+) | Keep. These are the POCOs that became the source of truth in #5411. | | `Settings/TuiConfigurationBuilder.cs` | Keep. Becomes the **only** configuration entry point. | | `Settings/TuiConfigurationExtensions.cs` | Keep. | -| `Settings/IThemeManager.cs`, `Settings/IThemeManager.cs`, `Settings/MecThemeManager.cs`, `Settings/MecSchemeManager.cs` | Keep but rewrite internals (see §5.2). | +| `Settings/IThemeManager.cs`, `Settings/ISchemeManager.cs`, `Settings/MecThemeManager.cs`, `Settings/MecSchemeManager.cs` | Keep. **A1 (event plumbing) is done post-#5411.** A2 (data ownership) is this PR's work — see §5.2. | ### 4.3 Types to **rename** (drop the `Mec` prefix once the legacy peers are gone) @@ -104,15 +109,16 @@ The legacy `SchemeManager` / `ThemeManager` are deleted in §4.1; the rename is | File | Current dependency | Replacement | |------|--------------------|-------------| -| `ModuleInitializers.cs` | Calls `ConfigurationManager.Initialize ()` then `mecBuilder.ApplyToStaticFacades ()` | Single call to `TuiConfigurationBuilder.ApplyToStaticFacades ()`. Delete the dual-init suppression. | -| `App/ApplicationImpl.Lifecycle.cs` | `ConfigurationManager.PrintJsonErrors ()` | Delete the call. MEC throws on parse error; STJ errors go through `Logging`. | +| `ModuleInitializers.cs` | Calls `ConfigurationManager.Initialize ()` then `mecBuilder.ApplyToStaticFacades ()` | Single call to `TuiConfigurationBuilder.ApplyToStaticFacades ()`. Delete the dual-init suppression. Gated by A2 completion. | +| `App/ApplicationImpl.Lifecycle.cs` | `ConfigurationManager.PrintJsonErrors ()` | Replace with an equivalent aggregated-error printer fed by `JsonConfigurationSource.OnLoadException`. See §5.4 and §6 Phase D. | | `App/Application.cs` | Several `ConfigurationManager.*` references in xmldoc/method bodies | Replace with `TuiConfigurationBuilder` references or delete. | | `App/Tracing/Trace.cs` | One CM reference | Replace with `TraceSettings.Defaults.EnabledCategories`. | -| `Views/Menu/Menu.cs`, `Views/Menu/MenuBar.cs`, `Views/StatusBar.cs`, `Drawing/LineCanvas/LineCanvas.cs` | Subscribe/unsubscribe to `ConfigurationManager.Applied` to re-read theme-driven defaults | Subscribe to `IOptionsMonitor.OnChange` (exposed via a small `ThemeChanges.Observed` helper, or via `IThemeManager.ThemeChanged`). Drop the `#pragma warning disable CS0618` suppressions. | | `Drawing/Scheme.cs` | xmldoc references CM in a comment | Update wording. | | `Drawing/Glyphs.cs` | (None — already POCO-backed via `GlyphSettings.Defaults`.) | No change. Confirm via grep after removal. | | `Input/CommandBindingsBase.cs` | Comment refers to `ConfigurationManager.Apply` / `DeepMemberWiseCopy` | Update comment; the dictionary‑copy workaround stays because the underlying race is now in `IOptionsMonitor` callbacks. | -| `Views/Menu/PopoverMenu.cs`, `Views/Selectors/SelectorBase.cs`, `Views/Window.cs`, `Views/Dialog.cs`, `Views/FrameView.cs`, `Views/MessageBox.cs`, `Views/HexView.cs`, `Views/CharMap/CharMap.cs`, `Views/CheckBox.cs`, `Views/Button.cs`, `Views/FileDialogs/FileDialog.cs`, `Views/FileDialogs/FileDialogStyle.cs`, `Views/LinearRange/LinearRangeDefaults.cs`, `Views/LinearRange/LinearRangeViewBase.cs`, `Views/TextInput/TextField/TextField.cs`, `Views/TextInput/TextView/TextView.cs`, `Views/TreeView/TreeViewT.cs`, `ViewBase/View.Keyboard.cs`, `ViewBase/Mouse/View.Mouse.cs`, `ViewBase/Adornment/BorderView.Arrangement.cs`, `Text/NerdFonts.cs`, `FileServices/FileSystemIconProvider.cs`, `Drawing/Color/Color.cs`, `Input/Keyboard/Key.cs`, `Drivers/Driver.cs`, `App/Legacy/Application.Mouse.cs` | Each contains one or two stale doc-comment references to `ConfigurationManager`, `SettingsScope`, or `ThemeScope` (no live code dependency confirmed by grep `[ConfigurationProperty(` returning only the attribute file). | Sweep comments only. | +| Stale `using Terminal.Gui.Configuration;` / xmldoc references in: `Views/Menu/PopoverMenu.cs`, `Views/Selectors/SelectorBase.cs`, `Views/Window.cs`, `Views/Dialog.cs`, `Views/FrameView.cs`, `Views/MessageBox.cs`, `Views/HexView.cs`, `Views/CharMap/CharMap.cs`, `Views/CheckBox.cs`, `Views/Button.cs`, `Views/FileDialogs/FileDialog.cs`, `Views/FileDialogs/FileDialogStyle.cs`, `Views/LinearRange/LinearRangeDefaults.cs`, `Views/LinearRange/LinearRangeViewBase.cs`, `Views/TextInput/TextField/TextField.cs`, `Views/TextInput/TextView/TextView.cs`, `Views/TreeView/TreeViewT.cs`, `ViewBase/View.Keyboard.cs`, `ViewBase/Mouse/View.Mouse.cs`, `ViewBase/Adornment/BorderView.Arrangement.cs`, `Text/NerdFonts.cs`, `FileServices/FileSystemIconProvider.cs`, `Drawing/Color/Color.cs`, `Input/Keyboard/Key.cs`, `Drivers/Driver.cs`, `App/Legacy/Application.Mouse.cs` | No live code dependency (verified — `[ConfigurationProperty(` grep returns only the attribute file itself). | Mechanical comment sweep. | + +**Already rewired in #5411 (no action in this PR):** `Views/Menu/Menu.cs`, `Views/Menu/MenuBar.cs`, `Views/StatusBar.cs`, `Drawing/LineCanvas/LineCanvas.cs` all subscribe to `ThemeChanges.ThemeChanged` instead of `ConfigurationManager.Applied`. ### 4.5 Resource files @@ -193,20 +199,24 @@ TuiConfigurationBuilder.ApplyToStaticFacades() `ButtonSettings.Defaults` (and peers) remain the **static facade** used by all views per D‑01. The only change versus #5411 is that the facade is now updated *only* by the MEC monitor — never by `ConfigurationManager.Apply`. -### 5.2 ThemeManager / SchemeManager: from wrapper to owner +### 5.2 ThemeManager / SchemeManager: from event-bridging wrapper to data owner (A2) + +Post-#5411 status (**A1 — done**): `IThemeManager.ThemeChanged` exists; `MecThemeManager` subscribes to the legacy static `ThemeManager.ThemeChanged` in its constructor and forwards. `ThemeChanges.ThemeChanged` is the public observer facade used by internal views; it bridges both `ConfigurationManager.Applied` *and* `IThemeManager.ThemeChanged` because `CM.Apply()` writes `ConfigProperty` values directly and bypasses the C# setter, so `ThemeChanged` alone would miss most theme switches today. + +This PR's work (**A2**): make `MecThemeManager` / `MecSchemeManager` the **owners** of theme and scheme runtime data, so the `ConfigurationManager.Applied` half of the `ThemeChanges` bridge can be deleted along with `ConfigurationManager` itself. -`MecThemeManager` and `MecSchemeManager` currently delegate to the legacy static `ThemeManager` / `SchemeManager`. Once the legacy types are deleted, they become the owners: +Today the runtime theme/scheme dictionary lives in `ConfigurationManager.Settings["Themes"]`, parsed by `ScopeJsonConverter`. The target shape: ```csharp public sealed class ThemeManager : IThemeManager { private readonly IOptionsMonitor _monitor; - private readonly object _switchLock = new (); + private IDisposable? _subscription; public ThemeManager (IOptionsMonitor monitor) { _monitor = monitor; - _monitor.OnChange (HandleChanged); + _subscription = _monitor.OnChange (HandleChanged); } public string ActiveTheme @@ -219,25 +229,40 @@ public sealed class ThemeManager : IThemeManager public event EventHandler? ThemeChanged; - public void SwitchTheme (string name) { /* update + raise */ } + public void SwitchTheme (string name) { /* mutate in-memory provider + reload */ } - private void HandleChanged (ThemeSettings settings, string? name) => ThemeChanged?.Invoke (this, settings.ActiveTheme); + private void HandleChanged (ThemeSettings settings, string? name) + => ThemeChanged?.Invoke (this, settings.ActiveTheme); } ``` -`SchemeManager` becomes an instance class fronting `ThemeSettings.Schemes`. The static `SchemeManager.GetScheme(string)` API used in many views is kept as a **static convenience facade** that forwards to the registered service (via a static `Application.Services` accessor) or to a process-wide fallback when no app has been created — same shape as `XxxSettings.Defaults`. +A2 is the **gating prerequisite** for deleting `ScopeJsonConverter` and is tied to the D-02 resource-shape decision: the MEC binder cannot bind `ThemeSettings` from the legacy flat-key `Themes: [ { Name: { "Class.Prop": ... } } ]` shape without a custom source. Either: + +- **Option α (recommended):** Rewrite `Terminal.Gui/Resources/config.json` to the MEC-native nested shape, ship `TuiConfigMigrator` for user files. `ScopeJsonConverter` deletes cleanly. +- **Option β:** Keep the legacy shape and write a `LegacyTuiConfigurationSource : IConfigurationSource` that re-emits flat keys as nested MEC keys at load time. Smaller user blast radius; larger ongoing maintenance. + +§5.4 carries the analysis. + +`SchemeManager` becomes an instance class fronting `ThemeSettings.Schemes`. The static `SchemeManager.GetScheme(string)` API used pervasively in views is kept as a **static convenience facade** that forwards to the registered service (via a static `Application.Services` accessor) or to a process-wide fallback when no app has been created — same shape as the `XxxSettings.Defaults` pattern adopted in #5411. + +### 5.3 `ThemeChanges` bridge cleanup + +`ThemeChanges` currently raises its event in response to **either**: + +1. `ConfigurationManager.Applied` (legacy CM apply path), or +2. `IThemeManager.ThemeChanged` (MEC path, post‑A1). -### 5.3 Event replacement table +After A2 lands, (1) is dead — every theme switch goes through `MecThemeManager.SwitchTheme` ⇒ `IOptionsMonitor.OnChange` ⇒ `IThemeManager.ThemeChanged` ⇒ `ThemeChanges.ThemeChanged`. The `ConfigurationManager.Applied` subscription inside `ThemeChanges` deletes alongside `ConfigurationManager`. -| Old subscription | New subscription | File(s) affected | -|------------------|------------------|------------------| -| `ConfigurationManager.Applied += handler` (to re-read theme glyphs/border styles) | `ThemeChanges.Observed += handler` (small static event raised once after MEC apply finishes), OR `IThemeManager.ThemeChanged` | `Menu.cs`, `MenuBar.cs`, `StatusBar.cs`, `LineCanvas.cs`, `UICatalogRunnable.cs` | -| `ConfigurationManager.Updated += handler` | Delete — there is no MEC "loaded but not applied" state. | -| `ThemeManager.ThemeChanged` (legacy) | `IThemeManager.ThemeChanged` | +No view-side changes are required — `ThemeChanges` is the public surface and stays. -### 5.4 JSON config: flat → nested key migration +### 5.4 Library `config.json`: resolving D-02 -**Library `config.json`** is rewritten. Example snippet: +A2 cannot land until the MEC binder can produce a populated `ThemeSettings` from `Terminal.Gui/Resources/config.json`. Two viable paths: + +**Option α — Nested-only with `TuiConfigMigrator` helper (recommended).** + +Rewrite the library resource: Before (flat): ```json @@ -247,9 +272,7 @@ Before (flat): "Default": { "Button.DefaultShadow": "Opaque", "Glyphs.CheckStateChecked": "☑", - "Schemes": [ - { "Base": { "Normal": { "Foreground": "White", "Background": "Black" } } } - ] + "Schemes": [ { "Base": { "Normal": { "Foreground": "White", "Background": "Black" } } } ] } } ] @@ -261,23 +284,36 @@ After (nested, MEC-native): { "Themes": { "Default": { - "Button": { "DefaultShadow": "Opaque" }, - "Glyphs": { "CheckStateChecked": "☑" }, - "Schemes": { - "Base": { "Normal": { "Foreground": "White", "Background": "Black" } } - } + "Button": { "DefaultShadow": "Opaque" }, + "Glyphs": { "CheckStateChecked": "☑" }, + "Schemes": { "Base": { "Normal": { "Foreground": "White", "Background": "Black" } } } } } } ``` -**Breaking change for users with hand-authored `~/.tui/config.json` files.** A small migration helper (`TuiConfigMigrator.MigrateFlatToNested(string json)`) is shipped in `Terminal.Gui` for one release; the UICatalog `ConfigurationEditor` scenario gains a "Migrate" button. After one major version the helper is removed. +User config files in the legacy shape break. Mitigations: + +- Ship `TuiConfigMigrator.MigrateFlatToNested(string json)` in `Terminal.Gui` for one release. +- UICatalog `ConfigurationEditor` scenario gets a "Migrate" button. +- Release notes + `docfx/docs/migrate-cm-to-mec.md` migration guide. + +Trade-off: maximal AOT win (`ScopeJsonConverter` deletes), one-time pain for users with hand-authored configs. + +**Option β — Custom `LegacyTuiConfigurationSource`.** + +Implement `IConfigurationSource` + `IConfigurationProvider` that parses the legacy flat-key shape (using the existing `RuneJsonConverter` / `KeyJsonConverter` / `SchemeJsonConverter` from `TuiSerializerContext`) and surfaces the result as MEC-native nested keys. Insert it before the standard `JsonStream` provider for `config.json`. + +Trade-off: zero user-side breakage; keep a custom configuration provider plus the flat-key parser indefinitely. The custom provider is still smaller and more AOT-friendly than `ScopeJsonConverter`, but the maintenance debt is permanent. -(This supersedes predecessor spec D‑02 Option 3. Continuing to accept flat keys requires keeping `ScopeJsonConverter`, which is the largest AOT-hostile blob in the Configuration namespace. The cost/benefit no longer favors dual format support after #5411 makes the nested format viable.) +**Recommendation:** Option α. The whole point of #4943 is to delete CM-shaped artifacts; keeping a flat-key parser indefinitely defeats it. A one-release migration helper is a fair user-facing cost. If feedback during the alpha cycle is severe, fall back to Option β as a hotfix. ### 5.5 Source generation context cleanup -`SourceGenerationContext` currently lists `SettingsScope`, `ThemeScope`, `AppSettingsScope`, and various dictionary closures. Replace with the per-component POCOs: +`TuiSerializerContext` (created in #5411) already configures all custom converters and `JsonSerializerOptions`. Remaining work in this PR: + +- Audit `SourceGenerationContext`'s `[JsonSerializable]` entries. Remove anything that references `SettingsScope` / `ThemeScope` / `AppSettingsScope` (those types are being deleted). +- Ensure every Settings POCO actively bound by `TuiConfigurationBuilder` is registered: ```csharp [JsonSerializable (typeof (ThemeSettings))] @@ -291,18 +327,15 @@ After (nested, MEC-native): [JsonSerializable (typeof (Color))] [JsonSerializable (typeof (Dictionary))] [JsonSerializable (typeof (Dictionary))] -[JsonSourceGenerationOptions (Converters = new[] { - typeof (RuneJsonConverter), typeof (KeyJsonConverter), - typeof (ColorJsonConverter), typeof (AttributeJsonConverter), - typeof (SchemeJsonConverter), typeof (TraceCategoryJsonConverter) -})] internal partial class SourceGenerationContext : JsonSerializerContext { } ``` -This is what unlocks the AOT-friendly path: every type STJ touches is statically known, no `MakeGenericType`, no `Assembly.GetTypes()`. +The converter registration is owned by `TuiSerializerContext` and does not move. ### 5.6 ModuleInitializer simplification +After A2 + the legacy CM deletion: + ```csharp [ModuleInitializer] internal static void InitializeTuiConfiguration () @@ -314,37 +347,54 @@ internal static void InitializeTuiConfiguration () Single call. No `ConfigurationManager.Initialize ()`, no `#pragma warning disable CS0618`. ---- +### 5.7 `PrintJsonErrors` replacement (behavior-preserving) -## 6. Implementation Phases (within this PR) +The v2 contract is "don't fail-fast on bad config.json; collect errors and print at shutdown." MEC supports this via `JsonConfigurationSource.OnLoadException`: + +```csharp +builder.AddJsonStream (stream, source => +{ + source.OnLoadException = ctx => + { + TuiJsonErrors.Add ($"{ctx.Source}: {ctx.Exception.Message}"); + ctx.Ignored = true; + }; +}); +``` -Each phase is one or more commits. Tests must build and pass at every commit. +Wire the same hook on every JSON source registered by `TuiConfigurationExtensions`. `ApplicationImpl.Lifecycle.Shutdown` calls `TuiJsonErrors.Print ()` (or routes through `Logging`) in place of the current `ConfigurationManager.PrintJsonErrors ()` call. -### Phase A — Make `MecThemeManager` / `MecSchemeManager` self-sufficient -- Move theme/scheme storage from the legacy static `ThemeManager` / `SchemeManager` into `ThemeSettings` (already exists; populate it from `IOptionsMonitor`). -- Update `MecThemeManager` / `MecSchemeManager` to read/write the POCO directly. -- Add tests in `MecThemeTests` for switch / add / remove scheme paths that previously delegated to the legacy code. +Caveat: `OnLoadException` covers file/parse errors only. Bind/POCO validation errors (`OptionsValidationException`) don't have an equivalent hook in MEC and will throw on first `IOptions.Value` access. This is acceptable — bind errors are programmer-level (POCO shape mismatch), not user-level (typo in JSON). + +--- -### Phase B — Replace `ConfigurationManager.Applied` subscribers -- Introduce `Terminal.Gui.Configuration.ThemeChanges` (or expose `IThemeManager.ThemeChanged` via a static convenience accessor). -- Rewire `Menu`, `MenuBar`, `StatusBar`, `LineCanvas` to the new event. -- Delete the `#pragma warning disable CS0618` suppressions in those files. -- Update tests that assert the subscription chain. +## 6. Implementation Phases (within this PR) -### Phase C — JSON converter / `SourceGenerationContext` decoupling -- Replace all `ConfigurationManager.SerializerContext` references in `AttributeJsonConverter`, `SchemeJsonConverter`, `Concurrent/DictionaryJsonConverter`, `DeepCloner` (the last one is being deleted anyway) with `SourceGenerationContext.Default`. -- Audit `SourceGenerationContext` entries and replace scope types with the POCOs (§5.5). +Each phase is one or more commits. Tests must build and pass at every commit. **Phases A1, B, and the JSON-converter half of C landed in #5411 and are not repeated here.** -### Phase D — Library `config.json` rewrite + migration helper -- Rewrite `Terminal.Gui/Resources/config.json` in nested format. -- Add `TuiConfigMigrator.MigrateFlatToNested(string)` and tests. +### Phase D — Library `config.json` rewrite + migration helper *(blocks Phase A2)* +- Rewrite `Terminal.Gui/Resources/config.json` in the nested MEC-native shape (Option α of §5.4). +- Add `TuiConfigMigrator.MigrateFlatToNested(string)` and parallelizable tests. - Update `Examples/Config/example_config.json`. -- Update the JSON schema if hosted (file follow-up issue). +- File a follow-up issue to update the hosted JSON schema (Q‑03). +- Wire `JsonConfigurationSource.OnLoadException` per §5.7 so v2 deferred-error behavior is preserved. + +### Phase A2 — Mec managers own runtime theme/scheme data +- Have `MecThemeManager` / `MecSchemeManager` read theme and scheme dictionaries from `IOptionsMonitor.CurrentValue` instead of delegating to the legacy static `ThemeManager` / `SchemeManager`. +- `SwitchTheme` mutates the underlying in-memory MEC source and triggers `IOptionsMonitor.OnChange`. +- Confirm `ThemeChanges.ThemeChanged` is now raised exclusively through the `IThemeManager.ThemeChanged` path; verify no `ConfigurationManager.Applied` round-trip happens at runtime. +- Add tests in `MecThemeTests` for switch / add / remove scheme paths that previously delegated to the legacy code. + +### Phase C-finish — `SourceGenerationContext` POCO audit +- Remove `SettingsScope` / `ThemeScope` / `AppSettingsScope` from `[JsonSerializable]` list. +- Add per-component POCOs and `Dictionary` / `Dictionary` per §5.5. +- Delete the `ConfigurationManager.SerializerContext` one-line delegator field (kept post-#5411 purely to avoid an obsolete-attr breaking change in the parent PR). ### Phase E — Delete legacy CM types + tests - Delete every file listed in §4.1 and every test listed in §4.6. - Delete `ModuleInitializers.cs`'s `ConfigurationManager.Initialize ()` call (§5.6). -- Delete `ConfigurationManager.PrintJsonErrors ()` from `ApplicationImpl.Lifecycle.cs`. +- Replace `ApplicationImpl.Lifecycle`'s `ConfigurationManager.PrintJsonErrors ()` call with `TuiJsonErrors.Print ()` per §5.7. +- Delete the `ConfigurationManager.Applied` branch inside `ThemeChanges` per §5.3. - Sweep stale doc-comment references (§4.4 last row). ### Phase F — Rename `MecThemeManager`/`MecSchemeManager` → `ThemeManager`/`SchemeManager` @@ -354,10 +404,11 @@ Each phase is one or more commits. Tests must build and pass at every commit. ### Phase G — Update examples (Runner, UICatalog, NativeAot, etc.) - §4.8 sweep. -### Phase H — Update `docfx/docs/config.md` -- Rewrite to reflect the MEC contract. +### Phase H — Update `docfx/docs/config.md` + migration guide +- Rewrite `config.md` to reflect the MEC contract. - Remove every reference to `ConfigurationManager.Enable`, `ConfigLocations`, `ConfigurationProperty`, `SettingsScope`, `ThemeScope`, `AppSettingsScope`. -- Document `TuiConfigurationBuilder`, `IThemeManager`, `ISchemeManager`, the nested JSON schema, the migration helper. +- Document `TuiConfigurationBuilder`, `IThemeManager`, `ISchemeManager`, the nested JSON schema, the migration helper, `ThemeChanges`, `TuiSerializerContext`. +- Add `docfx/docs/migrate-cm-to-mec.md` cheatsheet. ### Phase I — Verification - Run full test matrix (`UnitTestsParallelizable`, `UnitTests.NonParallelizable`, `IntegrationTests`). @@ -400,10 +451,11 @@ A separate `docfx/docs/migrate-cm-to-mec.md` is added in Phase H with a side-by- | Risk | Mitigation | |------|------------| | Hidden third-party consumers still call obsolete CM API | They received an `[Obsolete]` warning in #5411 with the message pointing at `TuiConfigurationBuilder`. One release window has elapsed by the time this PR ships. | -| Nested JSON breaks every existing user config file | Ship `TuiConfigMigrator` + UICatalog "Migrate" button + clear release-note + `migrate-cm-to-mec.md` guide. | +| Nested JSON breaks every existing user config file | Ship `TuiConfigMigrator` + UICatalog "Migrate" button + clear release-note + `migrate-cm-to-mec.md` guide. If alpha-cycle feedback is severe, fall back to §5.4 Option β (custom legacy source). | | Renaming `MecThemeManager` → `ThemeManager` clashes with the deleted legacy `ThemeManager` if a partial revert lands | Phase F runs only after Phase E commits; if reverted, the rename is reverted with it. | | AOT size delta is smaller than predicted | Acceptable. The parallel-test and decoupling wins still justify the change. Record actual delta in the PR. | | `IOptionsMonitor.OnChange` is not invoked in NativeAOT due to missing trim roots | Verified in #5411. Re-verify with a smoke test in `Examples/NativeAot` as part of Phase I. | +| Bind-time (`OptionsValidationException`) errors are not aggregated like file-parse errors | Accepted. Bind errors are programmer-level (POCO shape mismatch). The user-level "typo in JSON" path is fully preserved via `JsonConfigurationSource.OnLoadException` (§5.7). | --- From a73f6a7adf5a5d562ac42fe6c4016c465b85d981 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 May 2026 11:59:47 -0600 Subject: [PATCH 05/13] Add CM removal prep artifact: AOT baseline, examples/test inventories - AOT baseline (post-#5411 @ 83ded73aa): NativeAot.exe = 22.77 MB, Terminal.Gui.dll = 1.77 MB, ConfigPropertyHostTypes roots 31 types. - Examples inventory: ~115 files but ~105 are the same one-line CM.Enable() call; identifies the 10 non-trivial scenarios that need per-site review (ConfigurationEditor, Runner, UICatalogRunnable, etc.). - Test inventory: ~13 files to delete, ~11 to keep, ~2 benchmarks to port. Flags glyph + apply-over-defaults behaviors that need MEC-side ports rather than straight deletion. - Records the reproduce command + threshold for the eventual size delta. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- specs/remove-legacy-cm-prep.md | 217 +++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 specs/remove-legacy-cm-prep.md diff --git a/specs/remove-legacy-cm-prep.md b/specs/remove-legacy-cm-prep.md new file mode 100644 index 0000000000..40776b0ce6 --- /dev/null +++ b/specs/remove-legacy-cm-prep.md @@ -0,0 +1,217 @@ +# CM Removal — Pre-Work Inventories & Baselines + +Companion artifact for [`remove-legacy-cm.md`](./remove-legacy-cm.md). +Captures the empirical "before" state used by phases **G** (examples sweep), +**E** (test deletion), and **I** (AOT measurement) so the eventual delta +claim is reproducible. + +Generated against base SHA `83ded73aa` (`origin/copilot/replace-cm-with-mec`, +post Phase A1/B/C of #5411). My branch is rebased onto this commit; spec +commit is `f122ebe6a`. + +--- + +## 1. AOT Size Baseline (Phase I) + +Command: `dotnet publish Examples/NativeAot/NativeAot.csproj -c Release -r win-x64` +Platform: Windows x64, .NET 10 SDK, clean `bin/obj` before publish. + +| Artifact | Bytes | MB | +|--------------------------------|--------------|--------| +| `NativeAot.exe` (AOT, single) | 23,873,536 | 22.77 | +| `Terminal.Gui.dll` (Release) | 1,854,464 | 1.77 | +| Publish dir total | 96,319,509 | 91.86 | + +### Reflection-rooting metric + +`Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs` currently roots +**31** types via `[DynamicDependency]`. Deleting this file in Phase E +should drop those 31 root anchors and let the AOT trimmer collect their +unused members. This is the headline number for the "AOT size reduction" +claim in #5416's PR description. + +### Reproducing this number after Phase E + +```powershell +Remove-Item Examples/NativeAot/bin, Examples/NativeAot/obj -Recurse -Force -EA 0 +Remove-Item Terminal.Gui/bin, Terminal.Gui/obj -Recurse -Force -EA 0 +dotnet publish Examples/NativeAot/NativeAot.csproj -c Release -r win-x64 +Get-Item Examples/NativeAot/bin/Release/net10.0/win-x64/publish/NativeAot.exe | + Select-Object Name, Length +``` + +Record the new `NativeAot.exe` byte count and the delta vs. 23,873,536. +A meaningful win should be ≥ 1 MB given the 31 rooted types include +view types with large transitive surfaces (`MenuBar`, `StatusBar`, +`TableView`, `TreeView`, `Dialog`, etc.). + +--- + +## 2. Examples Inventory (Phase G) + +Total `Examples/` files touching CM: **~115 source/doc files**, but +**99% are the same one-line incantation** — the actual rewrite surface +is small. + +### 2.1 Dominant pattern (~105 occurrences, mostly UICatalog scenarios) + +Single call in scenario constructor / `Main`: + +```csharp +ConfigurationManager.Enable (ConfigLocations.All); +``` + +**Replacement:** Replace with MEC equivalent (likely +`Application.Create ().UseTuiConfiguration ().Init ()` or whatever the +final builder shape settles on — see spec §5.6). + +Files using only this pattern (mechanical sed, no review needed): +all `Examples/UICatalog/Scenarios/*.cs` except the four below. + +### 2.2 Non-trivial scenarios needing review + +| File | Lines | What it does | Replacement notes | +|------|-------|--------------|-------------------| +| `Examples/UICatalog/UICatalogRunnable.cs` | 30, 131, 203, 715 | Subscribes to `ConfigurationManager.Applied`; reads `IsEnabled`; uses `[ConfigurationProperty (Scope = typeof (AppSettingsScope), OmitClassName = true)]` on a settings field | Subscribe to `ThemeChanges.ThemeChanged` (already exists post-#5411); move `[ConfigurationProperty]` to a POCO bound via `IOptionsMonitor` | +| `Examples/UICatalog/Runner.cs` | 14, 39, 373, 378, 379 | Sets `RuntimeConfig` JSON string for driver overrides; explicit `Load + Apply`; bridges `ThemeManager.ThemeChanged → CM.Apply` | Build an in-memory `IConfigurationSource` for the driver overrides; drop the bridge (MEC reload handles it) | +| `Examples/UICatalog/Scenarios/ConfigurationEditor.cs` | 23, 51, 75, 222, 227, 232, 247 | The CM editor scenario itself — enumerates `SourcesManager.Sources`, edits `RuntimeConfig`, calls `GetHardCodedConfig`/`GetEmptyConfig` | Rewrite to walk `IConfigurationRoot.Providers` and edit a designated `IConfigurationSource`. **This scenario is the heaviest item in Phase G.** | +| `Examples/UICatalog/Scenarios/Themes.cs` | 89 | `ConfigurationManager.Apply ()` after a theme switch | Drop the call (MEC reload-on-change) | +| `Examples/UICatalog/Scenarios/ThemeFallback.cs` | 73 | Same pattern | Drop the call | +| `Examples/UICatalog/Scenarios/CodeViewDemo.cs` | 153 | Same pattern | Drop the call | +| `Examples/UICatalog/Scenarios/TextInputControls.cs` | 384 | `ConfigurationManager.Applied += ...` | Subscribe to `ThemeChanges.ThemeChanged` | +| `Examples/UICatalog/Scenarios/Shortcuts.cs` | 345 | Reads `ConfigurationManager.IsEnabled` | Drop the check (MEC is always on or behind a builder opt-in) | +| `Examples/UICatalog/UICatalog.cs` | 296 | `Enable` call at app startup | Replace with builder call | +| `Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs` | 57 | `Enable` call | Replace | + +### 2.3 Standalone examples (each has one `Enable` call) + +`Example.cs`, `SelfContained/Program.cs`, `CommunityToolkitExample/Program.cs`, +`NativeAot/Program.cs`, `ShortcutTest.cs`, `ReactiveExample/Program.cs`, +`PromptExample/Program.cs`, `ScenarioRunner/Program.cs` (2 calls). + +`Examples/Example/Example.cs:13` additionally sets `RuntimeConfig` +to a JSON literal — needs the same in-memory-source treatment as +`Runner.cs`. + +### 2.4 Docs / READMEs to update (Phase H) + +`Examples/Config/README.md`, `Examples/CommunityToolkitExample/README.md`, +`Examples/SelfContained/README.md`, `Examples/ReactiveExample/README.md`, +`Examples/NativeAot/README.md` — all reference the legacy `Enable` +incantation. + +`Examples/Config/example_config.json` — uses the legacy flat +`"ConfigurationManager.ThrowOnJsonErrors"` key. Migrate when D-02 +lands. + +### 2.5 AOT-blocker note + +`Examples/NativeAot/README.md` explicitly documents `ConfigurationManager.Initialize`, +`DeepCloner`, `SourceGenerationContext` as the reasons AOT works the way +it does today. After CM removal, this README needs a full rewrite — the +trim warnings should largely disappear. + +--- + +## 3. Test Inventory (Phase E) + +Total Configuration tests across all test projects: **25 files**. + +### 3.1 DELETE — pure CM/scope/Initialize tests (8 files) + +| File | Project | CM refs | +|------|---------|---------| +| `ConfigurationMangerTests.cs` | NonParallelizable | 49 | +| `ConfigPropertyAssemblyScanTests.cs` | NonParallelizable | 9 | +| `SourcesManagerLoadNullJsonTests.cs` | NonParallelizable | 5 | +| `GlyphTests.cs` | NonParallelizable | uses `static CM` import, `RuntimeConfig`, `ThemeManager.GetCurrentTheme()` | +| `SourcesManagerTests.cs` | Parallelizable | 69 | +| `ScopeTests.cs` | Parallelizable | 18 | +| `ScopeJsonConverterTests.cs` | Parallelizable | 5 | +| `SettingsScopeTests.cs` / `ThemeScopeTests.cs` | Parallelizable | 2 / 4 | +| `ConfigPropertyTests.cs` / `ConfigPropertyHostTypesTests.cs` / `ConfigurationPropertyAttributeTests.cs` | Parallelizable | 10 / 10 / 2 | +| `DeepClonerTests.cs` | Parallelizable | 7 | + +(That's 13 files counted, not 8 — the 8/13 split depends on whether +glyph behavior gets re-tested against MEC; see §3.3 *port* row.) + +### 3.2 KEEP — JSON converter tests still valid against `TuiSerializerContext` (8 files) + +These test converter behavior, not CM machinery. The converters +themselves are kept (per spec §4.2) and now wire through +`TuiSerializerContext`. + +- `AttributeJsonConverterTests.cs` +- `ColorJsonConverterTests.cs` +- `ConcurrentDictionaryJsonConverterTests.cs` +- `KeyJsonConverterTests.cs` +- `KeyCodeJsonConverterTests.cs` +- `RuneJsonConverterTests.cs` +- `SchemeJsonConverterTests.cs` *(verify it doesn't depend on `ThemeScope` resolution)* +- `MemorySizeEstimator.cs` *(helper, not a test)* + +### 3.3 KEEP — already MEC-based (3 files) + +- `MecThemeTests.cs` +- `MecSettingsTests.cs` +- `MecAppSettingsTests.cs` +- `SchemeManagerTests.cs` *(verify which manager — likely MEC since legacy `SchemeManager` is `[Obsolete]`)* + +### 3.4 PORT — behaviors that should survive as MEC tests + +The `GlyphTests` "Apply over defaults" behavior is a real +user-observable contract: when you override a glyph in `config.json`, +`Glyphs.LeftBracket` reflects it. After deletion, add an equivalent +test against the MEC pipeline (load JSON → assert `Glyphs.*`). + +Same applies to any `ConfigurationMangerTests` case that asserts +end-to-end behavior (file-load → apply → property-reflects) that isn't +already covered by `MecSettingsTests` / `MecThemeTests`. A line-by-line +audit is required in Phase E — not in this prep doc. + +### 3.5 Benchmarks (Phase E) + +| File | Action | +|------|--------| +| `Tests/Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs` | **Port** to measure `Application.Create().UseTuiConfiguration().Init()` cold-start, or delete if MEC startup is well-characterized elsewhere | +| `Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs` | **Port** to measure `IThemeManager.Theme = "X"` (which triggers `ThemeChanges.ThemeChanged`) — this is still the same user-facing operation | + +These benchmarks are the only quantitative basis for any "startup is +faster after CM removal" claim, so porting (not deleting) is preferred. + +### 3.6 Cross-test-project sanity check + +One outlier hit in +`Tests/UnitTestsParallelizable/Input/Keyboard/CommandInsertCaretKeyBindingTests.cs` +matched the broad regex but contains no actual CM references — false +positive (likely `Settings` substring). No action. + +--- + +## 4. What This Prep Did NOT Touch + +- D-02 (`config.json` shape decision) — still gated on source-session input. +- `TuiConfigMigrator` design — blocked on D-02. +- Migration guide draft — deferred until D-02 resolves so the user-facing + examples are accurate. +- Phase A2 (Mec managers own theme/scheme data) — owned by the source + session per the last cross-session sync. + +--- + +## 5. Next Executable Step (when unblocked) + +Once D-02 is decided: + +1. **If α (nested + migrator):** rewrite `Terminal.Gui/Resources/config.json` + to the nested MEC shape; implement `TuiConfigMigrator.MigrateFlatToNested`; + add round-trip tests; wire `OnLoadException → TuiJsonErrors` aggregator + in `TuiConfigurationExtensions`. +2. **If β (legacy source):** implement + `LegacyTuiConfigurationSource : IConfigurationSource` + + `LegacyTuiConfigurationProvider : ConfigurationProvider` that maps + `"X.Y.Z"` flat keys to nested `IConfiguration` paths. Keep `config.json` + unchanged. + +Either path unblocks Phase A2 (Mec managers consuming the parsed config +directly instead of via `Settings["Themes"]`). From a72475523d32b909fbc8665ca93b9c49e9b6709a Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 May 2026 12:09:34 -0600 Subject: [PATCH 06/13] Resolve D-02 to alpha-lite (detect+warn, no library-side migration) Per source session decision: - Spec section 5.4 rewritten: pure-nested MEC read path + ~20 LOC peek-and-warn in TuiConfigurationBuilder.AddTuiJsonFile. Legacy shapes are NOT parsed; affected settings fall through to defaults. - Phase D scope shrinks: no TuiConfigMigrator, no LegacyTuiConfigurationSource, no round-trip tests. Two tests only (flat-key warn, array-themes warn). - Standalone Tools/MigrateConfig/ console app (not shipped in Terminal.Gui.dll) as migration aid; deletable any time. - Records explicitly-rejected alternatives (keep-both, silent-translate, throw) so future reviewers don't re-litigate. - Updated section 7 JSON-breaking-change wording and section 8 risk row to reflect the warn-and-default contract. - Prep artifact section 5 updated to reflect the now-unblocked Phase D step list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- specs/remove-legacy-cm-prep.md | 38 ++++++++++++++----------- specs/remove-legacy-cm.md | 51 +++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/specs/remove-legacy-cm-prep.md b/specs/remove-legacy-cm-prep.md index 40776b0ce6..ff2921a97b 100644 --- a/specs/remove-legacy-cm-prep.md +++ b/specs/remove-legacy-cm-prep.md @@ -199,19 +199,25 @@ positive (likely `Settings` substring). No action. --- -## 5. Next Executable Step (when unblocked) - -Once D-02 is decided: - -1. **If α (nested + migrator):** rewrite `Terminal.Gui/Resources/config.json` - to the nested MEC shape; implement `TuiConfigMigrator.MigrateFlatToNested`; - add round-trip tests; wire `OnLoadException → TuiJsonErrors` aggregator - in `TuiConfigurationExtensions`. -2. **If β (legacy source):** implement - `LegacyTuiConfigurationSource : IConfigurationSource` + - `LegacyTuiConfigurationProvider : ConfigurationProvider` that maps - `"X.Y.Z"` flat keys to nested `IConfiguration` paths. Keep `config.json` - unchanged. - -Either path unblocks Phase A2 (Mec managers consuming the parsed config -directly instead of via `Settings["Themes"]`). +## 5. Next Executable Step (now unblocked) + +D-02 resolved to **α-lite** (detect + warn; no library-side migration). +See `remove-legacy-cm.md` §5.4 and Phase D for the full scope. Concretely: + +1. Rewrite `Terminal.Gui/Resources/config.json` once (flat → nested, + array-themes → dict-themes). +2. Add ~20 LOC peek-and-warn to `TuiConfigurationBuilder.AddTuiJsonFile` + (detect top-level keys containing `.` or `Themes` as a JSON array; + emit one `WARN` log per offending file). +3. Delete `Terminal.Gui/Configuration/ScopeJsonConverter.cs` — nothing + left in the library reads the legacy shape. +4. Wire `JsonConfigurationSource.OnLoadException → TuiJsonErrors` + aggregator in `TuiConfigurationExtensions` (spec §5.7). +5. Rewrite every `Examples/.../config.json` to nested. +6. Add `Tools/MigrateConfig/` console app (~50 LOC, separate csproj, + not in any shipping solution). +7. Two parallelizable tests: one asserts the warning fires on a + flat-key sample, one on an array-themes sample. **No translation + logic to test.** + +Once Phase D lands, Phase A2 (Mec managers consume `IOptionsMonitor.CurrentValue` directly) unblocks. diff --git a/specs/remove-legacy-cm.md b/specs/remove-legacy-cm.md index 296f2911a1..149e6cfd1a 100644 --- a/specs/remove-legacy-cm.md +++ b/specs/remove-legacy-cm.md @@ -258,11 +258,13 @@ No view-side changes are required — `ThemeChanges` is the public surface and s ### 5.4 Library `config.json`: resolving D-02 -A2 cannot land until the MEC binder can produce a populated `ThemeSettings` from `Terminal.Gui/Resources/config.json`. Two viable paths: +**Resolution: α-lite (detect + warn, no library-side migration).** -**Option α — Nested-only with `TuiConfigMigrator` helper (recommended).** +The MEC read path is pure-nested. `TuiConfigurationBuilder.AddTuiJsonFile` (and the equivalent extension overloads) peek the JSON before binding and, if they detect pre-MEC shapes — top-level keys containing `.`, or `Themes` as a JSON array — emit a single `WARN`-level log identifying the file path and pointing at the migration documentation URL. The legacy shape is **not** parsed; affected settings fall through to defaults. -Rewrite the library resource: +**Rationale.** v2 is GA, so silent breakage (γ) is inappropriate. But hand-edited `config.json` usage is rare and concentrated in app authors who control their own upgrade timing, so a perpetual translation layer (β `LegacyTuiConfigurationSource`) or a full bidirectional migrator (α `TuiConfigMigrator`) buys little and costs permanent library surface. α-lite is the minimal GA-appropriate response: ~20 LOC of detection + one log line — no parsing of the legacy shape, no auto-migration, no round-trip tests, no deprecation cycle to engineer. + +**Library resource rewrite (one-time, in this PR).** `Terminal.Gui/Resources/config.json` is regenerated in the nested MEC-native shape: Before (flat): ```json @@ -292,21 +294,24 @@ After (nested, MEC-native): } ``` -User config files in the legacy shape break. Mitigations: +`Examples/Config/example_config.json` and every UICatalog/example config file gets the same treatment. + +**Detection heuristic (implementation note).** Single `JsonDocument` walk, no schema knowledge required: -- Ship `TuiConfigMigrator.MigrateFlatToNested(string json)` in `Terminal.Gui` for one release. -- UICatalog `ConfigurationEditor` scenario gets a "Migrate" button. -- Release notes + `docfx/docs/migrate-cm-to-mec.md` migration guide. +- *Flat key* = any top-level property name in the root object whose name contains `.`. +- *Array themes* = the `Themes` property exists and its `ValueKind == JsonValueKind.Array`. -Trade-off: maximal AOT win (`ScopeJsonConverter` deletes), one-time pain for users with hand-authored configs. +Both checks together: < 20 LOC, no allocation beyond the `JsonDocument`. The warn message names the file and links to `docfx/docs/migrate-cm-to-mec.md` (and the migration tool below). -**Option β — Custom `LegacyTuiConfigurationSource`.** +**Migration aid.** A standalone `Tools/MigrateConfig/` console app (~50 LOC) lives in the repo but is **not** shipped in `Terminal.Gui.dll` and **not** added to any solution that ships. The detection warning references it. The tool is deletable any time without a deprecation cycle. -Implement `IConfigurationSource` + `IConfigurationProvider` that parses the legacy flat-key shape (using the existing `RuneJsonConverter` / `KeyJsonConverter` / `SchemeJsonConverter` from `TuiSerializerContext`) and surfaces the result as MEC-native nested keys. Insert it before the standard `JsonStream` provider for `config.json`. +**Cleanup horizon.** The detection-and-warn code itself can be deleted in a future minor when issue volume indicates no remaining users hit it. It is small enough to keep indefinitely if preferred — the decision is non-coupled to anything in this PR. -Trade-off: zero user-side breakage; keep a custom configuration provider plus the flat-key parser indefinitely. The custom provider is still smaller and more AOT-friendly than `ScopeJsonConverter`, but the maintenance debt is permanent. +**Explicitly rejected alternatives** (recorded so reviewers don't re-litigate): -**Recommendation:** Option α. The whole point of #4943 is to delete CM-shaped artifacts; keeping a flat-key parser indefinitely defeats it. A one-release migration helper is a fair user-facing cost. If feedback during the alpha cycle is severe, fall back to Option β as a hotfix. +- ❌ *"Keep both shapes forever, just document both."* Combines β's permanent maintenance cost with α's user confusion cost. No upside. +- ❌ *"Silently translate legacy shapes."* Opaque failure mode if translation is wrong; GA-inappropriate. +- ❌ *"Throw on legacy shape."* Too aggressive for a non-malformed JSON file. Warn-and-ignore is the right contract — the file is still valid JSON, it just no longer matches our schema. ### 5.5 Source generation context cleanup @@ -372,12 +377,18 @@ Caveat: `OnLoadException` covers file/parse errors only. Bind/POCO validation er Each phase is one or more commits. Tests must build and pass at every commit. **Phases A1, B, and the JSON-converter half of C landed in #5411 and are not repeated here.** -### Phase D — Library `config.json` rewrite + migration helper *(blocks Phase A2)* -- Rewrite `Terminal.Gui/Resources/config.json` in the nested MEC-native shape (Option α of §5.4). -- Add `TuiConfigMigrator.MigrateFlatToNested(string)` and parallelizable tests. -- Update `Examples/Config/example_config.json`. -- File a follow-up issue to update the hosted JSON schema (Q‑03). -- Wire `JsonConfigurationSource.OnLoadException` per §5.7 so v2 deferred-error behavior is preserved. +### Phase D — Library `config.json` rewrite + α-lite detection *(blocks Phase A2)* +Scope (per §5.4 resolution): +1. Rewrite `Terminal.Gui/Resources/config.json` in the nested MEC-native shape. +2. Add the ~20 LOC peek-and-warn to `TuiConfigurationBuilder.AddTuiJsonFile` (and any sibling overloads). Detection heuristic: flat top-level keys containing `.`, or `Themes` as a JSON array. +3. Delete `Terminal.Gui/Configuration/ScopeJsonConverter.cs` (with the legacy parser gone, nothing reads the legacy shape inside `Terminal.Gui.dll`). +4. Rewrite `Examples/Config/example_config.json` and every UICatalog / example config to nested. +5. Add `Tools/MigrateConfig/` — standalone console app (~50 LOC), separate csproj, not in any shipping solution. +6. Add CHANGELOG entry pointing at the tool and the migration guide URL. +7. Wire `JsonConfigurationSource.OnLoadException` per §5.7 so v2 deferred-error behavior is preserved. +8. File a follow-up issue to update the hosted JSON schema (Q-03). + +Tests for Phase D: **two** parallelizable tests — one asserts the warning fires on a flat-key sample, one asserts it fires on an array-themes sample. No translation logic to test, so no round-trip tests are required. ### Phase A2 — Mec managers own runtime theme/scheme data - Have `MecThemeManager` / `MecSchemeManager` read theme and scheme dictionaries from `IOptionsMonitor.CurrentValue` instead of delegating to the legacy static `ThemeManager` / `SchemeManager`. @@ -438,7 +449,7 @@ The following public surface is **removed**. Source-incompatible for any consume ### JSON file breaking change -User config files in the legacy flat-key format (`"Button.DefaultShadow": "..."`) must be migrated. A migration helper ships for one release. +User config files in the legacy flat-key format (`"Button.DefaultShadow": "..."`) or array-themes format are **not parsed** by the new pipeline; affected settings fall through to defaults. A `WARN`-level log identifies offending files and points at the migration documentation + standalone migration tool (`Tools/MigrateConfig/`). No library-side auto-migration ships (per §5.4 α-lite resolution). ### Migration guide @@ -451,7 +462,7 @@ A separate `docfx/docs/migrate-cm-to-mec.md` is added in Phase H with a side-by- | Risk | Mitigation | |------|------------| | Hidden third-party consumers still call obsolete CM API | They received an `[Obsolete]` warning in #5411 with the message pointing at `TuiConfigurationBuilder`. One release window has elapsed by the time this PR ships. | -| Nested JSON breaks every existing user config file | Ship `TuiConfigMigrator` + UICatalog "Migrate" button + clear release-note + `migrate-cm-to-mec.md` guide. If alpha-cycle feedback is severe, fall back to §5.4 Option β (custom legacy source). | +| Nested JSON breaks existing user config files | α-lite (§5.4): detect-and-warn at load time, fall through to defaults rather than fail. Standalone `Tools/MigrateConfig/` console app + `migrate-cm-to-mec.md` guide give users a one-shot fix path. If issue volume after release indicates more help is needed, β (`LegacyTuiConfigurationSource`) remains a viable hotfix. | | Renaming `MecThemeManager` → `ThemeManager` clashes with the deleted legacy `ThemeManager` if a partial revert lands | Phase F runs only after Phase E commits; if reverted, the rename is reverted with it. | | AOT size delta is smaller than predicted | Acceptable. The parallel-test and decoupling wins still justify the change. Record actual delta in the PR. | | `IOptionsMonitor.OnChange` is not invoked in NativeAOT due to missing trim roots | Verified in #5411. Re-verify with a smoke test in `Examples/NativeAot` as part of Phase I. | From 29ccf1a2d00fcfdbe96c77a6ed09e0872c2f2aaa Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 May 2026 12:13:54 -0600 Subject: [PATCH 07/13] Add Tools/MigrateConfig console app for legacy config.json migration Standalone .NET 10 console utility, ~190 LOC including comments. Not included in Terminal.slnx and not part of any shipping artifact - exists solely so users on the legacy flat-key config.json shape have a one-shot upgrade path when the library stops parsing it. Transforms: - Top-level dotted property names split into nested objects. - 'Themes' array-of-single-key-objects collapses to dict. - 'Schemes' inside a theme follows the same collapse. Verified end-to-end on Terminal.Gui/Resources/config.json: 52 KB flat input -> 49 KB nested output, structurally correct. Includes Tools/README.md establishing the folder convention (not Examples, not in Terminal.slnx, deletable any release) and Tools/MigrateConfig/README.md with usage + lifecycle notes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Tools/MigrateConfig/MigrateConfig.csproj | 18 ++ Tools/MigrateConfig/Program.cs | 208 +++++++++++++++++++++++ Tools/MigrateConfig/README.md | 79 +++++++++ Tools/README.md | 28 +++ 4 files changed, 333 insertions(+) create mode 100644 Tools/MigrateConfig/MigrateConfig.csproj create mode 100644 Tools/MigrateConfig/Program.cs create mode 100644 Tools/MigrateConfig/README.md create mode 100644 Tools/README.md diff --git a/Tools/MigrateConfig/MigrateConfig.csproj b/Tools/MigrateConfig/MigrateConfig.csproj new file mode 100644 index 0000000000..7e312839d0 --- /dev/null +++ b/Tools/MigrateConfig/MigrateConfig.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + Terminal.Gui.Tools.MigrateConfig + migrate-tui-config + + false + + + diff --git a/Tools/MigrateConfig/Program.cs b/Tools/MigrateConfig/Program.cs new file mode 100644 index 0000000000..17210851d2 --- /dev/null +++ b/Tools/MigrateConfig/Program.cs @@ -0,0 +1,208 @@ +// Migrates a pre-MEC Terminal.Gui config.json (flat-key, array-themes shape) +// to the nested MEC-native shape consumed by TuiConfigurationBuilder. +// +// Transforms applied (recursively): +// 1. Any object property name containing '.' is split into nested objects. +// "Button.DefaultShadow": "Opaque" -> "Button": { "DefaultShadow": "Opaque" } +// 2. "Themes" as an array of single-key objects becomes a dictionary. +// "Themes": [ { "Dark": { ... } } ] -> "Themes": { "Dark": { ... } } +// 3. "Schemes" inside a theme follows the same array -> dictionary collapse. +// +// Usage: +// migrate-tui-config [output.json] +// +// If output.json is omitted, the migrated JSON is written to stdout. +// Exit codes: 0 success, 1 usage error, 2 I/O or JSON parse error. + +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Terminal.Gui.Tools.MigrateConfig; + +internal static class Program +{ + private static int Main (string [] args) + { + if (args.Length is < 1 or > 2) + { + Console.Error.WriteLine ("Usage: migrate-tui-config [output.json]"); + Console.Error.WriteLine ("If output.json is omitted, the result is written to stdout."); + + return 1; + } + + string inputPath = args [0]; + string? outputPath = args.Length == 2 ? args [1] : null; + + string text; + + try + { + text = File.ReadAllText (inputPath); + } + catch (Exception ex) + { + Console.Error.WriteLine ($"Could not read \"{inputPath}\": {ex.Message}"); + + return 2; + } + + JsonNodeOptions nodeOpts = new () { PropertyNameCaseInsensitive = false }; + + JsonDocumentOptions docOpts = new () + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + JsonNode? root; + + try + { + root = JsonNode.Parse (text, nodeOpts, docOpts); + } + catch (JsonException ex) + { + Console.Error.WriteLine ($"Could not parse \"{inputPath}\" as JSON: {ex.Message}"); + + return 2; + } + + if (root is not JsonObject rootObj) + { + Console.Error.WriteLine ("Top-level JSON value is not an object; cannot migrate."); + + return 2; + } + + JsonObject migrated = MigrateObject (rootObj); + + JsonSerializerOptions writeOpts = new () + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + string output = migrated.ToJsonString (writeOpts); + + if (outputPath is null) + { + Console.WriteLine (output); + } + else + { + try + { + File.WriteAllText (outputPath, output); + Console.Error.WriteLine ($"Migrated \"{inputPath}\" -> \"{outputPath}\"."); + } + catch (Exception ex) + { + Console.Error.WriteLine ($"Could not write \"{outputPath}\": {ex.Message}"); + + return 2; + } + } + + return 0; + } + + private static JsonObject MigrateObject (JsonObject src) + { + JsonObject result = []; + + foreach (KeyValuePair pair in src) + { + JsonNode? value = pair.Value is null ? null : Clone (pair.Value); + JsonNode? migratedValue = MigrateValue (pair.Key, value); + + MergeDottedKey (result, pair.Key, migratedValue); + } + + return result; + } + + private static JsonNode? MigrateValue (string keyName, JsonNode? value) + { + if (value is JsonObject obj) + { + return MigrateObject (obj); + } + + if (value is JsonArray arr && IsArrayOfSingleKeyObjects (arr) && IsArrayDictKey (keyName)) + { + JsonObject dict = []; + + foreach (JsonNode? item in arr) + { + if (item is not JsonObject itemObj) + { + continue; + } + + foreach (KeyValuePair entry in itemObj) + { + JsonNode? entryValue = entry.Value is null ? null : MigrateValue (entry.Key, Clone (entry.Value)); + dict [entry.Key] = entryValue; + } + } + + return dict; + } + + return value; + } + + private static bool IsArrayDictKey (string keyName) => + keyName is "Themes" or "Schemes"; + + private static bool IsArrayOfSingleKeyObjects (JsonArray arr) + { + if (arr.Count == 0) + { + return false; + } + + foreach (JsonNode? item in arr) + { + if (item is not JsonObject obj || obj.Count != 1) + { + return false; + } + } + + return true; + } + + private static void MergeDottedKey (JsonObject target, string key, JsonNode? value) + { + if (!key.Contains ('.')) + { + target [key] = value; + + return; + } + + string [] parts = key.Split ('.'); + JsonObject cursor = target; + + for (var i = 0; i < parts.Length - 1; i++) + { + string part = parts [i]; + + if (cursor [part] is not JsonObject next) + { + next = []; + cursor [part] = next; + } + + cursor = next; + } + + cursor [parts [^1]] = value; + } + + private static JsonNode Clone (JsonNode node) => + JsonNode.Parse (node.ToJsonString ())!; +} diff --git a/Tools/MigrateConfig/README.md b/Tools/MigrateConfig/README.md new file mode 100644 index 0000000000..1c7cf6d05e --- /dev/null +++ b/Tools/MigrateConfig/README.md @@ -0,0 +1,79 @@ +# migrate-tui-config + +Migrates a pre-MEC Terminal.Gui `config.json` (flat-key, array-themes +shape) to the nested shape consumed by `TuiConfigurationBuilder`. + +## When you need this + +Terminal.Gui v2.x originally used a flat-key `config.json` format: + +```json +{ + "Button.DefaultShadow": "Opaque", + "Themes": [ + { "Dark": { "Glyphs.CheckStateChecked": "☑" } } + ] +} +``` + +Starting in the release that ships PR #5416, the library only reads +the nested MEC-native shape: + +```json +{ + "Button": { "DefaultShadow": "Opaque" }, + "Themes": { + "Dark": { "Glyphs": { "CheckStateChecked": "☑" } } + } +} +``` + +Files in the old shape are detected at load time and ignored with a +`WARN` log; their settings fall through to defaults. This tool produces +a migrated copy you can drop in place. + +## Usage + +```bash +# Write to a new file +dotnet run --project Tools/MigrateConfig -- ./.tui/config.json ./.tui/config.migrated.json + +# Or pipe to stdout +dotnet run --project Tools/MigrateConfig -- ./.tui/config.json +``` + +Exit codes: + +- `0` — success +- `1` — usage error (wrong number of arguments) +- `2` — I/O or JSON parse error + +## What it does + +Three transforms applied recursively: + +1. Property names containing `.` are split into nested objects. + `"Button.DefaultShadow": "Opaque"` becomes + `"Button": { "DefaultShadow": "Opaque" }`. +2. `"Themes"` arrays of single-key objects become dictionaries. + `"Themes": [{"Dark": {...}}]` becomes `"Themes": {"Dark": {...}}`. +3. `"Schemes"` arrays inside a theme get the same collapse. + +JSON comments and trailing commas in the input are tolerated but +**not** preserved in the output — re-add hand-edited comments after +migration. + +## What it doesn't do + +- It does not validate that the resulting JSON binds cleanly to the + v2.x Settings POCOs. Run your app afterwards and watch for any + `OptionsValidationException`. +- It does not preserve property order beyond what `System.Text.Json` + guarantees for a `JsonObject`. + +## Lifecycle + +This tool is not part of the shipping `Terminal.Gui` library and is +not included in `Terminal.slnx`. It exists to ease the one-time +migration in the release that drops the legacy shape. It will be +removed in a future release once the upgrade window is past. diff --git a/Tools/README.md b/Tools/README.md new file mode 100644 index 0000000000..66494a5333 --- /dev/null +++ b/Tools/README.md @@ -0,0 +1,28 @@ +# Tools/ + +Standalone utilities that are **not** part of the shipping `Terminal.Gui` +library and **not** part of the `Examples/` set users learn the library +from. + +## What lives here + +| Tool | Purpose | Shipped? | +|------|---------|----------| +| [`MigrateConfig/`](./MigrateConfig/) | Converts a pre-MEC flat-key `config.json` to the nested MEC schema | No — standalone console app, run on demand | + +## Conventions + +- Each tool gets its own folder + `.csproj` + `README.md`. +- Tool csprojs are **not** added to `Terminal.slnx`. They are built + on demand (`dotnet run --project Tools//`). +- Tools are not user-facing examples. If you want to demonstrate + Terminal.Gui idioms, add to `Examples/` instead. +- Tools may be deleted in any release without a deprecation cycle. + +## Why a separate folder? + +Without this folder, one-off utilities tend to land either inside +`Terminal.Gui.dll` (bloating AOT output) or inside `Examples/` (where +they mislead newcomers into thinking they're recommended app patterns). +The `Tools/` convention is also used by `dotnet/roslyn`, +`dotnet/runtime`, and `dotnet/aspnetcore`. From df5fcb605725908ed15749835d9ab5ae44a46de7 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 May 2026 12:22:34 -0600 Subject: [PATCH 08/13] Add ThemeDefinition POCO bind target for nested Themes config (foundation only) Step 1 of N in the Phase A2/D commit chain for #5416. Lands the bind target for the post-D-rewrite nested `Themes` section in config.json. No production code consumes ThemeDefinition yet; the consumer (rewired MecThemeManager reading via IOptionsMonitor) lands in a subsequent commit. Binding correctness validated by tests against in-memory MEC providers. Shape (named explicitly so reviewers can object to specific subsections): public class ThemeDefinition { public Dictionary? Schemes; // 18 nullable per-component override POCOs, matching every ThemeScope- // flavored BindSection call in TuiConfigurationBuilder.ApplyToStaticFacades: public ButtonSettings? Button; public CheckBoxSettings? CheckBox; public CharMapSettings? CharMap; public DialogSettings? Dialog; public FrameViewSettings? FrameView; public HexViewSettings? HexView; public LinearRangeSettings? LinearRange; public MenuBarSettings? MenuBar; public MenuSettings? Menu; public MessageBoxSettings? MessageBox; public NerdFontsSettings? NerdFonts; public PopoverMenuSettings? PopoverMenu; public SelectorBaseSettings? SelectorBase; public StatusBarSettings? StatusBar; public TextFieldSettings? TextField; public TextViewSettings? TextView; public WindowSettings? Window; public GlyphSettings? Glyphs; // JSON section name is "Glyphs", not "Glyph" } Design choice: null = no theme-level override. Considered alternatives, rejected: (a) "Missing entry in dictionary" - forces a stringly-typed lookup; loses the IDE/compiler awareness that a strongly-typed nullable property gives. (b) "Explicit empty object" - makes "I appear in JSON but override nothing" indistinguishable from "I appear in JSON to assert my-own-values-as-overrides". Nullability avoids that ambiguity at the cost of zero ergonomics. Whether a non-null subsection (i) wholesale-replaces the root *Settings or (ii) property-level merges with it is a manager-rewire concern and is NOT encoded in the POCO. Both consumption strategies are compatible with this shape. Surfaced as an open design question for the PR thread. Source-gen registrations added for ThemeDefinition and Dictionary. These are additive, AOT-safe, and meaningful even before any consumer exists (they unblock the next commit without further SourceGenerationContext edits). Tests (Tests/UnitTestsParallelizable/Configuration/ThemeDefinitionBindingTests.cs): - Bind_FullAndPartialThemes_PartialHasNullsInOmittedSubsections - binds a nested sample with one fully-populated theme and one partial-override theme, asserts the partial theme has nulls in every unmentioned subsection. - Bind_SchemesDictionaryInsideTheme_PopulatesSchemes - binds Schemes as a Dictionary inside a ThemeDefinition. Verifies the MEC reflection binder handles Scheme directly via its parameterless ctor + init-only Normal property (PASSES - no SchemeDefinition DTO wrapper needed). - Bind_EmptyThemesSection_ProducesEmptyDictionary - degenerate case. All tests use AddJsonStream against in-memory JSON, not Resources/config.json, so they remain valid while the embedded library config keeps its legacy flat shape through Commits A and B. Explicit deferrals (subsequent commits): - MecThemeManager/MecSchemeManager rewire to consume ThemeDefinition via IOptionsMonitor. - SwitchTheme overlay/replace algorithm. - Adding `Themes: Dictionary` to ThemeSettings. - TuiConfigurationBuilder.ApplyToStaticFacades themes binding. - Removal of legacy `ThemeManager.Theme = ...` and `.GetThemeNames()` delegation. - Resources/config.json rewrite to nested. - Peek-and-warn for legacy shapes. - ScopeJsonConverter deletion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Configuration/Settings/ThemeDefinition.cs | 100 ++++++++++ .../Configuration/SourceGenerationContext.cs | 2 + .../ThemeDefinitionBindingTests.cs | 174 ++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 Terminal.Gui/Configuration/Settings/ThemeDefinition.cs create mode 100644 Tests/UnitTestsParallelizable/Configuration/ThemeDefinitionBindingTests.cs diff --git a/Terminal.Gui/Configuration/Settings/ThemeDefinition.cs b/Terminal.Gui/Configuration/Settings/ThemeDefinition.cs new file mode 100644 index 0000000000..8530b11b90 --- /dev/null +++ b/Terminal.Gui/Configuration/Settings/ThemeDefinition.cs @@ -0,0 +1,100 @@ +using Terminal.Gui.Drawing; + +namespace Terminal.Gui.Configuration; + +/// +/// POCO that represents a single named theme in the MEC-bound configuration tree. +/// +/// +/// +/// A contains an optional dictionary of s plus an optional +/// per-component override for each of the 18 ThemeScope-flavored settings POCOs bound by +/// . +/// +/// +/// Null = no theme-level override. Each override property is nullable. When a property is , +/// the theme does not contribute a value for that component and the root-level *Settings section continues to +/// supply the effective default. When a property is non-, the theme contributes a +/// fully-populated replacement POCO; how the consumer combines it with the root defaults +/// (wholesale-replace vs. property-level merge) is a manager-rewire concern and is not encoded here. +/// +/// +/// Why nullable subsections (not "missing dictionary entry" or "explicit empty object"): using nullability on +/// strongly-typed properties keeps the binder honest — MEC populates a property iff the JSON section is present, and +/// a consumer can ask theme.Button is null without reflecting over a generic bag. A "missing entry in +/// dictionary" alternative would force a stringly-typed lookup; an "explicit empty object" alternative would make +/// "I appear in JSON but override nothing" indistinguishable from "I appear in JSON to override defaults to their +/// own values" — both ambiguities are avoided by nullability. +/// +/// +/// This type is the bind target for the Themes section of config.json after the Phase D rewrite. No +/// production code consumes it yet; the consumer (a rewired MecThemeManager reading via +/// IOptionsMonitor<ThemeSettings>) lands in a subsequent commit. This type ships with binding tests +/// only; reviewers can object to specific subsections without reading manager code that does not yet exist. +/// +/// +public class ThemeDefinition +{ + /// + /// Gets or sets the dictionary of named s contributed by this theme. + /// means the theme contributes no schemes. + /// + public Dictionary? Schemes { get; set; } + + /// Per-theme override for . = no override. + public ButtonSettings? Button { get; set; } + + /// Per-theme override for . = no override. + public CheckBoxSettings? CheckBox { get; set; } + + /// Per-theme override for . = no override. + public CharMapSettings? CharMap { get; set; } + + /// Per-theme override for . = no override. + public DialogSettings? Dialog { get; set; } + + /// Per-theme override for . = no override. + public FrameViewSettings? FrameView { get; set; } + + /// Per-theme override for . = no override. + public HexViewSettings? HexView { get; set; } + + /// Per-theme override for . = no override. + public LinearRangeSettings? LinearRange { get; set; } + + /// Per-theme override for . = no override. + public MenuBarSettings? MenuBar { get; set; } + + /// Per-theme override for . = no override. + public MenuSettings? Menu { get; set; } + + /// Per-theme override for . = no override. + public MessageBoxSettings? MessageBox { get; set; } + + /// Per-theme override for . = no override. + public NerdFontsSettings? NerdFonts { get; set; } + + /// Per-theme override for . = no override. + public PopoverMenuSettings? PopoverMenu { get; set; } + + /// Per-theme override for . = no override. + public SelectorBaseSettings? SelectorBase { get; set; } + + /// Per-theme override for . = no override. + public StatusBarSettings? StatusBar { get; set; } + + /// Per-theme override for . = no override. + public TextFieldSettings? TextField { get; set; } + + /// Per-theme override for . = no override. + public TextViewSettings? TextView { get; set; } + + /// Per-theme override for . = no override. + public WindowSettings? Window { get; set; } + + /// + /// Per-theme override for . = no override. + /// Section name in JSON is Glyphs (matching ). + /// + public GlyphSettings? Glyphs { get; set; } +} diff --git a/Terminal.Gui/Configuration/SourceGenerationContext.cs b/Terminal.Gui/Configuration/SourceGenerationContext.cs index 800dc4dad7..91464a85b4 100644 --- a/Terminal.Gui/Configuration/SourceGenerationContext.cs +++ b/Terminal.Gui/Configuration/SourceGenerationContext.cs @@ -48,6 +48,8 @@ namespace Terminal.Gui.Configuration; [JsonSerializable (typeof (ConcurrentDictionary))] [JsonSerializable (typeof (Scheme))] [JsonSerializable (typeof (Dictionary))] +[JsonSerializable (typeof (ThemeDefinition))] +[JsonSerializable (typeof (Dictionary))] [JsonSerializable (typeof (TraceCategory))] [JsonSerializable (typeof (SizeDetectionMode))] diff --git a/Tests/UnitTestsParallelizable/Configuration/ThemeDefinitionBindingTests.cs b/Tests/UnitTestsParallelizable/Configuration/ThemeDefinitionBindingTests.cs new file mode 100644 index 0000000000..4a8fe8a5bb --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/ThemeDefinitionBindingTests.cs @@ -0,0 +1,174 @@ +// Copilot - Claude Opus 4.7 + +using Microsoft.Extensions.Configuration; +using Terminal.Gui.Configuration; +using Terminal.Gui.Drawing; + +namespace ConfigurationTests; + +/// +/// Binding tests for against in-memory MEC providers. +/// +/// +/// +/// These tests validate the bind-target shape only. No production code consumes +/// yet — the consumer (rewired MecThemeManager) lands in a subsequent +/// commit. Tests use AddJsonStream against in-memory JSON, not Resources/config.json, so the +/// tests remain valid while the embedded library config keeps its legacy flat shape. +/// +/// +public class ThemeDefinitionBindingTests +{ + /// + /// A nested JSON sample with two themes — one fully-populated, one partial-override — binds to + /// Dictionary<string, ThemeDefinition>. The partial theme has s in every + /// subsection it did not mention. + /// + [Fact] + public void Bind_FullAndPartialThemes_PartialHasNullsInOmittedSubsections () + { + string json = """ + { + "Themes": { + "Full": { + "Button": { "DefaultShadow": "None" }, + "CheckBox": { }, + "Dialog": { "DefaultBorderStyle": "Single" }, + "FrameView": { }, + "HexView": { }, + "LinearRange":{ }, + "MenuBar": { }, + "Menu": { }, + "MessageBox": { "DefaultBorderStyle": "Double" }, + "NerdFonts": { }, + "PopoverMenu":{ }, + "SelectorBase":{ }, + "StatusBar": { }, + "TextField": { }, + "TextView": { }, + "Window": { "DefaultBorderStyle": "Heavy" }, + "Glyphs": { }, + "CharMap": { } + }, + "Partial": { + "Button": { "DefaultShadow": "Transparent" } + } + } + } + """; + + IConfigurationBuilder builder = new ConfigurationBuilder () + .AddJsonStream (JsonStream (json)); + IConfiguration config = builder.Build (); + + Dictionary themes = new (); + config.GetSection ("Themes").Bind (themes); + + Assert.Equal (2, themes.Count); + Assert.True (themes.ContainsKey ("Full")); + Assert.True (themes.ContainsKey ("Partial")); + + ThemeDefinition full = themes ["Full"]; + Assert.NotNull (full.Button); + Assert.Equal (ShadowStyles.None, full.Button!.DefaultShadow); + Assert.NotNull (full.Dialog); + Assert.Equal (LineStyle.Single, full.Dialog!.DefaultBorderStyle); + Assert.NotNull (full.MessageBox); + Assert.Equal (LineStyle.Double, full.MessageBox!.DefaultBorderStyle); + Assert.NotNull (full.Window); + Assert.Equal (LineStyle.Heavy, full.Window!.DefaultBorderStyle); + + ThemeDefinition partial = themes ["Partial"]; + Assert.NotNull (partial.Button); + Assert.Equal (ShadowStyles.Transparent, partial.Button!.DefaultShadow); + + // Every subsection the partial theme did not mention must be null. + Assert.Null (partial.CheckBox); + Assert.Null (partial.CharMap); + Assert.Null (partial.Dialog); + Assert.Null (partial.FrameView); + Assert.Null (partial.HexView); + Assert.Null (partial.LinearRange); + Assert.Null (partial.MenuBar); + Assert.Null (partial.Menu); + Assert.Null (partial.MessageBox); + Assert.Null (partial.NerdFonts); + Assert.Null (partial.PopoverMenu); + Assert.Null (partial.SelectorBase); + Assert.Null (partial.StatusBar); + Assert.Null (partial.TextField); + Assert.Null (partial.TextView); + Assert.Null (partial.Window); + Assert.Null (partial.Glyphs); + Assert.Null (partial.Schemes); + } + + /// + /// A nested JSON sample with Schemes as a dictionary inside a binds. This + /// test surfaces whether MEC's reflection-based binder can populate the immutable via its + /// parameterless constructor and init-only property. If this test fails it + /// signals that the manager-rewire commit will need a SchemeDefinition DTO wrapper to mediate binding. + /// + [Fact] + public void Bind_SchemesDictionaryInsideTheme_PopulatesSchemes () + { + string json = """ + { + "Themes": { + "Test": { + "Schemes": { + "Base": { }, + "Toplevel":{ } + } + } + } + } + """; + + IConfigurationBuilder builder = new ConfigurationBuilder () + .AddJsonStream (JsonStream (json)); + IConfiguration config = builder.Build (); + + Dictionary themes = new (); + config.GetSection ("Themes").Bind (themes); + + Assert.True (themes.ContainsKey ("Test")); + ThemeDefinition test = themes ["Test"]; + + Assert.NotNull (test.Schemes); + Assert.Equal (2, test.Schemes!.Count); + Assert.True (test.Schemes.ContainsKey ("Base")); + Assert.True (test.Schemes.ContainsKey ("Toplevel")); + Assert.NotNull (test.Schemes ["Base"]); + Assert.NotNull (test.Schemes ["Toplevel"]); + } + + /// + /// An empty Themes section binds to an empty dictionary without throwing. + /// + [Fact] + public void Bind_EmptyThemesSection_ProducesEmptyDictionary () + { + string json = """{ "Themes": { } }"""; + + IConfigurationBuilder builder = new ConfigurationBuilder () + .AddJsonStream (JsonStream (json)); + IConfiguration config = builder.Build (); + + Dictionary themes = new (); + config.GetSection ("Themes").Bind (themes); + + Assert.Empty (themes); + } + + private static Stream JsonStream (string json) + { + MemoryStream stream = new (); + StreamWriter writer = new (stream); + writer.Write (json); + writer.Flush (); + stream.Position = 0; + + return stream; + } +} From 84dbd6a5df602cb8770b15dbcd39e0133b8276de Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 27 May 2026 17:07:26 -0600 Subject: [PATCH 09/13] Spike: document MEC binder accessibility for two-pass overlay (A2 prep) Before mass-converting 18 *Settings POCOs to records with { get; internal set; } per the A2 design contract, validated the binder's behavior on the proposed accessibility shapes via a focused spike. Three Facts in MecBinderAccessibilitySpike, all passing, documenting observed behavior against this MEC version: 1. TwoPassBind_InternalSet_SilentlyIgnoredByBinder `{ get; internal set; }` is NOT written by Bind(existingInstance) under default BinderOptions. Both passes complete with no exception and the POCO stays at constructor defaults. Default BindingFlags = Public|Instance exclude internal accessors. 2. TwoPassBind_InitOnly_OverlaysCorrectly `{ get; init; }` IS written by Bind(existingInstance). Root-pass populates property A; subsequent overlay pass writes property B without disturbing A. Two-pass merge semantics are preserved end-to-end. 3. TwoPassBind_InternalSet_WorksWithBindNonPublicProperties Opting into o.BindNonPublicProperties = true at each Bind() call site rescues internal set. Trade-off: extra trim hint, non-default code path, two-line invocation at every bind site. Implication for the A2 manager-rewire commit: do NOT use internal set as the sender's design contract proposed. Two viable replacements: (A) Use { get; init; } on all 18 *Settings records. Works with MEC's default binder. Preserves immutability to public consumers. No accessibility escape hatch. Recommended. (B) Keep { get; internal set; } and opt into BindNonPublicProperties at every MecThemeManager bind call. Works, but adds a code-path divergence from the idiomatic MEC pattern and requires manual maintenance to stay enabled. Recommendation: (A). The sender's stated objection to init ("doesn't work via all reflection binder paths") is falsified by Fact 2 in this MEC version. If the sender has a specific reflection path in mind where init breaks (e.g. AOT source-gen binder for top-level Configure(), as distinct from Bind(instance)), that should be named so we can validate with another spike. No production code changed in this commit. Spike file documents the wall so the sender can re-litigate the design choice before the 430+ call-site rollout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MecBinderAccessibilitySpike.cs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 Tests/UnitTestsParallelizable/Configuration/MecBinderAccessibilitySpike.cs diff --git a/Tests/UnitTestsParallelizable/Configuration/MecBinderAccessibilitySpike.cs b/Tests/UnitTestsParallelizable/Configuration/MecBinderAccessibilitySpike.cs new file mode 100644 index 0000000000..4942cf9d37 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/MecBinderAccessibilitySpike.cs @@ -0,0 +1,121 @@ +// Copilot - Claude Opus 4.7 - SPIKE (delete before mass rollout if pattern fails) + +using Microsoft.Extensions.Configuration; + +namespace ConfigurationTests; + +/// +/// Spike: validate that the MEC binder writes to { get; internal set; } properties on records used as +/// nested bind targets via the two-pass Bind(existingInstance) overlay pattern. +/// +/// +/// The sender's A2 design contract for #5416 requires immutable *Settings records with internal set, +/// atomic swap via Volatile.Write, and a two-pass MEC overlay. If the MEC binder honors only public setters +/// by default (which my read of Microsoft.Extensions.Configuration.Binder suggests), the pattern collapses +/// and the design needs revisiting. +/// +public class MecBinderAccessibilitySpike +{ + public sealed record InternalSetPoco + { + public string Name { get; internal set; } = "default-name"; + public int Count { get; internal set; } = 0; + } + + public sealed record InitOnlyPoco + { + public string Name { get; init; } = "default-name"; + public int Count { get; init; } = 0; + } + + private static IConfiguration ConfigFromJson (string json) + { + MemoryStream stream = new (); + StreamWriter writer = new (stream); + writer.Write (json); + writer.Flush (); + stream.Position = 0; + + return new ConfigurationBuilder ().AddJsonStream (stream).Build (); + } + + /// + /// Documents that { get; internal set; } properties are silently ignored by MEC's binder via + /// the default Bind(existingInstance) path. Both passes complete with no exception, but neither pass + /// mutates the POCO — both properties stay at their constructor defaults. + /// + /// + /// This contradicts the A2 design contract proposed by the source session for #5416, which prescribed + /// { get; internal set; }. The opposite holds: internal set fails; init works. + /// + [Fact] + public void TwoPassBind_InternalSet_SilentlyIgnoredByBinder () + { + IConfiguration cfg = ConfigFromJson (""" + { + "Root": { "Name": "from-root", "Count": 10 }, + "Themes": { "Dark": { "Poco": { "Count": 99 } } } + } + """); + + InternalSetPoco next = new (); + cfg.GetSection ("Root").Bind (next); + cfg.GetSection ("Themes:Dark:Poco").Bind (next); + + // Observed: ctor defaults preserved on both properties; binder never wrote. + // (Default BindingFlags = Public | Instance excludes internal accessors.) + Assert.Equal ("default-name", next.Name); + Assert.Equal (0, next.Count); + } + + /// + /// Documents that { get; init; } properties are written by MEC's Bind(existingInstance) + /// via the two-pass overlay. The root pass populates one property, a subsequent overlay pass writes a second + /// property without disturbing the first. + /// + /// + /// This is the working alternative to the failed internal set pattern. It is what the A2 manager + /// rewire should use for 's 18 nullable subsection POCOs. + /// + [Fact] + public void TwoPassBind_InitOnly_OverlaysCorrectly () + { + IConfiguration cfg = ConfigFromJson (""" + { + "Root": { "Name": "from-root", "Count": 10 }, + "Themes": { "Dark": { "Poco": { "Count": 99 } } } + } + """); + + InitOnlyPoco next = new (); + cfg.GetSection ("Root").Bind (next); + cfg.GetSection ("Themes:Dark:Poco").Bind (next); + + Assert.Equal ("from-root", next.Name); + Assert.Equal (99, next.Count); + } + + /// + /// Documents that opting into BinderOptions.BindNonPublicProperties = true rescues the + /// internal set pattern — the binder writes both properties. This is the escape hatch if the design + /// truly requires assembly-private mutability; the trade-off is an extra trim hint and a non-default code path + /// at every bind site. + /// + [Fact] + public void TwoPassBind_InternalSet_WorksWithBindNonPublicProperties () + { + IConfiguration cfg = ConfigFromJson (""" + { + "Root": { "Name": "from-root", "Count": 10 }, + "Themes": { "Dark": { "Poco": { "Count": 99 } } } + } + """); + + InternalSetPoco next = new (); + cfg.GetSection ("Root").Bind (next, o => o.BindNonPublicProperties = true); + cfg.GetSection ("Themes:Dark:Poco").Bind (next, o => o.BindNonPublicProperties = true); + + Assert.Equal ("from-root", next.Name); + Assert.Equal (99, next.Count); + } +} From 2f7c13a6eb42fdd409fb1bfc7f2edd3482359170 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 27 May 2026 17:34:27 -0600 Subject: [PATCH 10/13] A2.1: convert 17 ThemeScope POCOs to immutable records with atomic Current swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lands the heart of A2: replaces the mutable `*Settings.Defaults` pattern with immutable `sealed record` POCOs and a `Volatile`-swapped `Current` property, binding each ThemeScope POCO through MEC's two-pass overlay (root section + `Themes::
`). Pattern (per POCO): public sealed record FooSettings { public T Prop { get; init; } = ...; public static FooSettings Default { get; } = new (); public static FooSettings Current { get => Volatile.Read (ref _current); internal set => Volatile.Write (ref _current, value); } private static FooSettings _current = Default; } Why `init` (not `internal set`): the spike at Tests/UnitTestsParallelizable/Configuration/MecBinderAccessibilitySpike.cs (commit 84dbd6a5d) proves that `Bind(existingInstance)` silently ignores `internal set` under default BindingFlags=Public|Instance, while `init` accessors are written normally. `init` keeps the binder happy without an opt-in `BindNonPublicProperties=true` at every call site. Why a record (not a class) + `with`-swap setter on the view facade: records give us a free `with` expression for atomic replacement. The static View facades (e.g. `Button.DefaultShadow`) now read `Current.X` on get and do `Current = Current with { X = value }` on set. That keeps the legacy CM `ConfigProperty.Apply` reflection-write path functional during the transition (it calls `PropertyInfo.SetValue (null, value)` against the static facade, which still works), while MEC writes `Current` wholesale. Two-pass overlay (`TuiConfigurationBuilder.BindThemeScope`): T next = new (); config.GetSection (sectionName).Bind (next); // root config.GetSection ($"Themes:{activeTheme}:{sectionName}").Bind (next); // overlay apply (next); // atomic Volatile.Write to Current MEC's `Bind(existing)` only writes properties present in JSON, so unmentioned overlay properties survive — property-level merge for free, matching legacy CM `Scope.Apply` semantics. No `Merge` helper, no DeepCloner equivalent. Converted (17 POCOs): ButtonSettings, CheckBoxSettings, CharMapSettings, DialogSettings, FrameViewSettings, HexViewSettings, LinearRangeSettings, MenuBarSettings, MenuSettings, MessageBoxSettings, NerdFontsSettings, PopoverMenuSettings, SelectorBaseSettings, StatusBarSettings, TextFieldSettings, TextViewSettings, WindowSettings. View facades updated to `with`-swap (16 files, ~28 setter pairs): Button.cs, CheckBox.cs, CharMap.cs, Dialog.cs, FrameView.cs, HexView.cs, LinearRangeDefaults.cs, Menu.cs, MenuBar.cs, PopoverMenu.cs, MessageBox.cs, SelectorBase.cs, StatusBar.cs, TextField.cs, TextView.cs, Window.cs, Text/NerdFonts.cs. Deferred to A2.2: `Glyphs` facade redesign. `GlyphSettings` keeps its mutable `Defaults` pattern in this commit; `TuiConfigurationBuilder` still uses `BindSection` for it. 288 call sites under `Glyphs.X` get the dedicated commit there. Deferred to A2.3: `NerdFonts` static facade redesign (the POCO is converted here; the facade still has a setter that does `with`-swap). Deferred to A2.4: removal of uncalled public static setters on View types. Out of scope (kept mutable, SettingsScope not ThemeScope): ApplicationSettings, DriverSettings, FileDialogSettings, FileDialogStyleSettings, KeySettings, ThemeSettings, TraceSettings. Tests: - New `ThemeOverlayMergeTests` (3 Facts) validates two-pass merge: overlay-overrides-root, no-overlay-uses-root, atomic-swap-does-not-mutate. - Updated `MecSettingsTests.StaticFacade_CanBeOverridden` to use `Current`. - Full suites green: 17,275 / 0 / 17 parallelizable; 72 / 0 / 2 non-parallelizable. Cross-assembly `init`: MEC's source-gen binder emits direct assignments for `Bind(existing)`, which would fail cross-assembly against `init`-only properties. Since `MecThemeManager` and `TuiConfigurationBuilder` live in `Terminal.Gui.dll` alongside the POCOs, intra-assembly calls compile fine. End-users hypothetically calling `cfg.Bind(ButtonSettings.Current)` from their app would hit a compile error — but no real consumer does this; they read `Current.X`. Reflection binder fallback works for that case anyway. Refs #4943 (CM-to-MEC replacement spec, A2). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Configuration/Settings/ButtonSettings.cs | 36 +++-- .../Configuration/Settings/CharMapSettings.cs | 24 ++-- .../Settings/CheckBoxSettings.cs | 24 ++-- .../Configuration/Settings/DialogSettings.cs | 36 +++-- .../Settings/FrameViewSettings.cs | 24 ++-- .../Configuration/Settings/HexViewSettings.cs | 24 ++-- .../Settings/LinearRangeSettings.cs | 24 ++-- .../Configuration/Settings/MenuBarSettings.cs | 28 ++-- .../Configuration/Settings/MenuSettings.cs | 24 ++-- .../Settings/MessageBoxSettings.cs | 28 ++-- .../Settings/NerdFontsSettings.cs | 24 ++-- .../Settings/PopoverMenuSettings.cs | 24 ++-- .../Settings/SelectorBaseSettings.cs | 24 ++-- .../Settings/StatusBarSettings.cs | 24 ++-- .../Settings/TextFieldSettings.cs | 24 ++-- .../Settings/TextViewSettings.cs | 24 ++-- .../Settings/TuiConfigurationBuilder.cs | 52 ++++--- .../Configuration/Settings/WindowSettings.cs | 28 ++-- Terminal.Gui/Text/NerdFonts.cs | 4 +- Terminal.Gui/Views/Button.cs | 14 +- Terminal.Gui/Views/CharMap/CharMap.cs | 4 +- Terminal.Gui/Views/CheckBox.cs | 4 +- Terminal.Gui/Views/Dialog.cs | 16 +-- Terminal.Gui/Views/FrameView.cs | 4 +- Terminal.Gui/Views/HexView.cs | 4 +- .../Views/LinearRange/LinearRangeDefaults.cs | 4 +- Terminal.Gui/Views/Menu/Menu.cs | 4 +- Terminal.Gui/Views/Menu/MenuBar.cs | 8 +- Terminal.Gui/Views/Menu/PopoverMenu.cs | 4 +- Terminal.Gui/Views/MessageBox.cs | 8 +- Terminal.Gui/Views/Selectors/SelectorBase.cs | 4 +- Terminal.Gui/Views/StatusBar.cs | 4 +- .../Views/TextInput/TextField/TextField.cs | 4 +- .../Views/TextInput/TextView/TextView.cs | 4 +- Terminal.Gui/Views/Window.cs | 8 +- .../Configuration/MecSettingsTests.cs | 8 +- .../Configuration/ThemeOverlayMergeTests.cs | 132 ++++++++++++++++++ 37 files changed, 498 insertions(+), 240 deletions(-) create mode 100644 Tests/UnitTestsParallelizable/Configuration/ThemeOverlayMergeTests.cs diff --git a/Terminal.Gui/Configuration/Settings/ButtonSettings.cs b/Terminal.Gui/Configuration/Settings/ButtonSettings.cs index 2ec80020f8..6e67c7eb1a 100644 --- a/Terminal.Gui/Configuration/Settings/ButtonSettings.cs +++ b/Terminal.Gui/Configuration/Settings/ButtonSettings.cs @@ -1,19 +1,33 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class ButtonSettings +/// +/// +/// is the compile-time-known fallback (constructor defaults). +/// holds the currently effective values and is updated atomically by +/// via Volatile.Write at startup and on theme switch. Mid-render +/// consumers always observe either the previous or the next reference — never a partially populated one. +/// +/// +public sealed record ButtonSettings { - /// Gets or sets the default shadow style for buttons. - public ShadowStyles DefaultShadow { get; set; } = ShadowStyles.Opaque; + /// Gets the default shadow style for buttons. + public ShadowStyles DefaultShadow { get; init; } = ShadowStyles.Opaque; - /// Gets or sets the default mouse highlight states for buttons. - public MouseState DefaultMouseHighlightStates { get; set; } = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; + /// Gets the default mouse highlight states for buttons. + public MouseState DefaultMouseHighlightStates { get; init; } = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static ButtonSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static ButtonSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static ButtonSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static ButtonSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/CharMapSettings.cs b/Terminal.Gui/Configuration/Settings/CharMapSettings.cs index e476205c9e..437b626eae 100644 --- a/Terminal.Gui/Configuration/Settings/CharMapSettings.cs +++ b/Terminal.Gui/Configuration/Settings/CharMapSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class CharMapSettings +public sealed record CharMapSettings { - /// Gets or sets the default cursor style for character map views. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBlock; + /// Gets the default cursor style for character map views. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBlock; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static CharMapSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static CharMapSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static CharMapSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static CharMapSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/CheckBoxSettings.cs b/Terminal.Gui/Configuration/Settings/CheckBoxSettings.cs index 408d20ed43..e75b562b94 100644 --- a/Terminal.Gui/Configuration/Settings/CheckBoxSettings.cs +++ b/Terminal.Gui/Configuration/Settings/CheckBoxSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class CheckBoxSettings +public sealed record CheckBoxSettings { - /// Gets or sets the default mouse highlight states for checkboxes. - public MouseState DefaultMouseHighlightStates { get; set; } = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; + /// Gets the default mouse highlight states for checkboxes. + public MouseState DefaultMouseHighlightStates { get; init; } = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static CheckBoxSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static CheckBoxSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static CheckBoxSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static CheckBoxSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/DialogSettings.cs b/Terminal.Gui/Configuration/Settings/DialogSettings.cs index 60c01d6aef..82f7d34ac8 100644 --- a/Terminal.Gui/Configuration/Settings/DialogSettings.cs +++ b/Terminal.Gui/Configuration/Settings/DialogSettings.cs @@ -1,25 +1,31 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class DialogSettings +public sealed record DialogSettings { - /// Gets or sets the default shadow style for dialogs. - public ShadowStyles DefaultShadow { get; set; } = ShadowStyles.Transparent; + /// Gets the default shadow style for dialogs. + public ShadowStyles DefaultShadow { get; init; } = ShadowStyles.Transparent; - /// Gets or sets the default border style for dialogs. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; + /// Gets the default border style for dialogs. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.Heavy; - /// Gets or sets the default button alignment for dialogs. - public Alignment DefaultButtonAlignment { get; set; } = Alignment.End; + /// Gets the default button alignment for dialogs. + public Alignment DefaultButtonAlignment { get; init; } = Alignment.End; - /// Gets or sets the default button alignment modes for dialogs. - public AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; + /// Gets the default button alignment modes for dialogs. + public AlignmentModes DefaultButtonAlignmentModes { get; init; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static DialogSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static DialogSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static DialogSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static DialogSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/FrameViewSettings.cs b/Terminal.Gui/Configuration/Settings/FrameViewSettings.cs index 89748a8fe2..f65bfac8f2 100644 --- a/Terminal.Gui/Configuration/Settings/FrameViewSettings.cs +++ b/Terminal.Gui/Configuration/Settings/FrameViewSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class FrameViewSettings +public sealed record FrameViewSettings { - /// Gets or sets the default border style for frame views. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.Rounded; + /// Gets the default border style for frame views. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.Rounded; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static FrameViewSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static FrameViewSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static FrameViewSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static FrameViewSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/HexViewSettings.cs b/Terminal.Gui/Configuration/Settings/HexViewSettings.cs index 10c7721a3e..de11f4a0bf 100644 --- a/Terminal.Gui/Configuration/Settings/HexViewSettings.cs +++ b/Terminal.Gui/Configuration/Settings/HexViewSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class HexViewSettings +public sealed record HexViewSettings { - /// Gets or sets the default cursor style for hex views. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBlock; + /// Gets the default cursor style for hex views. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBlock; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static HexViewSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static HexViewSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static HexViewSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static HexViewSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/LinearRangeSettings.cs b/Terminal.Gui/Configuration/Settings/LinearRangeSettings.cs index 5c155dc3f9..a0233914d7 100644 --- a/Terminal.Gui/Configuration/Settings/LinearRangeSettings.cs +++ b/Terminal.Gui/Configuration/Settings/LinearRangeSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class LinearRangeSettings +public sealed record LinearRangeSettings { - /// Gets or sets the default cursor style for linear range views. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBlock; + /// Gets the default cursor style for linear range views. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBlock; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static LinearRangeSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static LinearRangeSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static LinearRangeSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static LinearRangeSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/MenuBarSettings.cs b/Terminal.Gui/Configuration/Settings/MenuBarSettings.cs index 342497936a..143778d81c 100644 --- a/Terminal.Gui/Configuration/Settings/MenuBarSettings.cs +++ b/Terminal.Gui/Configuration/Settings/MenuBarSettings.cs @@ -1,19 +1,25 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults. +/// Immutable settings record for defaults. /// -public class MenuBarSettings +public sealed record MenuBarSettings { - /// Gets or sets the default border style for menu bars. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.None; + /// Gets the default border style for menu bars. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.None; - /// Gets or sets the default activation key for menu bars. - public Key DefaultKey { get; set; } = Key.F10; + /// Gets the default activation key for menu bars. + public Key DefaultKey { get; init; } = Key.F10; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static MenuBarSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static MenuBarSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static MenuBarSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static MenuBarSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/MenuSettings.cs b/Terminal.Gui/Configuration/Settings/MenuSettings.cs index 65fdc8f7ec..334961e3c5 100644 --- a/Terminal.Gui/Configuration/Settings/MenuSettings.cs +++ b/Terminal.Gui/Configuration/Settings/MenuSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class MenuSettings +public sealed record MenuSettings { - /// Gets or sets the default border style for menus. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.None; + /// Gets the default border style for menus. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.None; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static MenuSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static MenuSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static MenuSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static MenuSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/MessageBoxSettings.cs b/Terminal.Gui/Configuration/Settings/MessageBoxSettings.cs index 4e5ff7af13..2f7f0f67c4 100644 --- a/Terminal.Gui/Configuration/Settings/MessageBoxSettings.cs +++ b/Terminal.Gui/Configuration/Settings/MessageBoxSettings.cs @@ -1,19 +1,25 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class MessageBoxSettings +public sealed record MessageBoxSettings { - /// Gets or sets the default border style for message boxes. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; + /// Gets the default border style for message boxes. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.Heavy; - /// Gets or sets the default button alignment for message boxes. - public Alignment DefaultButtonAlignment { get; set; } = Alignment.Center; + /// Gets the default button alignment for message boxes. + public Alignment DefaultButtonAlignment { get; init; } = Alignment.Center; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static MessageBoxSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static MessageBoxSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static MessageBoxSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static MessageBoxSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/NerdFontsSettings.cs b/Terminal.Gui/Configuration/Settings/NerdFontsSettings.cs index b5a4a287c3..776f1759e7 100644 --- a/Terminal.Gui/Configuration/Settings/NerdFontsSettings.cs +++ b/Terminal.Gui/Configuration/Settings/NerdFontsSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class NerdFontsSettings +public sealed record NerdFontsSettings { - /// Gets or sets whether Nerd Fonts glyphs are enabled. - public bool Enable { get; set; } = false; + /// Gets whether Nerd Fonts glyphs are enabled. + public bool Enable { get; init; } = false; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static NerdFontsSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static NerdFontsSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static NerdFontsSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static NerdFontsSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/PopoverMenuSettings.cs b/Terminal.Gui/Configuration/Settings/PopoverMenuSettings.cs index 6cb41b741d..69ff95fba0 100644 --- a/Terminal.Gui/Configuration/Settings/PopoverMenuSettings.cs +++ b/Terminal.Gui/Configuration/Settings/PopoverMenuSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (SettingsScope). +/// Immutable settings record for defaults (SettingsScope). /// -public class PopoverMenuSettings +public sealed record PopoverMenuSettings { - /// Gets or sets the default activation key for popover menus. - public Key DefaultKey { get; set; } = Key.F10.WithShift; + /// Gets the default activation key for popover menus. + public Key DefaultKey { get; init; } = Key.F10.WithShift; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static PopoverMenuSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static PopoverMenuSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static PopoverMenuSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static PopoverMenuSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/SelectorBaseSettings.cs b/Terminal.Gui/Configuration/Settings/SelectorBaseSettings.cs index 9c00011337..18fade844e 100644 --- a/Terminal.Gui/Configuration/Settings/SelectorBaseSettings.cs +++ b/Terminal.Gui/Configuration/Settings/SelectorBaseSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class SelectorBaseSettings +public sealed record SelectorBaseSettings { - /// Gets or sets the default mouse highlight states for selectors. - public MouseState DefaultMouseHighlightStates { get; set; } = MouseState.In; + /// Gets the default mouse highlight states for selectors. + public MouseState DefaultMouseHighlightStates { get; init; } = MouseState.In; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static SelectorBaseSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static SelectorBaseSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static SelectorBaseSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static SelectorBaseSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/StatusBarSettings.cs b/Terminal.Gui/Configuration/Settings/StatusBarSettings.cs index 51932645ce..609f55b3bd 100644 --- a/Terminal.Gui/Configuration/Settings/StatusBarSettings.cs +++ b/Terminal.Gui/Configuration/Settings/StatusBarSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class StatusBarSettings +public sealed record StatusBarSettings { - /// Gets or sets the default separator line style for status bars. - public LineStyle DefaultSeparatorLineStyle { get; set; } = LineStyle.Single; + /// Gets the default separator line style for status bars. + public LineStyle DefaultSeparatorLineStyle { get; init; } = LineStyle.Single; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static StatusBarSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static StatusBarSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static StatusBarSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static StatusBarSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/TextFieldSettings.cs b/Terminal.Gui/Configuration/Settings/TextFieldSettings.cs index c0caf63bf7..b16955d7a7 100644 --- a/Terminal.Gui/Configuration/Settings/TextFieldSettings.cs +++ b/Terminal.Gui/Configuration/Settings/TextFieldSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class TextFieldSettings +public sealed record TextFieldSettings { - /// Gets or sets the default cursor style for text fields. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBar; + /// Gets the default cursor style for text fields. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBar; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static TextFieldSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static TextFieldSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static TextFieldSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static TextFieldSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/TextViewSettings.cs b/Terminal.Gui/Configuration/Settings/TextViewSettings.cs index 5505dedb2b..5b19d26845 100644 --- a/Terminal.Gui/Configuration/Settings/TextViewSettings.cs +++ b/Terminal.Gui/Configuration/Settings/TextViewSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class TextViewSettings +public sealed record TextViewSettings { - /// Gets or sets the default cursor style for text views. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBar; + /// Gets the default cursor style for text views. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBar; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static TextViewSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static TextViewSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static TextViewSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static TextViewSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs b/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs index 831b5e3d9d..f5ed994bf3 100644 --- a/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs +++ b/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs @@ -123,24 +123,25 @@ public void ApplyToStaticFacades () BindSection (config, "Key", s => KeySettings.Defaults = s); BindSection (config, "Trace", s => TraceSettings.Defaults = s); - // ThemeScope POCOs - BindSection (config, "Button", s => ButtonSettings.Defaults = s); - BindSection (config, "CheckBox", s => CheckBoxSettings.Defaults = s); - BindSection (config, "CharMap", s => CharMapSettings.Defaults = s); - BindSection (config, "Dialog", s => DialogSettings.Defaults = s); - BindSection (config, "FrameView", s => FrameViewSettings.Defaults = s); - BindSection (config, "HexView", s => HexViewSettings.Defaults = s); - BindSection (config, "LinearRange", s => LinearRangeSettings.Defaults = s); - BindSection (config, "MenuBar", s => MenuBarSettings.Defaults = s); - BindSection (config, "Menu", s => MenuSettings.Defaults = s); - BindSection (config, "MessageBox", s => MessageBoxSettings.Defaults = s); - BindSection (config, "NerdFonts", s => NerdFontsSettings.Defaults = s); - BindSection (config, "PopoverMenu", s => PopoverMenuSettings.Defaults = s); - BindSection (config, "SelectorBase", s => SelectorBaseSettings.Defaults = s); - BindSection (config, "StatusBar", s => StatusBarSettings.Defaults = s); - BindSection (config, "TextField", s => TextFieldSettings.Defaults = s); - BindSection (config, "TextView", s => TextViewSettings.Defaults = s); - BindSection (config, "Window", s => WindowSettings.Defaults = s); + // ThemeScope POCOs: two-pass overlay (root section + Themes::
) writes Current. + string activeTheme = ThemeSettings.Defaults.Theme; + BindThemeScope (config, "Button", activeTheme, s => ButtonSettings.Current = s); + BindThemeScope (config, "CheckBox", activeTheme, s => CheckBoxSettings.Current = s); + BindThemeScope (config, "CharMap", activeTheme, s => CharMapSettings.Current = s); + BindThemeScope (config, "Dialog", activeTheme, s => DialogSettings.Current = s); + BindThemeScope (config, "FrameView", activeTheme, s => FrameViewSettings.Current = s); + BindThemeScope (config, "HexView", activeTheme, s => HexViewSettings.Current = s); + BindThemeScope (config, "LinearRange", activeTheme, s => LinearRangeSettings.Current = s); + BindThemeScope (config, "MenuBar", activeTheme, s => MenuBarSettings.Current = s); + BindThemeScope (config, "Menu", activeTheme, s => MenuSettings.Current = s); + BindThemeScope (config, "MessageBox", activeTheme, s => MessageBoxSettings.Current = s); + BindThemeScope (config, "NerdFonts", activeTheme, s => NerdFontsSettings.Current = s); + BindThemeScope (config, "PopoverMenu", activeTheme, s => PopoverMenuSettings.Current = s); + BindThemeScope (config, "SelectorBase", activeTheme, s => SelectorBaseSettings.Current = s); + BindThemeScope (config, "StatusBar", activeTheme, s => StatusBarSettings.Current = s); + BindThemeScope (config, "TextField", activeTheme, s => TextFieldSettings.Current = s); + BindThemeScope (config, "TextView", activeTheme, s => TextViewSettings.Current = s); + BindThemeScope (config, "Window", activeTheme, s => WindowSettings.Current = s); BindSection (config, "Glyphs", s => GlyphSettings.Defaults = s); } @@ -171,4 +172,19 @@ public void ApplyToStaticFacades () config.GetSection (sectionName).Bind (settings); apply (settings); } + + /// + /// Two-pass overlay bind for ThemeScope POCOs. Binds the root section, then overlays + /// Themes::. Properties not present in the + /// overlay JSON retain the root value (property-level merge — matches legacy CM Scope.Apply semantics). + /// + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Settings POCOs are simple types preserved by DynamicDependency in ConfigPropertyHostTypes.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Settings POCOs are simple types; no generic instantiation needed at runtime.")] + private static void BindThemeScope (IConfiguration config, string sectionName, string activeTheme, Action apply) where T : new () + { + T settings = new (); + config.GetSection (sectionName).Bind (settings); + config.GetSection ($"Themes:{activeTheme}:{sectionName}").Bind (settings); + apply (settings); + } } diff --git a/Terminal.Gui/Configuration/Settings/WindowSettings.cs b/Terminal.Gui/Configuration/Settings/WindowSettings.cs index 13ace188ff..4a424290f7 100644 --- a/Terminal.Gui/Configuration/Settings/WindowSettings.cs +++ b/Terminal.Gui/Configuration/Settings/WindowSettings.cs @@ -1,19 +1,25 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class WindowSettings +public sealed record WindowSettings { - /// Gets or sets the default shadow style for windows. - public ShadowStyles DefaultShadow { get; set; } = ShadowStyles.None; + /// Gets the default shadow style for windows. + public ShadowStyles DefaultShadow { get; init; } = ShadowStyles.None; - /// Gets or sets the default border style for windows. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; + /// Gets the default border style for windows. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.Single; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static WindowSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static WindowSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static WindowSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static WindowSettings _current = Default; } diff --git a/Terminal.Gui/Text/NerdFonts.cs b/Terminal.Gui/Text/NerdFonts.cs index 2253a3ecef..e01d73507d 100644 --- a/Terminal.Gui/Text/NerdFonts.cs +++ b/Terminal.Gui/Text/NerdFonts.cs @@ -18,8 +18,8 @@ internal class NerdFonts [ConfigurationProperty (Scope = typeof (ThemeScope))] public static bool Enable { - get => NerdFontsSettings.Defaults.Enable; - set => NerdFontsSettings.Defaults.Enable = value; + get => NerdFontsSettings.Current.Enable; + set => NerdFontsSettings.Current = NerdFontsSettings.Current with { Enable = value }; } /// Mapping of file extension to name. diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 3e05362cc2..e3aa92bc07 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -56,8 +56,8 @@ public class Button : View, IDesignable, IAcceptTarget [ConfigurationProperty (Scope = typeof (ThemeScope))] public static ShadowStyles DefaultShadow { - get => ButtonSettings.Defaults.DefaultShadow; - set => ButtonSettings.Defaults.DefaultShadow = value; + get => ButtonSettings.Current.DefaultShadow; + set => ButtonSettings.Current = ButtonSettings.Current with { DefaultShadow = value }; } /// @@ -66,8 +66,8 @@ public static ShadowStyles DefaultShadow [ConfigurationProperty (Scope = typeof (ThemeScope))] public static MouseState DefaultMouseHighlightStates { - get => ButtonSettings.Defaults.DefaultMouseHighlightStates; - set => ButtonSettings.Defaults.DefaultMouseHighlightStates = value; + get => ButtonSettings.Current.DefaultMouseHighlightStates; + set => ButtonSettings.Current = ButtonSettings.Current with { DefaultMouseHighlightStates = value }; } /// Initializes a new instance of . @@ -108,7 +108,7 @@ public Button () /// /// Called before the Button's initial is applied during construction. - /// Override to change or suppress the default shadow set + /// Override to change or suppress the default shadow � set /// to the desired style, or set to /// to skip applying any shadow. /// @@ -131,10 +131,10 @@ private void RaiseInitializingShadowStyle () { ValueChangingEventArgs args = new (null, DefaultShadow); - // 1. Virtual method subclasses override to change/suppress the default shadow. + // 1. Virtual method � subclasses override to change/suppress the default shadow. OnInitializingShadowStyle (args); - // 2. Event external subscribers get a chance to customize. + // 2. Event � external subscribers get a chance to customize. InitializingShadowStyle?.Invoke (this, args); // 3. Apply the (potentially modified) shadow style unless already handled. diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 8cb641e2da..d8a3e6611f 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -57,8 +57,8 @@ public class CharMap : View, IDesignable, IValue [ConfigurationProperty (Scope = typeof (ThemeScope))] public static CursorStyle DefaultCursorStyle { - get => CharMapSettings.Defaults.DefaultCursorStyle; - set => CharMapSettings.Defaults.DefaultCursorStyle = value; + get => CharMapSettings.Current.DefaultCursorStyle; + set => CharMapSettings.Current = CharMapSettings.Current with { DefaultCursorStyle = value }; } private const int COLUMN_WIDTH = 3; // Width of each column of glyphs diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 6fb81ec76e..c5e4ceb791 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -27,8 +27,8 @@ public class CheckBox : View, IValue [ConfigurationProperty (Scope = typeof (ThemeScope))] public static MouseState DefaultMouseHighlightStates { - get => CheckBoxSettings.Defaults.DefaultMouseHighlightStates; - set => CheckBoxSettings.Defaults.DefaultMouseHighlightStates = value; + get => CheckBoxSettings.Current.DefaultMouseHighlightStates; + set => CheckBoxSettings.Current = CheckBoxSettings.Current with { DefaultMouseHighlightStates = value }; } /// diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index c67a03ce9b..0a335558f3 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -67,8 +67,8 @@ public class Dialog : Dialog [ConfigurationProperty (Scope = typeof (ThemeScope))] public static LineStyle DefaultBorderStyle { - get => DialogSettings.Defaults.DefaultBorderStyle; - set => DialogSettings.Defaults.DefaultBorderStyle = value; + get => DialogSettings.Current.DefaultBorderStyle; + set => DialogSettings.Current = DialogSettings.Current with { DefaultBorderStyle = value }; } /// @@ -77,8 +77,8 @@ public static LineStyle DefaultBorderStyle [ConfigurationProperty (Scope = typeof (ThemeScope))] public static Alignment DefaultButtonAlignment { - get => DialogSettings.Defaults.DefaultButtonAlignment; - set => DialogSettings.Defaults.DefaultButtonAlignment = value; + get => DialogSettings.Current.DefaultButtonAlignment; + set => DialogSettings.Current = DialogSettings.Current with { DefaultButtonAlignment = value }; } /// @@ -87,8 +87,8 @@ public static Alignment DefaultButtonAlignment [ConfigurationProperty (Scope = typeof (ThemeScope))] public static AlignmentModes DefaultButtonAlignmentModes { - get => DialogSettings.Defaults.DefaultButtonAlignmentModes; - set => DialogSettings.Defaults.DefaultButtonAlignmentModes = value; + get => DialogSettings.Current.DefaultButtonAlignmentModes; + set => DialogSettings.Current = DialogSettings.Current with { DefaultButtonAlignmentModes = value }; } /// @@ -97,8 +97,8 @@ public static AlignmentModes DefaultButtonAlignmentModes [ConfigurationProperty (Scope = typeof (ThemeScope))] public static ShadowStyles DefaultShadow { - get => DialogSettings.Defaults.DefaultShadow; - set => DialogSettings.Defaults.DefaultShadow = value; + get => DialogSettings.Current.DefaultShadow; + set => DialogSettings.Current = DialogSettings.Current with { DefaultShadow = value }; } /// diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index 68ad2940fd..f690a5c7a2 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -43,7 +43,7 @@ public FrameView () [ConfigurationProperty (Scope = typeof (ThemeScope))] public static LineStyle DefaultBorderStyle { - get => FrameViewSettings.Defaults.DefaultBorderStyle; - set => FrameViewSettings.Defaults.DefaultBorderStyle = value; + get => FrameViewSettings.Current.DefaultBorderStyle; + set => FrameViewSettings.Current = FrameViewSettings.Current with { DefaultBorderStyle = value }; } } diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index 5620faadfd..4c3473d2db 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -83,8 +83,8 @@ public class HexView : View, IDesignable [ConfigurationProperty (Scope = typeof (ThemeScope))] public static CursorStyle DefaultCursorStyle { - get => HexViewSettings.Defaults.DefaultCursorStyle; - set => HexViewSettings.Defaults.DefaultCursorStyle = value; + get => HexViewSettings.Current.DefaultCursorStyle; + set => HexViewSettings.Current = HexViewSettings.Current with { DefaultCursorStyle = value }; } /// diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs b/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs index 04f5b7e145..987d2b2b73 100644 --- a/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs @@ -11,7 +11,7 @@ public static class LinearRangeDefaults [ConfigurationProperty (Scope = typeof (ThemeScope))] public static CursorStyle DefaultCursorStyle { - get => LinearRangeSettings.Defaults.DefaultCursorStyle; - set => LinearRangeSettings.Defaults.DefaultCursorStyle = value; + get => LinearRangeSettings.Current.DefaultCursorStyle; + set => LinearRangeSettings.Current = LinearRangeSettings.Current with { DefaultCursorStyle = value }; } } diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index fbeab264de..f1628e55d5 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -55,8 +55,8 @@ public class Menu : Bar, IValue [ConfigurationProperty (Scope = typeof (ThemeScope))] public static LineStyle DefaultBorderStyle { - get => MenuSettings.Defaults.DefaultBorderStyle; - set => MenuSettings.Defaults.DefaultBorderStyle = value; + get => MenuSettings.Current.DefaultBorderStyle; + set => MenuSettings.Current = MenuSettings.Current with { DefaultBorderStyle = value }; } /// diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 278f132fa9..edc299fe80 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -443,16 +443,16 @@ internal set [ConfigurationProperty (Scope = typeof (ThemeScope))] public new static LineStyle DefaultBorderStyle { - get => MenuBarSettings.Defaults.DefaultBorderStyle; - set => MenuBarSettings.Defaults.DefaultBorderStyle = value; + get => MenuBarSettings.Current.DefaultBorderStyle; + set => MenuBarSettings.Current = MenuBarSettings.Current with { DefaultBorderStyle = value }; } /// The default key for activating menu bars. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key DefaultKey { - get => MenuBarSettings.Defaults.DefaultKey; - set => MenuBarSettings.Defaults.DefaultKey = value; + get => MenuBarSettings.Current.DefaultKey; + set => MenuBarSettings.Current = MenuBarSettings.Current with { DefaultKey = value }; } /// diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index 74494c39af..65a3338bd3 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -295,8 +295,8 @@ public Key Key [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key DefaultKey { - get => PopoverMenuSettings.Defaults.DefaultKey; - set => PopoverMenuSettings.Defaults.DefaultKey = value; + get => PopoverMenuSettings.Current.DefaultKey; + set => PopoverMenuSettings.Current = PopoverMenuSettings.Current with { DefaultKey = value }; } /// diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 5090380453..aa5a8623ca 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -60,8 +60,8 @@ public static class MessageBox [ConfigurationProperty (Scope = typeof (ThemeScope))] public static LineStyle DefaultBorderStyle { - get => MessageBoxSettings.Defaults.DefaultBorderStyle; - set => MessageBoxSettings.Defaults.DefaultBorderStyle = value; + get => MessageBoxSettings.Current.DefaultBorderStyle; + set => MessageBoxSettings.Current = MessageBoxSettings.Current with { DefaultBorderStyle = value }; } /// The default for . @@ -69,8 +69,8 @@ public static LineStyle DefaultBorderStyle [ConfigurationProperty (Scope = typeof (ThemeScope))] public static Alignment DefaultButtonAlignment { - get => MessageBoxSettings.Defaults.DefaultButtonAlignment; - set => MessageBoxSettings.Defaults.DefaultButtonAlignment = value; + get => MessageBoxSettings.Current.DefaultButtonAlignment; + set => MessageBoxSettings.Current = MessageBoxSettings.Current with { DefaultButtonAlignment = value }; } /// diff --git a/Terminal.Gui/Views/Selectors/SelectorBase.cs b/Terminal.Gui/Views/Selectors/SelectorBase.cs index 40d25627ab..92f26b87ff 100644 --- a/Terminal.Gui/Views/Selectors/SelectorBase.cs +++ b/Terminal.Gui/Views/Selectors/SelectorBase.cs @@ -28,8 +28,8 @@ public abstract class SelectorBase : View, IOrientation, IValue [ConfigurationProperty (Scope = typeof (ThemeScope))] public static MouseState DefaultMouseHighlightStates { - get => SelectorBaseSettings.Defaults.DefaultMouseHighlightStates; - set => SelectorBaseSettings.Defaults.DefaultMouseHighlightStates = value; + get => SelectorBaseSettings.Current.DefaultMouseHighlightStates; + set => SelectorBaseSettings.Current = SelectorBaseSettings.Current with { DefaultMouseHighlightStates = value }; } /// diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index 76331d459e..d29da9925f 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -62,8 +62,8 @@ private void OnThemeChanged (object? sender, App.EventArgs e) [ConfigurationProperty (Scope = typeof (ThemeScope))] public static LineStyle DefaultSeparatorLineStyle { - get => StatusBarSettings.Defaults.DefaultSeparatorLineStyle; - set => StatusBarSettings.Defaults.DefaultSeparatorLineStyle = value; + get => StatusBarSettings.Current.DefaultSeparatorLineStyle; + set => StatusBarSettings.Current = StatusBarSettings.Current with { DefaultSeparatorLineStyle = value }; } /// diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.cs index a4212617e7..b58216e64e 100644 --- a/Terminal.Gui/Views/TextInput/TextField/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField/TextField.cs @@ -82,8 +82,8 @@ public partial class TextField : View, IDesignable, IValue [ConfigurationProperty (Scope = typeof (ThemeScope))] public static CursorStyle DefaultCursorStyle { - get => TextFieldSettings.Defaults.DefaultCursorStyle; - set => TextFieldSettings.Defaults.DefaultCursorStyle = value; + get => TextFieldSettings.Current.DefaultCursorStyle; + set => TextFieldSettings.Current = TextFieldSettings.Current with { DefaultCursorStyle = value }; } /// diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.cs index 6879118f29..5c431e0315 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.cs @@ -107,8 +107,8 @@ public partial class TextView : View, IDesignable [ConfigurationProperty (Scope = typeof (ThemeScope))] public static CursorStyle DefaultCursorStyle { - get => TextViewSettings.Defaults.DefaultCursorStyle; - set => TextViewSettings.Defaults.DefaultCursorStyle = value; + get => TextViewSettings.Current.DefaultCursorStyle; + set => TextViewSettings.Current = TextViewSettings.Current with { DefaultCursorStyle = value }; } private CultureInfo? _currentCulture; diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs index a93b1a3301..510c7d2d55 100644 --- a/Terminal.Gui/Views/Window.cs +++ b/Terminal.Gui/Views/Window.cs @@ -36,8 +36,8 @@ public Window () [ConfigurationProperty (Scope = typeof (ThemeScope))] public static ShadowStyles DefaultShadow { - get => WindowSettings.Defaults.DefaultShadow; - set => WindowSettings.Defaults.DefaultShadow = value; + get => WindowSettings.Current.DefaultShadow; + set => WindowSettings.Current = WindowSettings.Current with { DefaultShadow = value }; } // TODO: enable this @@ -61,7 +61,7 @@ public static ShadowStyles DefaultShadow [ConfigurationProperty (Scope = typeof (ThemeScope))] public static LineStyle DefaultBorderStyle { - get => WindowSettings.Defaults.DefaultBorderStyle; - set => WindowSettings.Defaults.DefaultBorderStyle = value; + get => WindowSettings.Current.DefaultBorderStyle; + set => WindowSettings.Current = WindowSettings.Current with { DefaultBorderStyle = value }; } } diff --git a/Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs b/Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs index 6dae2cc2e8..1434d21e85 100644 --- a/Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs @@ -77,18 +77,18 @@ public void CheckBoxSettings_Defaults_HasCorrectValues () public void StaticFacade_CanBeOverridden () { // Save original - ButtonSettings original = ButtonSettings.Defaults; + ButtonSettings original = ButtonSettings.Current; try { ButtonSettings custom = new () { DefaultShadow = ShadowStyles.None }; - ButtonSettings.Defaults = custom; + ButtonSettings.Current = custom; - Assert.Equal (ShadowStyles.None, ButtonSettings.Defaults.DefaultShadow); + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); } finally { - ButtonSettings.Defaults = original; + ButtonSettings.Current = original; } } diff --git a/Tests/UnitTestsParallelizable/Configuration/ThemeOverlayMergeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ThemeOverlayMergeTests.cs new file mode 100644 index 0000000000..38be800a3c --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/ThemeOverlayMergeTests.cs @@ -0,0 +1,132 @@ +// Copilot - Claude Opus 4.7 + +using Terminal.Gui.Configuration; + +namespace ConfigurationTests; + +/// +/// End-to-end tests for the A2.1 two-pass MEC theme overlay applied through +/// . +/// +/// +/// +/// The contract under test: BindThemeScope<T> binds the root section first, then overlays +/// Themes:{active}:{section}. Properties present only in the root must survive; properties present +/// in the overlay must win. This mirrors legacy CM Scope.Apply property-level merge semantics. +/// +/// +public class ThemeOverlayMergeTests +{ + /// + /// When the theme overlay only mentions one property of a ThemeScope POCO, the other properties keep their + /// root-section values (not the compile-time defaults, not ). + /// + [Fact] + public void ApplyToStaticFacades_ThemeOverlay_PreservesRootDefaultsForUnmentionedProperties () + { + DialogSettings originalDialog = DialogSettings.Current; + ThemeSettings originalTheme = ThemeSettings.Defaults; + + try + { + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": { "Theme": "Custom" }, + "Dialog": { + "DefaultShadow": "Opaque", + "DefaultBorderStyle": "Double", + "DefaultButtonAlignment": "Start" + }, + "Themes": { + "Custom": { + "Dialog": { + "DefaultBorderStyle": "Single" + } + } + } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (LineStyle.Single, DialogSettings.Current.DefaultBorderStyle); + Assert.Equal (ShadowStyles.Opaque, DialogSettings.Current.DefaultShadow); + Assert.Equal (Alignment.Start, DialogSettings.Current.DefaultButtonAlignment); + } + finally + { + DialogSettings.Current = originalDialog; + ThemeSettings.Defaults = originalTheme; + } + } + + /// + /// When no theme overlay exists for a POCO, the root section's values are applied verbatim. + /// + [Fact] + public void ApplyToStaticFacades_NoOverlay_UsesRootValuesAsIs () + { + ButtonSettings originalButton = ButtonSettings.Current; + ThemeSettings originalTheme = ThemeSettings.Defaults; + + try + { + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": { "Theme": "Custom" }, + "Button": { + "DefaultShadow": "None" + }, + "Themes": { "Custom": { } } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); + } + finally + { + ButtonSettings.Current = originalButton; + ThemeSettings.Defaults = originalTheme; + } + } + + /// + /// The atomic-swap pattern produces a new reference on each apply, never + /// mutates the existing instance in place. A reader that captured the prior reference still sees the prior + /// values. + /// + [Fact] + public void ApplyToStaticFacades_AtomicSwap_DoesNotMutatePriorReference () + { + ButtonSettings originalButton = ButtonSettings.Current; + ThemeSettings originalTheme = ThemeSettings.Defaults; + + try + { + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = """{ "Button": { "DefaultShadow": "Transparent" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + ButtonSettings captured = ButtonSettings.Current; + Assert.Equal (ShadowStyles.Transparent, captured.DefaultShadow); + + tuiBuilder.RuntimeConfig = """{ "Button": { "DefaultShadow": "None" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (ShadowStyles.Transparent, captured.DefaultShadow); + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); + Assert.NotSame (captured, ButtonSettings.Current); + } + finally + { + ButtonSettings.Current = originalButton; + ThemeSettings.Defaults = originalTheme; + } + } +} From 441ef60f3937be9963e08ce480ec02900b1ed127 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 27 May 2026 17:46:04 -0600 Subject: [PATCH 11/13] A2.2: Glyphs facade redesign + GlyphSettings record conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts GlyphSettings to the immutable-record + atomic-swap Current pattern (matching A2.1's 17 ThemeScope POCOs) and collapses the Glyphs static facade from a CM-reflection target into a read-only projection over GlyphSettings.Current. Changes ======= Terminal.Gui/Configuration/Settings/GlyphSettings.cs - public class -> public sealed record. - ~143 Rune properties: { get; set; } -> { get; init; }. - Replaces static `Defaults` block with: Default (compile-time truth, never reassigned) Current (Volatile-read/write, internal set) _current (private backing, init'd to Default). - Bind target shape now matches all 17 A2.1 POCOs. Terminal.Gui/Drawing/Glyphs.cs - All 144 properties rewritten from [ConfigurationProperty] public static Rune NAME { get => Defaults.NAME; set => Defaults.NAME = value; } to bare expression-bodied readers: public static Rune NAME => GlyphSettings.Current.NAME; - Setters and [ConfigurationProperty] attributes fully removed. - File-level comment about "generates default config" dropped; the SaveDefaults reflection mechanism that produced that text is dead now that Glyphs has no [ConfigurationProperty] surface. The "Resources/config.json is source of truth at runtime" half stays accurate (theme overlay can still override compile-time defaults). - Call sites in Terminal.Gui/, Tests/, Examples/ unchanged: every Glyphs.NAME reader keeps working; only the host changed. Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs - Glyphs binding switched from BindSection to BindThemeScope (root -> overlay -> atomic publish), matching the other 17 theme-overlay POCOs. - activeTheme snapshot now has a TODO(A2) marker for the future ThemeSettings record conversion (review flag #4). Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs - Drops `typeof (Glyphs)` from the `_types` list and removes the matching `[DynamicDependency (PRESERVED_MEMBERS, typeof (Glyphs))]`. Glyphs is no longer a CM reflection host. Terminal.Gui/Configuration/SourceGenerationContext.cs - Drops `[JsonSerializable (typeof (Glyphs))]`. Glyphs has no JSON bind state of its own; `GlyphSettings` is the bind target and is already covered elsewhere. specs/remove-legacy-cm.md - ┬º4.2 row for `Settings/*Settings.cs` annotated with the SettingsScope vs ThemeScope pattern divergence rationale (review flag #3). Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs - Three Skip markers added with the rationale "A2.2: Glyphs lost [ConfigurationProperty]; Resources/config.json Glyphs.X flat keys are now CM-unknown. Test removed with CM in step D." affecting: Load_WithValidResource_UpdatesSettingsScope Load_Runtime_Overrides Load_AddsResourceSourceToCollection These tests load `Terminal.Gui.Resources.config.json` through CM's SourcesManager.Load, which deserializes flat `Glyphs.X` keys via ScopeJsonConverter looking up ConfigProperty hosts. With Glyphs no longer registered, that path throws a JsonException -> Load returns false. The legacy CM contract these tests assert (flat-key resolve against reflection-discovered hosts) is exactly what A2.2 removes for Glyphs and what step D removes wholesale. Skipping is correct; the tests die with CM. Non-Default theme Glyph dormancy ================================ The flat `Glyphs.X` keys inside non-Default theme subsections of Resources/config.json (TurboPascal 5, Anders, Dark, Light, etc.) are dormant from this commit through step D. Active theme at startup is "Default", whose theme block in config.json is intentionally empty - so default startup is structurally unchanged. Glyph values for the Default theme come from GlyphSettings's `init` defaults, which by spec are the canonical Default values. Theme switching (MecThemeManager.SwitchTheme("TurboPascal 5") etc.) does NOT apply Glyph overrides during this window: - CM path: [ConfigurationProperty] removed from Glyphs in A2.2; ScopeJsonConverter no-ops the keys. - MEC path: BindThemeScope reads section Themes::Glyphs, but the flat key form `"Glyphs.LeftBracket"` inside the theme block is treated by MEC as a literal top-level-of-theme key, not a Glyphs:LeftBracket nested path. Step D's config.json rewrite to nested form reactivates Glyph overrides for all theme-overlay POCOs uniformly. This dormancy is the same window that applies to every other ThemeScope POCO's flat keys in non-Default themes; Glyphs is not special. Test results ============ Tests/UnitTestsParallelizable: total 17292 / 17272 passed / 0 failed / 20 skipped (baseline post-A2.1 was 17275/0/17; delta is exactly the 3 new SourcesManagerTests skips). Tests/UnitTests.NonParallelizable: total 74 / 72 passed / 0 failed / 2 skipped (unchanged from baseline). Design context ============== This is commit A2.2 of the A2 series (POCO ownership migration on PR #5416, stacked on copilot/replace-cm-with-mec). A2.1 (commit 2f7c13a6e) landed the 17 ThemeScope POCOs and BindThemeScope. A2.2 brings the 18th (GlyphSettings) and finishes the Glyphs facade. A2.3 will repeat the facade redesign for NerdFonts. A2.4 will remove dead public static setters on Button.DefaultShadow etc. Cross-session review (PR #5411 owner) signed off on: - { get; init; } on the 18 records (concedes prior internal-set recommendation; init works on intra-assembly Bind(existing)) - Glyphs file-comment cleanup (drop the SaveDefaults half) - Three Skip markers on SourcesManagerTests as the resolution path for the CM-vs-Glyphs schema mismatch Deferred (per A2 contract) ========================== - `with`-swap setters on the view facades have a non-atomic read-modify- write window vs. MEC's Volatile.Write; zero practical impact (single- threaded reflection apply path). Bridge code removes in A2.4 / CM deletion, eliminating the only lost-write race on Current. - BindThemeScope's [UnconditionalSuppressMessage] still cites ConfigPropertyHostTypes. When CM deletes in step D, the citation will be re-justified against TuiSerializerContext's JsonSerializable entries (or wired through explicit [DynamicDependency] per POCO). - ThemeSettings itself stays mutable in this commit (record conversion is a future micro-commit; non-blocking for A2.2 review). - Resources/config.json still ships Glyphs.X as flat keys. That JSON shape is part of step B/D (nested-section rewrite), not A2.2. Refs: A2.1 = 2f7c13a6e, stacked on copilot/replace-cm-with-mec. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Configuration/ConfigPropertyHostTypes.cs | 2 - .../Configuration/Settings/GlyphSettings.cs | 306 ++-- .../Settings/TuiConfigurationBuilder.cs | 3 +- .../Configuration/SourceGenerationContext.cs | 1 - Terminal.Gui/Drawing/Glyphs.cs | 1408 ++--------------- .../Configuration/SourcesManagerTests.cs | 6 +- specs/remove-legacy-cm.md | 2 +- 7 files changed, 310 insertions(+), 1418 deletions(-) diff --git a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs index 4c88de5d1b..78c0d1433c 100644 --- a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs +++ b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs @@ -40,7 +40,6 @@ internal static class ConfigPropertyHostTypes typeof (SchemeManager), typeof (ThemeManager), typeof (Color), - typeof (Glyphs), typeof (Driver), typeof (Key), typeof (NerdFonts), @@ -72,7 +71,6 @@ internal static class ConfigPropertyHostTypes [DynamicDependency (PRESERVED_MEMBERS, typeof (SchemeManager))] [DynamicDependency (PRESERVED_MEMBERS, typeof (ThemeManager))] [DynamicDependency (PRESERVED_MEMBERS, typeof (Color))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Glyphs))] [DynamicDependency (PRESERVED_MEMBERS, typeof (Driver))] [DynamicDependency (PRESERVED_MEMBERS, typeof (Key))] [DynamicDependency (PRESERVED_MEMBERS, typeof (NerdFonts))] diff --git a/Terminal.Gui/Configuration/Settings/GlyphSettings.cs b/Terminal.Gui/Configuration/Settings/GlyphSettings.cs index 6806fe9366..6f8b658c6c 100644 --- a/Terminal.Gui/Configuration/Settings/GlyphSettings.cs +++ b/Terminal.Gui/Configuration/Settings/GlyphSettings.cs @@ -3,443 +3,449 @@ namespace Terminal.Gui.Configuration; /// /// Settings POCO for defaults (ThemeScope). /// -public class GlyphSettings +public sealed record GlyphSettings { /// Unicode replacement character; used when a wide glyph can't be output because it would be clipped. - public Rune WideGlyphReplacement { get; set; } = (Rune)' '; + public Rune WideGlyphReplacement { get; init; } = (Rune)' '; /// File icon. - public Rune File { get; set; } = (Rune)'☰'; + public Rune File { get; init; } = (Rune)'☰'; /// Folder icon. - public Rune Folder { get; set; } = (Rune)'꤉'; + public Rune Folder { get; init; } = (Rune)'꤉'; /// Horizontal Ellipsis. - public Rune HorizontalEllipsis { get; set; } = (Rune)'…'; + public Rune HorizontalEllipsis { get; init; } = (Rune)'…'; /// Vertical Four Dots. - public Rune VerticalFourDots { get; set; } = (Rune)'⁞'; + public Rune VerticalFourDots { get; init; } = (Rune)'⁞'; /// Null symbol. - public Rune Null { get; set; } = (Rune)'␀'; + public Rune Null { get; init; } = (Rune)'␀'; /// Checked indicator. - public Rune CheckStateChecked { get; set; } = (Rune)'☒'; + public Rune CheckStateChecked { get; init; } = (Rune)'☒'; /// Not Checked indicator. - public Rune CheckStateUnChecked { get; set; } = (Rune)'☐'; + public Rune CheckStateUnChecked { get; init; } = (Rune)'☐'; /// Null Checked indicator. - public Rune CheckStateNone { get; set; } = (Rune)'□'; + public Rune CheckStateNone { get; init; } = (Rune)'□'; /// Selected indicator. - public Rune Selected { get; set; } = (Rune)'◉'; + public Rune Selected { get; init; } = (Rune)'◉'; /// Not Selected indicator. - public Rune UnSelected { get; set; } = (Rune)'○'; + public Rune UnSelected { get; init; } = (Rune)'○'; /// Right arrow. - public Rune RightArrow { get; set; } = (Rune)'►'; + public Rune RightArrow { get; init; } = (Rune)'►'; /// Left arrow. - public Rune LeftArrow { get; set; } = (Rune)'◄'; + public Rune LeftArrow { get; init; } = (Rune)'◄'; /// Down arrow. - public Rune DownArrow { get; set; } = (Rune)'▼'; + public Rune DownArrow { get; init; } = (Rune)'▼'; /// Up arrow. - public Rune UpArrow { get; set; } = (Rune)'▲'; + public Rune UpArrow { get; init; } = (Rune)'▲'; /// Left default indicator. - public Rune LeftDefaultIndicator { get; set; } = (Rune)'►'; + public Rune LeftDefaultIndicator { get; init; } = (Rune)'►'; /// Right default indicator. - public Rune RightDefaultIndicator { get; set; } = (Rune)'◄'; + public Rune RightDefaultIndicator { get; init; } = (Rune)'◄'; /// Left Bracket. - public Rune LeftBracket { get; set; } = (Rune)'⟦'; + public Rune LeftBracket { get; init; } = (Rune)'⟦'; /// Right Bracket. - public Rune RightBracket { get; set; } = (Rune)'⟧'; + public Rune RightBracket { get; init; } = (Rune)'⟧'; /// Half block meter segment. - public Rune BlocksMeterSegment { get; set; } = (Rune)'▌'; + public Rune BlocksMeterSegment { get; init; } = (Rune)'▌'; /// Continuous block meter segment. - public Rune ContinuousMeterSegment { get; set; } = (Rune)'█'; + public Rune ContinuousMeterSegment { get; init; } = (Rune)'█'; /// Stipple pattern. - public Rune Stipple { get; set; } = (Rune)'░'; + public Rune Stipple { get; init; } = (Rune)'░'; /// Diamond. - public Rune Diamond { get; set; } = (Rune)'◊'; + public Rune Diamond { get; init; } = (Rune)'◊'; /// Close. - public Rune Close { get; set; } = (Rune)'✘'; + public Rune Close { get; init; } = (Rune)'✘'; /// Minimize. - public Rune Minimize { get; set; } = (Rune)'❏'; + public Rune Minimize { get; init; } = (Rune)'❏'; /// Maximize. - public Rune Maximize { get; set; } = (Rune)'✽'; + public Rune Maximize { get; init; } = (Rune)'✽'; /// Dot. - public Rune Dot { get; set; } = (Rune)'∙'; + public Rune Dot { get; init; } = (Rune)'∙'; /// Dotted Square. - public Rune DottedSquare { get; set; } = (Rune)'⬚'; + public Rune DottedSquare { get; init; } = (Rune)'⬚'; /// Black Circle. - public Rune BlackCircle { get; set; } = (Rune)'●'; + public Rune BlackCircle { get; init; } = (Rune)'●'; /// Expand. - public Rune Expand { get; set; } = (Rune)'+'; + public Rune Expand { get; init; } = (Rune)'+'; /// Collapse. - public Rune Collapse { get; set; } = (Rune)'-'; + public Rune Collapse { get; init; } = (Rune)'-'; /// Identical To. - public Rune IdenticalTo { get; set; } = (Rune)'≡'; + public Rune IdenticalTo { get; init; } = (Rune)'≡'; /// Move indicator. - public Rune Move { get; set; } = (Rune)'◊'; + public Rune Move { get; init; } = (Rune)'◊'; /// Size Horizontally indicator. - public Rune SizeHorizontal { get; set; } = (Rune)'↔'; + public Rune SizeHorizontal { get; init; } = (Rune)'↔'; /// Size Vertical indicator. - public Rune SizeVertical { get; set; } = (Rune)'↕'; + public Rune SizeVertical { get; init; } = (Rune)'↕'; /// Size Top Left indicator. - public Rune SizeTopLeft { get; set; } = (Rune)'↖'; + public Rune SizeTopLeft { get; init; } = (Rune)'↖'; /// Size Top Right indicator. - public Rune SizeTopRight { get; set; } = (Rune)'↗'; + public Rune SizeTopRight { get; init; } = (Rune)'↗'; /// Size Bottom Right indicator. - public Rune SizeBottomRight { get; set; } = (Rune)'↘'; + public Rune SizeBottomRight { get; init; } = (Rune)'↘'; /// Size Bottom Left indicator. - public Rune SizeBottomLeft { get; set; } = (Rune)'↙'; + public Rune SizeBottomLeft { get; init; } = (Rune)'↙'; /// Apple (non-BMP). - public Rune Apple { get; set; } = "🍎".ToRunes () [0]; + public Rune Apple { get; init; } = "🍎".ToRunes () [0]; /// Apple (BMP). - public Rune AppleBMP { get; set; } = (Rune)'❦'; + public Rune AppleBMP { get; init; } = (Rune)'❦'; /// Copy indicator. - public Rune Copy { get; set; } = (Rune)'⧉'; + public Rune Copy { get; init; } = (Rune)'⧉'; /// Box Drawings Horizontal Line - Light. - public Rune HLine { get; set; } = (Rune)'─'; + public Rune HLine { get; init; } = (Rune)'─'; /// Box Drawings Vertical Line - Light. - public Rune VLine { get; set; } = (Rune)'│'; + public Rune VLine { get; init; } = (Rune)'│'; /// Box Drawings Double Horizontal. - public Rune HLineDbl { get; set; } = (Rune)'═'; + public Rune HLineDbl { get; init; } = (Rune)'═'; /// Box Drawings Double Vertical. - public Rune VLineDbl { get; set; } = (Rune)'║'; + public Rune VLineDbl { get; init; } = (Rune)'║'; /// Box Drawings Heavy Double Dash Horizontal. - public Rune HLineHvDa2 { get; set; } = (Rune)'╍'; + public Rune HLineHvDa2 { get; init; } = (Rune)'╍'; /// Box Drawings Heavy Triple Dash Vertical. - public Rune VLineHvDa3 { get; set; } = (Rune)'┇'; + public Rune VLineHvDa3 { get; init; } = (Rune)'┇'; /// Box Drawings Heavy Triple Dash Horizontal. - public Rune HLineHvDa3 { get; set; } = (Rune)'┅'; + public Rune HLineHvDa3 { get; init; } = (Rune)'┅'; /// Box Drawings Heavy Quadruple Dash Horizontal. - public Rune HLineHvDa4 { get; set; } = (Rune)'┉'; + public Rune HLineHvDa4 { get; init; } = (Rune)'┉'; /// Box Drawings Heavy Double Dash Vertical. - public Rune VLineHvDa2 { get; set; } = (Rune)'╏'; + public Rune VLineHvDa2 { get; init; } = (Rune)'╏'; /// Box Drawings Heavy Quadruple Dash Vertical. - public Rune VLineHvDa4 { get; set; } = (Rune)'┋'; + public Rune VLineHvDa4 { get; init; } = (Rune)'┋'; /// Box Drawings Light Double Dash Horizontal. - public Rune HLineDa2 { get; set; } = (Rune)'╌'; + public Rune HLineDa2 { get; init; } = (Rune)'╌'; /// Box Drawings Light Triple Dash Vertical. - public Rune VLineDa3 { get; set; } = (Rune)'┆'; + public Rune VLineDa3 { get; init; } = (Rune)'┆'; /// Box Drawings Light Triple Dash Horizontal. - public Rune HLineDa3 { get; set; } = (Rune)'┄'; + public Rune HLineDa3 { get; init; } = (Rune)'┄'; /// Box Drawings Light Quadruple Dash Horizontal. - public Rune HLineDa4 { get; set; } = (Rune)'┈'; + public Rune HLineDa4 { get; init; } = (Rune)'┈'; /// Box Drawings Light Double Dash Vertical. - public Rune VLineDa2 { get; set; } = (Rune)'╎'; + public Rune VLineDa2 { get; init; } = (Rune)'╎'; /// Box Drawings Light Quadruple Dash Vertical. - public Rune VLineDa4 { get; set; } = (Rune)'┊'; + public Rune VLineDa4 { get; init; } = (Rune)'┊'; /// Box Drawings Heavy Horizontal. - public Rune HLineHv { get; set; } = (Rune)'━'; + public Rune HLineHv { get; init; } = (Rune)'━'; /// Box Drawings Heavy Vertical. - public Rune VLineHv { get; set; } = (Rune)'┃'; + public Rune VLineHv { get; init; } = (Rune)'┃'; /// Box Drawings Light Left. - public Rune HalfLeftLine { get; set; } = (Rune)'╴'; + public Rune HalfLeftLine { get; init; } = (Rune)'╴'; /// Box Drawings Light Up. - public Rune HalfTopLine { get; set; } = (Rune)'╵'; + public Rune HalfTopLine { get; init; } = (Rune)'╵'; /// Box Drawings Light Right. - public Rune HalfRightLine { get; set; } = (Rune)'╶'; + public Rune HalfRightLine { get; init; } = (Rune)'╶'; /// Box Drawings Light Down. - public Rune HalfBottomLine { get; set; } = (Rune)'╷'; + public Rune HalfBottomLine { get; init; } = (Rune)'╷'; /// Box Drawings Heavy Left. - public Rune HalfLeftLineHv { get; set; } = (Rune)'╸'; + public Rune HalfLeftLineHv { get; init; } = (Rune)'╸'; /// Box Drawings Heavy Up. - public Rune HalfTopLineHv { get; set; } = (Rune)'╹'; + public Rune HalfTopLineHv { get; init; } = (Rune)'╹'; /// Box Drawings Heavy Right. - public Rune HalfRightLineHv { get; set; } = (Rune)'╺'; + public Rune HalfRightLineHv { get; init; } = (Rune)'╺'; /// Box Drawings Light Down Heavy. - public Rune HalfBottomLineLt { get; set; } = (Rune)'╻'; + public Rune HalfBottomLineLt { get; init; } = (Rune)'╻'; /// Box Drawings Light Horizontal and Heavy Horizontal. - public Rune RightSideLineLtHv { get; set; } = (Rune)'╼'; + public Rune RightSideLineLtHv { get; init; } = (Rune)'╼'; /// Box Drawings Light Vertical and Heavy Horizontal. - public Rune BottomSideLineLtHv { get; set; } = (Rune)'╽'; + public Rune BottomSideLineLtHv { get; init; } = (Rune)'╽'; /// Box Drawings Heavy Left and Light Horizontal. - public Rune LeftSideLineHvLt { get; set; } = (Rune)'╾'; + public Rune LeftSideLineHvLt { get; init; } = (Rune)'╾'; /// Box Drawings Heavy Vertical and Light Horizontal. - public Rune TopSideLineHvLt { get; set; } = (Rune)'╿'; + public Rune TopSideLineHvLt { get; init; } = (Rune)'╿'; /// Box Drawings Upper Left Corner - Light. - public Rune ULCorner { get; set; } = (Rune)'┌'; + public Rune ULCorner { get; init; } = (Rune)'┌'; /// Box Drawings Upper Left Corner - Double. - public Rune ULCornerDbl { get; set; } = (Rune)'╔'; + public Rune ULCornerDbl { get; init; } = (Rune)'╔'; /// Box Drawings Upper Left Corner - Rounded. - public Rune ULCornerR { get; set; } = (Rune)'╭'; + public Rune ULCornerR { get; init; } = (Rune)'╭'; /// Box Drawings Upper Left Corner - Heavy. - public Rune ULCornerHv { get; set; } = (Rune)'┏'; + public Rune ULCornerHv { get; init; } = (Rune)'┏'; /// Box Drawings Upper Left Corner - Heavy Vertical Light Horizontal. - public Rune ULCornerHvLt { get; set; } = (Rune)'┎'; + public Rune ULCornerHvLt { get; init; } = (Rune)'┎'; /// Box Drawings Upper Left Corner - Light Vertical Heavy Horizontal. - public Rune ULCornerLtHv { get; set; } = (Rune)'┍'; + public Rune ULCornerLtHv { get; init; } = (Rune)'┍'; /// Box Drawings Upper Left Corner - Double Down Single Horizontal. - public Rune ULCornerDblSingle { get; set; } = (Rune)'╓'; + public Rune ULCornerDblSingle { get; init; } = (Rune)'╓'; /// Box Drawings Upper Left Corner - Single Down Double Horizontal. - public Rune ULCornerSingleDbl { get; set; } = (Rune)'╒'; + public Rune ULCornerSingleDbl { get; init; } = (Rune)'╒'; /// Box Drawings Lower Left Corner - Light. - public Rune LLCorner { get; set; } = (Rune)'└'; + public Rune LLCorner { get; init; } = (Rune)'└'; /// Box Drawings Lower Left Corner - Heavy. - public Rune LLCornerHv { get; set; } = (Rune)'┗'; + public Rune LLCornerHv { get; init; } = (Rune)'┗'; /// Box Drawings Lower Left Corner - Heavy Vertical Light Horizontal. - public Rune LLCornerHvLt { get; set; } = (Rune)'┖'; + public Rune LLCornerHvLt { get; init; } = (Rune)'┖'; /// Box Drawings Lower Left Corner - Light Vertical Heavy Horizontal. - public Rune LLCornerLtHv { get; set; } = (Rune)'┕'; + public Rune LLCornerLtHv { get; init; } = (Rune)'┕'; /// Box Drawings Lower Left Corner - Double. - public Rune LLCornerDbl { get; set; } = (Rune)'╚'; + public Rune LLCornerDbl { get; init; } = (Rune)'╚'; /// Box Drawings Lower Left Corner - Single Vertical Double Horizontal. - public Rune LLCornerSingleDbl { get; set; } = (Rune)'╘'; + public Rune LLCornerSingleDbl { get; init; } = (Rune)'╘'; /// Box Drawings Lower Left Corner - Double Vertical Single Horizontal. - public Rune LLCornerDblSingle { get; set; } = (Rune)'╙'; + public Rune LLCornerDblSingle { get; init; } = (Rune)'╙'; /// Box Drawings Lower Left Corner - Rounded. - public Rune LLCornerR { get; set; } = (Rune)'╰'; + public Rune LLCornerR { get; init; } = (Rune)'╰'; /// Box Drawings Upper Right Corner - Light. - public Rune URCorner { get; set; } = (Rune)'┐'; + public Rune URCorner { get; init; } = (Rune)'┐'; /// Box Drawings Upper Right Corner - Double. - public Rune URCornerDbl { get; set; } = (Rune)'╗'; + public Rune URCornerDbl { get; init; } = (Rune)'╗'; /// Box Drawings Upper Right Corner - Rounded. - public Rune URCornerR { get; set; } = (Rune)'╮'; + public Rune URCornerR { get; init; } = (Rune)'╮'; /// Box Drawings Upper Right Corner - Heavy. - public Rune URCornerHv { get; set; } = (Rune)'┓'; + public Rune URCornerHv { get; init; } = (Rune)'┓'; /// Box Drawings Upper Right Corner - Heavy Vertical Light Horizontal. - public Rune URCornerHvLt { get; set; } = (Rune)'┑'; + public Rune URCornerHvLt { get; init; } = (Rune)'┑'; /// Box Drawings Upper Right Corner - Light Vertical Heavy Horizontal. - public Rune URCornerLtHv { get; set; } = (Rune)'┒'; + public Rune URCornerLtHv { get; init; } = (Rune)'┒'; /// Box Drawings Upper Right Corner - Double Vertical Single Horizontal. - public Rune URCornerDblSingle { get; set; } = (Rune)'╖'; + public Rune URCornerDblSingle { get; init; } = (Rune)'╖'; /// Box Drawings Upper Right Corner - Single Vertical Double Horizontal. - public Rune URCornerSingleDbl { get; set; } = (Rune)'╕'; + public Rune URCornerSingleDbl { get; init; } = (Rune)'╕'; /// Box Drawings Lower Right Corner - Light. - public Rune LRCorner { get; set; } = (Rune)'┘'; + public Rune LRCorner { get; init; } = (Rune)'┘'; /// Box Drawings Lower Right Corner - Double. - public Rune LRCornerDbl { get; set; } = (Rune)'╝'; + public Rune LRCornerDbl { get; init; } = (Rune)'╝'; /// Box Drawings Lower Right Corner - Rounded. - public Rune LRCornerR { get; set; } = (Rune)'╯'; + public Rune LRCornerR { get; init; } = (Rune)'╯'; /// Box Drawings Lower Right Corner - Heavy. - public Rune LRCornerHv { get; set; } = (Rune)'┛'; + public Rune LRCornerHv { get; init; } = (Rune)'┛'; /// Box Drawings Lower Right Corner - Double Vertical Single Horizontal. - public Rune LRCornerDblSingle { get; set; } = (Rune)'╜'; + public Rune LRCornerDblSingle { get; init; } = (Rune)'╜'; /// Box Drawings Lower Right Corner - Single Vertical Double Horizontal. - public Rune LRCornerSingleDbl { get; set; } = (Rune)'╛'; + public Rune LRCornerSingleDbl { get; init; } = (Rune)'╛'; /// Box Drawings Lower Right Corner - Light Vertical Heavy Horizontal. - public Rune LRCornerLtHv { get; set; } = (Rune)'┙'; + public Rune LRCornerLtHv { get; init; } = (Rune)'┙'; /// Box Drawings Lower Right Corner - Heavy Vertical Light Horizontal. - public Rune LRCornerHvLt { get; set; } = (Rune)'┚'; + public Rune LRCornerHvLt { get; init; } = (Rune)'┚'; /// Box Drawings Left Tee - Light. - public Rune LeftTee { get; set; } = (Rune)'├'; + public Rune LeftTee { get; init; } = (Rune)'├'; /// Box Drawings Left Tee - Single Vertical Double Horizontal. - public Rune LeftTeeDblH { get; set; } = (Rune)'╞'; + public Rune LeftTeeDblH { get; init; } = (Rune)'╞'; /// Box Drawings Left Tee - Double Vertical Single Horizontal. - public Rune LeftTeeDblV { get; set; } = (Rune)'╟'; + public Rune LeftTeeDblV { get; init; } = (Rune)'╟'; /// Box Drawings Left Tee - Double. - public Rune LeftTeeDbl { get; set; } = (Rune)'╠'; + public Rune LeftTeeDbl { get; init; } = (Rune)'╠'; /// Box Drawings Left Tee - Heavy Horizontal Light Vertical. - public Rune LeftTeeHvH { get; set; } = (Rune)'┝'; + public Rune LeftTeeHvH { get; init; } = (Rune)'┝'; /// Box Drawings Left Tee - Light Horizontal Heavy Vertical. - public Rune LeftTeeHvV { get; set; } = (Rune)'┠'; + public Rune LeftTeeHvV { get; init; } = (Rune)'┠'; /// Box Drawings Left Tee - Heavy. - public Rune LeftTeeHvDblH { get; set; } = (Rune)'┣'; + public Rune LeftTeeHvDblH { get; init; } = (Rune)'┣'; /// Box Drawings Right Tee - Light. - public Rune RightTee { get; set; } = (Rune)'┤'; + public Rune RightTee { get; init; } = (Rune)'┤'; /// Box Drawings Right Tee - Single Vertical Double Horizontal. - public Rune RightTeeDblH { get; set; } = (Rune)'╡'; + public Rune RightTeeDblH { get; init; } = (Rune)'╡'; /// Box Drawings Right Tee - Double Vertical Single Horizontal. - public Rune RightTeeDblV { get; set; } = (Rune)'╢'; + public Rune RightTeeDblV { get; init; } = (Rune)'╢'; /// Box Drawings Right Tee - Double. - public Rune RightTeeDbl { get; set; } = (Rune)'╣'; + public Rune RightTeeDbl { get; init; } = (Rune)'╣'; /// Box Drawings Right Tee - Heavy Horizontal Light Vertical. - public Rune RightTeeHvH { get; set; } = (Rune)'┥'; + public Rune RightTeeHvH { get; init; } = (Rune)'┥'; /// Box Drawings Right Tee - Light Horizontal Heavy Vertical. - public Rune RightTeeHvV { get; set; } = (Rune)'┨'; + public Rune RightTeeHvV { get; init; } = (Rune)'┨'; /// Box Drawings Right Tee - Heavy. - public Rune RightTeeHvDblH { get; set; } = (Rune)'┫'; + public Rune RightTeeHvDblH { get; init; } = (Rune)'┫'; /// Box Drawings Top Tee - Light. - public Rune TopTee { get; set; } = (Rune)'┬'; + public Rune TopTee { get; init; } = (Rune)'┬'; /// Box Drawings Top Tee - Single Vertical Double Horizontal. - public Rune TopTeeDblH { get; set; } = (Rune)'╤'; + public Rune TopTeeDblH { get; init; } = (Rune)'╤'; /// Box Drawings Top Tee - Double Vertical Single Horizontal. - public Rune TopTeeDblV { get; set; } = (Rune)'╥'; + public Rune TopTeeDblV { get; init; } = (Rune)'╥'; /// Box Drawings Top Tee - Double. - public Rune TopTeeDbl { get; set; } = (Rune)'╦'; + public Rune TopTeeDbl { get; init; } = (Rune)'╦'; /// Box Drawings Top Tee - Heavy Horizontal Light Vertical. - public Rune TopTeeHvH { get; set; } = (Rune)'┯'; + public Rune TopTeeHvH { get; init; } = (Rune)'┯'; /// Box Drawings Top Tee - Light Horizontal Heavy Vertical. - public Rune TopTeeHvV { get; set; } = (Rune)'┰'; + public Rune TopTeeHvV { get; init; } = (Rune)'┰'; /// Box Drawings Top Tee - Heavy. - public Rune TopTeeHvDblH { get; set; } = (Rune)'┳'; + public Rune TopTeeHvDblH { get; init; } = (Rune)'┳'; /// Box Drawings Bottom Tee - Light. - public Rune BottomTee { get; set; } = (Rune)'┴'; + public Rune BottomTee { get; init; } = (Rune)'┴'; /// Box Drawings Bottom Tee - Single Vertical Double Horizontal. - public Rune BottomTeeDblH { get; set; } = (Rune)'╧'; + public Rune BottomTeeDblH { get; init; } = (Rune)'╧'; /// Box Drawings Bottom Tee - Double Vertical Single Horizontal. - public Rune BottomTeeDblV { get; set; } = (Rune)'╨'; + public Rune BottomTeeDblV { get; init; } = (Rune)'╨'; /// Box Drawings Bottom Tee - Double. - public Rune BottomTeeDbl { get; set; } = (Rune)'╩'; + public Rune BottomTeeDbl { get; init; } = (Rune)'╩'; /// Box Drawings Bottom Tee - Heavy Horizontal Light Vertical. - public Rune BottomTeeHvH { get; set; } = (Rune)'┷'; + public Rune BottomTeeHvH { get; init; } = (Rune)'┷'; /// Box Drawings Bottom Tee - Light Horizontal Heavy Vertical. - public Rune BottomTeeHvV { get; set; } = (Rune)'┸'; + public Rune BottomTeeHvV { get; init; } = (Rune)'┸'; /// Box Drawings Bottom Tee - Heavy. - public Rune BottomTeeHvDblH { get; set; } = (Rune)'┻'; + public Rune BottomTeeHvDblH { get; init; } = (Rune)'┻'; /// Box Drawings Cross - Light. - public Rune Cross { get; set; } = (Rune)'┼'; + public Rune Cross { get; init; } = (Rune)'┼'; /// Box Drawings Cross - Single Vertical Double Horizontal. - public Rune CrossDblH { get; set; } = (Rune)'╪'; + public Rune CrossDblH { get; init; } = (Rune)'╪'; /// Box Drawings Cross - Double Vertical Single Horizontal. - public Rune CrossDblV { get; set; } = (Rune)'╫'; + public Rune CrossDblV { get; init; } = (Rune)'╫'; /// Box Drawings Cross - Double. - public Rune CrossDbl { get; set; } = (Rune)'╬'; + public Rune CrossDbl { get; init; } = (Rune)'╬'; /// Box Drawings Cross - Heavy Horizontal Light Vertical. - public Rune CrossHvH { get; set; } = (Rune)'┿'; + public Rune CrossHvH { get; init; } = (Rune)'┿'; /// Box Drawings Cross - Light Horizontal Heavy Vertical. - public Rune CrossHvV { get; set; } = (Rune)'╂'; + public Rune CrossHvV { get; init; } = (Rune)'╂'; /// Box Drawings Cross - Heavy. - public Rune CrossHv { get; set; } = (Rune)'╋'; + public Rune CrossHv { get; init; } = (Rune)'╋'; /// Shadow - Vertical Start. - public Rune ShadowVerticalStart { get; set; } = (Rune)'▖'; + public Rune ShadowVerticalStart { get; init; } = (Rune)'▖'; /// Shadow - Vertical. - public Rune ShadowVertical { get; set; } = (Rune)'▌'; + public Rune ShadowVertical { get; init; } = (Rune)'▌'; /// Shadow - Horizontal Start. - public Rune ShadowHorizontalStart { get; set; } = (Rune)'▝'; + public Rune ShadowHorizontalStart { get; init; } = (Rune)'▝'; /// Shadow - Horizontal. - public Rune ShadowHorizontal { get; set; } = (Rune)'▀'; + public Rune ShadowHorizontal { get; init; } = (Rune)'▀'; /// Shadow - Horizontal End. - public Rune ShadowHorizontalEnd { get; set; } = (Rune)'▘'; + public Rune ShadowHorizontalEnd { get; init; } = (Rune)'▘'; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static GlyphSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static GlyphSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static GlyphSettings Current + { + get => Volatile.Read (ref _current); + internal set => Volatile.Write (ref _current, value); + } + + private static GlyphSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs b/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs index f5ed994bf3..6e1c1a7297 100644 --- a/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs +++ b/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs @@ -124,6 +124,7 @@ public void ApplyToStaticFacades () BindSection (config, "Trace", s => TraceSettings.Defaults = s); // ThemeScope POCOs: two-pass overlay (root section + Themes::
) writes Current. + // TODO(A2): when ThemeSettings converts to record + Current, this becomes an immutable snapshot. string activeTheme = ThemeSettings.Defaults.Theme; BindThemeScope (config, "Button", activeTheme, s => ButtonSettings.Current = s); BindThemeScope (config, "CheckBox", activeTheme, s => CheckBoxSettings.Current = s); @@ -142,7 +143,7 @@ public void ApplyToStaticFacades () BindThemeScope (config, "TextField", activeTheme, s => TextFieldSettings.Current = s); BindThemeScope (config, "TextView", activeTheme, s => TextViewSettings.Current = s); BindThemeScope (config, "Window", activeTheme, s => WindowSettings.Current = s); - BindSection (config, "Glyphs", s => GlyphSettings.Defaults = s); + BindThemeScope (config, "Glyphs", activeTheme, s => GlyphSettings.Current = s); } /// diff --git a/Terminal.Gui/Configuration/SourceGenerationContext.cs b/Terminal.Gui/Configuration/SourceGenerationContext.cs index 91464a85b4..d7e734b420 100644 --- a/Terminal.Gui/Configuration/SourceGenerationContext.cs +++ b/Terminal.Gui/Configuration/SourceGenerationContext.cs @@ -23,7 +23,6 @@ namespace Terminal.Gui.Configuration; [JsonSerializable (typeof (Color))] [JsonSerializable (typeof (Key))] [JsonSerializable (typeof (Key []))] -[JsonSerializable (typeof (Glyphs))] [JsonSerializable (typeof (Alignment))] [JsonSerializable (typeof (AlignmentModes))] [JsonSerializable (typeof (LineStyle))] diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs index 7b0bcb4f64..4f39bf7178 100644 --- a/Terminal.Gui/Drawing/Glyphs.cs +++ b/Terminal.Gui/Drawing/Glyphs.cs @@ -21,723 +21,259 @@ namespace Terminal.Gui.Drawing; /// public class Glyphs { - // IMPORTANT: If you change these, make sure to update the ./Resources/config.json file as - // IMPORTANT: it is the source of truth for the default glyphs at runtime. - // IMPORTANT: Configuration Manager test SaveDefaults uses this class to generate the default config file - // IMPORTANT: in ./UnitTests/bin/Debug/netX.0/config.json + // The default glyph values live on the GlyphSettings record's `init` defaults. + // Resources/config.json is the source of truth for the runtime glyphs; the embedded + // config is loaded and applied via TuiConfigurationBuilder.ApplyToStaticFacades, with + // theme overlays composed under "Themes::Glyphs". /// Unicode replacement character; used by Drivers when rendering in cases where a wide glyph can't /// be output because it would be clipped. Defaults to ' ' (Space). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune WideGlyphReplacement - { - get => GlyphSettings.Defaults.WideGlyphReplacement; - set => GlyphSettings.Defaults.WideGlyphReplacement = value; - } + public static Rune WideGlyphReplacement => GlyphSettings.Current.WideGlyphReplacement; /// File icon. Defaults to ☰ (Trigram For Heaven) - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune File - { - get => GlyphSettings.Defaults.File; - set => GlyphSettings.Defaults.File = value; - } + public static Rune File => GlyphSettings.Current.File; /// Folder icon. Defaults to ꤉ (Kayah Li Digit Nine) - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Folder - { - get => GlyphSettings.Defaults.Folder; - set => GlyphSettings.Defaults.Folder = value; - } + public static Rune Folder => GlyphSettings.Current.Folder; /// Horizontal Ellipsis - … U+2026 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HorizontalEllipsis - { - get => GlyphSettings.Defaults.HorizontalEllipsis; - set => GlyphSettings.Defaults.HorizontalEllipsis = value; - } + public static Rune HorizontalEllipsis => GlyphSettings.Current.HorizontalEllipsis; /// Vertical Four Dots - ⁞ U+205e - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VerticalFourDots - { - get => GlyphSettings.Defaults.VerticalFourDots; - set => GlyphSettings.Defaults.VerticalFourDots = value; - } + public static Rune VerticalFourDots => GlyphSettings.Current.VerticalFourDots; #region ----------------- Single Glyphs ----------------- /// Null symbol ('␀') - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Null - { - get => GlyphSettings.Defaults.Null; - set => GlyphSettings.Defaults.Null = value; - } + public static Rune Null => GlyphSettings.Current.Null; /// Checked indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CheckStateChecked // '☑' is colored - { - get => GlyphSettings.Defaults.CheckStateChecked; - set => GlyphSettings.Defaults.CheckStateChecked = value; - } + public static Rune CheckStateChecked => GlyphSettings.Current.CheckStateChecked; /// Not Checked indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CheckStateUnChecked - { - get => GlyphSettings.Defaults.CheckStateUnChecked; - set => GlyphSettings.Defaults.CheckStateUnChecked = value; - } + public static Rune CheckStateUnChecked => GlyphSettings.Current.CheckStateUnChecked; /// Null Checked indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CheckStateNone // TODO: Verify this works as broadly as possible - { - get => GlyphSettings.Defaults.CheckStateNone; - set => GlyphSettings.Defaults.CheckStateNone = value; - } + public static Rune CheckStateNone => GlyphSettings.Current.CheckStateNone; /// Selected indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Selected - { - get => GlyphSettings.Defaults.Selected; - set => GlyphSettings.Defaults.Selected = value; - } + public static Rune Selected => GlyphSettings.Current.Selected; /// Not Selected indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune UnSelected - { - get => GlyphSettings.Defaults.UnSelected; - set => GlyphSettings.Defaults.UnSelected = value; - } + public static Rune UnSelected => GlyphSettings.Current.UnSelected; /// Horizontal arrow. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightArrow - { - get => GlyphSettings.Defaults.RightArrow; - set => GlyphSettings.Defaults.RightArrow = value; - } + public static Rune RightArrow => GlyphSettings.Current.RightArrow; /// Left arrow. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftArrow - { - get => GlyphSettings.Defaults.LeftArrow; - set => GlyphSettings.Defaults.LeftArrow = value; - } + public static Rune LeftArrow => GlyphSettings.Current.LeftArrow; /// Down arrow. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune DownArrow - { - get => GlyphSettings.Defaults.DownArrow; - set => GlyphSettings.Defaults.DownArrow = value; - } + public static Rune DownArrow => GlyphSettings.Current.DownArrow; /// Vertical arrow. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune UpArrow - { - get => GlyphSettings.Defaults.UpArrow; - set => GlyphSettings.Defaults.UpArrow = value; - } + public static Rune UpArrow => GlyphSettings.Current.UpArrow; /// Left default indicator (e.g. for . - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftDefaultIndicator - { - get => GlyphSettings.Defaults.LeftDefaultIndicator; - set => GlyphSettings.Defaults.LeftDefaultIndicator = value; - } + public static Rune LeftDefaultIndicator => GlyphSettings.Current.LeftDefaultIndicator; /// Horizontal default indicator (e.g. for . - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightDefaultIndicator - { - get => GlyphSettings.Defaults.RightDefaultIndicator; - set => GlyphSettings.Defaults.RightDefaultIndicator = value; - } + public static Rune RightDefaultIndicator => GlyphSettings.Current.RightDefaultIndicator; /// Left Bracket (e.g. for . Default is (U+005B) - [. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftBracket - { - get => GlyphSettings.Defaults.LeftBracket; - set => GlyphSettings.Defaults.LeftBracket = value; - } + public static Rune LeftBracket => GlyphSettings.Current.LeftBracket; /// Horizontal Bracket (e.g. for . Default is (U+005D) - ]. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightBracket - { - get => GlyphSettings.Defaults.RightBracket; - set => GlyphSettings.Defaults.RightBracket = value; - } + public static Rune RightBracket => GlyphSettings.Current.RightBracket; /// Half block meter segment (e.g. for ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BlocksMeterSegment - { - get => GlyphSettings.Defaults.BlocksMeterSegment; - set => GlyphSettings.Defaults.BlocksMeterSegment = value; - } + public static Rune BlocksMeterSegment => GlyphSettings.Current.BlocksMeterSegment; /// Continuous block meter segment (e.g. for ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ContinuousMeterSegment - { - get => GlyphSettings.Defaults.ContinuousMeterSegment; - set => GlyphSettings.Defaults.ContinuousMeterSegment = value; - } + public static Rune ContinuousMeterSegment => GlyphSettings.Current.ContinuousMeterSegment; /// Stipple pattern (e.g. for ). Default is Light Shade (U+2591) - â–‘. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Stipple - { - get => GlyphSettings.Defaults.Stipple; - set => GlyphSettings.Defaults.Stipple = value; - } + public static Rune Stipple => GlyphSettings.Current.Stipple; /// Diamond. Default is Lozenge (U+25CA) - â—Š. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Diamond - { - get => GlyphSettings.Defaults.Diamond; - set => GlyphSettings.Defaults.Diamond = value; - } + public static Rune Diamond => GlyphSettings.Current.Diamond; /// Close. Default is Heavy Ballot X (U+2718) - ✘. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Close - { - get => GlyphSettings.Defaults.Close; - set => GlyphSettings.Defaults.Close = value; - } + public static Rune Close => GlyphSettings.Current.Close; /// Minimize. Default is Lower Horizontal Shadowed White Circle (U+274F) - ❏. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Minimize - { - get => GlyphSettings.Defaults.Minimize; - set => GlyphSettings.Defaults.Minimize = value; - } + public static Rune Minimize => GlyphSettings.Current.Minimize; /// Maximize. Default is Upper Horizontal Shadowed White Circle (U+273D) - ✽. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Maximize - { - get => GlyphSettings.Defaults.Maximize; - set => GlyphSettings.Defaults.Maximize = value; - } + public static Rune Maximize => GlyphSettings.Current.Maximize; /// Dot. Default is (U+2219) - ∙. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Dot - { - get => GlyphSettings.Defaults.Dot; - set => GlyphSettings.Defaults.Dot = value; - } + public static Rune Dot => GlyphSettings.Current.Dot; /// Dotted Square - ⬚ U+02b1a┝ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune DottedSquare - { - get => GlyphSettings.Defaults.DottedSquare; - set => GlyphSettings.Defaults.DottedSquare = value; - } + public static Rune DottedSquare => GlyphSettings.Current.DottedSquare; /// Black Circle . Default is (U+025cf) - ●. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BlackCircle // Black Circle - ● U+025cf - { - get => GlyphSettings.Defaults.BlackCircle; - set => GlyphSettings.Defaults.BlackCircle = value; - } + public static Rune BlackCircle => GlyphSettings.Current.BlackCircle; /// Expand (e.g. for . - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Expand - { - get => GlyphSettings.Defaults.Expand; - set => GlyphSettings.Defaults.Expand = value; - } + public static Rune Expand => GlyphSettings.Current.Expand; /// Expand (e.g. for . - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Collapse - { - get => GlyphSettings.Defaults.Collapse; - set => GlyphSettings.Defaults.Collapse = value; - } + public static Rune Collapse => GlyphSettings.Current.Collapse; /// Identical To (U+226) - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune IdenticalTo - { - get => GlyphSettings.Defaults.IdenticalTo; - set => GlyphSettings.Defaults.IdenticalTo = value; - } + public static Rune IdenticalTo => GlyphSettings.Current.IdenticalTo; /// Move indicator. Default is Lozenge (U+25CA) - â—Š. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Move - { - get => GlyphSettings.Defaults.Move; - set => GlyphSettings.Defaults.Move = value; - } + public static Rune Move => GlyphSettings.Current.Move; /// Size Horizontally indicator. Default is ┥Left Right Arrow - ↔ U+02194 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeHorizontal - { - get => GlyphSettings.Defaults.SizeHorizontal; - set => GlyphSettings.Defaults.SizeHorizontal = value; - } + public static Rune SizeHorizontal => GlyphSettings.Current.SizeHorizontal; /// Size Vertical indicator. Default Up Down Arrow - ↕ U+02195 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeVertical - { - get => GlyphSettings.Defaults.SizeVertical; - set => GlyphSettings.Defaults.SizeVertical = value; - } + public static Rune SizeVertical => GlyphSettings.Current.SizeVertical; /// Size Top Left indicator. North West Arrow - ↖ U+02196 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeTopLeft - { - get => GlyphSettings.Defaults.SizeTopLeft; - set => GlyphSettings.Defaults.SizeTopLeft = value; - } + public static Rune SizeTopLeft => GlyphSettings.Current.SizeTopLeft; /// Size Top Right indicator. North East Arrow - ↗ U+02197 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeTopRight - { - get => GlyphSettings.Defaults.SizeTopRight; - set => GlyphSettings.Defaults.SizeTopRight = value; - } + public static Rune SizeTopRight => GlyphSettings.Current.SizeTopRight; /// Size Bottom Right indicator. South East Arrow - ↘ U+02198 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeBottomRight - { - get => GlyphSettings.Defaults.SizeBottomRight; - set => GlyphSettings.Defaults.SizeBottomRight = value; - } + public static Rune SizeBottomRight => GlyphSettings.Current.SizeBottomRight; /// Size Bottom Left indicator. South West Arrow - ↙ U+02199 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeBottomLeft - { - get => GlyphSettings.Defaults.SizeBottomLeft; - set => GlyphSettings.Defaults.SizeBottomLeft = value; - } + public static Rune SizeBottomLeft => GlyphSettings.Current.SizeBottomLeft; /// Apple (non-BMP). Because snek. And because it's an example of a non-BMP surrogate pair. See Issue #2610. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Apple // nonBMP - { - get => GlyphSettings.Defaults.Apple; - set => GlyphSettings.Defaults.Apple = value; - } + public static Rune Apple => GlyphSettings.Current.Apple; /// Apple (BMP). Because snek. See Issue #2610. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune AppleBMP - { - get => GlyphSettings.Defaults.AppleBMP; - set => GlyphSettings.Defaults.AppleBMP = value; - } + public static Rune AppleBMP => GlyphSettings.Current.AppleBMP; /// Copy indicator. Two Joined Squares - ⧉ U+29C9. Used for code block copy buttons. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Copy - { - get => GlyphSettings.Defaults.Copy; - set => GlyphSettings.Defaults.Copy = value; - } + public static Rune Copy => GlyphSettings.Current.Copy; #endregion #region ----------------- Lines ----------------- /// Box Drawings Horizontal Line - Light (U+2500) - ─ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLine - { - get => GlyphSettings.Defaults.HLine; - set => GlyphSettings.Defaults.HLine = value; - } + public static Rune HLine => GlyphSettings.Current.HLine; /// Box Drawings Vertical Line - Light (U+2502) - │ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLine - { - get => GlyphSettings.Defaults.VLine; - set => GlyphSettings.Defaults.VLine = value; - } + public static Rune VLine => GlyphSettings.Current.VLine; /// Box Drawings Double Horizontal (U+2550) - ═ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineDbl - { - get => GlyphSettings.Defaults.HLineDbl; - set => GlyphSettings.Defaults.HLineDbl = value; - } + public static Rune HLineDbl => GlyphSettings.Current.HLineDbl; /// Box Drawings Double Vertical (U+2551) - â•‘ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineDbl - - { - - get => GlyphSettings.Defaults.VLineDbl; - - set => GlyphSettings.Defaults.VLineDbl = value; - - } + public static Rune VLineDbl => GlyphSettings.Current.VLineDbl; /// Box Drawings Heavy Double Dash Horizontal (U+254D) - ╍ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineHvDa2 - - { - - get => GlyphSettings.Defaults.HLineHvDa2; - - set => GlyphSettings.Defaults.HLineHvDa2 = value; - - } + public static Rune HLineHvDa2 => GlyphSettings.Current.HLineHvDa2; /// Box Drawings Heavy Triple Dash Vertical (U+2507) - ┇ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineHvDa3 - { - get => GlyphSettings.Defaults.VLineHvDa3; - set => GlyphSettings.Defaults.VLineHvDa3 = value; - } + public static Rune VLineHvDa3 => GlyphSettings.Current.VLineHvDa3; /// Box Drawings Heavy Triple Dash Horizontal (U+2505) - â”… - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineHvDa3 - - { - - get => GlyphSettings.Defaults.HLineHvDa3; - - set => GlyphSettings.Defaults.HLineHvDa3 = value; - - } + public static Rune HLineHvDa3 => GlyphSettings.Current.HLineHvDa3; /// Box Drawings Heavy Quadruple Dash Horizontal (U+2509) - ┉ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineHvDa4 - - { - - get => GlyphSettings.Defaults.HLineHvDa4; - - set => GlyphSettings.Defaults.HLineHvDa4 = value; - - } + public static Rune HLineHvDa4 => GlyphSettings.Current.HLineHvDa4; /// Box Drawings Heavy Double Dash Vertical (U+254F) - ╏ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineHvDa2 - - { - - get => GlyphSettings.Defaults.VLineHvDa2; - - set => GlyphSettings.Defaults.VLineHvDa2 = value; - - } + public static Rune VLineHvDa2 => GlyphSettings.Current.VLineHvDa2; /// Box Drawings Heavy Quadruple Dash Vertical (U+250B) - ┋ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineHvDa4 - - { - - get => GlyphSettings.Defaults.VLineHvDa4; - - set => GlyphSettings.Defaults.VLineHvDa4 = value; - - } + public static Rune VLineHvDa4 => GlyphSettings.Current.VLineHvDa4; /// Box Drawings Light Double Dash Horizontal (U+254C) - ╌ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineDa2 - - { - - get => GlyphSettings.Defaults.HLineDa2; - - set => GlyphSettings.Defaults.HLineDa2 = value; - - } + public static Rune HLineDa2 => GlyphSettings.Current.HLineDa2; /// Box Drawings Light Triple Dash Vertical (U+2506) - ┆ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineDa3 - - { - - get => GlyphSettings.Defaults.VLineDa3; - - set => GlyphSettings.Defaults.VLineDa3 = value; - - } + public static Rune VLineDa3 => GlyphSettings.Current.VLineDa3; /// Box Drawings Light Triple Dash Horizontal (U+2504) - ┄ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineDa3 - - { - - get => GlyphSettings.Defaults.HLineDa3; - - set => GlyphSettings.Defaults.HLineDa3 = value; - - } + public static Rune HLineDa3 => GlyphSettings.Current.HLineDa3; /// Box Drawings Light Quadruple Dash Horizontal (U+2508) - ┈ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineDa4 - - { - - get => GlyphSettings.Defaults.HLineDa4; - - set => GlyphSettings.Defaults.HLineDa4 = value; - - } + public static Rune HLineDa4 => GlyphSettings.Current.HLineDa4; /// Box Drawings Light Double Dash Vertical (U+254E) - ╎ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineDa2 - - { - - get => GlyphSettings.Defaults.VLineDa2; - - set => GlyphSettings.Defaults.VLineDa2 = value; - - } + public static Rune VLineDa2 => GlyphSettings.Current.VLineDa2; /// Box Drawings Light Quadruple Dash Vertical (U+250A) - ┊ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineDa4 - - { - - get => GlyphSettings.Defaults.VLineDa4; - - set => GlyphSettings.Defaults.VLineDa4 = value; - - } + public static Rune VLineDa4 => GlyphSettings.Current.VLineDa4; /// Box Drawings Heavy Horizontal (U+2501) - ━ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineHv - - { - - get => GlyphSettings.Defaults.HLineHv; - - set => GlyphSettings.Defaults.HLineHv = value; - - } + public static Rune HLineHv => GlyphSettings.Current.HLineHv; /// Box Drawings Heavy Vertical (U+2503) - ┃ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineHv - - { - - get => GlyphSettings.Defaults.VLineHv; - - set => GlyphSettings.Defaults.VLineHv = value; - - } + public static Rune VLineHv => GlyphSettings.Current.VLineHv; /// Box Drawings Light Left (U+2574) - â•´ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfLeftLine - - { - - get => GlyphSettings.Defaults.HalfLeftLine; - - set => GlyphSettings.Defaults.HalfLeftLine = value; - - } + public static Rune HalfLeftLine => GlyphSettings.Current.HalfLeftLine; /// Box Drawings Light Vertical (U+2575) - ╵ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfTopLine - - { - - get => GlyphSettings.Defaults.HalfTopLine; - - set => GlyphSettings.Defaults.HalfTopLine = value; - - } + public static Rune HalfTopLine => GlyphSettings.Current.HalfTopLine; /// Box Drawings Light Horizontal (U+2576) - â•¶ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfRightLine - - { - - get => GlyphSettings.Defaults.HalfRightLine; - - set => GlyphSettings.Defaults.HalfRightLine = value; - - } + public static Rune HalfRightLine => GlyphSettings.Current.HalfRightLine; /// Box Drawings Light Down (U+2577) - â•· - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfBottomLine - - { - - get => GlyphSettings.Defaults.HalfBottomLine; - - set => GlyphSettings.Defaults.HalfBottomLine = value; - - } + public static Rune HalfBottomLine => GlyphSettings.Current.HalfBottomLine; /// Box Drawings Heavy Left (U+2578) - ╸ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfLeftLineHv - - { - - get => GlyphSettings.Defaults.HalfLeftLineHv; - - set => GlyphSettings.Defaults.HalfLeftLineHv = value; - - } + public static Rune HalfLeftLineHv => GlyphSettings.Current.HalfLeftLineHv; /// Box Drawings Heavy Vertical (U+2579) - ╹ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfTopLineHv - - { - - get => GlyphSettings.Defaults.HalfTopLineHv; - - set => GlyphSettings.Defaults.HalfTopLineHv = value; - - } + public static Rune HalfTopLineHv => GlyphSettings.Current.HalfTopLineHv; /// Box Drawings Heavy Horizontal (U+257A) - ╺ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfRightLineHv - - { - - get => GlyphSettings.Defaults.HalfRightLineHv; - - set => GlyphSettings.Defaults.HalfRightLineHv = value; - - } + public static Rune HalfRightLineHv => GlyphSettings.Current.HalfRightLineHv; /// Box Drawings Light Vertical and Horizontal (U+257B) - â•» - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfBottomLineLt - - { - - get => GlyphSettings.Defaults.HalfBottomLineLt; - - set => GlyphSettings.Defaults.HalfBottomLineLt = value; - - } + public static Rune HalfBottomLineLt => GlyphSettings.Current.HalfBottomLineLt; /// Box Drawings Light Horizontal and Heavy Horizontal (U+257C) - ╼ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightSideLineLtHv - - { - - get => GlyphSettings.Defaults.RightSideLineLtHv; - - set => GlyphSettings.Defaults.RightSideLineLtHv = value; - - } + public static Rune RightSideLineLtHv => GlyphSettings.Current.RightSideLineLtHv; /// Box Drawings Light Vertical and Heavy Horizontal (U+257D) - ╽ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomSideLineLtHv - - { - - get => GlyphSettings.Defaults.BottomSideLineLtHv; - - set => GlyphSettings.Defaults.BottomSideLineLtHv = value; - - } + public static Rune BottomSideLineLtHv => GlyphSettings.Current.BottomSideLineLtHv; /// Box Drawings Heavy Left and Light Horizontal (U+257E) - ╾ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftSideLineHvLt - - { - - get => GlyphSettings.Defaults.LeftSideLineHvLt; - - set => GlyphSettings.Defaults.LeftSideLineHvLt = value; - - } + public static Rune LeftSideLineHvLt => GlyphSettings.Current.LeftSideLineHvLt; /// Box Drawings Heavy Vertical and Light Horizontal (U+257F) - â•¿ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopSideLineHvLt - - { - - get => GlyphSettings.Defaults.TopSideLineHvLt; - - set => GlyphSettings.Defaults.TopSideLineHvLt = value; - - } + public static Rune TopSideLineHvLt => GlyphSettings.Current.TopSideLineHvLt; #endregion @@ -745,107 +281,35 @@ public static Rune TopSideLineHvLt /// Box Drawings Upper Left Corner - Light Vertical and Light Horizontal (U+250C) - ┌ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCorner - - { - - get => GlyphSettings.Defaults.ULCorner; - - set => GlyphSettings.Defaults.ULCorner = value; - - } + public static Rune ULCorner => GlyphSettings.Current.ULCorner; /// Box Drawings Upper Left Corner - Double (U+2554) - â•” - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerDbl - - { - - get => GlyphSettings.Defaults.ULCornerDbl; - - set => GlyphSettings.Defaults.ULCornerDbl = value; - - } + public static Rune ULCornerDbl => GlyphSettings.Current.ULCornerDbl; /// Box Drawings Upper Left Corner - Light Arc Down and Horizontal (U+256D) - â•­ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerR - - { - - get => GlyphSettings.Defaults.ULCornerR; - - set => GlyphSettings.Defaults.ULCornerR = value; - - } + public static Rune ULCornerR => GlyphSettings.Current.ULCornerR; /// Box Drawings Heavy Down and Horizontal (U+250F) - ┏ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerHv - - { - - get => GlyphSettings.Defaults.ULCornerHv; - - set => GlyphSettings.Defaults.ULCornerHv = value; - - } + public static Rune ULCornerHv => GlyphSettings.Current.ULCornerHv; /// Box Drawings Down Heavy and Horizontal Light (U+251E) - ┎ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerHvLt - - { - - get => GlyphSettings.Defaults.ULCornerHvLt; - - set => GlyphSettings.Defaults.ULCornerHvLt = value; - - } + public static Rune ULCornerHvLt => GlyphSettings.Current.ULCornerHvLt; /// Box Drawings Down Light and Horizontal Heavy (U+250D) - ┎ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerLtHv - - { - - get => GlyphSettings.Defaults.ULCornerLtHv; - - set => GlyphSettings.Defaults.ULCornerLtHv = value; - - } + public static Rune ULCornerLtHv => GlyphSettings.Current.ULCornerLtHv; /// Box Drawings Double Down and Single Horizontal (U+2553) - â•“ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerDblSingle - - { - - get => GlyphSettings.Defaults.ULCornerDblSingle; - - set => GlyphSettings.Defaults.ULCornerDblSingle = value; - - } + public static Rune ULCornerDblSingle => GlyphSettings.Current.ULCornerDblSingle; /// Box Drawings Single Down and Double Horizontal (U+2552) - â•’ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerSingleDbl - - { - - get => GlyphSettings.Defaults.ULCornerSingleDbl; - - set => GlyphSettings.Defaults.ULCornerSingleDbl = value; - - } + public static Rune ULCornerSingleDbl => GlyphSettings.Current.ULCornerSingleDbl; #endregion @@ -853,107 +317,35 @@ public static Rune ULCornerSingleDbl /// Box Drawings Lower Left Corner - Light Vertical and Light Horizontal (U+2514) - â”” - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCorner - - { - - get => GlyphSettings.Defaults.LLCorner; - - set => GlyphSettings.Defaults.LLCorner = value; - - } + public static Rune LLCorner => GlyphSettings.Current.LLCorner; /// Box Drawings Heavy Vertical and Horizontal (U+2517) - â”— - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerHv - - { - - get => GlyphSettings.Defaults.LLCornerHv; - - set => GlyphSettings.Defaults.LLCornerHv = value; - - } + public static Rune LLCornerHv => GlyphSettings.Current.LLCornerHv; /// Box Drawings Heavy Vertical and Horizontal Light (U+2516) - â”– - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerHvLt - - { - - get => GlyphSettings.Defaults.LLCornerHvLt; - - set => GlyphSettings.Defaults.LLCornerHvLt = value; - - } + public static Rune LLCornerHvLt => GlyphSettings.Current.LLCornerHvLt; /// Box Drawings Vertical Light and Horizontal Heavy (U+2511) - ┕ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerLtHv - - { - - get => GlyphSettings.Defaults.LLCornerLtHv; - - set => GlyphSettings.Defaults.LLCornerLtHv = value; - - } + public static Rune LLCornerLtHv => GlyphSettings.Current.LLCornerLtHv; /// Box Drawings Double Vertical and Double Left (U+255A) - ╚ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerDbl - - { - - get => GlyphSettings.Defaults.LLCornerDbl; - - set => GlyphSettings.Defaults.LLCornerDbl = value; - - } + public static Rune LLCornerDbl => GlyphSettings.Current.LLCornerDbl; /// Box Drawings Single Vertical and Double Left (U+2558) - ╘ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerSingleDbl - - { - - get => GlyphSettings.Defaults.LLCornerSingleDbl; - - set => GlyphSettings.Defaults.LLCornerSingleDbl = value; - - } + public static Rune LLCornerSingleDbl => GlyphSettings.Current.LLCornerSingleDbl; /// Box Drawings Double Down and Single Left (U+2559) - â•™ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerDblSingle - - { - - get => GlyphSettings.Defaults.LLCornerDblSingle; - - set => GlyphSettings.Defaults.LLCornerDblSingle = value; - - } + public static Rune LLCornerDblSingle => GlyphSettings.Current.LLCornerDblSingle; /// Box Drawings Upper Left Corner - Light Arc Down and Left (U+2570) - â•° - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerR - - { - - get => GlyphSettings.Defaults.LLCornerR; - - set => GlyphSettings.Defaults.LLCornerR = value; - - } + public static Rune LLCornerR => GlyphSettings.Current.LLCornerR; #endregion @@ -961,107 +353,35 @@ public static Rune LLCornerR /// Box Drawings Upper Horizontal Corner - Light Vertical and Light Horizontal (U+2510) - ┐ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCorner - - { - - get => GlyphSettings.Defaults.URCorner; - - set => GlyphSettings.Defaults.URCorner = value; - - } + public static Rune URCorner => GlyphSettings.Current.URCorner; /// Box Drawings Upper Horizontal Corner - Double Vertical and Double Horizontal (U+2557) - â•— - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerDbl - - { - - get => GlyphSettings.Defaults.URCornerDbl; - - set => GlyphSettings.Defaults.URCornerDbl = value; - - } + public static Rune URCornerDbl => GlyphSettings.Current.URCornerDbl; /// Box Drawings Upper Horizontal Corner - Light Arc Vertical and Horizontal (U+256E) - â•® - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerR - - { - - get => GlyphSettings.Defaults.URCornerR; - - set => GlyphSettings.Defaults.URCornerR = value; - - } + public static Rune URCornerR => GlyphSettings.Current.URCornerR; /// Box Drawings Heavy Down and Left (U+2513) - ┓ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerHv - - { - - get => GlyphSettings.Defaults.URCornerHv; - - set => GlyphSettings.Defaults.URCornerHv = value; - - } + public static Rune URCornerHv => GlyphSettings.Current.URCornerHv; /// Box Drawings Heavy Vertical and Left Down Light (U+2511) - ┑ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerHvLt - - { - - get => GlyphSettings.Defaults.URCornerHvLt; - - set => GlyphSettings.Defaults.URCornerHvLt = value; - - } + public static Rune URCornerHvLt => GlyphSettings.Current.URCornerHvLt; /// Box Drawings Down Light and Horizontal Heavy (U+2514) - â”’ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerLtHv - - { - - get => GlyphSettings.Defaults.URCornerLtHv; - - set => GlyphSettings.Defaults.URCornerLtHv = value; - - } + public static Rune URCornerLtHv => GlyphSettings.Current.URCornerLtHv; /// Box Drawings Double Vertical and Single Left (U+2556) - â•– - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerDblSingle - - { - - get => GlyphSettings.Defaults.URCornerDblSingle; - - set => GlyphSettings.Defaults.URCornerDblSingle = value; - - } + public static Rune URCornerDblSingle => GlyphSettings.Current.URCornerDblSingle; /// Box Drawings Single Vertical and Double Left (U+2555) - â•• - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerSingleDbl - - { - - get => GlyphSettings.Defaults.URCornerSingleDbl; - - set => GlyphSettings.Defaults.URCornerSingleDbl = value; - - } + public static Rune URCornerSingleDbl => GlyphSettings.Current.URCornerSingleDbl; #endregion @@ -1069,107 +389,35 @@ public static Rune URCornerSingleDbl /// Box Drawings Lower Right Corner - Light (U+2518) - ┘ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCorner - - { - - get => GlyphSettings.Defaults.LRCorner; - - set => GlyphSettings.Defaults.LRCorner = value; - - } + public static Rune LRCorner => GlyphSettings.Current.LRCorner; /// Box Drawings Lower Right Corner - Double (U+255D) - ╝ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerDbl - - { - - get => GlyphSettings.Defaults.LRCornerDbl; - - set => GlyphSettings.Defaults.LRCornerDbl = value; - - } + public static Rune LRCornerDbl => GlyphSettings.Current.LRCornerDbl; /// Box Drawings Lower Right Corner - Rounded (U+256F) - ╯ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerR - - { - - get => GlyphSettings.Defaults.LRCornerR; - - set => GlyphSettings.Defaults.LRCornerR = value; - - } + public static Rune LRCornerR => GlyphSettings.Current.LRCornerR; /// Box Drawings Lower Right Corner - Heavy (U+251B) - â”› - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerHv - - { - - get => GlyphSettings.Defaults.LRCornerHv; - - set => GlyphSettings.Defaults.LRCornerHv = value; - - } + public static Rune LRCornerHv => GlyphSettings.Current.LRCornerHv; /// Box Drawings Lower Right Corner - Double Vertical and Single Horizontal (U+255C) - ╜ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerDblSingle - - { - - get => GlyphSettings.Defaults.LRCornerDblSingle; - - set => GlyphSettings.Defaults.LRCornerDblSingle = value; - - } + public static Rune LRCornerDblSingle => GlyphSettings.Current.LRCornerDblSingle; /// Box Drawings Lower Right Corner - Single Vertical and Double Horizontal (U+255B) - â•› - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerSingleDbl - - { - - get => GlyphSettings.Defaults.LRCornerSingleDbl; - - set => GlyphSettings.Defaults.LRCornerSingleDbl = value; - - } + public static Rune LRCornerSingleDbl => GlyphSettings.Current.LRCornerSingleDbl; /// Box Drawings Lower Right Corner - Light Vertical and Heavy Horizontal (U+2519) - â”™ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerLtHv - - { - - get => GlyphSettings.Defaults.LRCornerLtHv; - - set => GlyphSettings.Defaults.LRCornerLtHv = value; - - } + public static Rune LRCornerLtHv => GlyphSettings.Current.LRCornerLtHv; /// Box Drawings Lower Right Corner - Heavy Vertical and Light Horizontal (U+251A) - ┚ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerHvLt - - { - - get => GlyphSettings.Defaults.LRCornerHvLt; - - set => GlyphSettings.Defaults.LRCornerHvLt = value; - - } + public static Rune LRCornerHvLt => GlyphSettings.Current.LRCornerHvLt; #endregion @@ -1177,367 +425,115 @@ public static Rune LRCornerHvLt /// Box Drawings Left Tee - Single Vertical and Single Horizontal (U+251C) - ├ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTee - - { - - get => GlyphSettings.Defaults.LeftTee; - - set => GlyphSettings.Defaults.LeftTee = value; - - } + public static Rune LeftTee => GlyphSettings.Current.LeftTee; /// Box Drawings Left Tee - Single Vertical and Double Horizontal (U+255E) - ╞ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeDblH - - { - - get => GlyphSettings.Defaults.LeftTeeDblH; - - set => GlyphSettings.Defaults.LeftTeeDblH = value; - - } + public static Rune LeftTeeDblH => GlyphSettings.Current.LeftTeeDblH; /// Box Drawings Left Tee - Double Vertical and Single Horizontal (U+255F) - ╟ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeDblV - - { - - get => GlyphSettings.Defaults.LeftTeeDblV; - - set => GlyphSettings.Defaults.LeftTeeDblV = value; - - } + public static Rune LeftTeeDblV => GlyphSettings.Current.LeftTeeDblV; /// Box Drawings Left Tee - Double Vertical and Double Horizontal (U+2560) - â•  - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeDbl - - { - - get => GlyphSettings.Defaults.LeftTeeDbl; - - set => GlyphSettings.Defaults.LeftTeeDbl = value; - - } + public static Rune LeftTeeDbl => GlyphSettings.Current.LeftTeeDbl; /// Box Drawings Left Tee - Heavy Horizontal and Light Vertical (U+2523) - ┝ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeHvH - - { - - get => GlyphSettings.Defaults.LeftTeeHvH; - - set => GlyphSettings.Defaults.LeftTeeHvH = value; - - } + public static Rune LeftTeeHvH => GlyphSettings.Current.LeftTeeHvH; /// Box Drawings Left Tee - Light Horizontal and Heavy Vertical (U+252B) - â”  - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeHvV - - { - - get => GlyphSettings.Defaults.LeftTeeHvV; - - set => GlyphSettings.Defaults.LeftTeeHvV = value; - - } + public static Rune LeftTeeHvV => GlyphSettings.Current.LeftTeeHvV; /// Box Drawings Left Tee - Heavy Vertical and Heavy Horizontal (U+2527) - ┣ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeHvDblH - - { - - get => GlyphSettings.Defaults.LeftTeeHvDblH; - - set => GlyphSettings.Defaults.LeftTeeHvDblH = value; - - } + public static Rune LeftTeeHvDblH => GlyphSettings.Current.LeftTeeHvDblH; /// Box Drawings Right Tee - Single Vertical and Single Horizontal (U+2524) - ┤ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTee - - { - - get => GlyphSettings.Defaults.RightTee; - - set => GlyphSettings.Defaults.RightTee = value; - - } + public static Rune RightTee => GlyphSettings.Current.RightTee; /// Box Drawings Right Tee - Single Vertical and Double Horizontal (U+2561) - â•¡ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeDblH - - { - - get => GlyphSettings.Defaults.RightTeeDblH; - - set => GlyphSettings.Defaults.RightTeeDblH = value; - - } + public static Rune RightTeeDblH => GlyphSettings.Current.RightTeeDblH; /// Box Drawings Right Tee - Double Vertical and Single Horizontal (U+2562) - â•¢ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeDblV - - { - - get => GlyphSettings.Defaults.RightTeeDblV; - - set => GlyphSettings.Defaults.RightTeeDblV = value; - - } + public static Rune RightTeeDblV => GlyphSettings.Current.RightTeeDblV; /// Box Drawings Right Tee - Double Vertical and Double Horizontal (U+2563) - â•£ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeDbl - - { - - get => GlyphSettings.Defaults.RightTeeDbl; - - set => GlyphSettings.Defaults.RightTeeDbl = value; - - } + public static Rune RightTeeDbl => GlyphSettings.Current.RightTeeDbl; /// Box Drawings Right Tee - Heavy Horizontal and Light Vertical (U+2528) - ┥ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeHvH - - { - - get => GlyphSettings.Defaults.RightTeeHvH; - - set => GlyphSettings.Defaults.RightTeeHvH = value; - - } + public static Rune RightTeeHvH => GlyphSettings.Current.RightTeeHvH; /// Box Drawings Right Tee - Light Horizontal and Heavy Vertical (U+2530) - ┨ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeHvV - - { - - get => GlyphSettings.Defaults.RightTeeHvV; - - set => GlyphSettings.Defaults.RightTeeHvV = value; - - } + public static Rune RightTeeHvV => GlyphSettings.Current.RightTeeHvV; /// Box Drawings Right Tee - Heavy Vertical and Heavy Horizontal (U+252C) - ┫ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeHvDblH - - { - - get => GlyphSettings.Defaults.RightTeeHvDblH; - - set => GlyphSettings.Defaults.RightTeeHvDblH = value; - - } + public static Rune RightTeeHvDblH => GlyphSettings.Current.RightTeeHvDblH; /// Box Drawings Top Tee - Single Vertical and Single Horizontal (U+252C) - ┬ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTee - - { - - get => GlyphSettings.Defaults.TopTee; - - set => GlyphSettings.Defaults.TopTee = value; - - } + public static Rune TopTee => GlyphSettings.Current.TopTee; /// Box Drawings Top Tee - Single Vertical and Double Horizontal (U+2564) - ╤ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeDblH - - { - - get => GlyphSettings.Defaults.TopTeeDblH; - - set => GlyphSettings.Defaults.TopTeeDblH = value; - - } + public static Rune TopTeeDblH => GlyphSettings.Current.TopTeeDblH; /// Box Drawings Top Tee - Double Vertical and Single Horizontal (U+2565) - â•¥ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeDblV - - { - - get => GlyphSettings.Defaults.TopTeeDblV; - - set => GlyphSettings.Defaults.TopTeeDblV = value; - - } + public static Rune TopTeeDblV => GlyphSettings.Current.TopTeeDblV; /// Box Drawings Top Tee - Double Vertical and Double Horizontal (U+2566) - ╦ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeDbl - - { - - get => GlyphSettings.Defaults.TopTeeDbl; - - set => GlyphSettings.Defaults.TopTeeDbl = value; - - } + public static Rune TopTeeDbl => GlyphSettings.Current.TopTeeDbl; /// Box Drawings Top Tee - Heavy Horizontal and Light Vertical (U+252F) - ┯ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeHvH - - { - - get => GlyphSettings.Defaults.TopTeeHvH; - - set => GlyphSettings.Defaults.TopTeeHvH = value; - - } + public static Rune TopTeeHvH => GlyphSettings.Current.TopTeeHvH; /// Box Drawings Top Tee - Light Horizontal and Heavy Vertical (U+2537) - â”° - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeHvV - - { - - get => GlyphSettings.Defaults.TopTeeHvV; - - set => GlyphSettings.Defaults.TopTeeHvV = value; - - } + public static Rune TopTeeHvV => GlyphSettings.Current.TopTeeHvV; /// Box Drawings Top Tee - Heavy Vertical and Heavy Horizontal (U+2533) - ┳ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeHvDblH - - { - - get => GlyphSettings.Defaults.TopTeeHvDblH; - - set => GlyphSettings.Defaults.TopTeeHvDblH = value; - - } + public static Rune TopTeeHvDblH => GlyphSettings.Current.TopTeeHvDblH; /// Box Drawings Bottom Tee - Single Vertical and Single Horizontal (U+2534) - â”´ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTee - - { - - get => GlyphSettings.Defaults.BottomTee; - - set => GlyphSettings.Defaults.BottomTee = value; - - } + public static Rune BottomTee => GlyphSettings.Current.BottomTee; /// Box Drawings Bottom Tee - Single Vertical and Double Horizontal (U+2567) - â•§ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeDblH - - { - - get => GlyphSettings.Defaults.BottomTeeDblH; - - set => GlyphSettings.Defaults.BottomTeeDblH = value; - - } + public static Rune BottomTeeDblH => GlyphSettings.Current.BottomTeeDblH; /// Box Drawings Bottom Tee - Double Vertical and Single Horizontal (U+2568) - ╨ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeDblV - - { - - get => GlyphSettings.Defaults.BottomTeeDblV; - - set => GlyphSettings.Defaults.BottomTeeDblV = value; - - } + public static Rune BottomTeeDblV => GlyphSettings.Current.BottomTeeDblV; /// Box Drawings Bottom Tee - Double Vertical and Double Horizontal (U+2569) - â•© - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeDbl - - { - - get => GlyphSettings.Defaults.BottomTeeDbl; - - set => GlyphSettings.Defaults.BottomTeeDbl = value; - - } + public static Rune BottomTeeDbl => GlyphSettings.Current.BottomTeeDbl; /// Box Drawings Bottom Tee - Heavy Horizontal and Light Vertical (U+2535) - â”· - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeHvH - - { - - get => GlyphSettings.Defaults.BottomTeeHvH; - - set => GlyphSettings.Defaults.BottomTeeHvH = value; - - } + public static Rune BottomTeeHvH => GlyphSettings.Current.BottomTeeHvH; /// Box Drawings Bottom Tee - Light Horizontal and Heavy Vertical (U+253D) - ┸ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeHvV - - { - - get => GlyphSettings.Defaults.BottomTeeHvV; - - set => GlyphSettings.Defaults.BottomTeeHvV = value; - - } + public static Rune BottomTeeHvV => GlyphSettings.Current.BottomTeeHvV; /// Box Drawings Bottom Tee - Heavy Vertical and Heavy Horizontal (U+2539) - â”» - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeHvDblH - - { - - get => GlyphSettings.Defaults.BottomTeeHvDblH; - - set => GlyphSettings.Defaults.BottomTeeHvDblH = value; - - } + public static Rune BottomTeeHvDblH => GlyphSettings.Current.BottomTeeHvDblH; #endregion @@ -1545,94 +541,31 @@ public static Rune BottomTeeHvDblH /// Box Drawings Cross - Single Vertical and Single Horizontal (U+253C) - ┼ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Cross - - { - - get => GlyphSettings.Defaults.Cross; - - set => GlyphSettings.Defaults.Cross = value; - - } + public static Rune Cross => GlyphSettings.Current.Cross; /// Box Drawings Cross - Single Vertical and Double Horizontal (U+256A) - ╪ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossDblH - - { - - get => GlyphSettings.Defaults.CrossDblH; - - set => GlyphSettings.Defaults.CrossDblH = value; - - } + public static Rune CrossDblH => GlyphSettings.Current.CrossDblH; /// Box Drawings Cross - Double Vertical and Single Horizontal (U+256B) - â•« - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossDblV - - { - - get => GlyphSettings.Defaults.CrossDblV; - - set => GlyphSettings.Defaults.CrossDblV = value; - - } + public static Rune CrossDblV => GlyphSettings.Current.CrossDblV; /// Box Drawings Cross - Double Vertical and Double Horizontal (U+256C) - ╬ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossDbl - - { - - get => GlyphSettings.Defaults.CrossDbl; - - set => GlyphSettings.Defaults.CrossDbl = value; - - } + public static Rune CrossDbl => GlyphSettings.Current.CrossDbl; /// Box Drawings Cross - Heavy Horizontal and Light Vertical (U+253F) - ┿ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossHvH - - { - - get => GlyphSettings.Defaults.CrossHvH; - - set => GlyphSettings.Defaults.CrossHvH = value; - - } + public static Rune CrossHvH => GlyphSettings.Current.CrossHvH; /// Box Drawings Cross - Light Horizontal and Heavy Vertical (U+2541) - â•‚ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossHvV - - { - - get => GlyphSettings.Defaults.CrossHvV; - - set => GlyphSettings.Defaults.CrossHvV = value; - - } + public static Rune CrossHvV => GlyphSettings.Current.CrossHvV; /// Box Drawings Cross - Heavy Vertical and Heavy Horizontal (U+254B) - â•‹ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossHv - - { - - get => GlyphSettings.Defaults.CrossHv; - - set => GlyphSettings.Defaults.CrossHv = value; - - } + public static Rune CrossHv => GlyphSettings.Current.CrossHv; #endregion @@ -1640,68 +573,23 @@ public static Rune CrossHv /// Shadow - Vertical Start - Left Half Block - â–Œ U+0258c - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowVerticalStart // Half: '\u2596' â––; - - { - - get => GlyphSettings.Defaults.ShadowVerticalStart; - - set => GlyphSettings.Defaults.ShadowVerticalStart = value; - - } + public static Rune ShadowVerticalStart => GlyphSettings.Current.ShadowVerticalStart; /// Shadow - Vertical - Left Half Block - â–Œ U+0258c - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowVertical - - { - - get => GlyphSettings.Defaults.ShadowVertical; - - set => GlyphSettings.Defaults.ShadowVertical = value; - - } + public static Rune ShadowVertical => GlyphSettings.Current.ShadowVertical; /// Shadow - Horizontal Start - Upper Half Block - â–€ U+02580 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowHorizontalStart // Half: ▝ U+0259d; - - { - - get => GlyphSettings.Defaults.ShadowHorizontalStart; - - set => GlyphSettings.Defaults.ShadowHorizontalStart = value; - - } + public static Rune ShadowHorizontalStart => GlyphSettings.Current.ShadowHorizontalStart; /// Shadow - Horizontal - Upper Half Block - â–€ U+02580 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowHorizontal - - { - - get => GlyphSettings.Defaults.ShadowHorizontal; - - set => GlyphSettings.Defaults.ShadowHorizontal = value; - - } + public static Rune ShadowHorizontal => GlyphSettings.Current.ShadowHorizontal; /// Shadow - Horizontal End - Quadrant Upper Left - â–˜ U+02598 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowHorizontalEnd - - { - - get => GlyphSettings.Defaults.ShadowHorizontalEnd; - - set => GlyphSettings.Defaults.ShadowHorizontalEnd = value; - - } + public static Rune ShadowHorizontalEnd => GlyphSettings.Current.ShadowHorizontalEnd; #endregion } \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs index bad5ec8d3b..afbfe60067 100644 --- a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs @@ -240,7 +240,7 @@ public void Load_WithNullResourceName_ReturnsFalse () Assert.False (result); } - [Fact] + [Fact (Skip = "A2.2: Glyphs lost [ConfigurationProperty]; Resources/config.json Glyphs.X flat keys are now CM-unknown. Test removed with CM in step D.")] public void Load_WithValidResource_UpdatesSettingsScope () { // Arrange @@ -260,7 +260,7 @@ public void Load_WithValidResource_UpdatesSettingsScope () // Verify settingsScope is updated as expected } - [Fact] + [Fact (Skip = "A2.2: Glyphs lost [ConfigurationProperty]; Resources/config.json Glyphs.X flat keys are now CM-unknown. Test removed with CM in step D.")] public void Load_Runtime_Overrides () { // Arrange @@ -398,7 +398,7 @@ public void Load_WithDifferentLocations_AddsAllSourcesToCollection () } } - [Fact] + [Fact (Skip = "A2.2: Glyphs lost [ConfigurationProperty]; Resources/config.json Glyphs.X flat keys are now CM-unknown. Test removed with CM in step D.")] public void Load_AddsResourceSourceToCollection () { // Arrange diff --git a/specs/remove-legacy-cm.md b/specs/remove-legacy-cm.md index 149e6cfd1a..e06b376e99 100644 --- a/specs/remove-legacy-cm.md +++ b/specs/remove-legacy-cm.md @@ -88,7 +88,7 @@ The exhaustive inventory of what still has to go. Discovered by `grep`ping the w | `SourceGenerationContext.cs` | Keep. Re-audit `[JsonSerializable]` entries — remove any that reference `SettingsScope` / `ThemeScope` / `AppSettingsScope`. Add `ThemeSettings` and the per-component settings POCOs. | | `ThemeChanges.cs` | **Already exists post-#5411.** Keep as the supported observer facade. In this PR: remove the `ConfigurationManager.Applied` bridge inside it (one branch of an `OR`) once `ConfigurationManager` is deleted. | | `AttributeJsonConverter.cs`, `SchemeJsonConverter.cs`, `RuneJsonConverter.cs`, `KeyJsonConverter.cs`, `ColorJsonConverter.cs`, `TraceCategoryJsonConverter.cs`, `DictionaryJsonConverter.cs`, `ConcurrentDictionaryJsonConverter.cs` | Keep. **Already point at `TuiSerializerContext.Instance` post-#5411.** No further work. | -| `Settings/*Settings.cs` (all 30+) | Keep. These are the POCOs that became the source of truth in #5411. | +| `Settings/*Settings.cs` (all 30+) | Keep. These are the POCOs that became the source of truth in #5411. **Pattern divergence (A2.1+):** ThemeScope POCOs (the 17/18 bind targets of `BindThemeScope`) are immutable `sealed record` + `Default`/`Current` with `Volatile`-swapped atomic publish; SettingsScope POCOs remain mutable `Defaults`. Rationale: only ThemeScope participates in theme overlay merge; SettingsScope is bound once at app start and never per-theme. | | `Settings/TuiConfigurationBuilder.cs` | Keep. Becomes the **only** configuration entry point. | | `Settings/TuiConfigurationExtensions.cs` | Keep. | | `Settings/IThemeManager.cs`, `Settings/ISchemeManager.cs`, `Settings/MecThemeManager.cs`, `Settings/MecSchemeManager.cs` | Keep. **A1 (event plumbing) is done post-#5411.** A2 (data ownership) is this PR's work — see §5.2. | From f85e930c3d095ff6c19b8328e43ee27040880162 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 27 May 2026 17:56:08 -0600 Subject: [PATCH 12/13] A2.3: NerdFonts facade redesign Mops up the small static facade for NerdFonts.Enable, matching the A2.2 pattern used for the Glyphs facade. NerdFontsSettings was already converted to a sealed record + Default/Current in A2.1; A2.3 just removes the CM-reflection scaffolding on the consumer-facing static facade. Changes ======= Terminal.Gui/Text/NerdFonts.cs - NerdFonts.Enable rewritten from [ConfigurationProperty (Scope = typeof (ThemeScope))] public static bool Enable { get => NerdFontsSettings.Current.Enable; set => NerdFontsSettings.Current = NerdFontsSettings.Current with { Enable = value }; } to a bare expression-bodied reader: public static bool Enable => NerdFontsSettings.Current.Enable; - [ConfigurationProperty] attribute removed. - `with`-swap setter removed; NerdFontsSettings.Current is now exclusively written by MecThemeManager via BindThemeScope. - Caller surface unchanged: every NerdFonts.Enable reader keeps working; only the host changed. Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs - Drops `typeof (NerdFonts)` from the `_types` list and removes the matching [DynamicDependency (PRESERVED_MEMBERS, typeof (NerdFonts))]. NerdFonts is no longer a CM reflection host. No Resources/config.json change needed ====================================== Grep against `Resources/config.json` for `NerdFonts` returns zero matches; the file has never carried NerdFonts.X overrides. The A2.2 non-Default-theme dormancy footnote therefore does not apply here. NerdFonts.Enable resolves to the C# init default (NerdFontsSettings.Default.Enable = false) at startup and remains so unless a consumer assigns NerdFontsSettings.Current via MEC binding, which today happens only via TuiConfigurationBuilder's BindThemeScope against a (currently absent) MEC section. Test results ============ Tests/UnitTestsParallelizable: total 17292 / 17272 passed / 0 failed / 20 skipped (unchanged from A2.2; no new skips, no regressions). Tests/UnitTests.NonParallelizable: total 74 / 72 passed / 0 failed / 2 skipped (unchanged). Design context ============== This is commit A2.3 of the A2 series (POCO ownership migration on PR #5416, stacked on copilot/replace-cm-with-mec). A2.1 (2f7c13a6e) landed the 17 ThemeScope POCOs and BindThemeScope. A2.2 (441ef60f3 post-amend) landed the 18th (GlyphSettings) + Glyphs facade. A2.3 mops up NerdFonts. A2.4 will remove dead public static setters on Button.DefaultShadow etc. Cross-session review (PR #5411 owner) signed off on: - One-line reader pattern for NerdFonts.Enable; drop the `with`-swap setter that bridged CM's reflection write path. - ConfigPropertyHostTypes row removal mirrors A2.2's Glyphs treatment. Deferred (per A2 contract) ========================== - A2.4: removal of dead public static setters on view facades (Button.DefaultShadow, etc.) -- next commit. - Step D: CM deletion, config.json schema rewrite, BindThemeScope suppression re-justification. Refs: A2.1 = 2f7c13a6e, A2.2 = 441ef60f3, stacked on copilot/replace-cm-with-mec. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs | 2 -- Terminal.Gui/Text/NerdFonts.cs | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs index 78c0d1433c..5da01848e1 100644 --- a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs +++ b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs @@ -42,7 +42,6 @@ internal static class ConfigPropertyHostTypes typeof (Color), typeof (Driver), typeof (Key), - typeof (NerdFonts), typeof (Trace), typeof (View), typeof (BorderView), @@ -73,7 +72,6 @@ internal static class ConfigPropertyHostTypes [DynamicDependency (PRESERVED_MEMBERS, typeof (Color))] [DynamicDependency (PRESERVED_MEMBERS, typeof (Driver))] [DynamicDependency (PRESERVED_MEMBERS, typeof (Key))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (NerdFonts))] [DynamicDependency (PRESERVED_MEMBERS, typeof (Trace))] [DynamicDependency (PRESERVED_MEMBERS, typeof (View))] [DynamicDependency (PRESERVED_MEMBERS, typeof (BorderView))] diff --git a/Terminal.Gui/Text/NerdFonts.cs b/Terminal.Gui/Text/NerdFonts.cs index e01d73507d..dd1d05a1c3 100644 --- a/Terminal.Gui/Text/NerdFonts.cs +++ b/Terminal.Gui/Text/NerdFonts.cs @@ -15,12 +15,7 @@ internal class NerdFonts /// If , enables the use of Nerd unicode symbols. This requires specific font(s) to be /// installed on the users machine to work correctly. Defaults to . /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static bool Enable - { - get => NerdFontsSettings.Current.Enable; - set => NerdFontsSettings.Current = NerdFontsSettings.Current with { Enable = value }; - } + public static bool Enable => NerdFontsSettings.Current.Enable; /// Mapping of file extension to name. public Dictionary ExtensionToIcon { get; set; } = new () From 5197696d7c9db4c3f32e4a68fd3b22170c390498 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 27 May 2026 18:03:34 -0600 Subject: [PATCH 13/13] A2.4: Remove dead public static setters on view facades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes [ConfigurationProperty (Scope = typeof (ThemeScope))] from all view-class static facade properties (Button.DefaultShadow, Dialog.DefaultBorderStyle, etc.) and collapses their bridge get/set bodies to bare expression-bodied readers, matching the A2.2 Glyphs and A2.3 NerdFonts patterns. These properties existed solely as CM-reflection bind targets — the `with`-swap setter (`Current = Current with { X = value }`) was the only writer, called by ConfigProperty.Apply via PropertyInfo.SetValue against the embedded Resources/config.json flat keys. Audit confirmed zero external callers of the setters; tests that read the getters (e.g. ButtonTests asserting `button.ShadowStyle == Button.DefaultShadow`) continue to work — only the host plumbing changed. Net effect at runtime ===================== For every affected property: - Default theme: value comes from `Settings.Default.`'s C# init default. Unchanged. - Non-Default themes (Dark, Light, TurboPascal 5, Anders, etc.): flat-key overrides like `"Button.DefaultShadow": "Opaque"` in Resources/config.json are dormant from this commit through step D. Same scope as the A2.2 Glyphs dormancy: CM path no longer matches these keys (the [ConfigurationProperty] hosts are gone) and MEC path can't read them (the JSON is still flat-keyed, not nested). Step D rewrites Resources/config.json to nested form, which reactivates non-Default theme view-facade overrides for all theme-overlay POCOs uniformly via the existing BindThemeScope / / etc. calls in TuiConfigurationBuilder.ApplyToStaticFacades. Files changed ============= Terminal.Gui/Views/Button.cs (2 props) Terminal.Gui/Views/CheckBox.cs (1 prop) Terminal.Gui/Views/CharMap/CharMap.cs (1 prop) Terminal.Gui/Views/Dialog.cs (4 props) Terminal.Gui/Views/FrameView.cs (1 prop) Terminal.Gui/Views/HexView.cs (1 prop) Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs (1 prop) Terminal.Gui/Views/Menu/Menu.cs (1 prop) Terminal.Gui/Views/Menu/MenuBar.cs (1 prop) Terminal.Gui/Views/MessageBox.cs (2 props) Terminal.Gui/Views/Selectors/SelectorBase.cs (1 prop) Terminal.Gui/Views/StatusBar.cs (1 prop) Terminal.Gui/Views/TextInput/TextField/TextField.cs (1 prop) Terminal.Gui/Views/TextInput/TextView/TextView.cs (1 prop) Terminal.Gui/Views/Window.cs (2 props) All ThemeScope-scoped view-facade props converted; total 21 properties across 15 files. Pattern per property: Before: /// ... [ConfigurationProperty (Scope = typeof (ThemeScope))] public static T Name { get => XSettings.Current.Name; set => XSettings.Current = XSettings.Current with { Name = value }; } After: /// ... public static T Name => XSettings.Current.Name; Out of scope ============ SettingsScope-scoped [ConfigurationProperty] on view classes (FileDialog.MaxSearchResults, FileDialogStyle.DefaultUseColors, MenuBar.DefaultKey, PopoverMenu.DefaultKey, View.DefaultMouseBindings, View.ViewMouseBindings, BorderView.DefaultMouseBindings) are NOT touched. SettingsScope follows the mutable-Defaults pattern (per A2.1's divergence note in specs/remove-legacy-cm.md §4.2) and remains CM-managed until step D. Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs - Drops 14 entries (typeof + matching [DynamicDependency]) for types whose only [ConfigurationProperty] attrs were ThemeScope and are therefore now empty hosts: Button, CharMap, CheckBox, Dialog, FrameView, HexView, LinearRangeDefaults, Menu, MessageBox, SelectorBase, StatusBar, TextField, TextView, Window. - Keeps entries that still hold SettingsScope [ConfigurationProperty]: FileDialog, FileDialogStyle, MenuBar, PopoverMenu, View, BorderView, plus the unchanged Application / Color / Driver / Key / Trace / ConfigurationManager / SchemeManager / ThemeManager facades. Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs - Drops the one InlineData row that exercised CM's ScopeJsonConverter with `"Dialog.DefaultButtonAlignment": "End"` (Dialog.DefaultButtonAlignment is one of the 21 properties this commit removes [ConfigurationProperty] from; the converter now rejects the key as Unknown). Comment notes the rationale and the expected removal alongside CM in step D. Sibling InlineData rows that don't reference dropped facade props continue to test the converter. Test results ============ Tests/UnitTestsParallelizable: total 17291 / 17271 passed / 0 failed / 20 skipped (one fewer test row vs. A2.3 baseline because the ScopeJsonConverter InlineData row was removed by design; no failures, no new skips, ConfigPropertyHostTypes drift-detector still green because it tracks reflected hosts and we removed matching list entries in lockstep). Tests/UnitTests.NonParallelizable: total 74 / 72 passed / 0 failed / 2 skipped (unchanged). Design context ============== This is commit A2.4 of the A2 series (POCO ownership migration on PR #5416, stacked on copilot/replace-cm-with-mec). A2.1 (2f7c13a6e) landed the 17 ThemeScope POCOs and BindThemeScope. A2.2 (441ef60f3) landed GlyphSettings + Glyphs facade. A2.3 (f85e930c3) mopped up NerdFonts. A2.4 finishes the series by removing the dead view-facade setter scaffolding. A2 status: complete. The cleared-out view-facade properties leave ButtonSettings, CheckBoxSettings, DialogSettings, FrameViewSettings, etc. as the sole owners of theme-overlayed state; MecThemeManager mutates `Settings.Current` exclusively via BindThemeScope intra-assembly; consumer reads go through the bare expression-bodied getters on the view facades or directly through `Settings.Current`. Cross-session review (PR #5411 owner) signed off on: - Removal of public static setters on view facades; "zero external callers" audit accepted. - Inheriting the A2.2 dormancy framing — Dark/Light/etc. theme overrides for view facades are dormant from A2.4 through step D's config.json rewrite. Same window as Glyphs. Deferred to step D / later commits ================================== - Resources/config.json rewrite to nested form (B/D). - CM deletion (D). - BindThemeScope [UnconditionalSuppressMessage] re-justification once ConfigPropertyHostTypes goes (D). - ThemeSettings record + Current conversion (future micro-commit). - Mec* prefix drops on the manager types (post-D). Refs: A2.1 = 2f7c13a6e, A2.2 = 441ef60f3, A2.3 = f85e930c3, stacked on copilot/replace-cm-with-mec. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Configuration/ConfigPropertyHostTypes.cs | 28 ------------------- Terminal.Gui/Views/Button.cs | 14 ++-------- Terminal.Gui/Views/CharMap/CharMap.cs | 7 +---- Terminal.Gui/Views/CheckBox.cs | 7 +---- Terminal.Gui/Views/Dialog.cs | 28 +++---------------- Terminal.Gui/Views/FrameView.cs | 7 +---- Terminal.Gui/Views/HexView.cs | 7 +---- .../Views/LinearRange/LinearRangeDefaults.cs | 7 +---- Terminal.Gui/Views/Menu/Menu.cs | 7 +---- Terminal.Gui/Views/Menu/MenuBar.cs | 7 +---- Terminal.Gui/Views/MessageBox.cs | 14 ++-------- Terminal.Gui/Views/Selectors/SelectorBase.cs | 7 +---- Terminal.Gui/Views/StatusBar.cs | 7 +---- .../Views/TextInput/TextField/TextField.cs | 7 +---- .../Views/TextInput/TextView/TextView.cs | 7 +---- Terminal.Gui/Views/Window.cs | 14 ++-------- .../Configuration/ScopeJsonConverterTests.cs | 4 ++- 17 files changed, 24 insertions(+), 155 deletions(-) diff --git a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs index 5da01848e1..87febde13f 100644 --- a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs +++ b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs @@ -45,24 +45,10 @@ internal static class ConfigPropertyHostTypes typeof (Trace), typeof (View), typeof (BorderView), - typeof (Button), - typeof (CharMap), - typeof (CheckBox), - typeof (Dialog), typeof (FileDialog), typeof (FileDialogStyle), - typeof (FrameView), - typeof (HexView), - typeof (LinearRangeDefaults), - typeof (Menu), typeof (MenuBar), - typeof (MessageBox), typeof (PopoverMenu), - typeof (SelectorBase), - typeof (StatusBar), - typeof (TextField), - typeof (TextView), - typeof (Window) ]; [DynamicDependency (PRESERVED_MEMBERS, typeof (Application))] @@ -75,23 +61,9 @@ internal static class ConfigPropertyHostTypes [DynamicDependency (PRESERVED_MEMBERS, typeof (Trace))] [DynamicDependency (PRESERVED_MEMBERS, typeof (View))] [DynamicDependency (PRESERVED_MEMBERS, typeof (BorderView))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Button))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (CharMap))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (CheckBox))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Dialog))] [DynamicDependency (PRESERVED_MEMBERS, typeof (FileDialog))] [DynamicDependency (PRESERVED_MEMBERS, typeof (FileDialogStyle))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (FrameView))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (HexView))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (LinearRangeDefaults))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Menu))] [DynamicDependency (PRESERVED_MEMBERS, typeof (MenuBar))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (MessageBox))] [DynamicDependency (PRESERVED_MEMBERS, typeof (PopoverMenu))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (SelectorBase))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (StatusBar))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (TextField))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (TextView))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Window))] internal static Type [] GetTypes () => _types; } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index e3aa92bc07..6a9a4718fa 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -53,22 +53,12 @@ public class Button : View, IDesignable, IAcceptTarget /// /// Gets or sets whether s are shown with a shadow effect by default. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyles DefaultShadow - { - get => ButtonSettings.Current.DefaultShadow; - set => ButtonSettings.Current = ButtonSettings.Current with { DefaultShadow = value }; - } + public static ShadowStyles DefaultShadow => ButtonSettings.Current.DefaultShadow; /// /// Gets or sets the default Highlight Style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultMouseHighlightStates - { - get => ButtonSettings.Current.DefaultMouseHighlightStates; - set => ButtonSettings.Current = ButtonSettings.Current with { DefaultMouseHighlightStates = value }; - } + public static MouseState DefaultMouseHighlightStates => ButtonSettings.Current.DefaultMouseHighlightStates; /// Initializes a new instance of . public Button () diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index d8a3e6611f..33e3d049cb 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -54,12 +54,7 @@ public class CharMap : View, IDesignable, IValue /// /// Gets or sets the default cursor style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => CharMapSettings.Current.DefaultCursorStyle; - set => CharMapSettings.Current = CharMapSettings.Current with { DefaultCursorStyle = value }; - } + public static CursorStyle DefaultCursorStyle => CharMapSettings.Current.DefaultCursorStyle; private const int COLUMN_WIDTH = 3; // Width of each column of glyphs private const int HEADER_HEIGHT = 1; // Height of the header diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index c5e4ceb791..6d6254a0b2 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -24,12 +24,7 @@ public class CheckBox : View, IValue /// /// Gets or sets the default Highlight Style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultMouseHighlightStates - { - get => CheckBoxSettings.Current.DefaultMouseHighlightStates; - set => CheckBoxSettings.Current = CheckBoxSettings.Current with { DefaultMouseHighlightStates = value }; - } + public static MouseState DefaultMouseHighlightStates => CheckBoxSettings.Current.DefaultMouseHighlightStates; /// /// Initializes a new instance of . diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 0a335558f3..43dfad7934 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -64,42 +64,22 @@ public class Dialog : Dialog /// The default border style for new instances. Can be configured via /// and theme files. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => DialogSettings.Current.DefaultBorderStyle; - set => DialogSettings.Current = DialogSettings.Current with { DefaultBorderStyle = value }; - } + public static LineStyle DefaultBorderStyle => DialogSettings.Current.DefaultBorderStyle; /// /// The default button alignment for new instances. Can be configured via theme files. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment - { - get => DialogSettings.Current.DefaultButtonAlignment; - set => DialogSettings.Current = DialogSettings.Current with { DefaultButtonAlignment = value }; - } + public static Alignment DefaultButtonAlignment => DialogSettings.Current.DefaultButtonAlignment; /// /// The default button alignment modes for new instances. Can be configured via theme files. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static AlignmentModes DefaultButtonAlignmentModes - { - get => DialogSettings.Current.DefaultButtonAlignmentModes; - set => DialogSettings.Current = DialogSettings.Current with { DefaultButtonAlignmentModes = value }; - } + public static AlignmentModes DefaultButtonAlignmentModes => DialogSettings.Current.DefaultButtonAlignmentModes; /// /// The default shadow style for new instances. Can be configured via theme files. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyles DefaultShadow - { - get => DialogSettings.Current.DefaultShadow; - set => DialogSettings.Current = DialogSettings.Current with { DefaultShadow = value }; - } + public static ShadowStyles DefaultShadow => DialogSettings.Current.DefaultShadow; /// /// Helper property that gets whether the dialog was canceled (Result is or 1). diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index f690a5c7a2..8202312d84 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -40,10 +40,5 @@ public FrameView () /// This property can be set in a Theme to change the default for all /// s. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => FrameViewSettings.Current.DefaultBorderStyle; - set => FrameViewSettings.Current = FrameViewSettings.Current with { DefaultBorderStyle = value }; - } + public static LineStyle DefaultBorderStyle => FrameViewSettings.Current.DefaultBorderStyle; } diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index 4c3473d2db..b61a4f9cef 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -80,12 +80,7 @@ public class HexView : View, IDesignable /// /// Gets or sets the default cursor style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => HexViewSettings.Current.DefaultCursorStyle; - set => HexViewSettings.Current = HexViewSettings.Current with { DefaultCursorStyle = value }; - } + public static CursorStyle DefaultCursorStyle => HexViewSettings.Current.DefaultCursorStyle; /// /// Gets or sets the view-specific default key bindings for . Contains only bindings diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs b/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs index 987d2b2b73..11bf70e814 100644 --- a/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs @@ -8,10 +8,5 @@ namespace Terminal.Gui.Views; public static class LinearRangeDefaults { /// Gets or sets the default cursor style applied to a new linear range view. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => LinearRangeSettings.Current.DefaultCursorStyle; - set => LinearRangeSettings.Current = LinearRangeSettings.Current with { DefaultCursorStyle = value }; - } + public static CursorStyle DefaultCursorStyle => LinearRangeSettings.Current.DefaultCursorStyle; } diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index f1628e55d5..8ff47c80fc 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -52,12 +52,7 @@ public class Menu : Bar, IValue /// /// Gets or sets the default Border Style for Menus. The default is . /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => MenuSettings.Current.DefaultBorderStyle; - set => MenuSettings.Current = MenuSettings.Current with { DefaultBorderStyle = value }; - } + public static LineStyle DefaultBorderStyle => MenuSettings.Current.DefaultBorderStyle; /// public Menu () : this ([]) { } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index edc299fe80..63a317f86f 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -440,12 +440,7 @@ internal set /// /// Gets or sets the default Border Style for the MenuBar. The default is . /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public new static LineStyle DefaultBorderStyle - { - get => MenuBarSettings.Current.DefaultBorderStyle; - set => MenuBarSettings.Current = MenuBarSettings.Current with { DefaultBorderStyle = value }; - } + public new static LineStyle DefaultBorderStyle => MenuBarSettings.Current.DefaultBorderStyle; /// The default key for activating menu bars. [ConfigurationProperty (Scope = typeof (SettingsScope))] diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index aa5a8623ca..a4511d5b08 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -57,21 +57,11 @@ public static class MessageBox /// Defines the default border styling for . Can be configured via /// . /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => MessageBoxSettings.Current.DefaultBorderStyle; - set => MessageBoxSettings.Current = MessageBoxSettings.Current with { DefaultBorderStyle = value }; - } + public static LineStyle DefaultBorderStyle => MessageBoxSettings.Current.DefaultBorderStyle; /// The default for . /// This property can be set in a Theme. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment - { - get => MessageBoxSettings.Current.DefaultButtonAlignment; - set => MessageBoxSettings.Current = MessageBoxSettings.Current with { DefaultButtonAlignment = value }; - } + public static Alignment DefaultButtonAlignment => MessageBoxSettings.Current.DefaultButtonAlignment; /// /// Displays an auto-sized error . diff --git a/Terminal.Gui/Views/Selectors/SelectorBase.cs b/Terminal.Gui/Views/Selectors/SelectorBase.cs index 92f26b87ff..894dc89e42 100644 --- a/Terminal.Gui/Views/Selectors/SelectorBase.cs +++ b/Terminal.Gui/Views/Selectors/SelectorBase.cs @@ -25,12 +25,7 @@ public abstract class SelectorBase : View, IOrientation, IValue /// /// Gets or sets the default Highlight Style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultMouseHighlightStates - { - get => SelectorBaseSettings.Current.DefaultMouseHighlightStates; - set => SelectorBaseSettings.Current = SelectorBaseSettings.Current with { DefaultMouseHighlightStates = value }; - } + public static MouseState DefaultMouseHighlightStates => SelectorBaseSettings.Current.DefaultMouseHighlightStates; /// /// Initializes a new instance of the class. diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index d29da9925f..593adb6d19 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -59,12 +59,7 @@ private void OnThemeChanged (object? sender, App.EventArgs e) /// /// Gets or sets the default Line Style for the separators between the shortcuts of the StatusBar. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultSeparatorLineStyle - { - get => StatusBarSettings.Current.DefaultSeparatorLineStyle; - set => StatusBarSettings.Current = StatusBarSettings.Current with { DefaultSeparatorLineStyle = value }; - } + public static LineStyle DefaultSeparatorLineStyle => StatusBarSettings.Current.DefaultSeparatorLineStyle; /// protected override void OnSubViewLayout (LayoutEventArgs args) diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.cs index b58216e64e..c53c32023a 100644 --- a/Terminal.Gui/Views/TextInput/TextField/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField/TextField.cs @@ -79,12 +79,7 @@ public partial class TextField : View, IDesignable, IValue /// /// Gets or sets the default cursor style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => TextFieldSettings.Current.DefaultCursorStyle; - set => TextFieldSettings.Current = TextFieldSettings.Current with { DefaultCursorStyle = value }; - } + public static CursorStyle DefaultCursorStyle => TextFieldSettings.Current.DefaultCursorStyle; /// /// Initializes a new instance of the class. diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.cs index 5c431e0315..28b89b7066 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.cs @@ -104,12 +104,7 @@ public partial class TextView : View, IDesignable /// /// Gets or sets the default cursor style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => TextViewSettings.Current.DefaultCursorStyle; - set => TextViewSettings.Current = TextViewSettings.Current with { DefaultCursorStyle = value }; - } + public static CursorStyle DefaultCursorStyle => TextViewSettings.Current.DefaultCursorStyle; private CultureInfo? _currentCulture; diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs index 510c7d2d55..e6a90b6213 100644 --- a/Terminal.Gui/Views/Window.cs +++ b/Terminal.Gui/Views/Window.cs @@ -33,12 +33,7 @@ public Window () /// /// Gets or sets whether all s are shown with a shadow effect by default. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyles DefaultShadow - { - get => WindowSettings.Current.DefaultShadow; - set => WindowSettings.Current = WindowSettings.Current with { DefaultShadow = value }; - } + public static ShadowStyles DefaultShadow => WindowSettings.Current.DefaultShadow; // TODO: enable this ///// @@ -58,10 +53,5 @@ public static ShadowStyles DefaultShadow /// This property can be set in a Theme to change the default for all /// s. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => WindowSettings.Current.DefaultBorderStyle; - set => WindowSettings.Current = WindowSettings.Current with { DefaultBorderStyle = value }; - } + public static LineStyle DefaultBorderStyle => WindowSettings.Current.DefaultBorderStyle; } diff --git a/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs index 36760eedfd..7ca92b0fd0 100644 --- a/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs @@ -10,7 +10,9 @@ public class ScopeJsonConverterTests [InlineData ("\"Key.Separator\":\"@\"")] [InlineData ("\"Themes\":[]")] [InlineData ("\"Themes\":[{\"themeName\":{}}]")] - [InlineData ("\"Themes\":[{\"themeName\":{\"Dialog.DefaultButtonAlignment\":\"End\"}}]")] + // A2.4: dropped "Dialog.DefaultButtonAlignment" InlineData row — Dialog.DefaultButtonAlignment lost + // [ConfigurationProperty] in A2.4 when view-facade theme setters were removed; ScopeJsonConverter now + // rejects it. CM and this test are removed in step D. public void RoundTripConversion_Property_Positive (string configPropertyJson) { // Arrange