Skip to content

Add configurable color themes#83

Open
mkrueger wants to merge 14 commits into
mainfrom
dev/mkrueger/highlighting
Open

Add configurable color themes#83
mkrueger wants to merge 14 commits into
mainfrom
dev/mkrueger/highlighting

Conversation

@mkrueger
Copy link
Copy Markdown
Contributor

Adds shell-wide color theming with built-in profiles, TOML-based custom themes, and a theme command for inspection, switching, validation, save, and reload.

Highlights

  • Built-in profiles (default, light, dark, monochrome) using only ANSI 16 colors so output follows the terminal palette (Solarized, Dracula, Campbell, ...).
  • Custom theme files in ~/.cosmosdbshell/themes/*.toml with extends inheritance and a self-contained theme save round-trip.
  • theme command: current, list, show [name], use [name], load [path], validate [path|dir], save [name], reload.
  • Strict validation: per-section rules (color slots accept one ANSI 16 color; style slots allow modifiers plus at most one color), aggregated multi-error reports, "Did you mean ..." suggestions, bracket-cycle warnings, directory scan, and --strict to promote warnings to errors.
  • Theme-aware output: replaced ad-hoc markup across connect, settings, query, dir, list, bucket, ftab, help, reverse-history search, and shell error reporting with Theme.Format* helpers so monochrome and light themes work end-to-end.
  • Startup selection via --theme=<name> and COSMOSDB_SHELL_THEME env var.
  • Localization: every user-visible theme string lives in lang/en.ftl.
  • Docs: README has a Theming section and docs/commands.md documents the theme command.

Tests

Added focused tests: theme profile lint, file parse/save round-trip, registry behavior, command surfaces (validate / use / load / save), monochrome reverse-search highlighting regression, and localization key audit.

mkrueger added 13 commits May 11, 2026 14:21
Replaces hard-coded extended-256 Spectre color names (lightyellow3,
seagreen2, gold1, orchid, deepskyblue1, violet, plum3, cyan, magenta)
with the standard ANSI 16 palette (yellow, green, fuchsia, aqua,
purple, blue, etc.). ANSI 0-15 escape sequences resolve through the
terminal's configured palette, so the shell's appearance now follows
the user's terminal theme (Solarized, Dracula, Campbell, etc.)
instead of being stuck at fixed RGB values.

Key gotcha: Spectre's 'cyan' and 'magenta' map to fixed 256-color
indices and bypass terminal theming. The themable equivalents are
'aqua' and 'fuchsia', and we use both names today, so this is not a
no-op rename.

All color choices now flow through Theme.cs constants (Theme.X-
ColorName) and helpers (Theme.FormatX). Inline [color]...[/] markup
literals were swept out of HelpCommand, ConnectCommand, DirCommand,
ListCommand, SettingsCommand, QueryCommand, BucketCommand, and
ShellInterpreter so the palette has a single source of truth.

JsonOutputHighlighter tests now derive expected colors from Theme
constants instead of literal color names so future palette tweaks
do not churn the assertions.

Also removed two dead [magenta]...[/] wrappers around Theme.Format-
Keyword in HelpCommand: the inner style already wins inside Spectre
so the outer color was never visible.

Verified: dotnet build clean, 877 tests pass / 0 fail / 74 skipped
(integration tests requiring the emulator).
Follow-up to 7587eb4. The earlier sweep regex matched [color] and
[color ...] forms but missed [bold cyan] in HelpCommand category and
statement-help headers. The dark teal Spectre 'cyan' (256-color #51)
bypasses terminal theming and is unreadable on light backgrounds.
Replaced both with Theme.ConnectedPromptColorName ('aqua') so the
headers now follow the terminal palette.

Verified the rest of the codebase by sweeping every named non-ANSI-16
Spectre color (cyan/magenta/violet/orchid/gold1/plum3/deepskyblue1/
seagreen2/lightyellow3/etc.); only doc comments remain.
The screenshots showed two readability problems on light terminal
backgrounds:

- Category headers (Connection, Data Operations, ...) used 'aqua'
  (ANSI 14 / bright cyan), which Windows Terminal Campbell Light
  renders as a borderline-dark teal.
- Command descriptions and option/argument help text used 'silver'
  and 'white' (ANSI 7 and 15), both of which render as light gray
  on light backgrounds and become invisible against white.

Spectre has no [default] color marker, so the cleanest fix is to
emit no foreground color at all and let the terminal's configured
default foreground handle contrast in both light and dark themes.

Added three intent-named Theme helpers:
  - FormatHelpHeader  -> [bold]X[/]    (terminal foreground, bold)
  - FormatHelpName    -> [bold]X[/]    (parameter/option syntax)
  - FormatHelpDescription -> Markup.Escape(X)  (no styling)

Swept all of the affected HelpCommand call sites to use them:
category headers, command/statement descriptions, argument and
option help tables, example ordinals, statement help table,
and the section rule title.

Accent uses (the >> example bullet, syntax highlighting in the code
block) keep their colors on purpose - they're single characters or
decorative tokens where the bright color is intentional.

Verified: build clean, 51 targeted tests pass.
The 'Available Commands' and 'Control Flow Statements' Panel titles
live in en.ftl as [bold white]...[/]. White on a light terminal
background is invisible (matches the screenshot showing an empty
bordered box). Switched both to [bold] so the title falls through
to the terminal's default foreground, consistent with the help
headers fixed in e37b59b.
Localization data should describe content, not styling. Stripped
five entries:

- command-query-request_charge: dropped [white]...[/] wrapper
  (caller already uses MarkupLine, so the value just renders in
  the terminal default fg)
- mcp-error-creating-server / mcp-error-server-failed-start:
  dropped [red]...[/] wrappers (callers use AnsiConsole.WriteLine
  not MarkupLine, so the markup was being printed literally as text -
  pre-existing bug, now no-op)
- help-available-commands-styled / help-control-flow-statements-styled:
  removed entirely; the two Panel call sites in HelpCommand now wrap
  the bare keys through Theme.FormatHelpHeader, matching the rest of
  the help styling

Normalised the colon on help-control-flow-statements: stripped from
the FTL value, appended at the plain-text call site so the styled
panel title doesn't end with a stray ':'.

Verified: build clean, help and localization-audit tests pass.
Introduces a runtime-swappable ThemeOptions record consumed by every
Theme.* helper. Defaults are unchanged, so existing call sites keep
working via the same XColorName accessors (now properties that read
from Theme.Current).

Three user-visible surfaces:

1. Startup CLI flag --theme=<name>, with fallback to the
   COSMOSDB_SHELL_THEME environment variable. Unknown names emit a
   warning and fall back to the default profile.
2. Built-in profiles defined in ThemeProfiles:
   - default / dark: today's palette (ANSI 16, dark-bg friendly)
   - light: darker hues for brackets/literals/prompt so they stay
     readable on white backgrounds
   - monochrome: no foreground colors at all, only bold/dim/underline
3. Interactive 'theme' command with sub-actions:
   - theme            -> show active profile name
   - theme list       -> list available profiles
   - theme show [n]   -> render a sample of every role using profile
     n (or active if omitted), without changing the active theme
   - theme use <n>    -> swap active profile for the rest of the session

Localisation:
  - help-Theme, command-theme-* keys for the new command
  - warning-unknown-theme for the CLI fallback path
  All values are color-free, matching the FTL hygiene rule established
  earlier on this branch.

Tests (ThemeProfileTests):
  - TryGet matrix (known/unknown/blank)
  - Apply round-trip and helper-output coupling
  - Monochrome emits no foreground colors
  - Lint guard: every slot in every built-in profile is either empty,
    an ANSI 16 color name, or a Spectre style modifier; catches any
    accidental reintroduction of fixed-256 names like cyan/violet/
    plum3 going forward
  - Localization key audit for the new keys

All 877 existing tests still pass (verified with HelpCommand, Highlighter,
JsonOutput, LocalizationKeyAudit suites). Smoke-tested 'theme list',
'--theme=monochrome -c theme', and the unknown-theme warning path.

Out of scope (Phase B follow-ups): file-based themes, --theme-color
per-role overrides at the CLI, runtime persistence ('theme save'),
true-color hex support, profile-customization.
The Theme.CommandColor accessor returned the open-tag fragment '[yellow]'
(or empty string for monochrome), and two HelpCommand call sites paired
it with a hardcoded '[/]'. In monochrome that produced 'connect[/]' --
an unbalanced markup string that crashed Spectre with 'Encountered
closing tag when none was expected near position 9'.

Fix: use the existing Theme.FormatCommand helper, which always emits
balanced markup including the empty-style case. Removed the now-unused
open-tag accessor and its helper so the same pattern can't be
reintroduced.

Added a regression test in ThemeProfileTests that runs every Theme.Format*
helper for every built-in profile through Spectre's Markup parser. The
old code would have failed this test for the monochrome profile.

Verified: monochrome 'help' renders cleanly; 24 theme/help tests pass.
Users can drop *.toml files in ~/.cosmosdbshell/themes/ to define color
themes that appear alongside the built-in default/light/dark/monochrome
profiles. File schema is small and uses the existing slot vocabulary:

  name        = "solarized-light"
  description = "Solarized-style light palette"
  extends     = "default"

  [colors]
  literal           = "purple"
  bracket_cycle     = ["purple", "maroon", "navy"]

  [styles]
  help_header = "bold underline"

New code:

- ThemeOptions / Theme.Apply already existed; this change adds
  ThemeFile (TOML parse/save with extends), ThemePalette (allowed
  values), and ThemeRegistry (merged built-in + file lookup with
  cycle / unknown-extends detection).
- Program.cs scans the user themes directory at startup so --theme=
  <name> and COSMOSDB_SHELL_THEME hit the merged set.
- ThemeCommand gains three new sub-actions:
    theme load <path>     -> load and switch to a single file
    theme save <name>     -> write the active theme to ~/.cosmosdbshell
                             /themes/<name>.toml (or a custom path,
                             with --force to overwrite)
    theme reload          -> rescan the user themes directory
  'theme list' now shows source (built-in vs file) and the file path.

Validation:
  - All values must be ANSI 16 color names or Spectre style modifiers
    (centralised in ThemePalette; the existing built-in profile lint
    guard now reuses the same set).
  - extends chain depth capped at 8; cycles and unknown bases are
    rejected as ThemeLoadException with localized messages.
  - File themes shadowing built-ins emit a warning, not an error.
  - Save writes only slots that differ from the default profile, so
    user files stay minimal; bracket_cycle is folded into the single
    [colors] section so the TOML is valid.

Tests:
  - ThemeFileTests (10): parse/load/save round trip, defaults,
    unknown-key warnings, invalid color rejection, unknown/self
    extends, empty bracket cycle, multi-token style.
  - ThemeRegistryTests (6): built-ins seeded, file load + warnings,
    cycle detection, ResetToBuiltIns, shadowing warning.
  All 41 theme-related tests pass; existing help and localization
  audit tests still pass.

Smoke-tested end to end:
  - File in user dir is auto-discovered: 'theme list' shows
    'ocean (~/.cosmosdbshell/themes/ocean.toml)'.
  - --theme=ocean activates it at startup.
  - 'theme save my-light' from --theme=light writes a clean file with
    only the differing slots and a single [colors] section.

Package: adds Tomlyn 0.19.0 (MIT).
Three fixes from the user's transcript:

1. 'theme save' used to write a minimal file with extends='default' and
   silently leave the registry stale. Behaviour was confusing: the new
   theme didn't appear in 'theme list' until the next session. Now
   save:
   - Writes every slot explicitly with no extends line, so files are
     fully self-contained and don't depend on any base profile being
     present at load time.
   - Prints a hint pointing at 'theme reload' / 'theme load <name>'
     so users know how to pick up the change.
   No auto-registration: the user explicitly said they don't want
   that, because it wouldn't reflect files copied/removed externally.

2. 'theme load mylight' previously failed with 'Theme file not found:
   mylight' because the argument was treated as a literal path. The
   resolver now: if the argument has no path separator and isn't an
   existing file, treat it as a name in the user themes directory and
   look up '~/.cosmosdbshell/themes/<name>.toml' (with or without the
   .toml suffix).

3. ThemeFile.Save signature simplified: dropped the baseline and
   extends parameters since the new behaviour writes everything.
   Removed the no-longer-needed AppendSection and RenderBracketCycle-
   Line helpers. Tests updated:
   - SaveAndLoad_RoundTripsModifiedSlots now asserts every slot is
     written and 'extends' is absent.
   - New Save_WithoutExtends_ProducesSelfContainedFile proves a saved
     file loads correctly even when no base profile is available.

Localisation: new key command-theme-save-hint-reload.

Build clean, 42 theme/help/localization tests pass. Smoke-tested
end-to-end: save writes the file but list omits it (no auto-reg);
load-by-name resolves to the user dir; saved file is fully populated
with no extends line.
@mkrueger mkrueger requested review from a team and Copilot May 12, 2026 08:55
@mkrueger mkrueger requested a review from sevoku May 12, 2026 08:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces shell-wide color theming for CosmosDBShell, including built-in ANSI-16-only theme profiles, TOML-based custom themes (with extends), startup selection, and a new theme command to inspect/switch/load/validate/save/reload themes. It also updates existing command output to use theme-aware formatting helpers, adds localized strings, and includes focused test coverage for the new theming surface.

Changes:

  • Add a theming core (theme options/profiles, palette, registry, TOML load/save/validation) and apply a selected theme at startup.
  • Add a theme shell command for listing/showing/using/loading/validating/saving/reloading themes.
  • Replace ad-hoc Spectre markup across commands/help/errors with theme-aware Theme.Format* helpers; update docs/localization and add tests.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
README.md Documents --theme startup flag and custom theme workflow.
docs/commands.md Documents the new theme command usage and behavior.
Directory.Packages.props Adds central package version for Tomlyn (TOML parsing).
CosmosDBShell/CosmosDBShell.csproj Adds Tomlyn package reference.
CosmosDBShell/Program.cs Applies selected theme at startup; loads user theme directory into registry.
CosmosDBShell/lang/en.ftl Adds theme-related localized strings; adjusts some existing strings for theme formatting.
CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs Adds localized help text for the new --theme option.
CosmosDBShell/Azure.Data.Cosmos.Shell.KeyBindings/ReverseHistorySearch.cs Uses themed search-match style for reverse history highlighting.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/Theme.cs Implements theme state (Current) plus formatting helpers and compatibility accessors.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeOptions.cs Defines the full set of theme slots used across the shell.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeProfiles.cs Adds built-in profiles (default/light/dark/monochrome) and lookup.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemePalette.cs Centralizes allowed ANSI-16 colors/modifiers and suggestion logic.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeFile.cs Implements TOML parse/validate/save with warnings/errors aggregation.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeFileResult.cs Represents successful theme-file load/parse results (including warnings).
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeLoadException.cs Defines an exception type for theme file load failures.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeRegistry.cs Maintains merged built-in + file themes; supports directory scans and ad-hoc file loads.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeRegistration.cs Represents a theme entry (source, path, description, extends).
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeSource.cs Adds an enum for theme origin (built-in vs file).
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs Routes error/warning output through theme formatting helpers.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs Adds the new theme command implementation and JSON outputs.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/SettingsCommand.cs Updates output styling to use theme helpers.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/QueryCommand.cs Updates table metric output styling to use theme helpers.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ListCommand.cs Updates listing output styling to use theme helpers.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs Updates help styling (headers/names/descriptions) to be theme-aware.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/FtabCommand.cs Updates table header styling to use theme helpers.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DirCommand.cs Updates directory listing styling to use theme helpers.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs Updates connection info output styling to use theme helpers.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BucketCommand.cs Updates throughput bucket output styling to use theme helpers.
CosmosDBShell.Tests/UtilTest/JsonOutputHighlighterTests.cs Aligns JSON highlighting assertions with theme-based color slots.
CosmosDBShell.Tests/Shell/ThemeRegistryTests.cs Adds tests for registry load/reset behavior and warnings.
CosmosDBShell.Tests/Shell/ThemeProfileTests.cs Adds tests to lint built-in profiles and validate helper markup across profiles.
CosmosDBShell.Tests/Shell/ThemeFileTests.cs Adds tests for TOML parsing/validation/suggestions and save/load round-trips.
CosmosDBShell.Tests/Shell/HotkeyCommandTests.cs Adds regression test for monochrome reverse-search highlighting.
CosmosDBShell.Tests/CommandTests/ThemeCommandTests.cs Adds tests for theme validate behavior (file/dir/strict).

Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ThemeProfiles.cs Outdated
Comment thread CosmosDBShell/lang/en.ftl
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs Outdated
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs Outdated
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs Outdated
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs Outdated
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ThemeCommand.cs Outdated
- Make ThemeProfiles.Dark a distinct instance from Default so
  ResolveActiveName's reference-equality lookup correctly maps the
  active theme back to "dark" (and "default" stays "default").
- Drop Markup.Escape on MCP error messages; the en.ftl strings no
  longer contain markup and the callers use AnsiConsole.WriteLine,
  which would otherwise emit literal [[ / ]] sequences.
- Escape user-controlled values (theme names, file paths, action,
  directory) in ThemeCommand markup output so a theme name or path
  containing [ / ] cannot break Spectre markup parsing or inject
  styling.
- Include current and the use/set alias in the unknown-action
  message so it reflects the real command surface.
- Add regression test pinning that ThemeProfiles.Default and Dark
  are distinct instances.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants