Convert Color Schemes + Themes

This commit is contained in:
Carlos Zamora
2026-06-05 14:23:55 -07:00
parent 956a059263
commit 9cbd70dd3f
7 changed files with 400 additions and 90 deletions

View File

@@ -16,10 +16,6 @@ using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
using namespace winrt::Windows::UI;
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view ForegroundKey{ "foreground" };
static constexpr std::string_view BackgroundKey{ "background" };
static constexpr std::string_view SelectionBackgroundKey{ "selectionBackground" };
static constexpr std::string_view CursorColorKey{ "cursorColor" };
static constexpr size_t ColorSchemeExpectedSize = 16;
static constexpr std::array<std::pair<std::string_view, size_t>, 18> TableColorsMapping{ {
@@ -46,28 +42,48 @@ static constexpr std::array<std::pair<std::string_view, size_t>, 18> TableColors
{ "brightMagenta", 13 },
} };
#define COLORSCHEME_NAMED_COLORS(GEN) \
GEN(Foreground, "foreground", DEFAULT_FOREGROUND) \
GEN(Background, "background", DEFAULT_BACKGROUND) \
GEN(SelectionBackground, "selectionBackground", DEFAULT_FOREGROUND) \
GEN(CursorColor, "cursorColor", DEFAULT_CURSOR_COLOR)
// The named colors paired with their default values. Used to drive the
// constructor seed, _layerJson validation, and ToJson copy from a single list.
// The individual getters still reference their DEFAULT_* constant directly.
#define GEN_NAMED_COLOR_DEFAULT(name, jsonKey, defaultVal) \
{ jsonKey, static_cast<winrt::Microsoft::Terminal::Core::Color>(defaultVal) },
static const std::array<std::pair<std::string_view, winrt::Microsoft::Terminal::Core::Color>, 4> NamedColorDefaults{ {
COLORSCHEME_NAMED_COLORS(GEN_NAMED_COLOR_DEFAULT)
} };
#undef GEN_NAMED_COLOR_DEFAULT
ColorScheme::ColorScheme() noexcept :
ColorScheme{ winrt::hstring{} }
{
}
ColorScheme::ColorScheme(const winrt::hstring& name) noexcept :
_Name{ name },
_Origin{ OriginTag::User }
{
JsonUtils::SetValueForKey(_json, NameKey, name);
for (const auto& [key, defaultValue] : NamedColorDefaults)
{
JsonUtils::SetValueForKey(_json, key, defaultValue);
}
const auto table = Utils::CampbellColorTable();
std::copy_n(table.data(), table.size(), _table.data());
for (size_t i = 0; i < ColorSchemeExpectedSize; ++i)
{
const Core::Color color = til::at(table, i);
JsonUtils::SetValueForKey(_json, til::at(TableColorsMapping, i).first, color);
}
}
winrt::com_ptr<ColorScheme> ColorScheme::Copy() const
{
auto scheme{ winrt::make_self<ColorScheme>() };
scheme->_Name = _Name;
scheme->_Foreground = _Foreground;
scheme->_Background = _Background;
scheme->_SelectionBackground = _SelectionBackground;
scheme->_CursorColor = _CursorColor;
scheme->_table = _table;
auto scheme{ winrt::make_self<ColorScheme>(uninitialized_t{}) };
scheme->_json = _json;
scheme->_Origin = _Origin;
return scheme;
}
@@ -97,19 +113,22 @@ winrt::com_ptr<ColorScheme> ColorScheme::FromJson(const Json::Value& json)
bool ColorScheme::_layerJson(const Json::Value& json)
{
// Required fields
auto isValid = JsonUtils::GetValueForKey(json, NameKey, _Name);
winrt::hstring name{};
auto isValid = JsonUtils::GetValueForKey(json, NameKey, name);
// Optional fields (they have defaults in ColorScheme.h)
JsonUtils::GetValueForKey(json, ForegroundKey, _Foreground);
JsonUtils::GetValueForKey(json, BackgroundKey, _Background);
JsonUtils::GetValueForKey(json, SelectionBackgroundKey, _SelectionBackground);
JsonUtils::GetValueForKey(json, CursorColorKey, _CursorColor);
// Optional fields; parsing them here only validates their type.
Core::Color namedColor{};
for (const auto& [key, _] : NamedColorDefaults)
{
JsonUtils::GetValueForKey(json, key, namedColor);
}
// Required fields
std::array<Core::Color, COLOR_TABLE_SIZE> table{};
size_t colorCount = 0;
for (const auto& [key, index] : TableColorsMapping)
{
colorCount += JsonUtils::GetValueForKey(json, key, til::at(_table, index));
colorCount += JsonUtils::GetValueForKey(json, key, til::at(table, index));
if (colorCount == ColorSchemeExpectedSize)
{
break;
@@ -117,8 +136,43 @@ bool ColorScheme::_layerJson(const Json::Value& json)
}
isValid &= (colorCount == 16); // Valid schemes should have exactly 16 colors
if (!isValid)
{
return false;
}
return isValid;
JsonUtils::MergeJsonKeys(json, _json);
_normalizeTableAliasKeys();
return true;
}
// Moves any alias color key (magenta -> purple, brightMagenta -> brightPurple;
// GH#11456) onto its canonical key so getters/ToJson only deal with canonical
// keys. The canonical key wins if both are present (matching the _layerJson
// validation loop's canonical-first preference). The node is copied whole, so an
// attached comment travels with the value.
void ColorScheme::_normalizeTableAliasKeys()
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 2> aliases{ {
{ "magenta", "purple" },
{ "brightMagenta", "brightPurple" },
} };
for (const auto& [alias, canonical] : aliases)
{
const auto aliasKey = JsonKey(alias);
if (_json.isMember(aliasKey))
{
const auto canonicalKey = JsonKey(canonical);
if (!_json.isMember(canonicalKey))
{
// Copy via a temporary to avoid jsoncpp self-aliasing concerns.
const Json::Value moved = _json[aliasKey];
_json[canonicalKey] = moved;
}
_json.removeMember(aliasKey);
}
}
}
// Method Description:
@@ -131,24 +185,72 @@ Json::Value ColorScheme::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, NameKey, _Name);
JsonUtils::SetValueForKey(json, ForegroundKey, _Foreground);
JsonUtils::SetValueForKey(json, BackgroundKey, _Background);
JsonUtils::SetValueForKey(json, SelectionBackgroundKey, _SelectionBackground);
JsonUtils::SetValueForKey(json, CursorColorKey, _CursorColor);
for (size_t i = 0; i < ColorSchemeExpectedSize; ++i)
JsonUtils::CopyKeyIfPresent(_json, json, NameKey);
for (const auto& [key, _] : NamedColorDefaults)
{
const auto& key = til::at(TableColorsMapping, i).first;
JsonUtils::SetValueForKey(json, key, til::at(_table, i));
JsonUtils::CopyKeyIfPresent(_json, json, key);
}
for (uint32_t i = 0; i < ColorSchemeExpectedSize; ++i)
{
JsonUtils::CopyKeyIfPresent(_json, json, til::at(TableColorsMapping, i).first);
}
return json;
}
winrt::com_array<winrt::Microsoft::Terminal::Core::Color> ColorScheme::Table() const noexcept
winrt::hstring ColorScheme::Name() const
{
return winrt::com_array<Core::Color>{ _table };
winrt::hstring name{};
JsonUtils::GetValueForKey(_json, NameKey, name);
return name;
}
void ColorScheme::Name(const winrt::hstring& value)
{
JsonUtils::SetValueForKeyPreservingComments(_json, NameKey, value);
}
// Reads a color from _json, falling back to defaultValue when the key is absent.
winrt::Microsoft::Terminal::Core::Color ColorScheme::_getColor(std::string_view key, const Core::Color& defaultValue) const
{
auto value = defaultValue;
JsonUtils::GetValueForKey(_json, key, value);
return value;
}
// Writes a color to _json while preserving any comment attached to the key.
void ColorScheme::_setColor(std::string_view key, const Core::Color& value)
{
JsonUtils::SetValueForKeyPreservingComments(_json, key, value);
}
#define GEN_NAMED_COLOR_ACCESSOR(name, jsonKey, defaultVal) \
winrt::Microsoft::Terminal::Core::Color ColorScheme::name() const \
{ \
return _getColor(jsonKey, static_cast<Core::Color>(defaultVal)); \
} \
void ColorScheme::name(const Core::Color& value) \
{ \
_setColor(jsonKey, value); \
}
COLORSCHEME_NAMED_COLORS(GEN_NAMED_COLOR_ACCESSOR)
#undef GEN_NAMED_COLOR_ACCESSOR
#undef COLORSCHEME_NAMED_COLORS
// Method Description:
// - Returns the resolved 16-entry color table, reading each entry from _json and
// falling back to the Campbell value for any entry not set.
winrt::com_array<winrt::Microsoft::Terminal::Core::Color> ColorScheme::Table() const
{
std::array<Core::Color, COLOR_TABLE_SIZE> table{};
const auto campbell = Utils::CampbellColorTable();
for (size_t i = 0; i < ColorSchemeExpectedSize; ++i)
{
til::at(table, i) = _getColor(til::at(TableColorsMapping, i).first, static_cast<Core::Color>(til::at(campbell, i)));
}
return winrt::com_array<Core::Color>{ table };
}
// Method Description:
@@ -158,10 +260,10 @@ winrt::com_array<winrt::Microsoft::Terminal::Core::Color> ColorScheme::Table() c
// - value: the color value we are setting the color table color to
// Return Value:
// - none
void ColorScheme::SetColorTableEntry(uint8_t index, const Core::Color& value) noexcept
void ColorScheme::SetColorTableEntry(uint8_t index, const Core::Color& value)
{
THROW_HR_IF(E_INVALIDARG, index >= _table.size());
_table[index] = value;
THROW_HR_IF(E_INVALIDARG, index >= COLOR_TABLE_SIZE);
_setColor(til::at(TableColorsMapping, index).first, value);
}
bool ColorScheme::IsEquivalentForSettingsMergePurposes(const winrt::com_ptr<ColorScheme>& other) noexcept
@@ -169,5 +271,14 @@ bool ColorScheme::IsEquivalentForSettingsMergePurposes(const winrt::com_ptr<Colo
// The caller likely only got here if the names were the same, so skip checking that one.
// We do not care about the cursor color or the selection background, as the main reason we are
// doing equivalence merging is to replace old, poorly-specified versions of those two properties.
return _table == other->_table && _Background == other->_Background && _Foreground == other->_Foreground;
const auto thisTable = Table();
const auto otherTable = other->Table();
for (uint32_t i = 0; i < thisTable.size(); ++i)
{
if (!(thisTable[i] == otherTable[i]))
{
return false;
}
}
return Background() == other->Background() && Foreground() == other->Foreground();
}

