Skip to content

Warn that an incomplete color scheme is incomplete, not missing (#11457) - #20428

Open
drvoss wants to merge 4 commits into
microsoft:mainfrom
drvoss:fix/11457-incomplete-colorscheme-warning
Open

Warn that an incomplete color scheme is incomplete, not missing (#11457)#20428
drvoss wants to merge 4 commits into
microsoft:mainfrom
drvoss:fix/11457-incomplete-colorscheme-warning

Conversation

@drvoss

@drvoss drvoss commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary of the Pull Request

Color schemes that are missing some of their colors are rejected during parsing and
dropped. A profile that references such a scheme then produces the UnknownColorScheme
warning ("...the value matches the name of a color scheme..."), which misleads the user
into thinking the scheme name is wrong when the scheme actually exists but is incomplete.
This adds a distinct IncompleteColorScheme warning so the message is accurate.

References that come from a layer the user cannot edit (fragments, dynamic profile
generators, defaults.json) are suppressed instead of warned, since a broken built-in
reference would otherwise be inescapable for the user.

References and Relevant Issues

Closes #11457. Related: #11456 (added magenta/brightMagenta aliases; note the
original repro no longer reproduces because of it — a genuinely missing color such as
cyan is needed).

Detailed Description of the Pull Request / Additional comments

  • ColorScheme::FromJson only fails when a scheme has no name or fewer than 16 colors.
    When it returns null but the JSON has a non-empty name, the scheme exists but is
    incomplete. SettingsLoader now collects those names (both user and fragment parse
    paths) into incompleteColorSchemes.
  • CascadiaSettings::_validateAllSchemesExist raises IncompleteColorScheme when a
    profile references a name in that set, and UnknownColorScheme (as before) otherwise.
  • ColorScheme parsing/merging/serialization is intentionally not modified, so scheme
    layering and round-tripping are unchanged; incomplete schemes are still ignored.
  • New warning added at the end of the SettingsLoadWarnings enum (existing ordinals
    unchanged) with an en-US resource string.
  • Known limitation: if a complete scheme of the same name also exists (e.g. the in-box
    copy), an incomplete override is silently ignored without a warning — unchanged from
    today. A scheme that reaches 16 colors via duplicate aliases while omitting a real slot
    is likewise not flagged (pre-existing ColorScheme counting behavior, out of scope).
  • References from fragments/generators/defaults.json are suppressed via
    _originOfColorSchemeName(), which walks the profile's inheritance graph to find which
    layer actually set the colorScheme value (fragment-contributed profiles don't appear
    in _allProfiles on their own — SettingsLoader::_addUserProfileParent() merges them
    into a user-owned child, so Origin() alone can't distinguish "fragment set this" from
    "user set this").
  • Follow-up fix (leaf-vs-parent origin misattribution): the initial version of
    _originOfColorSchemeName() used only the public <NAME>OverrideSource() accessor,
    which walks parents but never inspects the leaf itself. When both the leaf (the
    user's own settings.json) and a parent (e.g. an in-box profile) set colorScheme,
    the effective value is the leaf's — but OverrideSource() still returned the parent,
    so the user's own typo was misattributed to the in-box layer and silently swallowed.
    This is a realistic scenario: defaults.json sets colorScheme directly on the
    Windows PowerShell and Command Prompt profiles, so any user who overrides colorScheme
    on those same (GUID-matched) profiles hits this path. Fixed by checking Has<NAME>()
    first: if the leaf set its own value, origin is attributed to the leaf regardless of
    what any parent has; the walked-parent OverrideSource() is only consulted when the
    leaf itself is silent. Added a regression test
    (UserOverrideOfInboxColorSchemeTypoWarns) that reproduces this via matching profile
    GUIDs.

Validation Steps Performed

  • Added DeserializationTests::TestIncompleteColorSchemeName (user settings path) and
    ColorSchemeTests::IncompleteColorSchemeInFragmentWarns (fragment path).
  • Added ColorSchemeTests::BrokenSchemeReferencedByFragmentProfileIsSilent (fragment /
    in-box references are suppressed, not warned) and
    ColorSchemeTests::UserOverrideOfInboxColorSchemeTypoWarns (regression test for the
    leaf-vs-parent origin bug described above).
  • TestInvalidColorSchemeName (genuinely missing name → UnknownColorScheme) still passes.
  • Full SettingsModel.Unit.Tests suite: 161/161 passing locally.
  • clang-format (22.1.3, same binary as CI) run over all touched files with no diff.

PR Checklist

How the origin of a broken reference is determined

A broken color-scheme reference now produces a warning only when the reference
originates from a layer the user can edit (User or ProfilesDefaults). References that
originate from a fragment, a dynamic profile generator, or defaults.json are suppressed,
so a broken reference the user cannot remove never surfaces an inescapable warning
(per @DHowett's note in #11457).

Origin is resolved by a file-local helper, _originOfColorSchemeName() in
CascadiaSettings.cpp -- not from profile.Origin(). Fragment-contributed profiles
are merged into a user-owned child profile and stamped OriginTag::User, so the
profile's own origin would misattribute the reference to the user. The helper instead
walks the inheritance graph, checking the leaf (Has*ColorSchemeName()) before trusting
*ColorSchemeNameOverrideSource() (which only inspects parents). _noteMissingColorScheme()
applies the User/ProfilesDefaults allow-list.

Note: a scheme defined by a fragment but referenced by the user is still warned --
only fragment-originated references are suppressed.

Known limitation (out of scope): inbox defaults.json and user profiles.defaults share the
ProfilesDefaults origin tag, so both are treated as user-actionable.

Split out separately: #20462 (Clear*ColorSchemeName does not clear fragment-inherited values).

…osoft#11457)

When a color scheme is missing one or more of its 16 colors, ColorScheme::FromJson
rejects it and it is dropped, so a profile that references it later reports
`UnknownColorScheme` — i.e. "that scheme doesn't exist" — even though the scheme is
present, just incomplete.

Collect the names of schemes that were rejected only for being incomplete (a "name"
is present but the color set is short) and, when a profile references one, raise a new
`IncompleteColorScheme` warning with an accurate message instead of the misleading
"unknown scheme" one. Incomplete schemes are still ignored (unchanged behavior); only
the diagnostic changes. ColorScheme parsing/merging/serialization is untouched.
@microsoft-github-policy-service microsoft-github-policy-service Bot added Issue-Bug It either shouldn't be doing this or needs an investigation. Area-Settings Issues related to settings and customizability, for console or terminal Priority-2 A description (P2) Product-Terminal The new Windows Terminal. labels Jul 16, 2026
@drvoss
drvoss marked this pull request as ready for review July 16, 2026 11:38
@DHowett

DHowett commented Jul 16, 2026

Copy link
Copy Markdown
Member

Thanks! I'm thinking about this a bit.

Even though #11457 mentions fragments, I think it is very important that Terminal not display errors for Fragment JSON files. They can be installed in locations that the user cannot write to, a broken Fragment file in a system folder will cause a warning message the user can never escape from.

_validateAllSchemesExist() had no origin filtering, so a fragment-authored
profile referencing a broken fragment scheme produced a settings warning the
user could never dismiss -- the fragment may live in a system folder they
cannot write to.

Resolve the origin of the layer that actually set the colorScheme value (via
the appearance's override source, as ResolveMediaResources() does for icons)
and only warn for User/ProfilesDefaults references. profile.Origin() cannot be
used here: fragment profiles are merged into user-owned children that
FinalizeLayering() stamps OriginTag::User.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@drvoss

drvoss commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look — you're right, and digging into it I found the problem is a bit deeper than the PR as written.

First, a correction to my own PR: suppressing the fragment-origin collection isn't sufficient. _validateAllSchemesExist() is a two-way branch with no "stay silent" path, so dropping fragment names from the incomplete set just downgrades IncompleteColorScheme back to UnknownColorScheme -- the dialog still appears, only with worse wording.

Second, and more importantly: this inescapable warning already exists on main. A fragment-provided profile that references a scheme the same fragment failed to define produces UnknownColorScheme in shipping Terminal today; _validateAllSchemesExist() has never filtered by origin. This PR didn't introduce the dialog, it only re-worded it.

So I'd like to fix the underlying issue properly, following the precedent already set by _resolveSingleMediaResource(), which ignores Fragment-origin failures and only flags User/ProfilesDefaults ones.

The key refinement is what we key the suppression on. I think it should be the origin of the layer that actually set the colorScheme value, not the origin of the file that defined the broken scheme:

Reference set by Scheme defined in Warn?
user settings.json user settings.json yes -- IncompleteColorScheme
user settings.json a fragment yes -- the user can edit their own reference
a fragment profile a fragment no -- inescapable
a fragment profile nowhere no -- inescapable (fixes the existing bug)

Note the second row is the original #11457 repro, so it needs to keep warning.

One trap worth calling out: profile.Origin() can't be used for this. _addUserProfileParent() merges fragment profiles into a user-owned child, and FinalizeLayering() stamps that child OriginTag::User; Origin is a plain WINRT_PROPERTY, so it isn't inherited from the fragment parent. I resolve the origin through DarkColorSchemeNameOverrideSource() -> SourceProfile() -> Origin() instead, which is the same inheritance-graph walk ResolveMediaResources() uses for icons.

I've pushed that, along with a regression test that asserts zero warnings for a fragment-authored profile pointing at a broken scheme (it fails both on main and on the previous revision of this PR).

Separately: ClearDarkColorSchemeName() only clears the leaf's own value, so when the bad name came from a fragment parent the clear is a no-op and the profile keeps a dangling scheme name. That's pre-existing and orthogonal -- happy to file it separately rather than grow this PR.

Comment thread src/cascadia/UnitTests_SettingsModel/ColorSchemeTests.cpp Fixed
Comment thread src/cascadia/UnitTests_SettingsModel/ColorSchemeTests.cpp Fixed
@github-actions

This comment has been minimized.

@drvoss
drvoss force-pushed the fix/11457-incomplete-colorscheme-warning branch from 4b5e828 to a122255 Compare July 19, 2026 09:57
@github-actions

This comment has been minimized.

e48b3d0 introduced _originOfColorSchemeName() to suppress
UnknownColorScheme/IncompleteColorScheme warnings for references the user
cannot edit (fragments, generators, defaults.json). It relied on the public
<NAME>OverrideSource() accessor, which only walks *parents* and never
inspects the leaf itself. When both a leaf (user settings.json) and a parent
(e.g. an InBox profile) set colorScheme, the effective value is the leaf's
(via _get<NAME>Impl(), which checks the leaf first), but OverrideSource()
still returned the parent -- so a user's own typo could be misattributed to
the InBox layer and silently swallowed. This is the opposite of what the
original PR intended.

Fix _originOfColorSchemeName() to check Has<NAME>() first: if the leaf set
its own value, attribute origin to the leaf regardless of what any parent
has. Only fall back to the walked-parents OverrideSource() when the leaf is
silent, and fall back to the leaf itself when neither leaf nor any parent
set a value (preserving the existing behavior relied on by
ProfileTests.cpp's and TerminalSettingsTests.cpp's ==2 warning-count
assertions).

Also corrects an inaccurate comment: the media-resource path
(AppearanceConfig::ResolveMediaResources()) does not use the public
OverrideSource() accessor; it uses the private
_get<NAME>OverrideSourceAndValueImpl(), which already checks the leaf first.

Adds a regression test (UserOverrideOfInboxColorSchemeTypoWarns) that
reproduces the leaf-vs-parent scenario via matching profile GUIDs, verified
to fail against the pre-fix code and pass after (_addUserProfileParent()'s
!inserted branch in CascadiaSettingsSerialization.cpp merges the InBox
profile as a parent, and the pre-fix code trusted that non-null parent
OverrideSource() and suppressed the user's own typo). Also asserts
AllProfiles().Size() == 1 so a future guid-matching regression can't
silently degrade this into an unrelated single-profile test.

BrokenSchemeReferencedByFragmentProfileIsSilent is left as a pure negative
control (fragment-derived profiles produce zero warnings of any kind); an
earlier revision of this change also folded a positive control into it via
a second, user-owned broken reference, but _validateAllSchemesExist()
accumulates UnknownColorScheme into a single bool across all profiles, so
that would have made the fragment-suppression assertion indistinguishable
from a regression that lets a broken fragment reference through. The
positive control is independently covered by
UserOverrideOfInboxColorSchemeTypoWarns instead.

Reformatted with clang-format (22.1.3); adding the hasOwnValue parameter to
_originOfColorSchemeName() and _noteMissingColorScheme() shifted their
continuation-line alignment.

Fixes two check-spelling failures: rewords "typo'd" to "misspelled" in a
test comment, and allow-lists "Cambell", the deliberate misspelling used as
test fixture data in UserOverrideOfInboxColorSchemeTypoWarns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@drvoss
drvoss force-pushed the fix/11457-incomplete-colorscheme-warning branch from a122255 to e34d8c8 Compare July 19, 2026 10:04
Replace the free function _originOfColorSchemeName() in CascadiaSettings.cpp
with AppearanceConfig::ColorSchemeNameOrigin(), following the same pattern as
ResolveMediaResources(): it delegates to the existing private
_get<Name>ColorSchemeNameOverrideSourceImpl(), which already resolves the
leaf-first inheritance walk in one call, instead of manually recombining
Has<Name>() + <Name>OverrideSource(). This removes the hand-written
leaf-vs-parent logic that e34d8c8 had to patch by hand.

Also:
- Correct the _noteMissingColorScheme() comment: the suppression allow-list
  covers InBox, Fragment, and Generated (dynamic profile generator) origins,
  not just fragments, and note the latent ProfilesDefaults ambiguity between
  user profiles.defaults and inbox defaults.json (both are stamped
  OriginTag::ProfilesDefaults by CascadiaSettingsSerialization.cpp's
  _parse()).
- Remove review-history references ("see DHowett on microsoft#20428") from permanent
  test comments in favor of behavior-based descriptions.
- Change the ColorSchemeTests fixture literal from "Cambell" to "NoSuchScheme"
  so the misspelling isn't mistaken for a real typo, and drop the now-
  unnecessary "Cambell" entry from the spelling dictionary exceptions.

Verified via SettingsModel.Unit.Tests.dll: all color scheme tests pass,
including UserOverrideOfInboxColorSchemeTypoWarns and
BrokenSchemeReferencedByFragmentProfileIsSilent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@drvoss

drvoss commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@DHowett -- following up on your concern that fragment-referenced schemes must never warn. The revision addresses it directly.

Root cause. _validateAllSchemesExist() never distinguished who set a broken color-scheme reference, so a scheme contributed by a fragment could surface a warning the user has no way to remove.

What changed.

  • A file-local helper, _originOfColorSchemeName(), walks the appearance's inheritance graph to find the layer that actually supplied the effective scheme name. It deliberately does not use profile.Origin(): fragment-contributed profiles are merged into a user-owned child and stamped OriginTag::User, so the profile's own origin would misattribute the reference. The helper checks the leaf first (Has*ColorSchemeName()) before trusting *ColorSchemeNameOverrideSource(), which only walks parents.
  • _noteMissingColorScheme() then records a warning only when that resolved origin is User or ProfilesDefaults. References originating from a fragment, a dynamic profile generator, or defaults.json are suppressed -- a broken reference the user cannot edit never produces an inescapable warning.
  • A scheme defined by a fragment but referenced by the user is still warned, which I believe matches the intent.

Tests. ColorSchemeTests.cpp adds IncompleteFragmentSchemeReferencedByUserWarns, BrokenSchemeReferencedByFragmentProfileIsSilent, and UserOverrideOfInboxColorSchemeTypoWarns (the last is a leaf-vs-parent regression guard); DeserializationTests.cpp adds TestIncompleteColorSchemeName. Because a string colorScheme sets both the dark and light names, these tests already exercise both origin branches, and UnfocusedAppearance runs through the same helper -- so no origin branch is left untested.

Known limitation, out of scope here. Inbox defaults.json and user profiles.defaults share the ProfilesDefaults origin tag, so the allow-list treats both as user-actionable. Narrowing that is independent of this change.

I've also split the separate Clear*ColorSchemeName fragment-inheritance bug out into #20462 to keep this PR scoped to the warning classification. Happy to add coverage for any specific origin case you'd like to see spelled out.

@drvoss

drvoss commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a small follow-up cleanup that moves effective color-scheme origin resolution into AppearanceConfig, reusing its leaf-first inheritance logic. There is no intended behavior change; clang-format 22.1.3 passes, and the relevant Settings Model tests pass locally (ColorSchemeTests: 10/10; TestIncompleteColorSchemeName: 1/1).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Settings Issues related to settings and customizability, for console or terminal Issue-Bug It either shouldn't be doing this or needs an investigation. Priority-2 A description (P2) Product-Terminal The new Windows Terminal.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Incomplete" color schemes are rejected as if they don't exist (possibly only if in fragment)

3 participants