diff --git a/AGENTS.md b/AGENTS.md index 764a443d..ef75304f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,12 +32,12 @@ DOTNET_ROOT=$HOME/.dotnet dotnet test TuiCode.slnx # DOTNET_ROOT only needed ## Tests -- xUnit v3 on Microsoft.Testing.Platform. Don't switch to v2 + `Microsoft.NET.Test.Sdk` — pulls in `Microsoft.TestPlatform.CoreUtilities` 15.x, which TG's `ConfigurationManager` chokes on at startup. +- xUnit v3 on Microsoft.Testing.Platform. Don't switch to v2 + `Microsoft.NET.Test.Sdk` — pulls in `Microsoft.TestPlatform.CoreUtilities` 15.x, which TG 2.1's `ConfigurationManager` choked on at startup. - Test names: `Method_describes_what_should_happen` (snake_case after the method). - File-touching tests: `MockFileSystem` from `System.IO.Abstractions.TestingHelpers`. No temp dirs. - **Never assert a `MockFileSystem` path as a literal.** On Windows the mock resolves `/work` against the current drive, so `FullName` is `C:\work` and a hard-coded `"/work"` fails there and only there — a `HostSteps.Run` step waiting on one just reports `RunAsync timed out`. Put the expected path through `fs.Path.GetFullPath` (see `StartupArgumentsTests.Full`). - UI / focus / key bugs: drive via TG input injection (`host.App.InjectKey` from the `Iteration` event). See `WorkbenchHostTests.CtrlQ_quits_the_workbench` and TG's [testing docs](https://gui-cs.github.io/Terminal.Gui/docs/drivers.html#testing-and-input-injection). Faster than asking a human to retry manual steps. -- Any test that boots a TG `Application` (news up a `WorkbenchHost`, renders a View) or mutates `ThemeManager`/`ConfigurationManager` must derive from `StaticConfigurationTest` (issue #77). Those TG statics are process-global; under xUnit's default parallelism a theme mutation in one test makes TG's render path throw `KeyNotFoundException` in another. The base joins the serialised `StaticConfiguration` collection and snapshot/restores the theme. Most tests don't touch TG statics and stay parallel — only opt the ones that do into the base. A reentrancy guard in the base throws if the serialisation ever breaks, so a forgotten `[CollectionDefinition]` rename fails loudly instead of flaking. +- Any test that boots a TG `Application` (news up a `WorkbenchHost`, renders a View) or mutates `ThemeManager`/`TuiConfigurationBuilder` must derive from `StaticConfigurationTest` (issue #77). A test that needs our themes takes them with `LoadBundledThemes()`: TG 2.5 ignores a switch to a theme it hasn't registered. Those TG statics are process-global; under xUnit's default parallelism a theme mutation in one test makes TG's render path throw `KeyNotFoundException` in another. The base joins the serialised `StaticConfiguration` collection and snapshot/restores the theme. Most tests don't touch TG statics and stay parallel — only opt the ones that do into the base. A reentrancy guard in the base throws if the serialisation ever breaks, so a forgotten `[CollectionDefinition]` rename fails loudly instead of flaking. - Tests that boot a `WorkbenchHost` must also pass `driverName: DriverRegistry.Names.ANSI` (the headless, deterministic CI driver). With the default (`null`) TG auto-selects the platform driver; on a headless **Windows** runner that's `WindowsDriver`, which blocks on console input and hangs the run forever — `RunAsync` doesn't observe the cancellation token while blocked. Linux auto-selects ANSI already, which is why this only bit the Windows CI leg. Production passes `null` so a real terminal still gets the best driver. ## Terminal.Gui v2 @@ -45,14 +45,16 @@ DOTNET_ROOT=$HOME/.dotnet dotnet test TuiCode.slnx # DOTNET_ROOT only needed - Namespaces split: `Terminal.Gui.App`, `.Views`, `.ViewBase`, `.Drawing`, `.Input`. `global using` per assembly. - Static `Application` is `[Obsolete]`. Use `Application.Create(timeProvider).Init(...).Run(runnable)` against `IApplication`. `WorkbenchHost` owns it. - `Tabs.Add(view)`; tab title = child's `Title`; active = `Tabs.Value`. -- Setting a tab's `Title` doesn't redraw its header. TG copies the title into the header only during layout, and `Tabs` places headers using each header's cached width (`ITitleView.MeasuredTabLength`) before re-measuring it. So after changing a title, clear that cached width and call `SetNeedsLayout()` (see `EditorTab.UpdateTitle`). Otherwise the header shows the old text until something else forces a layout, and neighbouring headers overlap when the width changes. +- Setting a tab's `Title` doesn't redraw its header. TG copies the title into the header only during layout, and `Tabs` places headers using each header's cached width (`ITitleView.MeasuredTabLength`) before re-measuring it. So after changing a title, call `PaneTabs.ShowTitle`, which hands the header its text, clears the cached width and the header's old size constraint (TG 2.5 measures against it), and lays the tab out again. Otherwise the header shows the old text until something else forces a layout, and neighbouring headers overlap when the width changes. - Removing a focused modal makes TG hand focus back into the editor group, and a `Tabs` switches `Value` to whichever tab takes focus, which isn't always the active one. So close a modal before making a tab active, not after (see `OpenRevisionPicker`, #191). - `TextView.IsDirty` has no setter. `EditorTab` tracks `_dirty` via `TextView.ContentsChanged` (subscribed *after* the initial `Text =` so load doesn't dirty it). - `ContentsChanged` isn't a reliable edit signal in TG 2.1.0. The kill commands (`Ctrl+K`, `Ctrl+Delete`, and mid-line `Ctrl+Backspace` / `Ctrl+Shift+Backspace`) edit without raising it, and some no-op deletes (Backspace at the start of the file, Delete at the end) raise it anyway. So `EditorTextView.OnKeyDown` runs every key bound to an editing command (`EditCommands`), and `OnKeyDownNotHandled` runs typed text, with TG's own raises held, then raises once if the undo recorder found a change (see *Multiple cursors and undo*). -- `TextView.OnContentsChanged` throws away `TextModel`'s max-line-width cache on every edit (still true in TG 2.5.0), so the next `UpdateContentSize` measures every line: ~225 ms per typed character at 50,000 lines. `EditorTextView.OnContentsChanged` works out the new width first by folding in the current row (the cached max plus `_cachedMaxWidthPerLine`, the rows at that width), calls base, then puts the cache back. This relies on every in-place width change being on the current row: adding, removing or replacing a line already invalidates the cache, and so do edits at several cursors. It reaches TG's private members through `[UnsafeAccessor]`, plus reflection for `_model`, since `UnsafeAccessorType` can't return a `ref` to an inaccessible type. If a TG upgrade renames them, every edit throws and `EditorTextViewContentWidthTests` fails. Enter, line joins, and edits that make the widest line wider (or shrink the only widest line) still rescan once, because TG's own checks invalidate before the hook runs. `EditorTypingBenchmarkTests` (explicit) measures it. +- `TextView.OnContentsChanged` throws away `TextModel`'s max-line-width cache on every edit (still true in TG 2.5.0), so the next `UpdateContentSize` measures every line: ~225 ms per typed character at 50,000 lines. `EditorTextView.OnContentsChanged` works out the new width first by folding in the current row (the cached max plus `_cachedMaxWidthPerLine`, the rows at that width), calls base, then puts the cache back. This relies on every in-place width change being on the current row: adding, removing or replacing a line already invalidates the cache, and so do edits at several cursors. It reaches TG's private members through `[UnsafeAccessor]`, plus reflection for `_model`, since `UnsafeAccessorType` can't return a `ref` to an inaccessible type. If a TG upgrade renames them, every edit throws; `TerminalGuiInternalsTests` checks every `[UnsafeAccessor]` and reflected field still finds its member. Enter, line joins, and edits that make the widest line wider (or shrink the only widest line) still rescan once, because TG's own checks invalidate before the hook runs. `EditorTypingBenchmarkTests` (explicit) measures it. - `Application.RemoveDefaultKeyBinding(Command.Quit)` crashes `TextView.PopoverMenu` init. To stop Esc-as-Quit, reassign Quit to `Ctrl+Q` — see `WorkbenchHost.NeutralizeBuiltinQuitKey`. - `View.SetFocus()` returns false when any ancestor has `CanFocus = false`. Set `CanFocus = true` on container `View`s that should host focusable children. -- `ConfigurationManager` deserializes via source-generated `JsonTypeInfo` — only knows the types its built-in scopes use. Records, arrays, even `string[]` silently fail to load. Stick to primitives or persist to a dedicated file. +- TG 2.5 replaced `ConfigurationManager` with `TuiConfigurationBuilder` (Microsoft.Extensions.Configuration). Its JSON is nested: `Themes` and `Schemes` are objects keyed by name, and there are no dotted keys like `Dialog.DefaultShadow`. A file in the old shape is skipped with only a log warning. +- `TextView` is `[Obsolete]` in TG 2.5 in favour of Terminal.Gui.Editor; the CS0618 warnings it brings are expected. Its `Text` is no longer virtual, so `EditorTextView.Text` hides it with `new`: set the editor's text through an `EditorTextView` reference. +- TG 2.5's ANSI driver takes its size from the terminal it's attached to (80×25 only when detached, as under `dotnet test` on macOS/Linux). A host test that reads the screen or depends on the viewport's height pins it with `HostSteps.PinScreenSize`, or it fails on Windows CI. - `Terminal.Gui.Drawing.Attribute` collides with `System.Attribute`; fully qualify when constructing. ### UI controls @@ -179,7 +181,7 @@ What follows is how TuiCode implements it today. ## Editor drawing -- `EditorTextView` overrides `OnDrawingContent` with a copy of TG 2.1.0's `TextView` draw loop that stops at the viewport bottom. Upstream walks every row from `Viewport.Y` to EOF, so drawing the top of a 5,000-line file took ~900 ms per frame vs ~20 ms now, and cost grew with file length. +- `EditorTextView` overrides `OnDrawingContent` with a copy of TG's `TextView` draw loop (re-diffed against 2.5.0) that stops at the viewport bottom. Upstream walks every row from `Viewport.Y` to EOF, so drawing the top of a 5,000-line file took ~900 ms per frame vs ~20 ms now, and cost grew with file length. - Per-cell colouring (find highlights, and syntax highlighting per #21) belongs in `OnDrawNormalColor`, which is still called once per *visible* cell. It doesn't call base: base resolves the scheme attribute (allocating) and raises `DrawNormalColor` per cell, so nothing should subscribe to that event. Resolve attributes once per frame in `OnDrawingContent`, not per cell. - `EditorTextViewDrawTests` asserts the copy paints exactly what `TextView` does (tabs, wide glyphs, horizontal/vertical scroll, selection, overwrite cursor, read-only). On a TG upgrade, re-diff `TextView.Drawing.cs` against the copy; drop it once upstream bounds the loop. `EditorDrawBenchmarkTests` is `Explicit` (timing-based): `dotnet test tests/TuiCode.Tests/TuiCode.Tests.csproj -c Release -- --explicit only`. - Editor tabs use TG's built-in `ScrollBar`s (#294, #296), turned on with `ViewportSettingsFlags.HasScrollBars` on the instance `EditorTab` builds, not `TextView.ScrollBars`. Both are `Auto` and sit in the text view's `Padding`: the vertical one takes a column while the file is taller than the tab, the horizontal one a row while a line runs past the edge. The gutter's height follows the text view's bottom padding, so it ends level with the last text row. A bar that shows or hides during a layout pass (a terminal resize) changes that padding, but TG clears the relayout it asks for when the pass ends, so `EditorTab.OnSubViewsLaidOut` lays the tab out again whenever a bar's visibility has changed (#315). A resized bar also re-clamps its slider and TG reads that lossy position back into the viewport, which scrolled a background tab hundreds of lines on a terminal resize, so `ScrollBarResize.Hold` refuses any value a bar proposes while its own frame is changing, unless it's where the text view already is (#324). `ThinScrollBar` redraws the horizontal bar in editor and diff tabs as a row of `🀰` tiles on no stipple, since a full-height cell looks twice as thick as the vertical bar, and keeps a blank cell between the slider and an arrow, unless that would leave a one-cell slider nothing to draw (#325). @@ -353,20 +355,20 @@ What follows is how TuiCode implements it today. ## Settings & persistence -- `DefaultSettingsService` is a thin wrapper around TG's static `ConfigurationManager` / `ThemeManager`. `Theme` getter/setter delegate straight to `ThemeManager.Theme`; no backing field. `Load()` calls `ConfigurationManager.Enable(ConfigLocations.All)`; `Program.cs` invokes it on the resolved service before constructing the App, so `ThemeManager.Theme` is in place when `Application.Init()` paints. `ThemeManager.Theme` is a TG-native `[ConfigurationProperty(Scope = typeof(SettingsScope))]` and persists as `{"Theme": "Daylight"}` at the JSON root of `~/.tui/TuiCode.config.json`. `Save()` writes that format manually (TG exposes no save API). Picker exposes only our bundled themes (see Themes). +- `DefaultSettingsService` is a thin wrapper around TG's static `TuiConfigurationBuilder.Shared` / `ThemeManager`. `Theme` getter/setter delegate straight to `ThemeManager.Theme`; no backing field. `Load()` calls `TuiConfigurationBuilder.Shared.ApplyToStaticFacades()`, which reads `~/.tui/TuiCode.config.json` and `TUI_CONFIG`; `Program.cs` invokes it on the resolved service before constructing the App, so `ThemeManager.Theme` is in place when `Application.Init()` paints. `ThemeManager.Theme` is TG's own `Theme` setting and persists as `{"Theme": "Daylight"}` at the JSON root of `~/.tui/TuiCode.config.json`. `Save()` writes that format manually (TG exposes no save API). Picker exposes only our bundled themes (see Themes). - Keybindings persist to a dedicated file `~/.tui/TuiCode.keybindings.json` (read at `DefaultSettingsService` construction, written on `Save()`) — TG's serializer can't round-trip them. Diff style: `{ Keys, Label, Command }`, prefix `Command` with `-` to remove. A removal only unbinds the key while it still runs that command, so a default that moves to another command (#362) isn't lost to an old removal. Boot: `BindDefaults` then `ApplyKeybindings(settings.KeybindingOverrides)`. Picker calls `WorkbenchHost.ApplyEditedBindings` on save (recomputes diff from defaults so the file stays minimal). - **A binding's canonical identity is its chord of raw `KeyCode`s, not its display string (#89).** `Key.ToString()` is lossy — a three-modifier chord like `Ctrl+Alt+Shift++` (`0xD000002B`) stringifies to `"Ctrl+Alt+Shift++"`, which `Key.TryParse` *can't* read back (a TG round-trip bug), so a display-string identity made such chords un-bindable and crashed on save. So `KeyBinding`/`KeybindingOverride` carry an `IReadOnlyList` chord; `KeyChord.Canonical` (keycode-joined) is the identity used for binding, matching, diffing, dedup, and persistence, while `KeyChord.Display` (`ToString()`-joined) is **UI-only and never parsed back**. `IKeybindingService` exposes `Key`-list `Bind`/`Unbind`/`CheckConflict` as the canonical path; the `string` overloads remain as sugar for the hardcoded `BindDefaults` (safe literals). The picker feeds its captured `List` straight in — no stringify round-trip. Persistence writes `"Keys": […]` plus a decorative `"Label"` (the display string, ignored on load); load reconstructs each `Key` from its keycode via `new Key((KeyCode)u)`, which is lossless. - **Pre-#89 files are dropped, by design.** An old `{ "Key": "", "Command": … }` entry has no `"Keys"` array, so `LoadKeybindings` skips it (the same per-entry resilience as #90). Custom bindings from before the format change are lost on upgrade and re-saved in the keycode format on the next edit — there is no in-place migration. The crash class #90's *Bind-time* `try/catch` guarded (an unparseable key string) is now unrepresentable, so that guard fires only on a bad *command* (e.g. empty); the loader's per-entry skip is where format/corruption tolerance actually lives now. - **Workspace state (#13)** — the files open per folder and the active one — persists to `~/.tui/TuiCode.workspaces.json` via `WorkspaceStateStore`: a JSON array, most recently used folder first, capped at `MaxFolders` (30). The launch folder always wins (like `code .`); `Workbench.OpenFolder` (startup and the in-app Open) restores that folder's tabs, skipping files that are gone. Saves happen on every `ActiveTabChanged` (open, close, switch), so a crash loses nothing. `Workbench` nulls its folder while switching and on dispose so closing the old tabs isn't saved as that folder's state. Cursor positions aren't persisted yet. - **Open recent folder (#357)** (`or`, no default key) lists `WorkspaceStateStore.Folders()` as `RecentFolderPickerView`, less the folder open now and any that no longer exist; it only reads the file, so dead entries are hidden, not pruned. `RecentFolderList` (pure) builds the name + `~`-shortened parent rows and filters them. Enter is `Workbench.OpenFolder`. - **Open file path (#358)** (`opa`, no default key; `op` would shadow `opr`) is a `PathPromptView` for a folder to open by its full path; `FolderPath.Expand` turns a leading `~` into the home folder and drops a trailing separator. A relative path, a missing folder or a file is `No such folder: ` in the prompt, which stays open; Enter otherwise is `Workbench.OpenFolder`. -- Don't reach into `ConfigurationManager` / `ThemeManager` from feature code — go through `ISettingsService`. Tests use `InMemorySettingsService`. -- Tests that mutate `ThemeManager.Theme` (e.g. `DefaultSettingsServiceTests`) must use `[Collection("StaticConfiguration")]` plus a local `ThemeFixture` to snapshot+restore the static. Keybindings are instance state on the service, so keybinding-only tests need neither. +- Don't reach into `TuiConfigurationBuilder` / `ThemeManager` from feature code — go through `ISettingsService`. Tests use `InMemorySettingsService`. +- Tests that mutate `ThemeManager.Theme` (e.g. `DefaultSettingsServiceTests`) derive from `StaticConfigurationTest`, which snapshots and restores it. Keybindings are instance state on the service, so keybinding-only tests need neither. ## Themes (#11) - We offer only our own themes (`BundledThemes.Names`: Midnight, Daylight, Turbo Pascal, Modern Borland). TG's built-ins can't describe the gutter or cursor. `DefaultSettingsService.Load` migrates a saved theme we don't ship (TG's `Light` → Daylight, anything else → Midnight, the default). -- Each theme is two files that share a name. The UI half is TG theme JSON in `src/TuiCode.Workbench/Themes/themes.json`, handed to TG as `ConfigurationManager.RuntimeConfig`. TG merges `Themes` per theme, and the file sets no `Theme` key, so the user's saved choice still wins. The editor half is a VS Code-style token theme in `src/TuiCode.Syntax/Themes/` (embedded as `themes/`, checked before the zip). Its `tokenColors` colour syntax, and its `colors` carry what TG schemes can't: `editorCursor.foreground` and the gutter keys. `BundledThemes.TokenThemeFor` pairs them. Adding a theme means touching both files, `BundledThemes`, and the tests' theme lists. +- Each theme is two files that share a name. The UI half is TG theme JSON in `src/TuiCode.Workbench/Themes/themes.json`, handed to TG as `TuiConfigurationBuilder.Shared.RuntimeConfig`, which outranks every other source. TG merges `Themes` per theme, and the file sets no `Theme` key, so the user's saved choice still wins. The editor half is a VS Code-style token theme in `src/TuiCode.Syntax/Themes/` (embedded as `themes/`, checked before the zip). Its `tokenColors` colour syntax, and its `colors` carry what TG schemes can't: `editorCursor.foreground` and the gutter keys. `BundledThemes.TokenThemeFor` pairs them. Adding a theme means touching both files, `BundledThemes`, and the tests' theme lists. - Token themes may `include` others (`midnight.json` → `dark_plus.json` → `dark_vs.json`). TextMateSharp resolves only one level, so `GrammarBundle.GetTheme` flattens the chain itself, appending `tokenColors` and letting a theme's `colors` override its base. - Parts name their own schemes: `SidebarPart` uses `Sidebar`, `StatusBarPart` uses `StatusBar`; a missing scheme falls back to the SuperView's. Every bundled theme defines all of `Base`, `Accent`, `Dialog`, `Menu`, `Error`, `Warning`, `Sidebar`, `StatusBar` (a test enforces it). TG has no `VisualRole` for a warning, so `Warning` is read by name through `WarningColour.On`, which takes only its foreground and leaves whatever the row or tab header is drawn over alone. - Cursor colour: the editor uses the terminal's own cursor, whose colour TG never sets, so a terminal profile's cursor could vanish against the theme's background. `WorkbenchHost` sends OSC 12 with the theme's `editorCursor.foreground` at startup and on every `ISettingsService.ThemeChanged`, and OSC 112 on shutdown to restore the terminal's colour. Terminals without OSC 12 (Terminal.app) ignore it. A terminal profile we install must carry the colour too (`ITerminalCursorColour`): a profile switch reapplies the profile's colours over anything we sent (#299). F12 Diagnostics reads the colour back with `OSC 12 ; ?` (`CursorColourProbe`) each time it opens and shows it beside the one we asked for (#300). diff --git a/src/TuiCode.Abstractions/ISettingsService.cs b/src/TuiCode.Abstractions/ISettingsService.cs index 97915254..e2b57343 100644 --- a/src/TuiCode.Abstractions/ISettingsService.cs +++ b/src/TuiCode.Abstractions/ISettingsService.cs @@ -3,7 +3,7 @@ namespace TuiCode.Abstractions; /// /// User-facing settings: read current values, mutate them (with live preview side-effects), /// enumerate available themes, persist to disk. Wraps Terminal.Gui's static -/// ConfigurationManager / ThemeManager surface so the rest of the app stays DI-uniform. +/// TuiConfigurationBuilder / ThemeManager surface so the rest of the app stays DI-uniform. /// public interface ISettingsService { diff --git a/src/TuiCode.Abstractions/PaneTabs.cs b/src/TuiCode.Abstractions/PaneTabs.cs index e7fd29eb..85d294ba 100644 --- a/src/TuiCode.Abstractions/PaneTabs.cs +++ b/src/TuiCode.Abstractions/PaneTabs.cs @@ -1,3 +1,4 @@ +using Terminal.Gui.ViewBase; using Terminal.Gui.Views; namespace TuiCode.Abstractions; @@ -18,4 +19,14 @@ public PaneTabs() } private static bool? StayPut() => true; + + /// Redraws 's header after its title changes; TG would place it at the old width. + public static void ShowTitle(View tab) + { + if (tab.Border.View is not BorderView { TitleView: { } header }) return; + header.Text = tab.Title; + header.TextFormatter.ConstrainToSize = null; + if (header is ITitleView title) title.MeasuredTabLength = 0; + tab.SetNeedsLayout(); + } } diff --git a/src/TuiCode.Abstractions/TuiCode.Abstractions.csproj b/src/TuiCode.Abstractions/TuiCode.Abstractions.csproj index 464210e9..384bba16 100644 --- a/src/TuiCode.Abstractions/TuiCode.Abstractions.csproj +++ b/src/TuiCode.Abstractions/TuiCode.Abstractions.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/TuiCode.Editor/DiffTab.cs b/src/TuiCode.Editor/DiffTab.cs index f0dbb480..ff8a53d0 100644 --- a/src/TuiCode.Editor/DiffTab.cs +++ b/src/TuiCode.Editor/DiffTab.cs @@ -483,8 +483,7 @@ internal void UpdateTitle() Title = IsDeleted ? $"{File.Name} ↔ {LeftLabel} (deleted)" : RightLabel is { } right ? $"{File.Name} {LeftLabel} ↔ {right}" : $"{File.Name} ↔ {LeftLabel}"; - if (Border.View is BorderView { TitleView: ITitleView header }) header.MeasuredTabLength = 0; - SetNeedsLayout(); + PaneTabs.ShowTitle(this); } /// The file's lines as the editor would load them. diff --git a/src/TuiCode.Editor/EditorTab.cs b/src/TuiCode.Editor/EditorTab.cs index 3b1b6767..7ce18e09 100644 --- a/src/TuiCode.Editor/EditorTab.cs +++ b/src/TuiCode.Editor/EditorTab.cs @@ -586,13 +586,9 @@ private void UpdateTitle() // The trailing space keeps the marker off the tab's right border. var mark = DiskMark.For(_disk, IconStyle); Title = $"{(_dirty ? "● " : "")}{File.Name}{(mark.Length > 0 ? $" {mark} " : "")}"; - // TG redraws the tab header from Title only on layout, and positions headers from a cached width first. + PaneTabs.ShowTitle(this); if (Border.View is BorderView { TitleView: { } view }) - { - if (view is ITitleView header) header.MeasuredTabLength = 0; ColourWhileMarked(view); - } - SetNeedsLayout(); } // TG has no VisualRole for a warning and GetAttributeForRole isn't virtual in 2.1.0, so the marked diff --git a/src/TuiCode.Editor/EditorTextView.History.cs b/src/TuiCode.Editor/EditorTextView.History.cs index 0dd91659..44e2fa4a 100644 --- a/src/TuiCode.Editor/EditorTextView.History.cs +++ b/src/TuiCode.Editor/EditorTextView.History.cs @@ -25,7 +25,7 @@ public EditorTextView() AddCommand(Command.Cut, () => CopyAtCarets(cut: true)); } - public override string Text + public new string Text { get => base.Text; set diff --git a/src/TuiCode.Editor/TuiCode.Editor.csproj b/src/TuiCode.Editor/TuiCode.Editor.csproj index 36a5c701..5fa4356c 100644 --- a/src/TuiCode.Editor/TuiCode.Editor.csproj +++ b/src/TuiCode.Editor/TuiCode.Editor.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/TuiCode.Explorer/TuiCode.Explorer.csproj b/src/TuiCode.Explorer/TuiCode.Explorer.csproj index 26003cfb..88c52ff9 100644 --- a/src/TuiCode.Explorer/TuiCode.Explorer.csproj +++ b/src/TuiCode.Explorer/TuiCode.Explorer.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/TuiCode.Icons/TuiCode.Icons.csproj b/src/TuiCode.Icons/TuiCode.Icons.csproj index 282460f3..8d20a612 100644 --- a/src/TuiCode.Icons/TuiCode.Icons.csproj +++ b/src/TuiCode.Icons/TuiCode.Icons.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/TuiCode.Search/TuiCode.Search.csproj b/src/TuiCode.Search/TuiCode.Search.csproj index 4a60e008..73939293 100644 --- a/src/TuiCode.Search/TuiCode.Search.csproj +++ b/src/TuiCode.Search/TuiCode.Search.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/TuiCode.Syntax/TuiCode.Syntax.csproj b/src/TuiCode.Syntax/TuiCode.Syntax.csproj index df90c7b2..4c92f5c1 100644 --- a/src/TuiCode.Syntax/TuiCode.Syntax.csproj +++ b/src/TuiCode.Syntax/TuiCode.Syntax.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/TuiCode.Workbench/Configuration/DefaultSettingsService.cs b/src/TuiCode.Workbench/Configuration/DefaultSettingsService.cs index ad207506..11f95b38 100644 --- a/src/TuiCode.Workbench/Configuration/DefaultSettingsService.cs +++ b/src/TuiCode.Workbench/Configuration/DefaultSettingsService.cs @@ -9,16 +9,15 @@ namespace TuiCode.Workbench.Configuration; /// -/// DI-friendly thin wrapper around TG's static / +/// DI-friendly thin wrapper around TG's static / /// . Tests substitute an in-memory implementation; production /// code never touches the static surface directly. /// -/// Theme persists via TG's native ThemeManager.Theme -/// ([ConfigurationProperty(Scope = typeof(SettingsScope))]) written as +/// Theme persists via TG's native Theme setting, written as /// {"Theme": "Daylight"} at the JSON root of ~/.tui/TuiCode.config.json. -/// calls ConfigurationManager.Enable which reads the file and +/// calls TuiConfigurationBuilder.ApplyToStaticFacades, which reads the file and /// applies the theme — no custom load logic needed. Saving still goes through us because -/// ConfigurationManager exposes no Save API. +/// TG exposes no Save API. /// /// Keybindings persist to a sibling file ~/.tui/TuiCode.keybindings.json /// that we read and write directly — TG's source-generated JsonTypeInfo only @@ -56,7 +55,6 @@ public string Theme { if (string.Equals(ThemeManager.Theme, value, StringComparison.Ordinal)) return; ThemeManager.Theme = value; - ConfigurationManager.Apply(); ThemeChanged?.Invoke(this, EventArgs.Empty); } } @@ -65,7 +63,7 @@ public string Theme // TG's built-ins don't describe the editor's gutter or cursor, so we only offer our own. public IReadOnlyCollection AvailableThemes => - BundledThemes.Names.Where(theme => ThemeManager.Themes?.ContainsKey(theme) ?? false).ToArray(); + BundledThemes.Names.Where(ThemeManager.GetThemeNames().Contains).ToArray(); public IReadOnlyList KeybindingOverrides => _keybindings; @@ -91,8 +89,8 @@ public void SetGrammarAssociations(IReadOnlyDictionary associati public void Load() { - ConfigurationManager.RuntimeConfig = BundledThemes.Config; - ConfigurationManager.Enable(ConfigLocations.All); + TuiConfigurationBuilder.Shared.RuntimeConfig = BundledThemes.Config; + TuiConfigurationBuilder.Shared.ApplyToStaticFacades(); Theme = BundledThemes.Migrate(ThemeManager.Theme); } diff --git a/src/TuiCode.Workbench/Themes/themes.json b/src/TuiCode.Workbench/Themes/themes.json index 5b9e699d..f33b04c9 100644 --- a/src/TuiCode.Workbench/Themes/themes.json +++ b/src/TuiCode.Workbench/Themes/themes.json @@ -1,436 +1,380 @@ { - "Themes": [ - { - "Turbo Pascal": { - "Dialog.DefaultShadow": "Opaque", - "Button.DefaultShadow": "Opaque", - "Schemes": [ - { - "Base": { - "Normal": { "Foreground": "#FFFFFF", "Background": "#0000AA" }, - "Focus": { "Foreground": "#000000", "Background": "#00AAAA" }, - "HotNormal": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "HotFocus": { "Foreground": "#FFFF55", "Background": "#00AAAA" }, - "Active": { "Foreground": "#0000AA", "Background": "#AAAAAA" }, - "HotActive": { "Foreground": "#AA0000", "Background": "#AAAAAA" }, - "Highlight": { "Foreground": "#000000", "Background": "#00AAAA" }, - "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "ReadOnly": { "Foreground": "#AAAAAA", "Background": "#0000AA" }, - "Disabled": { "Foreground": "#5555FF", "Background": "#0000AA" } - } - }, - { - "Sidebar": { - "Normal": { "Foreground": "#000000", "Background": "#00AAAA" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#0000AA" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00AAAA" }, - "HotFocus": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#00AAAA", "Style": "Bold" }, - "HotActive": { "Foreground": "#FFFF55", "Background": "#00AAAA", "Style": "Bold" }, - "Highlight": { "Foreground": "#FFFF55", "Background": "#00AAAA" }, - "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "ReadOnly": { "Foreground": "#555555", "Background": "#00AAAA" }, - "Disabled": { "Foreground": "#555555", "Background": "#00AAAA" } - } - }, - { - "StatusBar": { - "Normal": { "Foreground": "#000000", "Background": "#AAAAAA" }, - "HotNormal": { "Foreground": "#AA0000", "Background": "#AAAAAA" } - } - }, - { - "Accent": { - "Normal": { "Foreground": "#000000", "Background": "#00AAAA" }, - "Focus": { "Foreground": "#000000", "Background": "#00AA00" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00AAAA" }, - "HotFocus": { "Foreground": "#FFFFFF", "Background": "#00AA00" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#00AAAA", "Style": "Bold" }, - "Highlight": { "Foreground": "#FFFF55", "Background": "#00AAAA" }, - "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "ReadOnly": { "Foreground": "#555555", "Background": "#00AAAA" } - } - }, - { - "Dialog": { - "Normal": { "Foreground": "#000000", "Background": "#AAAAAA" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#00AA00" }, - "HotNormal": { "Foreground": "#FFFF55", "Background": "#AAAAAA" }, - "HotFocus": { "Foreground": "#FFFF55", "Background": "#00AA00" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#AAAAAA", "Style": "Bold" }, - "HotActive": { "Foreground": "#FFFF55", "Background": "#AAAAAA", "Style": "Bold" }, - "Highlight": { "Foreground": "#000000", "Background": "#00AAAA" }, - "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "ReadOnly": { "Foreground": "#555555", "Background": "#AAAAAA" }, - "Disabled": { "Foreground": "#555555", "Background": "#AAAAAA" } - } - }, - { - "Menu": { - "Normal": { "Foreground": "#000000", "Background": "#AAAAAA" }, - "Focus": { "Foreground": "#000000", "Background": "#00AA00" }, - "HotNormal": { "Foreground": "#AA0000", "Background": "#AAAAAA", "Style": "Underline" }, - "HotFocus": { "Foreground": "#AA0000", "Background": "#00AA00", "Style": "Underline" }, - "Active": { "Foreground": "#000000", "Background": "#00AA00" }, - "HotActive": { "Foreground": "#AA0000", "Background": "#00AA00", "Style": "Underline" }, - "Highlight": { "Foreground": "#000000", "Background": "#00AAAA" }, - "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "ReadOnly": { "Foreground": "#555555", "Background": "#AAAAAA" }, - "Disabled": { "Foreground": "#555555", "Background": "#AAAAAA" } - } - }, - { - "Error": { - "Normal": { "Foreground": "#FFFFFF", "Background": "#AA0000" }, - "Focus": { "Foreground": "#000000", "Background": "#00AA00" }, - "HotNormal": { "Foreground": "#FFFF55", "Background": "#AA0000" }, - "HotFocus": { "Foreground": "#FFFF55", "Background": "#00AA00" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#AA0000", "Style": "Bold" }, - "Highlight": { "Foreground": "#000000", "Background": "#AAAAAA" }, - "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "ReadOnly": { "Foreground": "#AAAAAA", "Background": "#AA0000" }, - "Disabled": { "Foreground": "#AAAAAA", "Background": "#AA0000" } - } - }, - { - "Warning": { - "Normal": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "Focus": { "Foreground": "#000000", "Background": "#FFFF55" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#0000AA" }, - "HotFocus": { "Foreground": "#000000", "Background": "#FFFF55" }, - "Active": { "Foreground": "#000000", "Background": "#FFFF55", "Style": "Bold" }, - "Highlight": { "Foreground": "#000000", "Background": "#AAAAAA" }, - "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, - "ReadOnly": { "Foreground": "#AAAAAA", "Background": "#0000AA" }, - "Disabled": { "Foreground": "#5555FF", "Background": "#0000AA" } - } - } - ] + "Themes": { + "Turbo Pascal": { + "Dialog": { + "DefaultShadow": "Opaque" + }, + "Button": { + "DefaultShadow": "Opaque" + }, + "Schemes": { + "Base": { + "Normal": { "Foreground": "#FFFFFF", "Background": "#0000AA" }, + "Focus": { "Foreground": "#000000", "Background": "#00AAAA" }, + "HotNormal": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "HotFocus": { "Foreground": "#FFFF55", "Background": "#00AAAA" }, + "Active": { "Foreground": "#0000AA", "Background": "#AAAAAA" }, + "HotActive": { "Foreground": "#AA0000", "Background": "#AAAAAA" }, + "Highlight": { "Foreground": "#000000", "Background": "#00AAAA" }, + "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "ReadOnly": { "Foreground": "#AAAAAA", "Background": "#0000AA" }, + "Disabled": { "Foreground": "#5555FF", "Background": "#0000AA" } + }, + "Sidebar": { + "Normal": { "Foreground": "#000000", "Background": "#00AAAA" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#0000AA" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00AAAA" }, + "HotFocus": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#00AAAA", "Style": "Bold" }, + "HotActive": { "Foreground": "#FFFF55", "Background": "#00AAAA", "Style": "Bold" }, + "Highlight": { "Foreground": "#FFFF55", "Background": "#00AAAA" }, + "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "ReadOnly": { "Foreground": "#555555", "Background": "#00AAAA" }, + "Disabled": { "Foreground": "#555555", "Background": "#00AAAA" } + }, + "StatusBar": { + "Normal": { "Foreground": "#000000", "Background": "#AAAAAA" }, + "HotNormal": { "Foreground": "#AA0000", "Background": "#AAAAAA" } + }, + "Accent": { + "Normal": { "Foreground": "#000000", "Background": "#00AAAA" }, + "Focus": { "Foreground": "#000000", "Background": "#00AA00" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00AAAA" }, + "HotFocus": { "Foreground": "#FFFFFF", "Background": "#00AA00" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#00AAAA", "Style": "Bold" }, + "Highlight": { "Foreground": "#FFFF55", "Background": "#00AAAA" }, + "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "ReadOnly": { "Foreground": "#555555", "Background": "#00AAAA" } + }, + "Dialog": { + "Normal": { "Foreground": "#000000", "Background": "#AAAAAA" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#00AA00" }, + "HotNormal": { "Foreground": "#FFFF55", "Background": "#AAAAAA" }, + "HotFocus": { "Foreground": "#FFFF55", "Background": "#00AA00" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#AAAAAA", "Style": "Bold" }, + "HotActive": { "Foreground": "#FFFF55", "Background": "#AAAAAA", "Style": "Bold" }, + "Highlight": { "Foreground": "#000000", "Background": "#00AAAA" }, + "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "ReadOnly": { "Foreground": "#555555", "Background": "#AAAAAA" }, + "Disabled": { "Foreground": "#555555", "Background": "#AAAAAA" } + }, + "Menu": { + "Normal": { "Foreground": "#000000", "Background": "#AAAAAA" }, + "Focus": { "Foreground": "#000000", "Background": "#00AA00" }, + "HotNormal": { "Foreground": "#AA0000", "Background": "#AAAAAA", "Style": "Underline" }, + "HotFocus": { "Foreground": "#AA0000", "Background": "#00AA00", "Style": "Underline" }, + "Active": { "Foreground": "#000000", "Background": "#00AA00" }, + "HotActive": { "Foreground": "#AA0000", "Background": "#00AA00", "Style": "Underline" }, + "Highlight": { "Foreground": "#000000", "Background": "#00AAAA" }, + "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "ReadOnly": { "Foreground": "#555555", "Background": "#AAAAAA" }, + "Disabled": { "Foreground": "#555555", "Background": "#AAAAAA" } + }, + "Error": { + "Normal": { "Foreground": "#FFFFFF", "Background": "#AA0000" }, + "Focus": { "Foreground": "#000000", "Background": "#00AA00" }, + "HotNormal": { "Foreground": "#FFFF55", "Background": "#AA0000" }, + "HotFocus": { "Foreground": "#FFFF55", "Background": "#00AA00" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#AA0000", "Style": "Bold" }, + "Highlight": { "Foreground": "#000000", "Background": "#AAAAAA" }, + "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "ReadOnly": { "Foreground": "#AAAAAA", "Background": "#AA0000" }, + "Disabled": { "Foreground": "#AAAAAA", "Background": "#AA0000" } + }, + "Warning": { + "Normal": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "Focus": { "Foreground": "#000000", "Background": "#FFFF55" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#0000AA" }, + "HotFocus": { "Foreground": "#000000", "Background": "#FFFF55" }, + "Active": { "Foreground": "#000000", "Background": "#FFFF55", "Style": "Bold" }, + "Highlight": { "Foreground": "#000000", "Background": "#AAAAAA" }, + "Editable": { "Foreground": "#FFFF55", "Background": "#0000AA" }, + "ReadOnly": { "Foreground": "#AAAAAA", "Background": "#0000AA" }, + "Disabled": { "Foreground": "#5555FF", "Background": "#0000AA" } + } } }, - { - "Modern Borland": { - "Dialog.DefaultShadow": "Opaque", - "Button.DefaultShadow": "Opaque", - "Schemes": [ - { - "Base": { - "Normal": { "Foreground": "#A8A8A8", "Background": "#003078" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, - "HotNormal": { "Foreground": "#57FFFF", "Background": "#003078" }, - "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, - "Active": { "Foreground": "#003078", "Background": "#A8A8A8" }, - "HotActive": { "Foreground": "#A80000", "Background": "#A8A8A8" }, - "Highlight": { "Foreground": "#000000", "Background": "#00A8A8" }, - "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, - "ReadOnly": { "Foreground": "#A8A8A8", "Background": "#003078" }, - "Disabled": { "Foreground": "#575757", "Background": "#003078" } - } - }, - { - "Sidebar": { - "Normal": { "Foreground": "#000000", "Background": "#00A8A8" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00A8A8" }, - "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#006C90" }, - "HotActive": { "Foreground": "#FFFF57", "Background": "#006C90" }, - "Highlight": { "Foreground": "#FFFFFF", "Background": "#006C90" }, - "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, - "ReadOnly": { "Foreground": "#004E84", "Background": "#00A8A8" }, - "Disabled": { "Foreground": "#004E84", "Background": "#00A8A8" } - } - }, - { - "StatusBar": { - "Normal": { "Foreground": "#000000", "Background": "#308800" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#308800" } - } - }, - { - "Accent": { - "Normal": { "Foreground": "#000000", "Background": "#00A8A8" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00A8A8" }, - "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#006C90" }, - "Highlight": { "Foreground": "#FFFFFF", "Background": "#006C90" }, - "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, - "ReadOnly": { "Foreground": "#004E84", "Background": "#00A8A8" } - } - }, - { - "Dialog": { - "Normal": { "Foreground": "#000000", "Background": "#00A8A8" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00A8A8" }, - "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#006C90" }, - "HotActive": { "Foreground": "#FFFF57", "Background": "#006C90" }, - "Highlight": { "Foreground": "#FFFFFF", "Background": "#006C90" }, - "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, - "ReadOnly": { "Foreground": "#004E84", "Background": "#00A8A8" }, - "Disabled": { "Foreground": "#004E84", "Background": "#00A8A8" } - } - }, - { - "Menu": { - "Normal": { "Foreground": "#000000", "Background": "#A8A8A8" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, - "HotNormal": { "Foreground": "#A80000", "Background": "#A8A8A8", "Style": "Underline" }, - "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800", "Style": "Underline" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#308800" }, - "HotActive": { "Foreground": "#FFFF57", "Background": "#308800", "Style": "Underline" }, - "Highlight": { "Foreground": "#FFFFFF", "Background": "#006C90" }, - "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, - "ReadOnly": { "Foreground": "#545454", "Background": "#A8A8A8" }, - "Disabled": { "Foreground": "#545454", "Background": "#A8A8A8" } - } - }, - { - "Error": { - "Normal": { "Foreground": "#FFFF57", "Background": "#A80000" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#A80000" }, - "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#A80000", "Style": "Bold" }, - "Highlight": { "Foreground": "#000000", "Background": "#A8A8A8" }, - "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, - "ReadOnly": { "Foreground": "#A8A8A8", "Background": "#A80000" }, - "Disabled": { "Foreground": "#A8A8A8", "Background": "#A80000" } - } - }, - { - "Warning": { - "Normal": { "Foreground": "#FFFF57", "Background": "#003078" }, - "Focus": { "Foreground": "#000000", "Background": "#FFFF57" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#003078" }, - "HotFocus": { "Foreground": "#000000", "Background": "#FFFF57" }, - "Active": { "Foreground": "#000000", "Background": "#FFFF57", "Style": "Bold" }, - "Highlight": { "Foreground": "#000000", "Background": "#A8A8A8" }, - "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, - "ReadOnly": { "Foreground": "#A8A8A8", "Background": "#003078" }, - "Disabled": { "Foreground": "#004E84", "Background": "#003078" } - } - } - ] + "Modern Borland": { + "Dialog": { + "DefaultShadow": "Opaque" + }, + "Button": { + "DefaultShadow": "Opaque" + }, + "Schemes": { + "Base": { + "Normal": { "Foreground": "#A8A8A8", "Background": "#003078" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, + "HotNormal": { "Foreground": "#57FFFF", "Background": "#003078" }, + "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, + "Active": { "Foreground": "#003078", "Background": "#A8A8A8" }, + "HotActive": { "Foreground": "#A80000", "Background": "#A8A8A8" }, + "Highlight": { "Foreground": "#000000", "Background": "#00A8A8" }, + "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, + "ReadOnly": { "Foreground": "#A8A8A8", "Background": "#003078" }, + "Disabled": { "Foreground": "#575757", "Background": "#003078" } + }, + "Sidebar": { + "Normal": { "Foreground": "#000000", "Background": "#00A8A8" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00A8A8" }, + "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#006C90" }, + "HotActive": { "Foreground": "#FFFF57", "Background": "#006C90" }, + "Highlight": { "Foreground": "#FFFFFF", "Background": "#006C90" }, + "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, + "ReadOnly": { "Foreground": "#004E84", "Background": "#00A8A8" }, + "Disabled": { "Foreground": "#004E84", "Background": "#00A8A8" } + }, + "StatusBar": { + "Normal": { "Foreground": "#000000", "Background": "#308800" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#308800" } + }, + "Accent": { + "Normal": { "Foreground": "#000000", "Background": "#00A8A8" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00A8A8" }, + "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#006C90" }, + "Highlight": { "Foreground": "#FFFFFF", "Background": "#006C90" }, + "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, + "ReadOnly": { "Foreground": "#004E84", "Background": "#00A8A8" } + }, + "Dialog": { + "Normal": { "Foreground": "#000000", "Background": "#00A8A8" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#00A8A8" }, + "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#006C90" }, + "HotActive": { "Foreground": "#FFFF57", "Background": "#006C90" }, + "Highlight": { "Foreground": "#FFFFFF", "Background": "#006C90" }, + "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, + "ReadOnly": { "Foreground": "#004E84", "Background": "#00A8A8" }, + "Disabled": { "Foreground": "#004E84", "Background": "#00A8A8" } + }, + "Menu": { + "Normal": { "Foreground": "#000000", "Background": "#A8A8A8" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, + "HotNormal": { "Foreground": "#A80000", "Background": "#A8A8A8", "Style": "Underline" }, + "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800", "Style": "Underline" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#308800" }, + "HotActive": { "Foreground": "#FFFF57", "Background": "#308800", "Style": "Underline" }, + "Highlight": { "Foreground": "#FFFFFF", "Background": "#006C90" }, + "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, + "ReadOnly": { "Foreground": "#545454", "Background": "#A8A8A8" }, + "Disabled": { "Foreground": "#545454", "Background": "#A8A8A8" } + }, + "Error": { + "Normal": { "Foreground": "#FFFF57", "Background": "#A80000" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#308800" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#A80000" }, + "HotFocus": { "Foreground": "#FFFF57", "Background": "#308800" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#A80000", "Style": "Bold" }, + "Highlight": { "Foreground": "#000000", "Background": "#A8A8A8" }, + "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, + "ReadOnly": { "Foreground": "#A8A8A8", "Background": "#A80000" }, + "Disabled": { "Foreground": "#A8A8A8", "Background": "#A80000" } + }, + "Warning": { + "Normal": { "Foreground": "#FFFF57", "Background": "#003078" }, + "Focus": { "Foreground": "#000000", "Background": "#FFFF57" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#003078" }, + "HotFocus": { "Foreground": "#000000", "Background": "#FFFF57" }, + "Active": { "Foreground": "#000000", "Background": "#FFFF57", "Style": "Bold" }, + "Highlight": { "Foreground": "#000000", "Background": "#A8A8A8" }, + "Editable": { "Foreground": "#FFFF57", "Background": "#003078" }, + "ReadOnly": { "Foreground": "#A8A8A8", "Background": "#003078" }, + "Disabled": { "Foreground": "#004E84", "Background": "#003078" } + } } }, - { - "Midnight": { - "Dialog.DefaultShadow": "None", - "Button.DefaultShadow": "None", - "Schemes": [ - { - "Base": { - "Normal": { "Foreground": "#8B93A1", "Background": "#1B1E24" }, - "Focus": { "Foreground": "#E6E9EF", "Background": "#2F3A4D" }, - "HotNormal": { "Foreground": "#6CB6FF", "Background": "#1B1E24" }, - "HotFocus": { "Foreground": "#6CB6FF", "Background": "#2F3A4D" }, - "Active": { "Foreground": "#E6E9EF", "Background": "#264F78" }, - "HotActive": { "Foreground": "#6CB6FF", "Background": "#264F78" }, - "Highlight": { "Foreground": "#E6E9EF", "Background": "#5C4A14" }, - "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, - "ReadOnly": { "Foreground": "#8B93A1", "Background": "#1B1E24", "Style": "Italic" }, - "Disabled": { "Foreground": "#4B5263", "Background": "#1B1E24" } - } - }, - { - "Sidebar": { - "Normal": { "Foreground": "#A9B1BE", "Background": "#15171C" }, - "Focus": { "Foreground": "#E6E9EF", "Background": "#2F3A4D" }, - "HotNormal": { "Foreground": "#6CB6FF", "Background": "#15171C" }, - "HotFocus": { "Foreground": "#6CB6FF", "Background": "#2F3A4D" }, - "Active": { "Foreground": "#E6E9EF", "Background": "#262A33" }, - "HotActive": { "Foreground": "#6CB6FF", "Background": "#262A33" }, - "Highlight": { "Foreground": "#E6C07B", "Background": "#15171C" }, - "Editable": { "Foreground": "#D4D7DD", "Background": "#262A33" }, - "ReadOnly": { "Foreground": "#5C6370", "Background": "#15171C" }, - "Disabled": { "Foreground": "#4B5263", "Background": "#15171C" } - } - }, - { - "StatusBar": { - "Normal": { "Foreground": "#E6E9EF", "Background": "#2B5A8A" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#2B5A8A" } - } - }, - { - "Accent": { - "Normal": { "Foreground": "#A9B1BE", "Background": "#262A33" }, - "Focus": { "Foreground": "#E6E9EF", "Background": "#2F3A4D" }, - "HotNormal": { "Foreground": "#6CB6FF", "Background": "#262A33" }, - "HotFocus": { "Foreground": "#6CB6FF", "Background": "#2F3A4D" }, - "Active": { "Foreground": "#E6E9EF", "Background": "#262A33", "Style": "Bold" }, - "Highlight": { "Foreground": "#E6E9EF", "Background": "#3A4150" }, - "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, - "ReadOnly": { "Foreground": "#5C6370", "Background": "#262A33" } - } - }, - { - "Dialog": { - "Normal": { "Foreground": "#C5CAD3", "Background": "#262A33" }, - "Focus": { "Foreground": "#E6E9EF", "Background": "#2F4A6D" }, - "HotNormal": { "Foreground": "#6CB6FF", "Background": "#262A33" }, - "HotFocus": { "Foreground": "#6CB6FF", "Background": "#2F4A6D" }, - "Active": { "Foreground": "#E6E9EF", "Background": "#3A4150" }, - "HotActive": { "Foreground": "#6CB6FF", "Background": "#3A4150" }, - "Highlight": { "Foreground": "#E6C07B", "Background": "#262A33" }, - "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, - "ReadOnly": { "Foreground": "#7F8794", "Background": "#262A33" }, - "Disabled": { "Foreground": "#4B5263", "Background": "#262A33" } - } - }, - { - "Menu": { - "Normal": { "Foreground": "#FFFFFF", "Background": "#2B5A8A" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#0F3557" }, - "HotNormal": { "Foreground": "#FFD866", "Background": "#2B5A8A", "Style": "Underline" }, - "HotFocus": { "Foreground": "#FFD866", "Background": "#0F3557", "Style": "Underline" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#0F3557" }, - "HotActive": { "Foreground": "#FFD866", "Background": "#0F3557", "Style": "Underline" }, - "Highlight": { "Foreground": "#FFD866", "Background": "#2B5A8A" }, - "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, - "ReadOnly": { "Foreground": "#C7D4E2", "Background": "#2B5A8A" }, - "Disabled": { "Foreground": "#9CB4CC", "Background": "#2B5A8A" } - } - }, - { - "Error": { - "Normal": { "Foreground": "#F2B8B5", "Background": "#3B1E22" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#8C2F39" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#3B1E22" }, - "HotFocus": { "Foreground": "#FFFFFF", "Background": "#8C2F39" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#8C2F39" }, - "Highlight": { "Foreground": "#FFFFFF", "Background": "#5A2A30" }, - "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, - "ReadOnly": { "Foreground": "#B08A8A", "Background": "#3B1E22" }, - "Disabled": { "Foreground": "#6E4A4E", "Background": "#3B1E22" } - } - }, - { - "Warning": { - "Normal": { "Foreground": "#E3B341", "Background": "#3B3018" }, - "Focus": { "Foreground": "#1B1E24", "Background": "#E3B341" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#3B3018" }, - "HotFocus": { "Foreground": "#1B1E24", "Background": "#E3B341" }, - "Active": { "Foreground": "#1B1E24", "Background": "#E3B341" }, - "Highlight": { "Foreground": "#E6E9EF", "Background": "#5A4A22" }, - "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, - "ReadOnly": { "Foreground": "#B0975E", "Background": "#3B3018" }, - "Disabled": { "Foreground": "#6E5F3A", "Background": "#3B3018" } - } - } - ] + "Midnight": { + "Dialog": { + "DefaultShadow": "None" + }, + "Button": { + "DefaultShadow": "None" + }, + "Schemes": { + "Base": { + "Normal": { "Foreground": "#8B93A1", "Background": "#1B1E24" }, + "Focus": { "Foreground": "#E6E9EF", "Background": "#2F3A4D" }, + "HotNormal": { "Foreground": "#6CB6FF", "Background": "#1B1E24" }, + "HotFocus": { "Foreground": "#6CB6FF", "Background": "#2F3A4D" }, + "Active": { "Foreground": "#E6E9EF", "Background": "#264F78" }, + "HotActive": { "Foreground": "#6CB6FF", "Background": "#264F78" }, + "Highlight": { "Foreground": "#E6E9EF", "Background": "#5C4A14" }, + "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, + "ReadOnly": { "Foreground": "#8B93A1", "Background": "#1B1E24", "Style": "Italic" }, + "Disabled": { "Foreground": "#4B5263", "Background": "#1B1E24" } + }, + "Sidebar": { + "Normal": { "Foreground": "#A9B1BE", "Background": "#15171C" }, + "Focus": { "Foreground": "#E6E9EF", "Background": "#2F3A4D" }, + "HotNormal": { "Foreground": "#6CB6FF", "Background": "#15171C" }, + "HotFocus": { "Foreground": "#6CB6FF", "Background": "#2F3A4D" }, + "Active": { "Foreground": "#E6E9EF", "Background": "#262A33" }, + "HotActive": { "Foreground": "#6CB6FF", "Background": "#262A33" }, + "Highlight": { "Foreground": "#E6C07B", "Background": "#15171C" }, + "Editable": { "Foreground": "#D4D7DD", "Background": "#262A33" }, + "ReadOnly": { "Foreground": "#5C6370", "Background": "#15171C" }, + "Disabled": { "Foreground": "#4B5263", "Background": "#15171C" } + }, + "StatusBar": { + "Normal": { "Foreground": "#E6E9EF", "Background": "#2B5A8A" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#2B5A8A" } + }, + "Accent": { + "Normal": { "Foreground": "#A9B1BE", "Background": "#262A33" }, + "Focus": { "Foreground": "#E6E9EF", "Background": "#2F3A4D" }, + "HotNormal": { "Foreground": "#6CB6FF", "Background": "#262A33" }, + "HotFocus": { "Foreground": "#6CB6FF", "Background": "#2F3A4D" }, + "Active": { "Foreground": "#E6E9EF", "Background": "#262A33", "Style": "Bold" }, + "Highlight": { "Foreground": "#E6E9EF", "Background": "#3A4150" }, + "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, + "ReadOnly": { "Foreground": "#5C6370", "Background": "#262A33" } + }, + "Dialog": { + "Normal": { "Foreground": "#C5CAD3", "Background": "#262A33" }, + "Focus": { "Foreground": "#E6E9EF", "Background": "#2F4A6D" }, + "HotNormal": { "Foreground": "#6CB6FF", "Background": "#262A33" }, + "HotFocus": { "Foreground": "#6CB6FF", "Background": "#2F4A6D" }, + "Active": { "Foreground": "#E6E9EF", "Background": "#3A4150" }, + "HotActive": { "Foreground": "#6CB6FF", "Background": "#3A4150" }, + "Highlight": { "Foreground": "#E6C07B", "Background": "#262A33" }, + "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, + "ReadOnly": { "Foreground": "#7F8794", "Background": "#262A33" }, + "Disabled": { "Foreground": "#4B5263", "Background": "#262A33" } + }, + "Menu": { + "Normal": { "Foreground": "#FFFFFF", "Background": "#2B5A8A" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#0F3557" }, + "HotNormal": { "Foreground": "#FFD866", "Background": "#2B5A8A", "Style": "Underline" }, + "HotFocus": { "Foreground": "#FFD866", "Background": "#0F3557", "Style": "Underline" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#0F3557" }, + "HotActive": { "Foreground": "#FFD866", "Background": "#0F3557", "Style": "Underline" }, + "Highlight": { "Foreground": "#FFD866", "Background": "#2B5A8A" }, + "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, + "ReadOnly": { "Foreground": "#C7D4E2", "Background": "#2B5A8A" }, + "Disabled": { "Foreground": "#9CB4CC", "Background": "#2B5A8A" } + }, + "Error": { + "Normal": { "Foreground": "#F2B8B5", "Background": "#3B1E22" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#8C2F39" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#3B1E22" }, + "HotFocus": { "Foreground": "#FFFFFF", "Background": "#8C2F39" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#8C2F39" }, + "Highlight": { "Foreground": "#FFFFFF", "Background": "#5A2A30" }, + "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, + "ReadOnly": { "Foreground": "#B08A8A", "Background": "#3B1E22" }, + "Disabled": { "Foreground": "#6E4A4E", "Background": "#3B1E22" } + }, + "Warning": { + "Normal": { "Foreground": "#E3B341", "Background": "#3B3018" }, + "Focus": { "Foreground": "#1B1E24", "Background": "#E3B341" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#3B3018" }, + "HotFocus": { "Foreground": "#1B1E24", "Background": "#E3B341" }, + "Active": { "Foreground": "#1B1E24", "Background": "#E3B341" }, + "Highlight": { "Foreground": "#E6E9EF", "Background": "#5A4A22" }, + "Editable": { "Foreground": "#D4D7DD", "Background": "#1B1E24" }, + "ReadOnly": { "Foreground": "#B0975E", "Background": "#3B3018" }, + "Disabled": { "Foreground": "#6E5F3A", "Background": "#3B3018" } + } } }, - { - "Daylight": { - "Dialog.DefaultShadow": "None", - "Button.DefaultShadow": "None", - "Schemes": [ - { - "Base": { - "Normal": { "Foreground": "#6A6F78", "Background": "#FAFAFA" }, - "Focus": { "Foreground": "#1F2328", "Background": "#DCE6F3" }, - "HotNormal": { "Foreground": "#0969DA", "Background": "#FAFAFA" }, - "HotFocus": { "Foreground": "#0969DA", "Background": "#DCE6F3" }, - "Active": { "Foreground": "#1F2328", "Background": "#BFD7F2" }, - "HotActive": { "Foreground": "#0969DA", "Background": "#BFD7F2" }, - "Highlight": { "Foreground": "#1F2328", "Background": "#F5E3A3" }, - "Editable": { "Foreground": "#383A42", "Background": "#FAFAFA" }, - "ReadOnly": { "Foreground": "#6A6F78", "Background": "#FAFAFA", "Style": "Italic" }, - "Disabled": { "Foreground": "#A0A4AC", "Background": "#FAFAFA" } - } - }, - { - "Sidebar": { - "Normal": { "Foreground": "#4B5058", "Background": "#F0F1F4" }, - "Focus": { "Foreground": "#1F2328", "Background": "#DCE6F3" }, - "HotNormal": { "Foreground": "#0969DA", "Background": "#F0F1F4" }, - "HotFocus": { "Foreground": "#0969DA", "Background": "#DCE6F3" }, - "Active": { "Foreground": "#1F2328", "Background": "#E2E4E9" }, - "HotActive": { "Foreground": "#0969DA", "Background": "#E2E4E9" }, - "Highlight": { "Foreground": "#9A6700", "Background": "#F0F1F4" }, - "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, - "ReadOnly": { "Foreground": "#8A8F98", "Background": "#F0F1F4" }, - "Disabled": { "Foreground": "#A0A4AC", "Background": "#F0F1F4" } - } - }, - { - "StatusBar": { - "Normal": { "Foreground": "#FFFFFF", "Background": "#3B6EA8" }, - "HotNormal": { "Foreground": "#FFFFFF", "Background": "#3B6EA8" } - } - }, - { - "Accent": { - "Normal": { "Foreground": "#4B5058", "Background": "#EDEEF1" }, - "Focus": { "Foreground": "#1F2328", "Background": "#DCE6F3" }, - "HotNormal": { "Foreground": "#0969DA", "Background": "#EDEEF1" }, - "HotFocus": { "Foreground": "#0969DA", "Background": "#DCE6F3" }, - "Active": { "Foreground": "#1F2328", "Background": "#EDEEF1", "Style": "Bold" }, - "Highlight": { "Foreground": "#1F2328", "Background": "#E2E4E9" }, - "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, - "ReadOnly": { "Foreground": "#8A8F98", "Background": "#EDEEF1" } - } - }, - { - "Dialog": { - "Normal": { "Foreground": "#383A42", "Background": "#EDEEF1" }, - "Focus": { "Foreground": "#1F2328", "Background": "#CFE0F5" }, - "HotNormal": { "Foreground": "#0969DA", "Background": "#EDEEF1" }, - "HotFocus": { "Foreground": "#0969DA", "Background": "#CFE0F5" }, - "Active": { "Foreground": "#1F2328", "Background": "#DADDE3" }, - "HotActive": { "Foreground": "#0969DA", "Background": "#DADDE3" }, - "Highlight": { "Foreground": "#9A6700", "Background": "#EDEEF1" }, - "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, - "ReadOnly": { "Foreground": "#6A6F78", "Background": "#EDEEF1" }, - "Disabled": { "Foreground": "#A0A4AC", "Background": "#EDEEF1" } - } - }, - { - "Menu": { - "Normal": { "Foreground": "#FFFFFF", "Background": "#2B5A8A" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#0F3557" }, - "HotNormal": { "Foreground": "#FFD866", "Background": "#2B5A8A", "Style": "Underline" }, - "HotFocus": { "Foreground": "#FFD866", "Background": "#0F3557", "Style": "Underline" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#0F3557" }, - "HotActive": { "Foreground": "#FFD866", "Background": "#0F3557", "Style": "Underline" }, - "Highlight": { "Foreground": "#FFD866", "Background": "#2B5A8A" }, - "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, - "ReadOnly": { "Foreground": "#C7D4E2", "Background": "#2B5A8A" }, - "Disabled": { "Foreground": "#9CB4CC", "Background": "#2B5A8A" } - } - }, - { - "Error": { - "Normal": { "Foreground": "#B3261E", "Background": "#FBEAEA" }, - "Focus": { "Foreground": "#FFFFFF", "Background": "#CF222E" }, - "HotNormal": { "Foreground": "#1F2328", "Background": "#FBEAEA" }, - "HotFocus": { "Foreground": "#FFFFFF", "Background": "#CF222E" }, - "Active": { "Foreground": "#FFFFFF", "Background": "#CF222E" }, - "Highlight": { "Foreground": "#1F2328", "Background": "#F5C6C6" }, - "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, - "ReadOnly": { "Foreground": "#9A6B6B", "Background": "#FBEAEA" }, - "Disabled": { "Foreground": "#C9A0A0", "Background": "#FBEAEA" } - } - }, - { - "Warning": { - "Normal": { "Foreground": "#9A6700", "Background": "#FDF4DC" }, - "Focus": { "Foreground": "#1F2328", "Background": "#F5D98A" }, - "HotNormal": { "Foreground": "#1F2328", "Background": "#FDF4DC" }, - "HotFocus": { "Foreground": "#1F2328", "Background": "#F5D98A" }, - "Active": { "Foreground": "#1F2328", "Background": "#F5D98A" }, - "Highlight": { "Foreground": "#1F2328", "Background": "#F5E3A3" }, - "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, - "ReadOnly": { "Foreground": "#8A7340", "Background": "#FDF4DC" }, - "Disabled": { "Foreground": "#C2B48A", "Background": "#FDF4DC" } - } - } - ] + "Daylight": { + "Dialog": { + "DefaultShadow": "None" + }, + "Button": { + "DefaultShadow": "None" + }, + "Schemes": { + "Base": { + "Normal": { "Foreground": "#6A6F78", "Background": "#FAFAFA" }, + "Focus": { "Foreground": "#1F2328", "Background": "#DCE6F3" }, + "HotNormal": { "Foreground": "#0969DA", "Background": "#FAFAFA" }, + "HotFocus": { "Foreground": "#0969DA", "Background": "#DCE6F3" }, + "Active": { "Foreground": "#1F2328", "Background": "#BFD7F2" }, + "HotActive": { "Foreground": "#0969DA", "Background": "#BFD7F2" }, + "Highlight": { "Foreground": "#1F2328", "Background": "#F5E3A3" }, + "Editable": { "Foreground": "#383A42", "Background": "#FAFAFA" }, + "ReadOnly": { "Foreground": "#6A6F78", "Background": "#FAFAFA", "Style": "Italic" }, + "Disabled": { "Foreground": "#A0A4AC", "Background": "#FAFAFA" } + }, + "Sidebar": { + "Normal": { "Foreground": "#4B5058", "Background": "#F0F1F4" }, + "Focus": { "Foreground": "#1F2328", "Background": "#DCE6F3" }, + "HotNormal": { "Foreground": "#0969DA", "Background": "#F0F1F4" }, + "HotFocus": { "Foreground": "#0969DA", "Background": "#DCE6F3" }, + "Active": { "Foreground": "#1F2328", "Background": "#E2E4E9" }, + "HotActive": { "Foreground": "#0969DA", "Background": "#E2E4E9" }, + "Highlight": { "Foreground": "#9A6700", "Background": "#F0F1F4" }, + "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, + "ReadOnly": { "Foreground": "#8A8F98", "Background": "#F0F1F4" }, + "Disabled": { "Foreground": "#A0A4AC", "Background": "#F0F1F4" } + }, + "StatusBar": { + "Normal": { "Foreground": "#FFFFFF", "Background": "#3B6EA8" }, + "HotNormal": { "Foreground": "#FFFFFF", "Background": "#3B6EA8" } + }, + "Accent": { + "Normal": { "Foreground": "#4B5058", "Background": "#EDEEF1" }, + "Focus": { "Foreground": "#1F2328", "Background": "#DCE6F3" }, + "HotNormal": { "Foreground": "#0969DA", "Background": "#EDEEF1" }, + "HotFocus": { "Foreground": "#0969DA", "Background": "#DCE6F3" }, + "Active": { "Foreground": "#1F2328", "Background": "#EDEEF1", "Style": "Bold" }, + "Highlight": { "Foreground": "#1F2328", "Background": "#E2E4E9" }, + "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, + "ReadOnly": { "Foreground": "#8A8F98", "Background": "#EDEEF1" } + }, + "Dialog": { + "Normal": { "Foreground": "#383A42", "Background": "#EDEEF1" }, + "Focus": { "Foreground": "#1F2328", "Background": "#CFE0F5" }, + "HotNormal": { "Foreground": "#0969DA", "Background": "#EDEEF1" }, + "HotFocus": { "Foreground": "#0969DA", "Background": "#CFE0F5" }, + "Active": { "Foreground": "#1F2328", "Background": "#DADDE3" }, + "HotActive": { "Foreground": "#0969DA", "Background": "#DADDE3" }, + "Highlight": { "Foreground": "#9A6700", "Background": "#EDEEF1" }, + "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, + "ReadOnly": { "Foreground": "#6A6F78", "Background": "#EDEEF1" }, + "Disabled": { "Foreground": "#A0A4AC", "Background": "#EDEEF1" } + }, + "Menu": { + "Normal": { "Foreground": "#FFFFFF", "Background": "#2B5A8A" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#0F3557" }, + "HotNormal": { "Foreground": "#FFD866", "Background": "#2B5A8A", "Style": "Underline" }, + "HotFocus": { "Foreground": "#FFD866", "Background": "#0F3557", "Style": "Underline" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#0F3557" }, + "HotActive": { "Foreground": "#FFD866", "Background": "#0F3557", "Style": "Underline" }, + "Highlight": { "Foreground": "#FFD866", "Background": "#2B5A8A" }, + "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, + "ReadOnly": { "Foreground": "#C7D4E2", "Background": "#2B5A8A" }, + "Disabled": { "Foreground": "#9CB4CC", "Background": "#2B5A8A" } + }, + "Error": { + "Normal": { "Foreground": "#B3261E", "Background": "#FBEAEA" }, + "Focus": { "Foreground": "#FFFFFF", "Background": "#CF222E" }, + "HotNormal": { "Foreground": "#1F2328", "Background": "#FBEAEA" }, + "HotFocus": { "Foreground": "#FFFFFF", "Background": "#CF222E" }, + "Active": { "Foreground": "#FFFFFF", "Background": "#CF222E" }, + "Highlight": { "Foreground": "#1F2328", "Background": "#F5C6C6" }, + "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, + "ReadOnly": { "Foreground": "#9A6B6B", "Background": "#FBEAEA" }, + "Disabled": { "Foreground": "#C9A0A0", "Background": "#FBEAEA" } + }, + "Warning": { + "Normal": { "Foreground": "#9A6700", "Background": "#FDF4DC" }, + "Focus": { "Foreground": "#1F2328", "Background": "#F5D98A" }, + "HotNormal": { "Foreground": "#1F2328", "Background": "#FDF4DC" }, + "HotFocus": { "Foreground": "#1F2328", "Background": "#F5D98A" }, + "Active": { "Foreground": "#1F2328", "Background": "#F5D98A" }, + "Highlight": { "Foreground": "#1F2328", "Background": "#F5E3A3" }, + "Editable": { "Foreground": "#383A42", "Background": "#FFFFFF" }, + "ReadOnly": { "Foreground": "#8A7340", "Background": "#FDF4DC" }, + "Disabled": { "Foreground": "#C2B48A", "Background": "#FDF4DC" } + } } } - ] + } } diff --git a/src/TuiCode.Workbench/TuiCode.Workbench.csproj b/src/TuiCode.Workbench/TuiCode.Workbench.csproj index 16977dbf..b0ad83d6 100644 --- a/src/TuiCode.Workbench/TuiCode.Workbench.csproj +++ b/src/TuiCode.Workbench/TuiCode.Workbench.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/tests/TuiCode.Tests/DefaultSettingsServiceTests.cs b/tests/TuiCode.Tests/DefaultSettingsServiceTests.cs index 7b1e178c..c1f74210 100644 --- a/tests/TuiCode.Tests/DefaultSettingsServiceTests.cs +++ b/tests/TuiCode.Tests/DefaultSettingsServiceTests.cs @@ -10,6 +10,14 @@ namespace TuiCode.Tests; // "StaticConfiguration" collection and snapshot/restores the theme (issue #77). public class DefaultSettingsServiceTests : StaticConfigurationTest { + private readonly IDisposable _themes = LoadBundledThemes(); + + public override void Dispose() + { + _themes.Dispose(); + base.Dispose(); + } + [Fact] public void Save_writes_empty_object_when_theme_is_default() { @@ -56,28 +64,16 @@ public void Save_creates_parent_directory_if_missing() [Fact] public void Only_our_themes_are_offered_and_each_defines_every_scheme() { - ConfigurationManager.Enable(ConfigLocations.None); - try - { - ConfigurationManager.RuntimeConfig = BundledThemes.Config; - ConfigurationManager.Load(ConfigLocations.LibraryResources | ConfigLocations.Runtime); - - Assert.Equal( - [BundledThemes.Midnight, BundledThemes.Daylight, BundledThemes.TurboPascal, BundledThemes.ModernBorland], - new DefaultSettingsService(new MockFileSystem()).AvailableThemes); - - foreach (var theme in BundledThemes.Names) - { - ThemeManager.Theme = theme; - ConfigurationManager.Apply(); - foreach (var scheme in new[] { "Base", "Accent", "Dialog", "Menu", "Error", "Warning", "Sidebar", "StatusBar" }) - Assert.True(SchemeManager.TryGetScheme(scheme, out _), $"{theme} has no {scheme} scheme"); - } - } - finally + using var themes = LoadBundledThemes(); + Assert.Equal( + [BundledThemes.Midnight, BundledThemes.Daylight, BundledThemes.TurboPascal, BundledThemes.ModernBorland], + new DefaultSettingsService(new MockFileSystem()).AvailableThemes); + + foreach (var theme in BundledThemes.Names) { - ThemeManager.Theme = "Default"; - ConfigurationManager.Disable(resetToHardCodedDefaults: true); + ThemeManager.Theme = theme; + foreach (var scheme in new[] { "Base", "Accent", "Dialog", "Menu", "Error", "Warning", "Sidebar", "StatusBar" }) + Assert.True(SchemeManager.TryGetScheme(scheme, out _), $"{theme} has no {scheme} scheme"); } } diff --git a/tests/TuiCode.Tests/DiskChangeColourTests.cs b/tests/TuiCode.Tests/DiskChangeColourTests.cs index 083da89b..1b713fa4 100644 --- a/tests/TuiCode.Tests/DiskChangeColourTests.cs +++ b/tests/TuiCode.Tests/DiskChangeColourTests.cs @@ -19,14 +19,12 @@ public class DiskChangeColourTests : StaticConfigurationTest private readonly IApplication _app = Application.Create().Init(DriverRegistry.Names.ANSI); private readonly MockFileSystem _fs = new(); private readonly IFileInfo _file; + private readonly IDisposable _themes; public DiskChangeColourTests() { - ConfigurationManager.Enable(ConfigLocations.None); - ConfigurationManager.RuntimeConfig = BundledThemes.Config; - ConfigurationManager.Load(ConfigLocations.LibraryResources | ConfigLocations.Runtime); + _themes = LoadBundledThemes(); ThemeManager.Theme = BundledThemes.Midnight; - ConfigurationManager.Apply(); _fs.AddFile("/work/a.txt", new MockFileData("one\n")); _file = _fs.FileInfo.New("/work/a.txt"); _app.Driver!.SetScreenSize(Width, 6); @@ -35,7 +33,7 @@ public DiskChangeColourTests() public override void Dispose() { _app.Dispose(); - ConfigurationManager.Disable(resetToHardCodedDefaults: true); + _themes.Dispose(); base.Dispose(); } @@ -47,7 +45,6 @@ public override void Dispose() public void The_explorer_names_a_changed_file_in_the_warning_colour(string theme) { ThemeManager.Theme = theme; - ConfigurationManager.Apply(); using var explorer = new FileExplorerView { App = _app, Width = Width, Height = 4 }; explorer.Open(_fs.DirectoryInfo.New("/work")); diff --git a/tests/TuiCode.Tests/EditorGutterTests.cs b/tests/TuiCode.Tests/EditorGutterTests.cs index 4df4ea63..903201db 100644 --- a/tests/TuiCode.Tests/EditorGutterTests.cs +++ b/tests/TuiCode.Tests/EditorGutterTests.cs @@ -113,6 +113,7 @@ public async Task Gutter_draws_line_numbers_with_a_change_marker_and_tg_hides_it var commands = new CommandService(); using var host = new WorkbenchHost(workbench, commands, new KeybindingService(commands), new InputScopeStack(), new InMemorySettingsService(), driverName: DriverRegistry.Names.ANSI); + HostSteps.PinScreenSize(host, 80, 25); EditorTab? tab = null; var withGutter = ""; var withoutGutter = ""; diff --git a/tests/TuiCode.Tests/EditorScrollBarTests.cs b/tests/TuiCode.Tests/EditorScrollBarTests.cs index b584051d..b3bcb5e1 100644 --- a/tests/TuiCode.Tests/EditorScrollBarTests.cs +++ b/tests/TuiCode.Tests/EditorScrollBarTests.cs @@ -108,6 +108,7 @@ public async Task A_tall_file_draws_the_bar_and_leaves_the_gutter_alone() var commands = new CommandService(); using var host = new WorkbenchHost(workbench, commands, new KeybindingService(commands), new InputScopeStack(), new InMemorySettingsService(), driverName: DriverRegistry.Names.ANSI); + HostSteps.PinScreenSize(host, 80, 25); var screen = ""; await HostSteps.Run(host, diff --git a/tests/TuiCode.Tests/FocusReadoutHostTests.cs b/tests/TuiCode.Tests/FocusReadoutHostTests.cs index ee67e08d..ec984c59 100644 --- a/tests/TuiCode.Tests/FocusReadoutHostTests.cs +++ b/tests/TuiCode.Tests/FocusReadoutHostTests.cs @@ -152,31 +152,19 @@ await HostSteps.Run(host, [Fact] public void Every_bundled_theme_draws_a_focused_pane_differently_from_an_unfocused_one() { - ConfigurationManager.Enable(ConfigLocations.None); - try + using var themes = LoadBundledThemes(); + foreach (var theme in BundledThemes.Names) { - ConfigurationManager.RuntimeConfig = BundledThemes.Config; - ConfigurationManager.Load(ConfigLocations.LibraryResources | ConfigLocations.Runtime); - - foreach (var theme in BundledThemes.Names) + ThemeManager.Theme = theme; + foreach (var name in new[] { "Base", "Sidebar" }) { - ThemeManager.Theme = theme; - ConfigurationManager.Apply(); - foreach (var name in new[] { "Base", "Sidebar" }) - { - Assert.True(SchemeManager.TryGetScheme(name, out var scheme)); - var normal = scheme!.GetAttributeForRole(VisualRole.Normal, null); - var focus = scheme.GetAttributeForRole(VisualRole.Focus, null); - Assert.True(normal != focus, $"{theme}'s {name} draws a focused border like an unfocused one"); - Assert.True(focus.Foreground != focus.Background, $"{theme}'s {name} focus colour is unreadable"); - } + Assert.True(SchemeManager.TryGetScheme(name, out var scheme)); + var normal = scheme!.GetAttributeForRole(VisualRole.Normal, null); + var focus = scheme.GetAttributeForRole(VisualRole.Focus, null); + Assert.True(normal != focus, $"{theme}'s {name} draws a focused border like an unfocused one"); + Assert.True(focus.Foreground != focus.Background, $"{theme}'s {name} focus colour is unreadable"); } } - finally - { - ThemeManager.Theme = "Default"; - ConfigurationManager.Disable(resetToHardCodedDefaults: true); - } } private static (Attribute? Sidebar, Attribute? Editor) Borders(WorkbenchHost host, Workbench.Workbench workbench) => diff --git a/tests/TuiCode.Tests/MenuBarHostTests.cs b/tests/TuiCode.Tests/MenuBarHostTests.cs index 0a99c8ce..37e26d06 100644 --- a/tests/TuiCode.Tests/MenuBarHostTests.cs +++ b/tests/TuiCode.Tests/MenuBarHostTests.cs @@ -462,29 +462,18 @@ public void The_bar_takes_the_themes_menu_colours_and_stands_apart_from_the_expl { using var workbench = BuildWorkbench(); using var host = BuildHost(workbench, out _); - ConfigurationManager.Enable(ConfigLocations.None); - try + using var themes = LoadBundledThemes(); + var drawn = new List(); + foreach (var theme in BundledThemes.Names) { - ConfigurationManager.RuntimeConfig = BundledThemes.Config; - ConfigurationManager.Load(ConfigLocations.LibraryResources | ConfigLocations.Runtime); - var drawn = new List(); - foreach (var theme in BundledThemes.Names) - { - ThemeManager.Theme = theme; - ConfigurationManager.Apply(); - var menu = SchemeManager.GetScheme(Schemes.Menu).GetAttributeForRole(VisualRole.Normal, null); - var sidebar = SchemeManager.GetScheme("Sidebar").GetAttributeForRole(VisualRole.Normal, null); - Assert.Equal(menu, workbench.MenuBar.GetAttributeForRole(VisualRole.Normal)); - Assert.NotEqual(sidebar.Background, menu.Background); - drawn.Add(menu); - } - Assert.True(drawn.Distinct().Count() > 1); - } - finally - { - ThemeManager.Theme = "Default"; - ConfigurationManager.Disable(resetToHardCodedDefaults: true); + ThemeManager.Theme = theme; + var menu = SchemeManager.GetScheme(Schemes.Menu).GetAttributeForRole(VisualRole.Normal, null); + var sidebar = SchemeManager.GetScheme("Sidebar").GetAttributeForRole(VisualRole.Normal, null); + Assert.Equal(menu, workbench.MenuBar.GetAttributeForRole(VisualRole.Normal)); + Assert.NotEqual(sidebar.Background, menu.Background); + drawn.Add(menu); } + Assert.True(drawn.Distinct().Count() > 1); } private static IEnumerable MenuIds() => diff --git a/tests/TuiCode.Tests/RowStripesTests.cs b/tests/TuiCode.Tests/RowStripesTests.cs index e62bc0ce..f3d6210e 100644 --- a/tests/TuiCode.Tests/RowStripesTests.cs +++ b/tests/TuiCode.Tests/RowStripesTests.cs @@ -38,29 +38,17 @@ public void Stripe_nudges_the_background_towards_the_text_and_keeps_the_text_col [Fact] public void Every_bundled_theme_keeps_the_stripe_apart_from_the_background_and_the_selection() { - ConfigurationManager.Enable(ConfigLocations.None); - try + using var themes = LoadBundledThemes(); + foreach (var theme in BundledThemes.Names) { - ConfigurationManager.RuntimeConfig = BundledThemes.Config; - ConfigurationManager.Load(ConfigLocations.LibraryResources | ConfigLocations.Runtime); - - foreach (var theme in BundledThemes.Names) + ThemeManager.Theme = theme; + foreach (var name in new[] { "Base", "Dialog" }) { - ThemeManager.Theme = theme; - ConfigurationManager.Apply(); - foreach (var name in new[] { "Base", "Dialog" }) - { - Assert.True(SchemeManager.TryGetScheme(name, out var scheme)); - var stripe = RowStripes.Stripe(scheme!.GetAttributeForRole(VisualRole.Normal)).Background; - foreach (var role in new[] { VisualRole.Normal, VisualRole.Focus, VisualRole.Active }) - Assert.True(scheme.GetAttributeForRole(role).Background != stripe, $"{theme} {name} {role}"); - } + Assert.True(SchemeManager.TryGetScheme(name, out var scheme)); + var stripe = RowStripes.Stripe(scheme!.GetAttributeForRole(VisualRole.Normal)).Background; + foreach (var role in new[] { VisualRole.Normal, VisualRole.Focus, VisualRole.Active }) + Assert.True(scheme.GetAttributeForRole(role).Background != stripe, $"{theme} {name} {role}"); } } - finally - { - ThemeManager.Theme = "Default"; - ConfigurationManager.Disable(resetToHardCodedDefaults: true); - } } } diff --git a/tests/TuiCode.Tests/StartupHostTests.cs b/tests/TuiCode.Tests/StartupHostTests.cs index f3e3a56c..0de9973e 100644 --- a/tests/TuiCode.Tests/StartupHostTests.cs +++ b/tests/TuiCode.Tests/StartupHostTests.cs @@ -154,7 +154,9 @@ private Workbench.Workbench BuildWorkbench(WorkspaceStateStore? workspaceState = private WorkbenchHost BuildHost(Workbench.Workbench workbench) { var commands = new CommandService(); - return new WorkbenchHost(workbench, commands, new KeybindingService(commands), new InputScopeStack(), + var host = new WorkbenchHost(workbench, commands, new KeybindingService(commands), new InputScopeStack(), new InMemorySettingsService(), driverName: DriverRegistry.Names.ANSI, git: _git, gitHub: _gitHub); + HostSteps.PinScreenSize(host, 80, 25); + return host; } } diff --git a/tests/TuiCode.Tests/StaticConfiguration.cs b/tests/TuiCode.Tests/StaticConfiguration.cs index 023eed64..7acc1242 100644 --- a/tests/TuiCode.Tests/StaticConfiguration.cs +++ b/tests/TuiCode.Tests/StaticConfiguration.cs @@ -11,7 +11,7 @@ public sealed class StaticConfigurationCollection { } /// /// Base for any test that boots a Terminal.Gui Application (e.g. news up a /// ) or mutates ThemeManager / -/// ConfigurationManager. Those are process-global TG statics and TG's render +/// TuiConfigurationBuilder. Those are process-global TG statics and TG's render /// path throws KeyNotFoundException if the theme dictionary is mutated by a /// parallel test (issue #77). /// @@ -20,7 +20,7 @@ public sealed class StaticConfigurationCollection { } /// ThemeManager.Theme so a test that changes the theme can't leak into the next. /// /// Rule of thumb: if your test news up a WorkbenchHost, renders a View, or -/// touches ThemeManager/ConfigurationManager, derive from this. +/// touches ThemeManager/TuiConfigurationBuilder, derive from this. /// [Collection("StaticConfiguration")] public abstract class StaticConfigurationTest : IDisposable @@ -44,6 +44,24 @@ protected StaticConfigurationTest() _theme = ThemeManager.Theme; } + /// Registers the bundled themes, and only those, until disposed. + protected static IDisposable LoadBundledThemes() + { + TuiConfigurationBuilder.Shared.RuntimeConfig = TuiCode.Workbench.Themes.BundledThemes.Config; + TuiConfigurationBuilder.Shared.ApplyToStaticFacades(); + return new BundledThemesLoaded(); + } + + private sealed class BundledThemesLoaded : IDisposable + { + public void Dispose() + { + ThemeManager.Theme = ThemeManager.DEFAULT_THEME_NAME; + TuiConfigurationBuilder.Shared.RuntimeConfig = null; + TuiConfigurationBuilder.Shared.ApplyToStaticFacades(); + } + } + public virtual void Dispose() { ThemeManager.Theme = _theme; diff --git a/tests/TuiCode.Tests/TerminalGuiInternalsTests.cs b/tests/TuiCode.Tests/TerminalGuiInternalsTests.cs new file mode 100644 index 00000000..a2ce4d80 --- /dev/null +++ b/tests/TuiCode.Tests/TerminalGuiInternalsTests.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using TuiCode.Editor; + +namespace TuiCode.Tests; + +// UnsafeAccessor and reflection only fail when they're used, so a TG upgrade that renames a private member breaks an edit at runtime. +public class TerminalGuiInternalsTests +{ + private const BindingFlags Members = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + + public static TheoryData Accessors() => + [.. AccessorMethods().Select(m => $"{m.DeclaringType!.Name}.{m.Name}")]; + + [Theory] + [MemberData(nameof(Accessors))] + public void Every_unsafe_accessor_finds_its_member(string accessor) + { + var method = AccessorMethods().Single(m => $"{m.DeclaringType!.Name}.{m.Name}" == accessor); + var attribute = method.GetCustomAttribute()!; + var parameters = method.GetParameters(); + var target = TypeOf(parameters[0]); + if (target.IsGenericType && target.GetGenericTypeDefinition() == typeof(List<>)) + target = typeof(List<>); + + if (attribute.Kind is UnsafeAccessorKind.Field) + { + Assert.NotNull(target.GetField(attribute.Name!, Members)); + return; + } + + var arguments = parameters.Skip(1).Select(TypeOf).Select(t => t.IsByRef ? t.GetElementType()! : t).ToArray(); + var found = target.GetMethods(Members).Where(m => m.Name == attribute.Name) + .Any(m => m.GetParameters().Select(p => p.ParameterType.IsByRef ? p.ParameterType.GetElementType()! : p.ParameterType).SequenceEqual(arguments)); + Assert.True(found, $"{target.Name} has no {attribute.Name}({string.Join(", ", arguments.Select(a => a.Name))})"); + } + + [Fact] + public void Every_reflected_field_is_found() + { + var reflected = typeof(EditorTab).Assembly.GetTypes() + .SelectMany(t => t.GetFields(BindingFlags.NonPublic | BindingFlags.Static)) + .Where(f => f.FieldType == typeof(FieldInfo)) + .ToList(); + + Assert.NotEmpty(reflected); + Assert.All(reflected, f => Assert.NotNull(f.GetValue(null))); + } + + private static IEnumerable AccessorMethods() => + typeof(EditorTab).Assembly.GetTypes() + .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly)) + .Where(m => m.GetCustomAttribute() is not null); + + private static Type TypeOf(ParameterInfo parameter) => + parameter.GetCustomAttribute() is { } named + ? Type.GetType(named.TypeName, throwOnError: true)! + : parameter.ParameterType; +} diff --git a/tests/TuiCode.Tests/TuiCode.Tests.csproj b/tests/TuiCode.Tests/TuiCode.Tests.csproj index 24c909ea..805fff27 100644 --- a/tests/TuiCode.Tests/TuiCode.Tests.csproj +++ b/tests/TuiCode.Tests/TuiCode.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/TuiCode.Tests/WorkbenchHostTests.cs b/tests/TuiCode.Tests/WorkbenchHostTests.cs index 66440cb3..fa85f3b4 100644 --- a/tests/TuiCode.Tests/WorkbenchHostTests.cs +++ b/tests/TuiCode.Tests/WorkbenchHostTests.cs @@ -6,6 +6,8 @@ using TuiCode.Workbench.Diagnostics; using TuiCode.Workbench.Parts; using TuiCode.Workbench.Services; +using TuiCode.Workbench.Themes; +using TuiCode.Workbench.Configuration; namespace TuiCode.Tests; @@ -38,6 +40,24 @@ void OnFirstIteration(object? sender, EventArgs e) } } + [Fact] + public async Task Esc_does_not_quit_even_after_a_theme_switch() + { + using var themes = LoadBundledThemes(); + using var workbench = BuildWorkbench(); + var commands = new CommandService(); + var settings = new DefaultSettingsService(new MockFileSystem()); + using var host = new WorkbenchHost(workbench, commands, new KeybindingService(commands), new InputScopeStack(), settings, driverName: DriverRegistry.Names.ANSI); + + await HostSteps.Run(host, + () => host.App.InjectKey(Key.Esc), + () => { settings.Theme = BundledThemes.TurboPascal; }, + () => host.App.InjectKey(Key.Esc), + () => { }); + + Assert.Equal(BundledThemes.TurboPascal, settings.Theme); + } + // #90: a hand-edited keybindings file can carry a malformed entry. Since #89 the chord is stored // by keycode (always a valid Key), so the bad part is now the *command* — here an empty string. // Applying it at startup must skip the bad entry rather than throw out of the constructor — the