View File

@@ -18,6 +18,7 @@ Author(s):
#include "../../inc/conattrs.hpp"
#include "DefaultSettings.h"
#include "JsonUtils.h"
#include "ColorScheme.g.h"
@@ -26,7 +27,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
struct ColorScheme : ColorSchemeT<ColorScheme>
{
// A ColorScheme constructed with uninitialized_t
// leaves _table uninitialized.
// leaves _json empty to be populated lazily.
struct uninitialized_t
{
};
@@ -46,22 +47,36 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static com_ptr<ColorScheme> FromJson(const Json::Value& json);
Json::Value ToJson() const;
com_array<Core::Color> Table() const noexcept;
void SetColorTableEntry(uint8_t index, const Core::Color& value) noexcept;
com_array<Core::Color> Table() const;
void SetColorTableEntry(uint8_t index, const Core::Color& value);
bool IsEquivalentForSettingsMergePurposes(const winrt::com_ptr<ColorScheme>& other) noexcept;
WINRT_PROPERTY(winrt::hstring, Name);
winrt::hstring Name() const;
void Name(const winrt::hstring& value);
Core::Color Foreground() const;
void Foreground(const Core::Color& value);
Core::Color Background() const;
void Background(const Core::Color& value);
Core::Color SelectionBackground() const;
void SelectionBackground(const Core::Color& value);
Core::Color CursorColor() const;
void CursorColor(const Core::Color& value);
WINRT_PROPERTY(OriginTag, Origin, OriginTag::None);
WINRT_PROPERTY(Core::Color, Foreground, static_cast<Core::Color>(DEFAULT_FOREGROUND));
WINRT_PROPERTY(Core::Color, Background, static_cast<Core::Color>(DEFAULT_BACKGROUND));
WINRT_PROPERTY(Core::Color, SelectionBackground, static_cast<Core::Color>(DEFAULT_FOREGROUND));
WINRT_PROPERTY(Core::Color, CursorColor, static_cast<Core::Color>(DEFAULT_CURSOR_COLOR));
private:
bool _layerJson(const Json::Value& json);
void _normalizeTableAliasKeys();
std::array<Core::Color, COLOR_TABLE_SIZE> _table;
Core::Color _getColor(std::string_view key, const Core::Color& defaultValue) const;
void _setColor(std::string_view key, const Core::Color& value);
Json::Value _json{ Json::ValueType::objectValue };
};
}

View File

@@ -250,6 +250,44 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils
SetValueForKey(json, key, target, ConversionTrait<std::decay_t<T>>{});
}
template<typename T>
void SetValueForKeyPreservingComments(Json::Value& json, std::string_view key, const T& target)
{
const auto jsonKey = JsonKey(key);
std::array<std::string, Json::numberOfCommentPlacement> savedComments;
std::array<bool, Json::numberOfCommentPlacement> hadComment{};
if (json.isMember(jsonKey))
{
const auto& node = json[jsonKey];
for (auto p = 0; p < Json::numberOfCommentPlacement; ++p)
{
const auto placement = static_cast<Json::CommentPlacement>(p);
if (node.hasComment(placement))
{
savedComments[p] = node.getComment(placement);
hadComment[p] = true;
}
}
}
SetValueForKey(json, key, target);
// SetValueForKey skips empty optionals, so the key may legitimately not
// exist afterward -- only restore comments if the node is present.
if (json.isMember(jsonKey))
{
auto& node = json[jsonKey];
for (auto p = 0; p < Json::numberOfCommentPlacement; ++p)
{
if (hadComment[p])
{
node.setComment(savedComments[p], static_cast<Json::CommentPlacement>(p));
}
}
}
}
// CopyKeyIfPresent: copies a key's raw Json::Value from source to target
// if it exists in source. No type conversion — just raw JSON copy.
// Used in ToJson() to copy JSON-backed settings from _json to output.

View File

@@ -35,25 +35,22 @@ static constexpr std::string_view DarkNameKey{ "dark" };
static constexpr wchar_t RegKeyDwm[] = L"Software\\Microsoft\\Windows\\DWM";
static constexpr wchar_t RegKeyAccentColor[] = L"AccentColor";
#define THEME_SETTINGS_COPY(type, name, jsonKey, ...) \
result->_##name = _##name;
#define THEME_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
#define THEME_OBJECT(className, macro) \
winrt::com_ptr<className> className::Copy() \
{ \
auto result{ winrt::make_self<className>() }; \
macro(THEME_SETTINGS_COPY); \
return result; \
} \
\
Json::Value className::ToJson() \
{ \
Json::Value json{ Json::ValueType::objectValue }; \
macro(THEME_SETTINGS_TO_JSON); \
return json; \
#define THEME_OBJECT(className, macro) \
winrt::com_ptr<className> className::Copy() \
{ \
auto result{ winrt::make_self<className>() }; \
result->_json = _json; \
return result; \
} \
\
Json::Value className::ToJson() \
{ \
return _json; \
} \
\
void className::LayerJson(const Json::Value& json) \
{ \
JsonUtils::MergeJsonKeys(json, _json); \
}
THEME_OBJECT(WindowTheme, MTSM_THEME_WINDOW_SETTINGS);
@@ -61,8 +58,6 @@ THEME_OBJECT(SettingsTheme, MTSM_THEME_SETTINGS_SETTINGS);
THEME_OBJECT(TabRowTheme, MTSM_THEME_TABROW_SETTINGS);
THEME_OBJECT(TabTheme, MTSM_THEME_TAB_SETTINGS);
#undef THEME_SETTINGS_COPY
#undef THEME_SETTINGS_TO_JSON
#undef THEME_OBJECT
winrt::Microsoft::Terminal::Settings::Model::ThemeColor ThemeColor::FromColor(const winrt::Microsoft::Terminal::Core::Color& coreColor) noexcept
@@ -183,14 +178,6 @@ uint8_t ThemeColor::UnfocusedTabOpacity() const noexcept
return 0;
}
#define THEME_SETTINGS_FROM_JSON(type, name, jsonKey, ...) \
{ \
std::optional<type> _val; \
_val = JsonUtils::GetValueForKey<std::optional<type>>(json, jsonKey); \
if (_val) \
result->name(*_val); \
}
#define THEME_OBJECT_CONVERTER(nameSpace, name, macro) \
template<> \
struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<nameSpace::name> \
@@ -200,7 +187,7 @@ uint8_t ThemeColor::UnfocusedTabOpacity() const noexcept
if (json == Json::Value::null) \
return nullptr; \
auto result = winrt::make_self<nameSpace::implementation::name>(); \
macro(THEME_SETTINGS_FROM_JSON); \
result->LayerJson(json); \
return *result; \
} \
\
@@ -225,8 +212,6 @@ THEME_OBJECT_CONVERTER(winrt::Microsoft::Terminal::Settings::Model, SettingsThem
THEME_OBJECT_CONVERTER(winrt::Microsoft::Terminal::Settings::Model, TabRowTheme, MTSM_THEME_TABROW_SETTINGS);
THEME_OBJECT_CONVERTER(winrt::Microsoft::Terminal::Settings::Model, TabTheme, MTSM_THEME_TAB_SETTINGS);
#undef THEME_SETTINGS_FROM_JSON
#undef THEME_SETTINGS_TO_JSON
#undef THEME_OBJECT_CONVERTER
Theme::Theme(const winrt::WUX::ElementTheme& requestedTheme) noexcept
@@ -240,6 +225,7 @@ winrt::com_ptr<Theme> Theme::Copy() const
{
auto theme{ winrt::make_self<Theme>() };
theme->_json = _json;
theme->_Name = _Name;
if (_Window)
@@ -271,6 +257,7 @@ winrt::com_ptr<Theme> Theme::Copy() const
winrt::com_ptr<Theme> Theme::FromJson(const Json::Value& json)
{
auto result = winrt::make_self<Theme>();
result->_json = json;
JsonUtils::GetValueForKey(json, NameKey, result->_Name);
@@ -347,14 +334,30 @@ void Theme::LogSettingChanges(std::set<std::string>& changes, const std::string_
// - the JsonObject representing this instance
Json::Value Theme::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
Json::Value json{ _json };
JsonUtils::SetValueForKey(json, NameKey, _Name);
// Overlay Name, preserving its comment if present.
auto nameJson{ JsonUtils::ConversionTrait<winrt::hstring>().ToJson(_Name) };
if (json.isMember(JsonKey(NameKey)))
{
json[JsonKey(NameKey)].copyPayload(nameJson);
}
else
{
json[JsonKey(NameKey)] = nameJson;
}
// Don't serialize anything if the object is null.
#define THEME_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
if (_##name) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
#define THEME_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
if (_##name) \
{ \
Json::Value subJson{ Json::ValueType::objectValue }; \
JsonUtils::SetValueForKey(subJson, jsonKey, _##name); \
json[JsonKey(jsonKey)].copyPayload(subJson[JsonKey(jsonKey)]); \
} \
else \
{ \
json.removeMember(JsonKey(jsonKey)); \
}
MTSM_THEME_SETTINGS(THEME_SETTINGS_TO_JSON)
#undef THEME_SETTINGS_TO_JSON

View File

@@ -72,13 +72,40 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
#define THEME_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
WINRT_PROPERTY(type, name, ##__VA_ARGS__)
#define THEME_OBJECT(className, macro) \
struct className : className##T<className> \
{ \
winrt::com_ptr<className> Copy(); \
Json::Value ToJson(); \
\
macro(THEME_SETTINGS_INITIALIZE); \
#define THEME_SETTINGS_JSON_INITIALIZE(type, name, jsonKey, ...) \
public: \
type name() const \
{ \
if (auto val{ ::Microsoft::Terminal::Settings::Model::JsonUtils::GetValueForKey<std::optional<type>>(_json, jsonKey) }) \
{ \
return *val; \
} \
return type{ __VA_ARGS__ }; \
} \
void name(const type& value) \
{ \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(_json, jsonKey, value); \
} \
bool Has##name() const \
{ \
return _json.isMember(jsonKey) && !_json[jsonKey].isNull(); \
} \
void Clear##name() \
{ \
_json.removeMember(jsonKey); \
}
#define THEME_OBJECT(className, macro) \
struct className : className##T<className> \
{ \
winrt::com_ptr<className> Copy(); \
Json::Value ToJson(); \
void LayerJson(const Json::Value& json); \
\
macro(THEME_SETTINGS_JSON_INITIALIZE); \
\
private: \
Json::Value _json{ Json::ValueType::objectValue }; \
};
THEME_OBJECT(WindowTheme, MTSM_THEME_WINDOW_SETTINGS);
@@ -109,9 +136,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
MTSM_THEME_SETTINGS(THEME_SETTINGS_INITIALIZE)
private:
Json::Value _json{ Json::ValueType::objectValue };
};
#undef THEME_SETTINGS_INITIALIZE
#undef THEME_SETTINGS_JSON_INITIALIZE
#undef THEME_OBJECT
}

View File

@@ -25,6 +25,7 @@ namespace SettingsModelUnitTests
TEST_METHOD(ParseSimpleColorScheme);
TEST_METHOD(LayerColorSchemesOnArray);
TEST_METHOD(UpdateSchemeReferences);
TEST_METHOD(RoundtripPreservesComments);
TEST_METHOD(LayerColorSchemesWithUserOwnedCollision);
TEST_METHOD(LayerColorSchemesWithUserOwnedCollisionRetargetsAllProfiles);
@@ -87,6 +88,75 @@ namespace SettingsModelUnitTests
VERIFY_ARE_EQUAL(schemeObject, outJson);
}
void ColorSchemeTests::RoundtripPreservesComments()
{
// A complete scheme carrying comments on a few keys:
// - FG_NOTE: a standalone comment line before "foreground" (commentBefore)
// - BG_NOTE: a trailing same-line comment after "background"
// - ALIAS_NOTE: a comment before the alias key "magenta", which must
// travel with the value when it is normalized onto "purple"
// Distinct marker text avoids substring collisions when we scan the output.
const std::string schemeWithComments{ R"({
"name": "Commented",
// FG_NOTE
"foreground": "#F2F2F2",
"background": "#0C0C0C", // BG_NOTE
"selectionBackground": "#131313",
"cursorColor": "#FFFFFF",
"black": "#0C0C0C",
"red": "#C50F1F",
"green": "#13A10E",
"yellow": "#C19C00",
"blue": "#0037DA",
// ALIAS_NOTE
"magenta": "#881798",
"cyan": "#3A96DD",
"white": "#CCCCCC",
"brightBlack": "#767676",
"brightRed": "#E74856",
"brightGreen": "#16C60C",
"brightYellow": "#F9F1A5",
"brightBlue": "#3B78FF",
"brightPurple": "#B4009E",
"brightCyan": "#61D6D6",
"brightWhite": "#F2F2F2"
})" };
const auto schemeObject = VerifyParseSucceeded(schemeWithComments);
const auto scheme = ColorScheme::FromJson(schemeObject);
VERIFY_IS_NOT_NULL(scheme);
auto serialize = [](const Json::Value& json) {
Json::StreamWriterBuilder wbuilder;
wbuilder.settings_["commentStyle"] = "All";
return Json::writeString(wbuilder, json);
};
Log::Comment(L"Comments on known keys survive a load -> save roundtrip");
auto outJson{ scheme->ToJson() };
// Precise node-level check for the most reliable placement.
VERIFY_IS_TRUE(outJson["foreground"].hasComment(Json::commentBefore));
// End-to-end: the comment text lands in the serialized output.
auto serialized = serialize(outJson);
VERIFY_IS_TRUE(serialized.find("FG_NOTE") != std::string::npos);
VERIFY_IS_TRUE(serialized.find("BG_NOTE") != std::string::npos);
Log::Comment(L"A comment on an alias key moves to its canonical key");
VERIFY_IS_FALSE(outJson.isMember("magenta"));
VERIFY_IS_TRUE(outJson.isMember("purple"));
VERIFY_IS_TRUE(outJson["purple"].hasComment(Json::commentBefore));
VERIFY_IS_TRUE(serialized.find("ALIAS_NOTE") != std::string::npos);
Log::Comment(L"Editing a property preserves its existing comment");
scheme->Foreground(rgb(0x12, 0x34, 0x56));
outJson = scheme->ToJson();
VERIFY_IS_TRUE(outJson["foreground"].hasComment(Json::commentBefore));
serialized = serialize(outJson);
VERIFY_IS_TRUE(serialized.find("FG_NOTE") != std::string::npos);
}
void ColorSchemeTests::LayerColorSchemesOnArray()
{
static constexpr std::string_view inboxSettings{ R"({

View File

@@ -28,6 +28,7 @@ namespace SettingsModelUnitTests
TEST_METHOD(ParseNullWindowTheme);
TEST_METHOD(ParseThemeWithNullThemeColor);
TEST_METHOD(InvalidCurrentTheme);
TEST_METHOD(RoundtripThemeComments);
static Core::Color rgb(uint8_t r, uint8_t g, uint8_t b) noexcept
{
@@ -265,4 +266,47 @@ namespace SettingsModelUnitTests
throw e;
}
}
void ThemeTests::RoundtripThemeComments()
{
Log::Comment(L"A theme loaded then re-serialized should preserve JSON comments and unknown keys (GH#12424).");
// Comments are placed at representative positions:
// - before/after the "name" member
// - before a sub-object member ("window") -- the outer comment
// - before a key inside a sub-object ("useMica") -- an inner comment
// An unknown key ("unknownThemeKey") exercises forward-compatible retention.
static constexpr std::string_view commentedTheme{ R"json({
// comment-before-name
"name": "commented", // comment-after-name
// comment-before-window
"window":
{
// comment-before-useMica
"useMica": true
},
"unknownThemeKey": "keep-me"
})json" };
const auto themeObject = VerifyParseSucceeded(commentedTheme);
const auto theme = Theme::FromJson(themeObject);
VERIFY_ARE_EQUAL(L"commented", theme->Name());
const auto output = toString(theme->ToJson());
Log::Comment(NoThrowString().Format(til::u8u16(output).c_str()));
// Every comment must survive the round-trip.
VERIFY_IS_TRUE(output.find("comment-before-name") != std::string::npos, L"comment before 'name' preserved");
VERIFY_IS_TRUE(output.find("comment-after-name") != std::string::npos, L"trailing comment after 'name' preserved");
VERIFY_IS_TRUE(output.find("comment-before-window") != std::string::npos, L"outer comment before sub-object 'window' preserved");
VERIFY_IS_TRUE(output.find("comment-before-useMica") != std::string::npos, L"inner comment inside sub-object preserved");
// Unknown keys must round-trip rather than being silently dropped.
VERIFY_IS_TRUE(output.find("unknownThemeKey") != std::string::npos, L"unknown key preserved");
VERIFY_IS_TRUE(output.find("keep-me") != std::string::npos, L"unknown key value preserved");
// And the actual values must still be correct.
VERIFY_IS_NOT_NULL(theme->Window());
VERIFY_ARE_EQUAL(true, theme->Window().UseMica());
}
}