Compare commits

...

4 Commits

Author SHA1 Message Date
Carlos Zamora
7dc9bb1558 Convert NewTabMenu 2026-04-21 17:38:06 -07:00
Carlos Zamora
45e93cac42 Convert IMediaResource 2026-04-21 17:27:13 -07:00
Carlos Zamora
9bf994dd20 Convert more settings; ColorSchemeRef added 2026-04-21 16:49:00 -07:00
Carlos Zamora
a7380638a8 Make settings model JSON backed 2026-04-21 14:57:01 -07:00
12 changed files with 1900 additions and 406 deletions

View File

@@ -31,19 +31,18 @@ AppearanceConfig::AppearanceConfig(winrt::weak_ref<Model::Profile> sourceProfile
winrt::com_ptr<AppearanceConfig> AppearanceConfig::CopyAppearance(const AppearanceConfig* source, winrt::weak_ref<Model::Profile> sourceProfile)
{
auto appearance{ winrt::make_self<AppearanceConfig>(std::move(sourceProfile)) };
appearance->_Foreground = source->_Foreground;
appearance->_Background = source->_Background;
appearance->_SelectionBackground = source->_SelectionBackground;
appearance->_CursorColor = source->_CursorColor;
appearance->_Opacity = source->_Opacity;
appearance->_DarkColorSchemeName = source->_DarkColorSchemeName;
appearance->_LightColorSchemeName = source->_LightColorSchemeName;
appearance->_json = source->_json;
#define APPEARANCE_SETTINGS_COPY(type, name, jsonKey, ...) \
appearance->_##name = source->_##name;
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_COPY)
#undef APPEARANCE_SETTINGS_COPY
// JSON-backed settings (Foreground, Background, SelectionBackground, CursorColor,
// Opacity, DarkColorSchemeName, LightColorSchemeName, MTSM settings) all live in
// _json, which is already deep-copied above.
// Complex/mutable settings — backing fields for resolution lifecycle.
// _json (copied above) is the source of truth; backing fields hold resolved runtime state.
appearance->_PixelShaderPath = source->_PixelShaderPath;
appearance->_PixelShaderImagePath = source->_PixelShaderImagePath;
appearance->_BackgroundImagePath = source->_BackgroundImagePath;
return appearance;
}
@@ -52,33 +51,126 @@ Json::Value AppearanceConfig::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, ForegroundKey, _Foreground);
JsonUtils::SetValueForKey(json, BackgroundKey, _Background);
JsonUtils::SetValueForKey(json, SelectionBackgroundKey, _SelectionBackground);
JsonUtils::SetValueForKey(json, CursorColorKey, _CursorColor);
JsonUtils::SetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter<float, IntAsFloatPercentConversionTrait>{});
if (HasDarkColorSchemeName() || HasLightColorSchemeName())
// Nullable color settings: key presence matters (explicit null is valid)
for (const auto& key : { ForegroundKey, BackgroundKey, SelectionBackgroundKey, CursorColorKey })
{
// check if the setting is coming from the UI, if so grab the ColorSchemeName until the settings UI is fixed.
if (_LightColorSchemeName != _DarkColorSchemeName)
{
JsonUtils::SetValueForKey(json["colorScheme"], "dark", _DarkColorSchemeName);
JsonUtils::SetValueForKey(json["colorScheme"], "light", _LightColorSchemeName);
}
else
{
JsonUtils::SetValueForKey(json, "colorScheme", _DarkColorSchemeName);
}
JsonUtils::CopyKeyIfPresent(_json, json, key);
}
// Opacity: copy from _json (may be int or float — preserves original form)
JsonUtils::CopyKeyIfPresent(_json, json, OpacityKey);
// ColorScheme: ConversionTrait<ColorSchemeReference> handles string<->object form.
// _json already has the correct serialized form from SetValueForKey.
JsonUtils::CopyKeyIfPresent(_json, json, ColorSchemeKey);
// MTSM appearance settings: copy from _json (the source of truth)
#define APPEARANCE_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
JsonUtils::CopyKeyIfPresent(_json, json, jsonKey);
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_TO_JSON)
#undef APPEARANCE_SETTINGS_TO_JSON
// Complex/mutable settings — read from _json (source of truth), not backing fields
JsonUtils::CopyKeyIfPresent(_json, json, "experimental.pixelShaderPath");
JsonUtils::CopyKeyIfPresent(_json, json, "experimental.pixelShaderImagePath");
JsonUtils::CopyKeyIfPresent(_json, json, "backgroundImage");
return json;
}
bool AppearanceConfig::HasSetting(AppearanceSettingKey key) const
{
switch (key)
{
#define _APPEARANCE_HAS_SETTING(type, name, jsonKey, ...) \
case AppearanceSettingKey::name: \
return Has##name();
MTSM_APPEARANCE_SETTINGS(_APPEARANCE_HAS_SETTING)
#undef _APPEARANCE_HAS_SETTING
case AppearanceSettingKey::_Foreground:
return HasForeground();
case AppearanceSettingKey::_Background:
return HasBackground();
case AppearanceSettingKey::_SelectionBackground:
return HasSelectionBackground();
case AppearanceSettingKey::_CursorColor:
return HasCursorColor();
case AppearanceSettingKey::_Opacity:
return HasOpacity();
case AppearanceSettingKey::_DarkColorSchemeName:
return HasDarkColorSchemeName();
case AppearanceSettingKey::_LightColorSchemeName:
return HasLightColorSchemeName();
case AppearanceSettingKey::_PixelShaderPath:
return HasPixelShaderPath();
case AppearanceSettingKey::_PixelShaderImagePath:
return HasPixelShaderImagePath();
case AppearanceSettingKey::_BackgroundImagePath:
return HasBackgroundImagePath();
default:
return false;
}
}
void AppearanceConfig::ClearSetting(AppearanceSettingKey key)
{
switch (key)
{
#define _APPEARANCE_CLEAR_SETTING(type, name, jsonKey, ...) \
case AppearanceSettingKey::name: \
Clear##name(); \
break;
MTSM_APPEARANCE_SETTINGS(_APPEARANCE_CLEAR_SETTING)
#undef _APPEARANCE_CLEAR_SETTING
case AppearanceSettingKey::_Foreground:
ClearForeground();
break;
case AppearanceSettingKey::_Background:
ClearBackground();
break;
case AppearanceSettingKey::_SelectionBackground:
ClearSelectionBackground();
break;
case AppearanceSettingKey::_CursorColor:
ClearCursorColor();
break;
case AppearanceSettingKey::_Opacity:
ClearOpacity();
break;
case AppearanceSettingKey::_DarkColorSchemeName:
ClearDarkColorSchemeName();
break;
case AppearanceSettingKey::_LightColorSchemeName:
ClearLightColorSchemeName();
break;
case AppearanceSettingKey::_PixelShaderPath:
ClearPixelShaderPath();
break;
case AppearanceSettingKey::_PixelShaderImagePath:
ClearPixelShaderImagePath();
break;
case AppearanceSettingKey::_BackgroundImagePath:
ClearBackgroundImagePath();
break;
default:
break;
}
}
std::vector<AppearanceSettingKey> AppearanceConfig::CurrentSettings() const
{
std::vector<AppearanceSettingKey> result;
for (auto i = 0; i < static_cast<int>(AppearanceSettingKey::SETTINGS_SIZE); i++)
{
const auto key = static_cast<AppearanceSettingKey>(i);
if (HasSetting(key))
{
result.push_back(key);
}
}
return result;
}
// Method Description:
// - Layer values from the given json object on top of the existing properties
// of this object. For any keys we're expecting to be able to parse in the
@@ -92,45 +184,50 @@ Json::Value AppearanceConfig::ToJson() const
// - json: an object which should be a partial serialization of an AppearanceConfig object.
void AppearanceConfig::LayerJson(const Json::Value& json)
{
JsonUtils::GetValueForKey(json, ForegroundKey, _Foreground);
_logSettingIfSet(ForegroundKey, _Foreground.has_value());
// Merge incoming JSON keys into stored _json (key-wise, not replacement).
// AppearanceConfig receives the full profile JSON; we store all keys and
// read only appearance-relevant ones from it.
JsonUtils::MergeJsonKeys(json, _json);
JsonUtils::GetValueForKey(json, BackgroundKey, _Background);
_logSettingIfSet(BackgroundKey, _Background.has_value());
// Nullable color settings are now JSON-backed. Log which were set.
_logSettingIfSet(ForegroundKey, HasForeground());
_logSettingIfSet(BackgroundKey, HasBackground());
_logSettingIfSet(SelectionBackgroundKey, HasSelectionBackground());
_logSettingIfSet(CursorColorKey, HasCursorColor());
JsonUtils::GetValueForKey(json, SelectionBackgroundKey, _SelectionBackground);
_logSettingIfSet(SelectionBackgroundKey, _SelectionBackground.has_value());
JsonUtils::GetValueForKey(json, CursorColorKey, _CursorColor);
_logSettingIfSet(CursorColorKey, _CursorColor.has_value());
JsonUtils::GetValueForKey(json, LegacyAcrylicTransparencyKey, _Opacity);
JsonUtils::GetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter<float, IntAsFloatPercentConversionTrait>{});
_logSettingIfSet(OpacityKey, _Opacity.has_value());
if (json["colorScheme"].isString())
// Normalize legacy opacity key into canonical _json key
if (json.isMember(JsonKey(LegacyAcrylicTransparencyKey)))
{
// to make the UI happy, set ColorSchemeName.
JsonUtils::GetValueForKey(json, ColorSchemeKey, _DarkColorSchemeName);
_LightColorSchemeName = _DarkColorSchemeName;
_logSettingSet(ColorSchemeKey);
_json[JsonKey(OpacityKey)] = json[JsonKey(LegacyAcrylicTransparencyKey)];
}
else if (json["colorScheme"].isObject())
// Normalize integer percent to float (e.g. 50 → 0.5)
if (_json.isMember(JsonKey(OpacityKey)) && _json[JsonKey(OpacityKey)].isInt())
{
// to make the UI happy, set ColorSchemeName to whatever the dark value is.
JsonUtils::GetValueForKey(json["colorScheme"], "dark", _DarkColorSchemeName);
JsonUtils::GetValueForKey(json["colorScheme"], "light", _LightColorSchemeName);
_logSettingSet("colorScheme.dark");
_logSettingSet("colorScheme.light");
_json[JsonKey(OpacityKey)] = _json[JsonKey(OpacityKey)].asInt() / 100.0f;
}
_logSettingIfSet(OpacityKey, HasOpacity());
// ColorScheme: ConversionTrait<ColorSchemeReference> handles string↔object normalization.
// The raw JSON is stored in _json; the trait interprets it on read.
_logSettingIfSet(ColorSchemeKey, HasColorSchemeRef());
// MTSM settings are now JSON-backed (no backing fields).
// Values are already in _json from the merge step above.
// We only need to log which settings were set in this layer.
#define APPEARANCE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
_logSettingIfSet(jsonKey, json.isMember(jsonKey) && !json[jsonKey].isNull());
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_LAYER_JSON)
#undef APPEARANCE_SETTINGS_LAYER_JSON
// Complex/mutable settings — backing fields populated from _json for resolution lifecycle.
// _json is the source of truth for serialization; backing fields are for resolution lifecycle.
JsonUtils::GetValueForKey(json, "experimental.pixelShaderPath", _PixelShaderPath);
_logSettingIfSet("experimental.pixelShaderPath", _PixelShaderPath.has_value());
JsonUtils::GetValueForKey(json, "experimental.pixelShaderImagePath", _PixelShaderImagePath);
_logSettingIfSet("experimental.pixelShaderImagePath", _PixelShaderImagePath.has_value());
JsonUtils::GetValueForKey(json, "backgroundImage", _BackgroundImagePath);
_logSettingIfSet("backgroundImage", _BackgroundImagePath.has_value());
}
winrt::Microsoft::Terminal::Settings::Model::Profile AppearanceConfig::SourceProfile()

View File

@@ -18,11 +18,80 @@ Author(s):
#include "AppearanceConfig.g.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "IInheritable.h"
#include "MTSMSettings.h"
#include "MediaResourceSupport.h"
#include <DefaultSettings.h>
// ColorSchemeReference: a pair of color scheme names for dark and light themes.
// Represents the polymorphic "colorScheme" JSON key, which can be either:
// "colorScheme": "Campbell" → { dark: "Campbell", light: "Campbell" }
// "colorScheme": { "dark": "X", "light": "Y" } → { dark: "X", light: "Y" }
struct ColorSchemeReference
{
winrt::hstring dark{ L"Campbell" };
winrt::hstring light{ L"Campbell" };
static ColorSchemeReference Default() { return {}; }
bool operator==(const ColorSchemeReference&) const = default;
};
namespace Microsoft::Terminal::Settings::Model::JsonUtils
{
template<>
struct ConversionTrait<ColorSchemeReference>
{
ColorSchemeReference FromJson(const Json::Value& json)
{
ColorSchemeReference result;
if (json.isString())
{
// Simple form: both dark and light use the same scheme
const auto name = winrt::hstring{ til::u8u16(json.asString()) };
result.dark = name;
result.light = name;
}
else if (json.isObject())
{
// Structured form: { "dark": "...", "light": "..." }
if (json.isMember("dark"))
{
result.dark = winrt::hstring{ til::u8u16(json["dark"].asString()) };
}
if (json.isMember("light"))
{
result.light = winrt::hstring{ til::u8u16(json["light"].asString()) };
}
}
return result;
}
bool CanConvert(const Json::Value& json) const
{
return json.isString() || json.isObject();
}
Json::Value ToJson(const ColorSchemeReference& val)
{
// Collapse to string when dark == light
if (val.dark == val.light)
{
return til::u16u8(val.dark);
}
Json::Value obj{ Json::ValueType::objectValue };
obj["dark"] = til::u16u8(val.dark);
obj["light"] = til::u16u8(val.light);
return obj;
}
std::string TypeDescription() const
{
return "color scheme name (string or {dark, light})";
}
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct AppearanceConfig : AppearanceConfigT<AppearanceConfig, IMediaResourceContainer>, IInheritable<AppearanceConfig>
@@ -38,22 +107,65 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void ResolveMediaResources(const Model::MediaResourceResolver& resolver);
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Foreground, nullptr);
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Background, nullptr);
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, SelectionBackground, nullptr);
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, CursorColor, nullptr);
INHERITABLE_SETTING(Model::IAppearanceConfig, float, Opacity, 1.0f);
// Generic setting access via SettingKey
bool HasSetting(AppearanceSettingKey key) const;
void ClearSetting(AppearanceSettingKey key);
std::vector<AppearanceSettingKey> CurrentSettings() const;
INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, DarkColorSchemeName, L"Campbell");
INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, LightColorSchemeName, L"Campbell");
// Nullable color settings (JSON-backed)
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Foreground, "foreground", nullptr)
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Background, "background", nullptr)
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, SelectionBackground, "selectionBackground", nullptr)
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, CursorColor, "cursorColor", nullptr)
// Opacity: JSON-backed with normalization (int percent → float in LayerJson)
INHERITABLE_SETTING(Model::IAppearanceConfig, float, Opacity, "opacity", 1.0f)
// ColorScheme: JSON-backed via INHERITABLE_SETTING with ColorSchemeReference struct.
// The polymorphic JSON key ("colorScheme" as string or object) is handled by
// ConversionTrait<ColorSchemeReference>. The macro provides Has/Clear/OverrideSource.
// Thin wrappers expose the IDL-required DarkColorSchemeName / LightColorSchemeName.
INHERITABLE_SETTING(Model::IAppearanceConfig, ColorSchemeReference, ColorSchemeRef, "colorScheme", ColorSchemeReference::Default())
public:
hstring DarkColorSchemeName() const { return ColorSchemeRef().dark; }
void DarkColorSchemeName(const hstring& value)
{
auto ref = ColorSchemeRef();
ref.dark = value;
ColorSchemeRef(ref);
}
bool HasDarkColorSchemeName() const { return HasColorSchemeRef(); }
void ClearDarkColorSchemeName() { ClearColorSchemeRef(); }
Model::IAppearanceConfig DarkColorSchemeNameOverrideSource() { return ColorSchemeRefOverrideSource(); }
hstring LightColorSchemeName() const { return ColorSchemeRef().light; }
void LightColorSchemeName(const hstring& value)
{
auto ref = ColorSchemeRef();
ref.light = value;
ColorSchemeRef(ref);
}
bool HasLightColorSchemeName() const { return HasColorSchemeRef(); }
void ClearLightColorSchemeName() { ClearColorSchemeRef(); }
Model::IAppearanceConfig LightColorSchemeNameOverrideSource() { return ColorSchemeRefOverrideSource(); }
#define APPEARANCE_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
INHERITABLE_SETTING(Model::IAppearanceConfig, type, name, ##__VA_ARGS__)
INHERITABLE_SETTING(Model::IAppearanceConfig, type, name, jsonKey, ##__VA_ARGS__)
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_INITIALIZE)
#undef APPEARANCE_SETTINGS_INITIALIZE
// IMediaResource settings with backing fields for resolution lifecycle.
// See IInheritable.h for the INHERITABLE_MEDIA_RESOURCE_SETTING macro definition.
INHERITABLE_MEDIA_RESOURCE_SETTING(Model::IAppearanceConfig, PixelShaderPath, "experimental.pixelShaderPath", implementation::MediaResource::Empty())
INHERITABLE_MEDIA_RESOURCE_SETTING(Model::IAppearanceConfig, PixelShaderImagePath, "experimental.pixelShaderImagePath", implementation::MediaResource::Empty())
INHERITABLE_MEDIA_RESOURCE_SETTING(Model::IAppearanceConfig, BackgroundImagePath, "backgroundImage", implementation::MediaResource::Empty())
private:
winrt::weak_ref<Profile> _sourceProfile;
// Raw JSON for this layer (appearance-relevant keys only).
Json::Value _json{ Json::ValueType::objectValue };
std::set<std::string> _changeLog;
void _logSettingSet(const std::string_view& setting);

View File

@@ -12,6 +12,8 @@ using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::string_view FontInfoKey{ "font" };
static constexpr std::string_view FontAxesKey{ "axes" };
static constexpr std::string_view FontFeaturesKey{ "features" };
static constexpr std::string_view LegacyFontFaceKey{ "fontFace" };
static constexpr std::string_view LegacyFontSizeKey{ "fontSize" };
static constexpr std::string_view LegacyFontWeightKey{ "fontWeight" };
@@ -25,29 +27,11 @@ winrt::com_ptr<FontConfig> FontConfig::CopyFontInfo(const FontConfig* source, wi
{
auto fontInfo{ winrt::make_self<FontConfig>(std::move(sourceProfile)) };
#define FONT_SETTINGS_COPY(type, name, jsonKey, ...) \
fontInfo->_##name = source->_##name;
MTSM_FONT_SETTINGS(FONT_SETTINGS_COPY)
#undef FONT_SETTINGS_COPY
fontInfo->_json = source->_json;
// We cannot simply copy the font axes and features with `fontInfo->_FontAxes = source->_FontAxes;`
// since that'll just create a reference; we have to manually copy the values.
static constexpr auto cloneFontMap = [](const IFontFeatureMap& map) {
std::map<winrt::hstring, float> fontAxes;
for (const auto& [k, v] : map)
{
fontAxes.emplace(k, v);
}
return winrt::single_threaded_map(std::move(fontAxes));
};
if (source->_FontAxes)
{
fontInfo->_FontAxes = cloneFontMap(*source->_FontAxes);
}
if (source->_FontFeatures)
{
fontInfo->_FontFeatures = cloneFontMap(*source->_FontFeatures);
}
// FontAxes and FontFeatures are now JSON-backed — handled by _json copy above.
// No manual deep-copy needed; the JSON-backed getter returns a fresh deserialized
// collection each time.
return fontInfo;
}
@@ -56,14 +40,58 @@ Json::Value FontConfig::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
// MTSM font settings: copy from _json (the source of truth)
#define FONT_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
JsonUtils::CopyKeyIfPresent(_json, json, jsonKey);
MTSM_FONT_SETTINGS(FONT_SETTINGS_TO_JSON)
#undef FONT_SETTINGS_TO_JSON
return json;
}
bool FontConfig::HasSetting(FontSettingKey key) const
{
switch (key)
{
#define _FONT_HAS_SETTING(type, name, jsonKey, ...) \
case FontSettingKey::name: \
return Has##name();
MTSM_FONT_SETTINGS(_FONT_HAS_SETTING)
#undef _FONT_HAS_SETTING
default:
return false;
}
}
void FontConfig::ClearSetting(FontSettingKey key)
{
switch (key)
{
#define _FONT_CLEAR_SETTING(type, name, jsonKey, ...) \
case FontSettingKey::name: \
Clear##name(); \
break;
MTSM_FONT_SETTINGS(_FONT_CLEAR_SETTING)
#undef _FONT_CLEAR_SETTING
default:
break;
}
}
std::vector<FontSettingKey> FontConfig::CurrentSettings() const
{
std::vector<FontSettingKey> result;
for (auto i = 0; i < static_cast<int>(FontSettingKey::SETTINGS_SIZE); i++)
{
const auto key = static_cast<FontSettingKey>(i);
if (HasSetting(key))
{
result.push_back(key);
}
}
return result;
}
// Method Description:
// - Layer values from the given json object on top of the existing properties
// of this object. For any keys we're expecting to be able to parse in the
@@ -83,25 +111,38 @@ void FontConfig::LayerJson(const Json::Value& json)
{
// A font object is defined, use that
const auto fontInfoJson = json[JsonKey(FontInfoKey)];
#define FONT_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(fontInfoJson, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
// Merge the font sub-object into stored _json (font-object shape).
JsonUtils::MergeJsonKeys(fontInfoJson, _json);
// MTSM font settings are now JSON-backed (including FontAxes/FontFeatures).
// Values are already in _json. We only need to log which settings were set.
#define FONT_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
_logSettingIfSet(jsonKey, fontInfoJson.isMember(jsonKey) && !fontInfoJson[jsonKey].isNull());
MTSM_FONT_SETTINGS(FONT_SETTINGS_LAYER_JSON)
#undef FONT_SETTINGS_LAYER_JSON
}
else
{
// No font object is defined
// No font object is defined — normalize legacy flat keys into font-object shape.
if (json.isMember(JsonKey(LegacyFontFaceKey)))
{
_json["face"] = json[JsonKey(LegacyFontFaceKey)];
}
if (json.isMember(JsonKey(LegacyFontSizeKey)))
{
_json["size"] = json[JsonKey(LegacyFontSizeKey)];
}
if (json.isMember(JsonKey(LegacyFontWeightKey)))
{
_json["weight"] = json[JsonKey(LegacyFontWeightKey)];
}
// Log settings as if they were a part of the font object
JsonUtils::GetValueForKey(json, LegacyFontFaceKey, _FontFace);
_logSettingIfSet("face", _FontFace.has_value());
JsonUtils::GetValueForKey(json, LegacyFontSizeKey, _FontSize);
_logSettingIfSet("size", _FontSize.has_value());
JsonUtils::GetValueForKey(json, LegacyFontWeightKey, _FontWeight);
_logSettingIfSet("weight", _FontWeight.has_value());
_logSettingIfSet("face", json.isMember(JsonKey(LegacyFontFaceKey)));
_logSettingIfSet("size", json.isMember(JsonKey(LegacyFontSizeKey)));
_logSettingIfSet("weight", json.isMember(JsonKey(LegacyFontWeightKey)));
}
}
@@ -112,16 +153,16 @@ winrt::Microsoft::Terminal::Settings::Model::Profile FontConfig::SourceProfile()
void FontConfig::_logSettingSet(const std::string_view& setting)
{
if (setting == "axes" && _FontAxes.has_value())
if (setting == FontAxesKey && HasFontAxes())
{
for (const auto& [mapKey, _] : _FontAxes.value())
for (const auto& [mapKey, _] : FontAxes())
{
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, til::u16u8(mapKey)));
}
}
else if (setting == "features" && _FontFeatures.has_value())
else if (setting == FontFeaturesKey && HasFontFeatures())
{
for (const auto& [mapKey, _] : _FontFeatures.value())
for (const auto& [mapKey, _] : FontFeatures())
{
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, til::u16u8(mapKey)));
}

View File

@@ -19,6 +19,7 @@ Author(s):
#include "pch.h"
#include "FontConfig.g.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "MTSMSettings.h"
#include "IInheritable.h"
#include <DefaultSettings.h>
@@ -39,13 +40,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Model::Profile SourceProfile();
// Generic setting access via SettingKey
bool HasSetting(FontSettingKey key) const;
void ClearSetting(FontSettingKey key);
std::vector<FontSettingKey> CurrentSettings() const;
#define FONT_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
INHERITABLE_SETTING(Model::FontConfig, type, name, ##__VA_ARGS__)
INHERITABLE_SETTING(Model::FontConfig, type, name, jsonKey, ##__VA_ARGS__)
MTSM_FONT_SETTINGS(FONT_SETTINGS_INITIALIZE)
#undef FONT_SETTINGS_INITIALIZE
private:
winrt::weak_ref<Profile> _sourceProfile;
// Raw JSON for this layer (font sub-object shape).
Json::Value _json{ Json::ValueType::objectValue };
std::set<std::string> _changeLog;
void _logSettingSet(const std::string_view& setting);

View File

@@ -59,16 +59,13 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
{
auto globals{ winrt::make_self<GlobalAppSettings>() };
globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile;
globals->_defaultProfile = _defaultProfile;
globals->_actionMap = _actionMap->Copy();
globals->_keybindingsWarnings = _keybindingsWarnings;
globals->_json = _json;
#define GLOBAL_SETTINGS_COPY(type, name, jsonKey, ...) \
globals->_##name = _##name;
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_COPY)
#undef GLOBAL_SETTINGS_COPY
// JSON-backed settings (UnparsedDefaultProfile, MTSM settings) all live in _json,
// which is already deep-copied above. No per-setting copy needed.
if (_colorSchemes)
{
@@ -86,6 +83,9 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
globals->_themes.Insert(kv.Key(), *themeImpl->Copy());
}
}
// Complex/mutable settings — backing fields for mutation lifecycle.
// _json (copied above) is the source of truth; backing fields hold runtime state.
if (_NewTabMenu)
{
globals->_NewTabMenu = winrt::single_threaded_vector<Model::NewTabMenuEntry>();
@@ -94,14 +94,6 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
globals->_NewTabMenu->Append(get_self<NewTabMenuEntry>(entry)->Copy());
}
}
if (_DisabledProfileSources)
{
globals->_DisabledProfileSources = winrt::single_threaded_vector<hstring>();
for (const auto& src : *_DisabledProfileSources)
{
globals->_DisabledProfileSources->Append(src);
}
}
for (const auto& parent : _parents)
{
@@ -120,7 +112,7 @@ winrt::Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::Microso
void GlobalAppSettings::DefaultProfile(const winrt::guid& defaultProfile) noexcept
{
_defaultProfile = defaultProfile;
_UnparsedDefaultProfile = Utils::GuidToString(defaultProfile);
UnparsedDefaultProfile(hstring{ Utils::GuidToString(defaultProfile) });
}
winrt::guid GlobalAppSettings::DefaultProfile() const
@@ -150,25 +142,46 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::FromJson(const Json::Value&
void GlobalAppSettings::LayerJson(const Json::Value& json, const OriginTag origin)
{
JsonUtils::GetValueForKey(json, DefaultProfileKey, _UnparsedDefaultProfile);
// Merge incoming JSON keys into stored _json (key-wise, not replacement).
JsonUtils::MergeJsonKeys(json, _json);
// UnparsedDefaultProfile is now JSON-backed (in _json["defaultProfile"]).
// No backing field to populate.
// GH#8076 - when adding enum values to this key, we also changed it from
// "useTabSwitcher" to "tabSwitcherMode". Continue supporting
// "useTabSwitcher", but prefer "tabSwitcherMode"
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyUseTabSwitcherModeKey, _TabSwitcherMode) || _fixupsAppliedDuringLoad;
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyInputServiceWarningKey, _InputServiceWarning) || _fixupsAppliedDuringLoad;
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyWarnAboutLargePasteKey, _WarnAboutLargePaste) || _fixupsAppliedDuringLoad;
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyWarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste) || _fixupsAppliedDuringLoad;
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyConfirmCloseAllTabsKey, _ConfirmCloseAllTabs) || _fixupsAppliedDuringLoad;
// Normalize legacy keys into canonical _json keys for JSON-backed getters.
static constexpr std::pair<std::string_view, std::string_view> legacyKeyMappings[] = {
{ LegacyUseTabSwitcherModeKey, "tabSwitcherMode" },
{ LegacyInputServiceWarningKey, "warning.inputService" },
{ LegacyWarnAboutLargePasteKey, "warning.largePaste" },
{ LegacyWarnAboutMultiLinePasteKey, "warning.multiLinePaste" },
{ LegacyConfirmCloseAllTabsKey, "warning.confirmCloseAllTabs" },
};
for (const auto& [legacyKey, canonicalKey] : legacyKeyMappings)
{
if (json.isMember(JsonKey(legacyKey)))
{
_fixupsAppliedDuringLoad = true;
_json[JsonKey(canonicalKey)] = json[JsonKey(legacyKey)];
}
}
// MTSM settings are now JSON-backed (no backing fields).
// Values are already in _json from the merge step above.
// We only need to log which settings were set in this layer.
#define GLOBAL_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
_logSettingIfSet(jsonKey, json.isMember(jsonKey) && !json[jsonKey].isNull());
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_LAYER_JSON)
#undef GLOBAL_SETTINGS_LAYER_JSON
// Complex/mutable settings — backing fields populated from _json for runtime use.
// _json is the source of truth for serialization; backing fields are for mutation lifecycle.
JsonUtils::GetValueForKey(json, "newTabMenu", _NewTabMenu);
_logSettingIfSet("newTabMenu", _NewTabMenu.has_value());
// GH#11975 We only want to allow sensible values and prevent crashes, so we are clamping those values
// We only want to assign if the value did change through clamping,
// otherwise we could end up setting defaults that get persisted
@@ -304,47 +317,105 @@ Json::Value GlobalAppSettings::ToJson()
// These experimental options should be removed from the settings file if they're at their default value.
// This prevents them from sticking around forever, even if the user was just experimenting with them.
// One could consider this a workaround for the settings UI right now not having a "reset to default" button for these.
if (_GraphicsAPI == Control::GraphicsAPI::Automatic)
if (HasGraphicsAPI() && GraphicsAPI() == Control::GraphicsAPI::Automatic)
{
_GraphicsAPI.reset();
ClearGraphicsAPI();
}
if (_TextMeasurement == Control::TextMeasurement::Graphemes)
if (HasTextMeasurement() && TextMeasurement() == Control::TextMeasurement::Graphemes)
{
_TextMeasurement.reset();
ClearTextMeasurement();
}
if (_AmbiguousWidth == Control::AmbiguousWidth::Narrow)
if (HasAmbiguousWidth() && AmbiguousWidth() == Control::AmbiguousWidth::Narrow)
{
_AmbiguousWidth.reset();
ClearAmbiguousWidth();
}
if (_DefaultInputScope == Control::DefaultInputScope::Default)
if (HasDefaultInputScope() && DefaultInputScope() == Control::DefaultInputScope::Default)
{
_DefaultInputScope.reset();
ClearDefaultInputScope();
}
if (_DisablePartialInvalidation == false)
if (HasDisablePartialInvalidation() && DisablePartialInvalidation() == false)
{
_DisablePartialInvalidation.reset();
ClearDisablePartialInvalidation();
}
if (_SoftwareRendering == false)
if (HasSoftwareRendering() && SoftwareRendering() == false)
{
_SoftwareRendering.reset();
ClearSoftwareRendering();
}
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, DefaultProfileKey, _UnparsedDefaultProfile);
// DefaultProfile: copy from _json
JsonUtils::CopyKeyIfPresent(_json, json, DefaultProfileKey);
// MTSM global settings: copy from _json (the source of truth)
#define GLOBAL_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
JsonUtils::CopyKeyIfPresent(_json, json, jsonKey);
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_TO_JSON)
#undef GLOBAL_SETTINGS_TO_JSON
// Complex/mutable settings — read from _json (source of truth), not backing fields
JsonUtils::CopyKeyIfPresent(_json, json, "newTabMenu");
json[JsonKey(ActionsKey)] = _actionMap->ToJson();
json[JsonKey(KeybindingsKey)] = _actionMap->KeyBindingsToJson();
return json;
}
bool GlobalAppSettings::HasSetting(GlobalSettingKey key) const
{
switch (key)
{
#define _GLOBAL_HAS_SETTING(type, name, jsonKey, ...) \
case GlobalSettingKey::name: \
return Has##name();
MTSM_GLOBAL_SETTINGS(_GLOBAL_HAS_SETTING)
#undef _GLOBAL_HAS_SETTING
case GlobalSettingKey::_UnparsedDefaultProfile:
return HasUnparsedDefaultProfile();
case GlobalSettingKey::_NewTabMenu:
return HasNewTabMenu();
default:
return false;
}
}
void GlobalAppSettings::ClearSetting(GlobalSettingKey key)
{
switch (key)
{
#define _GLOBAL_CLEAR_SETTING(type, name, jsonKey, ...) \
case GlobalSettingKey::name: \
Clear##name(); \
break;
MTSM_GLOBAL_SETTINGS(_GLOBAL_CLEAR_SETTING)
#undef _GLOBAL_CLEAR_SETTING
case GlobalSettingKey::_UnparsedDefaultProfile:
ClearUnparsedDefaultProfile();
break;
case GlobalSettingKey::_NewTabMenu:
ClearNewTabMenu();
break;
default:
break;
}
}
std::vector<GlobalSettingKey> GlobalAppSettings::CurrentSettings() const
{
std::vector<GlobalSettingKey> result;
for (auto i = 0; i < static_cast<int>(GlobalSettingKey::SETTINGS_SIZE); i++)
{
const auto key = static_cast<GlobalSettingKey>(i);
if (HasSetting(key))
{
result.push_back(key);
}
}
return result;
}
bool GlobalAppSettings::FixupsAppliedDuringLoad()
{
return _fixupsAppliedDuringLoad || _actionMap->FixupsAppliedDuringLoad();
@@ -394,9 +465,9 @@ bool GlobalAppSettings::ShouldUsePersistedLayout() const
void GlobalAppSettings::ResolveMediaResources(const Model::MediaResourceResolver& resolver)
{
_actionMap->ResolveMediaResourcesWithBasePath(SourceBasePath, resolver);
if (_NewTabMenu)
if (HasNewTabMenu())
{
for (const auto& entry : *_NewTabMenu)
for (const auto& entry : NewTabMenu())
{
if (const auto resolvable{ entry.try_as<IPathlessMediaResourceContainer>() })
{
@@ -414,11 +485,12 @@ void GlobalAppSettings::_logSettingSet(const std::string_view& setting)
{
if (setting == "theme")
{
if (_Theme.has_value())
if (HasTheme())
{
const auto theme = Theme();
// ThemePair always has a Dark/Light value,
// so we need to check if they were explicitly set
if (_Theme->DarkName() == _Theme->LightName())
if (theme.DarkName() == theme.LightName())
{
_changeLog.emplace(setting);
}
@@ -431,9 +503,9 @@ void GlobalAppSettings::_logSettingSet(const std::string_view& setting)
}
else if (setting == "newTabMenu")
{
if (_NewTabMenu.has_value())
if (HasNewTabMenu())
{
for (const auto& entry : *_NewTabMenu)
for (const auto& entry : NewTabMenu())
{
std::string entryType;
switch (entry.Type())
@@ -476,7 +548,7 @@ void GlobalAppSettings::UpdateCommandID(const Model::Command& cmd, winrt::hstrin
_actionMap->UpdateCommandID(cmd, newID);
// newID might have been empty when this function was called, if so actionMap would have generated a new ID, use that
newID = cmd.ID();
if (_NewTabMenu)
if (HasNewTabMenu())
{
// Recursive lambda function to look through all the new tab menu entries and update IDs accordingly
std::function<void(const Model::NewTabMenuEntry&)> recursiveEntryIdUpdate;
@@ -503,7 +575,7 @@ void GlobalAppSettings::UpdateCommandID(const Model::Command& cmd, winrt::hstrin
}
};
for (const auto& entry : *_NewTabMenu)
for (const auto& entry : NewTabMenu())
{
recursiveEntryIdUpdate(entry);
}
@@ -515,8 +587,8 @@ void GlobalAppSettings::_logSettingIfSet(const std::string_view& setting, const
if (isSet)
{
// Exclude some false positives from userDefaults.json
const bool settingCopyFormattingToDefault = til::equals_insensitive_ascii(setting, "copyFormatting") && _CopyFormatting.has_value() && _CopyFormatting.value() == static_cast<Control::CopyFormat>(0);
const bool settingNTMToDefault = til::equals_insensitive_ascii(setting, "newTabMenu") && _NewTabMenu.has_value() && _NewTabMenu->Size() == 1 && _NewTabMenu->GetAt(0).Type() == NewTabMenuEntryType::RemainingProfiles;
const bool settingCopyFormattingToDefault = til::equals_insensitive_ascii(setting, "copyFormatting") && HasCopyFormatting() && CopyFormatting() == static_cast<Control::CopyFormat>(0);
const bool settingNTMToDefault = til::equals_insensitive_ascii(setting, "newTabMenu") && HasNewTabMenu() && NewTabMenu().Size() == 1 && NewTabMenu().GetAt(0).Type() == NewTabMenuEntryType::RemainingProfiles;
if (!settingCopyFormattingToDefault && !settingNTMToDefault)
{
_logSettingSet(setting);

View File

@@ -18,6 +18,7 @@ Author(s):
#include "GlobalAppSettings.g.h"
#include "IInheritable.h"
#include "MTSMSettings.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "ActionMap.h"
#include "Command.h"
@@ -56,6 +57,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value ToJson();
bool FixupsAppliedDuringLoad();
// Generic setting access via SettingKey
bool HasSetting(GlobalSettingKey key) const;
void ClearSetting(GlobalSettingKey key);
std::vector<GlobalSettingKey> CurrentSettings() const;
const std::vector<SettingsLoadWarnings>& KeybindingsWarnings() const;
// This DefaultProfile() setter is called by CascadiaSettings,
@@ -80,13 +86,28 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::hstring SourceBasePath;
INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L"");
INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, "defaultProfile", L"");
#define GLOBAL_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
INHERITABLE_SETTING_WITH_LOGGING(Model::GlobalAppSettings, type, name, jsonKey, ##__VA_ARGS__)
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_INITIALIZE)
#undef GLOBAL_SETTINGS_INITIALIZE
// NewTabMenu: mutable with backing field for editor in-place mutation.
// _json is the source of truth for serialization. Setter dual-writes to both.
_BASE_INHERITABLE_MUTABLE_SETTING(Model::GlobalAppSettings, std::optional<winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>>, NewTabMenu, winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} }))
public:
winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> NewTabMenu() const
{
const auto val{ _getNewTabMenuImpl() };
return val ? *val : winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} });
}
void NewTabMenu(const winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>& value)
{
_NewTabMenu = value;
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(_json, "newTabMenu", value);
}
private:
#ifdef NDEBUG
static constexpr bool debugFeaturesDefault{ false };
@@ -94,6 +115,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static constexpr bool debugFeaturesDefault{ true };
#endif
// Raw JSON for this layer. Populated by LayerJson(), will become the
// source of truth for settings once the JSON-backed refactor is complete.
Json::Value _json{ Json::ValueType::objectValue };
winrt::guid _defaultProfile{};
bool _fixupsAppliedDuringLoad{ false };
bool _legacyReloadEnvironmentVariables{ true };

View File

@@ -14,6 +14,8 @@ Author(s):
--*/
#pragma once
#include "JsonUtils.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
template<typename T>
@@ -79,189 +81,554 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
using NullableSetting = std::optional<std::optional<T>>;
}
// Shared implementation between the INHERITABLE_SETTING and INHERITABLE_NULLABLE_SETTING macros.
// See below for more details.
#define _BASE_INHERITABLE_SETTING(projectedType, storageType, name, ...) \
public: \
/* Returns true if the user explicitly set the value, false otherwise*/ \
bool Has##name() const \
{ \
return _##name.has_value(); \
} \
\
projectedType name##OverrideSource() \
{ \
/*user set value was not set*/ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return *source; \
} \
} \
\
/*no source was found*/ \
return nullptr; \
} \
\
/* Clear the user set value */ \
void Clear##name() \
{ \
_##name = std::nullopt; \
} \
\
private: \
storageType _##name{ std::nullopt }; \
\
storageType _get##name##Impl() const \
{ \
/*return user set value*/ \
if (_##name) \
{ \
return _##name; \
} \
\
/*user set value was not set*/ \
/*iterate through parents to find a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto val{ parent->_get##name##Impl() }) \
{ \
return val; \
} \
} \
\
/*no value was found*/ \
return std::nullopt; \
} \
\
auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \
{ \
/*we have a value*/ \
if (_##name) \
{ \
return get_strong(); \
} \
\
/*user set value was not set*/ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return source; \
} \
} \
\
/*no value was found*/ \
return nullptr; \
} \
\
auto _get##name##OverrideSourceAndValueImpl() \
->std::pair<decltype(get_strong()), storageType> \
{ \
/*we have a value*/ \
if (_##name) \
{ \
return { get_strong(), _##name }; \
} \
\
/*user set value was not set*/ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return { source, source->_##name }; \
} \
} \
\
/*no value was found*/ \
return { nullptr, std::nullopt }; \
// =============================================================================
// Inheritable setting macros
// =============================================================================
//
// These macros implement the standard functions for inheritable settings:
// - Has<NAME>(): checks if the user explicitly set a value for this setting
// - <NAME>OverrideSource(): returns the object that provides the resolved value
// - Getter(): returns the resolved value (this layer --> parent --> default)
// - Setter(): sets the value
// - Clear<NAME>(): clears the user set value (reverts to inherited/default)
//
// There are two storage strategies:
//
// 1. JSON-backed (preferred): All state lives in _json (a Json::Value member).
// Use these for any setting with a ConversionTrait. The owning class must
// declare a `Json::Value _json{ Json::ValueType::objectValue };` member.
//
// 2. Mutable backing-field (exception): State lives in a std::optional<T> member.
// Use these only for types that callers mutate in place after retrieval, such
// as IMediaResource (resolved via ResolveMediaResources), IVector, and IMap.
// A JSON-backed getter would deserialize a fresh copy on each call, losing
// those in-place mutations.
//
// When adding a new setting, use the JSON-backed macros unless the type requires
// in-place mutation. Existing backing-field settings should be migrated to
// JSON-backed as their mutation patterns are resolved.
//
// Available macros (JSON-backed):
// INHERITABLE_SETTING(projectedType, type, name, jsonKey, ...)
// INHERITABLE_SETTING_WITH_LOGGING(projectedType, type, name, jsonKey, ...)
// INHERITABLE_NULLABLE_SETTING(projectedType, type, name, jsonKey, ...)
// _BASE_INHERITABLE_SETTING(projectedType, type, name, jsonKey, ...)
//
// Available macros (mutable backing-field):
// INHERITABLE_MUTABLE_SETTING(projectedType, type, name, ...)
// INHERITABLE_NULLABLE_MUTABLE_SETTING(projectedType, type, name, ...)
//
// =============================================================================
// =============================================================================
// JSON-backed inheritable settings
// =============================================================================
// No std::optional<T> backing field — _json is the source of truth.
// Getters deserialize from _json via ConversionTrait on each call.
// Setters serialize to _json via SetValueForKey.
// Has/Clear check/modify _json directly.
// Shared base for JSON-backed inheritable settings.
// Provides Has, Clear, OverrideSource, and the private implementation helpers.
// Does NOT provide the public getter/setter — use INHERITABLE_SETTING or
// INHERITABLE_SETTING_WITH_LOGGING which add those on top of this base.
#define _BASE_INHERITABLE_SETTING(projectedType, type, name, jsonKey, ...) \
public: \
/* Returns true if the user explicitly set the value, false otherwise */ \
bool Has##name() const \
{ \
return _json.isMember(jsonKey) && !_json[jsonKey].isNull(); \
} \
\
/* Returns the object that provides the resolved value */ \
projectedType name##OverrideSource() \
{ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return *source; \
} \
} \
\
/*no source was found*/ \
return nullptr; \
} \
\
/* Clear the user set value */ \
void Clear##name() \
{ \
_json.removeMember(JsonKey(jsonKey)); \
} \
\
private: \
/* Read value from this layer's _json only (no parent walk) */ \
std::optional<type> _get##name##FromThisLayer() const \
{ \
if (Has##name()) \
{ \
type val{ __VA_ARGS__ }; \
::Microsoft::Terminal::Settings::Model::JsonUtils::GetValueForKey( \
_json, jsonKey, val); \
return val; \
} \
return std::nullopt; \
} \
\
/* Resolve the value: this layer --> parents --> nullopt */ \
std::optional<type> _get##name##Impl() const \
{ \
/*return value from this layer*/ \
if (auto val{ _get##name##FromThisLayer() }) \
{ \
return val; \
} \
\
/*iterate through parents to find a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto val{ parent->_get##name##Impl() }) \
{ \
return val; \
} \
} \
\
/*no value was found*/ \
return std::nullopt; \
} \
\
/* Find the object that provides the resolved value */ \
auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \
{ \
/*we have a value*/ \
if (Has##name()) \
{ \
return get_strong(); \
} \
\
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return source; \
} \
} \
\
/*no value was found*/ \
return nullptr; \
} \
\
/* Find the override source and the value it provides */ \
auto _get##name##OverrideSourceAndValueImpl() \
->std::pair<decltype(get_strong()), std::optional<type>> \
{ \
/*we have a value*/ \
if (Has##name()) \
{ \
return { get_strong(), _get##name##FromThisLayer() }; \
} \
\
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return { source, source->_get##name##FromThisLayer() }; \
} \
} \
\
/*no value was found*/ \
return { nullptr, std::nullopt }; \
}
// Use INHERITABLE_SETTING and INHERITABLE_NULLABLE_SETTING to implement
// standard functions for inheritable settings. This is similar to the WINRT_PROPERTY macro,
// except...
// - Has<NAME>(): checks if the user explicitly set a value for this setting
// - <NAME>OverrideSource(): return the object that provides the resolved value
// - Getter(): return the resolved value
// - Setter(): set the value directly
// - Clear(): clear the user set value
// - the setting is saved as an optional, where nullopt means
// that we must inherit the value from our parent
#define INHERITABLE_SETTING(projectedType, type, name, ...) \
_BASE_INHERITABLE_SETTING(projectedType, std::optional<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: user set value --> inherited value --> system set value */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const type& value) \
{ \
_##name = value; \
// JSON-backed inheritable setting.
// Getter returns the resolved value: this layer --> inherited --> default.
// Setter writes the value to _json.
#define INHERITABLE_SETTING(projectedType, type, name, jsonKey, ...) \
_BASE_INHERITABLE_SETTING(projectedType, type, name, jsonKey, __VA_ARGS__) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value in _json */ \
void name(const type& value) \
{ \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey( \
_json, jsonKey, value); \
}
#define INHERITABLE_SETTING_WITH_LOGGING(projectedType, type, name, jsonKey, ...) \
_BASE_INHERITABLE_SETTING(projectedType, std::optional<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: user set value --> inherited value --> system set value */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const type& value) \
{ \
if (!_##name.has_value() || _##name.value() != value) \
{ \
_logSettingSet(jsonKey); \
} \
_##name = value; \
// JSON-backed inheritable setting with change logging.
// Same as INHERITABLE_SETTING, but the setter also calls _logSettingSet(jsonKey)
// when the value actually changes (for settings change telemetry).
#define INHERITABLE_SETTING_WITH_LOGGING(projectedType, type, name, jsonKey, ...) \
_BASE_INHERITABLE_SETTING(projectedType, type, name, jsonKey, __VA_ARGS__) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value, log the change, and write to _json */ \
void name(const type& value) \
{ \
const auto existingVal{ _get##name##FromThisLayer() }; \
if (!existingVal.has_value() || existingVal.value() != value) \
{ \
_logSettingSet(jsonKey); \
} \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey( \
_json, jsonKey, value); \
}
// This macro is similar to the one above, but is reserved for optional settings
// like Profile.Foreground (where null is interpreted
// as an acceptable value, rather than "inherit")
// "type" is exposed as an IReference
#define INHERITABLE_NULLABLE_SETTING(projectedType, type, name, ...) \
_BASE_INHERITABLE_SETTING(projectedType, NullableSetting<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: user set value --> inherited value --> system set value */ \
winrt::Windows::Foundation::IReference<type> name() const \
{ \
const auto val{ _get##name##Impl() }; \
if (val) \
{ \
if (*val) \
{ \
return **val; \
} \
return nullptr; \
} \
return winrt::Windows::Foundation::IReference<type>{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const winrt::Windows::Foundation::IReference<type>& value) \
{ \
if (value) /*set value is different*/ \
{ \
_##name = std::optional<type>{ value.Value() }; \
} \
else \
{ \
/* note we're setting the _inner_ value */ \
_##name = std::optional<type>{ std::nullopt }; \
} \
// JSON-backed nullable inheritable setting.
// For settings where null is a valid explicit value (e.g., Foreground color).
// Key absent → inherit from parent
// Key present + null → explicitly "no value" (getter returns nullptr)
// Key present + value → has a value
// Getter returns IReference<T>.
#define INHERITABLE_NULLABLE_SETTING(projectedType, type, name, jsonKey, ...) \
public: \
/* Key presence means explicitly set (even if value is null) */ \
bool Has##name() const \
{ \
return _json.isMember(jsonKey); \
} \
\
/* Returns the object that provides the resolved value */ \
projectedType name##OverrideSource() \
{ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return *source; \
} \
} \
return nullptr; \
} \
\
/* Clear the user set value (remove key entirely → inherit from parent) */ \
void Clear##name() \
{ \
_json.removeMember(JsonKey(jsonKey)); \
} \
\
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
winrt::Windows::Foundation::IReference<type> name() const \
{ \
const auto val{ _get##name##Impl() }; \
if (val) \
{ \
if (*val) \
{ \
return **val; \
} \
return nullptr; \
} \
return winrt::Windows::Foundation::IReference<type>{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value in _json */ \
void name(const winrt::Windows::Foundation::IReference<type>& value) \
{ \
if (value) \
{ \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey( \
_json, jsonKey, value.Value()); \
} \
else \
{ \
/* explicitly set to null (not the same as clearing) */ \
_json[JsonKey(jsonKey)] = Json::nullValue; \
} \
} \
\
private: \
/* Read nullable value from this layer's _json only (no parent walk) */ \
/* Returns: nullopt = not set. optional{nullopt} = explicitly null. */ \
/* optional{optional{val}} = has value. */ \
NullableSetting<type> _get##name##FromThisLayer() const \
{ \
if (_json.isMember(jsonKey)) \
{ \
if (_json[jsonKey].isNull()) \
{ \
return std::optional<type>{ std::nullopt }; \
} \
type val{}; \
::Microsoft::Terminal::Settings::Model::JsonUtils::GetValueForKey( \
_json, jsonKey, val); \
return std::optional<type>{ val }; \
} \
return std::nullopt; \
} \
\
/* Resolve the nullable value: this layer --> parents --> nullopt */ \
NullableSetting<type> _get##name##Impl() const \
{ \
if (auto val{ _get##name##FromThisLayer() }) \
{ \
return val; \
} \
for (const auto& parent : _parents) \
{ \
if (auto val{ parent->_get##name##Impl() }) \
{ \
return val; \
} \
} \
return std::nullopt; \
} \
\
/* Find the object that provides the resolved value */ \
auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \
{ \
if (Has##name()) \
{ \
return get_strong(); \
} \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return source; \
} \
} \
return nullptr; \
} \
\
/* Find the override source and the value it provides */ \
auto _get##name##OverrideSourceAndValueImpl() \
->std::pair<decltype(get_strong()), NullableSetting<type>> \
{ \
if (Has##name()) \
{ \
return { get_strong(), _get##name##FromThisLayer() }; \
} \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return { source, source->_get##name##FromThisLayer() }; \
} \
} \
return { nullptr, std::nullopt }; \
}
// =============================================================================
// Mutable backing-field inheritable settings
// =============================================================================
// These use a std::optional<T> backing field instead of _json.
//
// Use these ONLY for types that callers mutate in place after retrieval.
// A JSON-backed getter would deserialize a fresh copy on each call, so
// in-place mutations (e.g. ResolveMediaResources) would be lost.
//
// Current uses:
// Profile: Icon, EnvironmentVariables, BellSound, UnfocusedAppearance
// GlobalAppSettings: DisabledProfileSources, NewTabMenu
// AppearanceConfig: PixelShaderPath, PixelShaderImagePath, BackgroundImagePath,
// DarkColorSchemeName, LightColorSchemeName
// FontConfig: FontAxes, FontFeatures
// Shared base for backing-field inheritable settings.
// Provides Has, Clear, OverrideSource, and the private implementation helpers.
#define _BASE_INHERITABLE_MUTABLE_SETTING(projectedType, storageType, name, ...) \
public: \
/* Returns true if the user explicitly set the value, false otherwise */ \
bool Has##name() const \
{ \
return _##name.has_value(); \
} \
\
/* Returns the object that provides the resolved value */ \
projectedType name##OverrideSource() \
{ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return *source; \
} \
} \
\
/*no source was found*/ \
return nullptr; \
} \
\
/* Clear the user set value */ \
void Clear##name() \
{ \
_##name = std::nullopt; \
} \
\
private: \
storageType _##name{ std::nullopt }; \
\
/* Resolve the value: this layer --> parents --> nullopt */ \
storageType _get##name##Impl() const \
{ \
/*return user set value*/ \
if (_##name) \
{ \
return _##name; \
} \
\
/*iterate through parents to find a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto val{ parent->_get##name##Impl() }) \
{ \
return val; \
} \
} \
\
/*no value was found*/ \
return std::nullopt; \
} \
\
/* Find the object that provides the resolved value */ \
auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \
{ \
/*we have a value*/ \
if (_##name) \
{ \
return get_strong(); \
} \
\
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return source; \
} \
} \
\
/*no value was found*/ \
return nullptr; \
} \
\
/* Find the override source and the value it provides */ \
auto _get##name##OverrideSourceAndValueImpl() \
->std::pair<decltype(get_strong()), storageType> \
{ \
/*we have a value*/ \
if (_##name) \
{ \
return { get_strong(), _##name }; \
} \
\
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return { source, source->_##name }; \
} \
} \
\
/*no value was found*/ \
return { nullptr, std::nullopt }; \
}
// Mutable backing-field setting.
// Getter returns the resolved value: this layer --> inherited --> default.
// Setter writes to the std::optional<T> backing field.
#define INHERITABLE_MUTABLE_SETTING(projectedType, type, name, ...) \
_BASE_INHERITABLE_MUTABLE_SETTING(projectedType, std::optional<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const type& value) \
{ \
_##name = value; \
}
// Mutable nullable backing-field setting.
// Same as INHERITABLE_MUTABLE_SETTING, but for optional settings where null is
// a valid explicit value. Getter returns IReference<T>.
#define INHERITABLE_NULLABLE_MUTABLE_SETTING(projectedType, type, name, ...) \
_BASE_INHERITABLE_MUTABLE_SETTING(projectedType, NullableSetting<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
winrt::Windows::Foundation::IReference<type> name() const \
{ \
const auto val{ _get##name##Impl() }; \
if (val) \
{ \
if (*val) \
{ \
return **val; \
} \
return nullptr; \
} \
return winrt::Windows::Foundation::IReference<type>{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const winrt::Windows::Foundation::IReference<type>& value) \
{ \
if (value) \
{ \
_##name = std::optional<type>{ value.Value() }; \
} \
else \
{ \
/* explicitly set to null (not the same as clearing) */ \
_##name = std::optional<type>{ std::nullopt }; \
} \
}
// =============================================================================
// IMediaResource settings with backing fields for resolution lifecycle.
// =============================================================================
//
// Use for settings of type IMediaResource that need in-place resolution
// (e.g., Icon, PixelShaderPath, BackgroundImagePath). The backing field
// holds the runtime-resolved object. _json is the source of truth for
// serialization. The setter dual-writes to both.
//
// Parameters:
// projectedType - the WinRT projected type (e.g., Model::Profile)
// name - the setting name (e.g., Icon)
// jsonKey - the JSON key (e.g., "icon")
// ... - default value (e.g., implementation::MediaResource::Empty())
//
#define INHERITABLE_MEDIA_RESOURCE_SETTING(projectedType, name, jsonKey, ...) \
_BASE_INHERITABLE_MUTABLE_SETTING(projectedType, std::optional<IMediaResource>, name, \
implementation::MediaResource::Empty()) \
public: \
/* Returns the resolved value: this layer --> inherited --> default */ \
IMediaResource name() const \
{ \
const auto v{ _get##name##Impl() }; \
return v ? *v : IMediaResource{ __VA_ARGS__ }; \
} \
\
/* Dual-write: backing field (for resolution) + _json (for serialization) */ \
void name(const IMediaResource& value) \
{ \
_##name = value; \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(_json, jsonKey, value); \
}

View File

@@ -250,6 +250,28 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils
SetValueForKey(json, key, target, ConversionTrait<std::decay_t<T>>{});
}
// 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.
inline void CopyKeyIfPresent(const Json::Value& source, Json::Value& target, const std::string_view key)
{
if (source.isMember(JsonKey(key)))
{
target[JsonKey(key)] = source[JsonKey(key)];
}
}
// MergeJsonKeys: copies all keys from source into target (key-wise merge).
// Existing keys in target are overwritten by source values.
// Used in LayerJson() to merge incoming JSON into stored _json.
inline void MergeJsonKeys(const Json::Value& source, Json::Value& target)
{
for (const auto& key : source.getMemberNames())
{
target[key] = source[key];
}
}
template<>
struct ConversionTrait<std::string>
{

View File

@@ -8,6 +8,7 @@ Module Name:
Abstract:
- Contains most of the settings within Terminal Settings Model (global, profile, font, appearance)
- To add a new setting to any one of those classes, simply add it to the respective list below, following the macro format
- Also defines SettingKey enums for generic setting access
Author(s):
- Pankaj Bhojwani - October 2021
@@ -62,16 +63,15 @@ Author(s):
X(Model::WindowingMode, WindowingBehavior, "windowingBehavior", Model::WindowingMode::UseNew) \
X(bool, MinimizeToNotificationArea, "minimizeToNotificationArea", false) \
X(bool, AlwaysShowNotificationIcon, "alwaysShowNotificationIcon", false) \
X(winrt::Windows::Foundation::Collections::IVector<winrt::hstring>, DisabledProfileSources, "disabledProfileSources", nullptr) \
X(bool, ShowAdminShield, "showAdminShield", true) \
X(bool, TrimPaste, "trimPaste", true) \
X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \
X(bool, EnableShellCompletionMenu, "experimental.enableShellCompletionMenu", false) \
X(bool, EnableUnfocusedAcrylic, "compatibility.enableUnfocusedAcrylic", true) \
X(winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>, NewTabMenu, "newTabMenu", winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} })) \
X(bool, AllowHeadless, "compatibility.allowHeadless", false) \
X(hstring, SearchWebDefaultQueryUrl, "searchWebDefaultQueryUrl", L"https://www.bing.com/search?q=%22%s%22") \
X(bool, ShowTabsFullscreen, "showTabsFullscreen", false)
X(bool, ShowTabsFullscreen, "showTabsFullscreen", false) \
X(winrt::Windows::Foundation::Collections::IVector<winrt::hstring>, DisabledProfileSources, "disabledProfileSources", nullptr)
// Also add these settings to:
// * Profile.idl
@@ -88,15 +88,12 @@ Author(s):
X(Microsoft::Terminal::Control::ScrollbarState, ScrollState, "scrollbarState", Microsoft::Terminal::Control::ScrollbarState::Visible) \
X(Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, "antialiasingMode", Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \
X(hstring, StartingDirectory, "startingDirectory") \
X(IMediaResource, Icon, "icon", implementation::MediaResource::FromString(L"\uE756")) \
X(bool, SuppressApplicationTitle, "suppressApplicationTitle", false) \
X(guid, ConnectionType, "connectionType") \
X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic) \
X(hstring, TabTitle, "tabTitle") \
X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \
X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \
X(bool, RightClickContextMenu, "rightClickContextMenu", false) \
X(Windows::Foundation::Collections::IVector<IMediaResource>, BellSound, "bellSound", nullptr) \
X(bool, Elevate, "elevate", false) \
X(bool, AutoMarkPrompts, "autoMarkPrompts", true) \
X(bool, ShowMarks, "showMarksOnScrollbar", false) \
@@ -109,7 +106,8 @@ Author(s):
X(bool, AllowVtClipboardWrite, "compatibility.allowOSC52", true) \
X(bool, AllowKeypadMode, "compatibility.allowDECNKM", false) \
X(hstring, DragDropDelimiter, "dragDropDelimiter", L" ") \
X(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, "pathTranslationStyle", Microsoft::Terminal::Control::PathTranslationStyle::None)
X(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, "pathTranslationStyle", Microsoft::Terminal::Control::PathTranslationStyle::None) \
X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr)
// Intentionally omitted Profile settings:
// * Name
@@ -124,12 +122,12 @@ Author(s):
X(hstring, FontFace, "face", DEFAULT_FONT_FACE) \
X(float, FontSize, "size", DEFAULT_FONT_SIZE) \
X(winrt::Windows::UI::Text::FontWeight, FontWeight, "weight", DEFAULT_FONT_WEIGHT) \
X(IFontAxesMap, FontAxes, "axes") \
X(IFontFeatureMap, FontFeatures, "features") \
X(bool, EnableBuiltinGlyphs, "builtinGlyphs", true) \
X(bool, EnableColorGlyphs, "colorGlyphs", true) \
X(winrt::hstring, CellWidth, "cellWidth") \
X(winrt::hstring, CellHeight, "cellHeight")
X(winrt::hstring, CellHeight, "cellHeight") \
X(IFontAxesMap, FontAxes, "axes") \
X(IFontFeatureMap, FontFeatures, "features")
#define MTSM_APPEARANCE_SETTINGS(X) \
X(Core::CursorStyle, CursorShape, "cursorShape", Core::CursorStyle::Bar) \
@@ -137,10 +135,7 @@ Author(s):
X(float, BackgroundImageOpacity, "backgroundImageOpacity", 1.0f) \
X(winrt::Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode, "backgroundImageStretchMode", winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill) \
X(bool, RetroTerminalEffect, "experimental.retroTerminalEffect", false) \
X(IMediaResource, PixelShaderPath, "experimental.pixelShaderPath", implementation::MediaResource::Empty()) \
X(IMediaResource, PixelShaderImagePath, "experimental.pixelShaderImagePath", implementation::MediaResource::Empty()) \
X(ConvergedAlignment, BackgroundImageAlignment, "backgroundImageAlignment", ConvergedAlignment::Horizontal_Center | ConvergedAlignment::Vertical_Center) \
X(IMediaResource, BackgroundImagePath, "backgroundImage", implementation::MediaResource::Empty()) \
X(Model::IntenseStyle, IntenseTextStyle, "intenseTextStyle", Model::IntenseStyle::Bright) \
X(Core::AdjustTextMode, AdjustIndistinguishableColors, "adjustIndistinguishableColors", Core::AdjustTextMode::Automatic) \
X(bool, UseAcrylic, "useAcrylic", false)
@@ -174,3 +169,163 @@ Author(s):
X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) \
X(winrt::Microsoft::Terminal::Settings::Model::IconStyle, IconStyle, "iconStyle", winrt::Microsoft::Terminal::Settings::Model::IconStyle::Default) \
X(winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility, ShowCloseButton, "showCloseButton", winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility::Always)
// SettingKey enums: provide a generic way to reference settings by key.
// Generated from the MTSM macros above. Also includes special-cased settings
// that are not part of the macro lists (prefixed with _ to avoid name collisions).
// SETTINGS_SIZE is a sentinel value that must remain last in each enum.
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
#define _MTSM_ENUM_VALUE(type, name, jsonKey, ...) name,
enum class ProfileSettingKey : int
{
MTSM_PROFILE_SETTINGS(_MTSM_ENUM_VALUE)
// Special-cased profile settings (not in MTSM_PROFILE_SETTINGS due to
// custom JSON parsing, but included here for completeness)
_Name,
_Guid,
_Source,
_Hidden,
_Padding,
_TabColor,
// Complex/mutable settings with backing fields
_Icon,
_BellSound,
SETTINGS_SIZE
};
enum class GlobalSettingKey : int
{
MTSM_GLOBAL_SETTINGS(_MTSM_ENUM_VALUE)
// Special-cased global settings
_UnparsedDefaultProfile,
// Complex/mutable settings with backing fields
_NewTabMenu,
SETTINGS_SIZE
};
enum class FontSettingKey : int
{
MTSM_FONT_SETTINGS(_MTSM_ENUM_VALUE)
SETTINGS_SIZE
};
enum class AppearanceSettingKey : int
{
MTSM_APPEARANCE_SETTINGS(_MTSM_ENUM_VALUE)
// Special-cased appearance settings
_Foreground,
_Background,
_SelectionBackground,
_CursorColor,
_Opacity,
_DarkColorSchemeName,
_LightColorSchemeName,
// Complex/mutable settings with backing fields
_PixelShaderPath,
_PixelShaderImagePath,
_BackgroundImagePath,
SETTINGS_SIZE
};
#undef _MTSM_ENUM_VALUE
// JSON key lookup: returns the JSON key string for a given SettingKey.
// Generated from the same macros to maintain a single source of truth.
constexpr std::string_view JsonKeyForSetting(ProfileSettingKey key)
{
#define _MTSM_KEY_CASE(type, name, jsonKey, ...) \
case ProfileSettingKey::name: \
return jsonKey;
switch (key)
{
MTSM_PROFILE_SETTINGS(_MTSM_KEY_CASE)
case ProfileSettingKey::_Name:
return "name";
case ProfileSettingKey::_Guid:
return "guid";
case ProfileSettingKey::_Source:
return "source";
case ProfileSettingKey::_Hidden:
return "hidden";
case ProfileSettingKey::_Padding:
return "padding";
case ProfileSettingKey::_TabColor:
return "tabColor";
case ProfileSettingKey::_Icon:
return "icon";
case ProfileSettingKey::_BellSound:
return "bellSound";
default:
return {};
}
#undef _MTSM_KEY_CASE
}
constexpr std::string_view JsonKeyForSetting(GlobalSettingKey key)
{
#define _MTSM_KEY_CASE(type, name, jsonKey, ...) \
case GlobalSettingKey::name: \
return jsonKey;
switch (key)
{
MTSM_GLOBAL_SETTINGS(_MTSM_KEY_CASE)
case GlobalSettingKey::_UnparsedDefaultProfile:
return "defaultProfile";
case GlobalSettingKey::_NewTabMenu:
return "newTabMenu";
default:
return {};
}
#undef _MTSM_KEY_CASE
}
constexpr std::string_view JsonKeyForSetting(FontSettingKey key)
{
#define _MTSM_KEY_CASE(type, name, jsonKey, ...) \
case FontSettingKey::name: \
return jsonKey;
switch (key)
{
MTSM_FONT_SETTINGS(_MTSM_KEY_CASE)
default:
return {};
}
#undef _MTSM_KEY_CASE
}
constexpr std::string_view JsonKeyForSetting(AppearanceSettingKey key)
{
#define _MTSM_KEY_CASE(type, name, jsonKey, ...) \
case AppearanceSettingKey::name: \
return jsonKey;
switch (key)
{
MTSM_APPEARANCE_SETTINGS(_MTSM_KEY_CASE)
case AppearanceSettingKey::_Foreground:
return "foreground";
case AppearanceSettingKey::_Background:
return "background";
case AppearanceSettingKey::_SelectionBackground:
return "selectionBackground";
case AppearanceSettingKey::_CursorColor:
return "cursorColor";
case AppearanceSettingKey::_Opacity:
return "opacity";
case AppearanceSettingKey::_DarkColorSchemeName:
return "colorScheme";
case AppearanceSettingKey::_LightColorSchemeName:
return "colorScheme";
case AppearanceSettingKey::_PixelShaderPath:
return "experimental.pixelShaderPath";
case AppearanceSettingKey::_PixelShaderImagePath:
return "experimental.pixelShaderImagePath";
case AppearanceSettingKey::_BackgroundImagePath:
return "backgroundImage";
default:
return {};
}
#undef _MTSM_KEY_CASE
}
}

View File

@@ -37,9 +37,9 @@ static constexpr std::string_view LegacyAutoMarkPromptsKey{ "experimental.autoMa
static constexpr std::string_view LegacyShowMarksKey{ "experimental.showMarksOnScrollbar" };
static constexpr std::string_view LegacyRightClickContextMenuKey{ "experimental.rightClickContextMenu" };
Profile::Profile(guid guid) noexcept :
_Guid(guid)
Profile::Profile(guid guid)
{
Guid(guid);
}
void Profile::CreateUnfocusedAppearance()
@@ -105,25 +105,23 @@ winrt::com_ptr<Profile> Profile::CopySettings() const
profile->_Deleted = _Deleted;
profile->_Orphaned = _Orphaned;
profile->_Updates = _Updates;
profile->_Guid = _Guid;
profile->_Name = _Name;
profile->_Source = _Source;
profile->_Hidden = _Hidden;
profile->_TabColor = _TabColor;
profile->_Padding = _Padding;
profile->_Origin = _Origin;
profile->_FontInfo = *fontInfo;
profile->_DefaultAppearance = *defaultAppearance;
profile->_json = _json;
#define PROFILE_SETTINGS_COPY(type, name, jsonKey, ...) \
profile->_##name = _##name;
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_COPY)
#undef PROFILE_SETTINGS_COPY
// JSON-backed settings (Name, Source, Hidden, Guid, Padding, TabColor, MTSM settings)
// all live in _json, which is already deep-copied above. No per-setting copy needed.
// Complex/mutable settings — backing fields for resolution lifecycle.
// _json (copied above) is the source of truth; backing fields hold resolved runtime state.
profile->_Icon = _Icon;
// BellSound is an IVector<>, so we need to make a new vector pointing at the same objects
if (_BellSound)
{
// BellSound is an IVector<>, so we need to make a new vector pointing at the same objects
profile->_BellSound = winrt::single_threaded_vector(wil::to_vector(*_BellSound));
}
@@ -169,6 +167,10 @@ winrt::com_ptr<winrt::Microsoft::Terminal::Settings::Model::implementation::Prof
// <none>
void Profile::LayerJson(const Json::Value& json)
{
// Merge incoming JSON keys into stored _json (key-wise, not replacement).
// This preserves keys from earlier LayerJson calls that aren't in the new JSON.
JsonUtils::MergeJsonKeys(json, _json);
// Appearance Settings
auto defaultAppearanceImpl = winrt::get_self<implementation::AppearanceConfig>(_DefaultAppearance);
defaultAppearanceImpl->LayerJson(json);
@@ -177,37 +179,53 @@ void Profile::LayerJson(const Json::Value& json)
auto fontInfoImpl = winrt::get_self<implementation::FontConfig>(_FontInfo);
fontInfoImpl->LayerJson(json);
// Profile-specific Settings
JsonUtils::GetValueForKey(json, NameKey, _Name);
// Profile-specific settings: now JSON-backed. Values are already in _json
// from the merge step above. Only need to handle non-JSON fields and logging.
JsonUtils::GetValueForKey(json, UpdatesKey, _Updates);
JsonUtils::GetValueForKey(json, GuidKey, _Guid);
// Normalize Padding: allow integer → string coercion (e.g. "padding": 8 → "8")
if (_json.isMember(JsonKey(PaddingKey)) && _json[JsonKey(PaddingKey)].isInt())
{
_json[JsonKey(PaddingKey)] = std::to_string(_json[JsonKey(PaddingKey)].asInt());
}
// Make sure Source is before Hidden! We use that to exclude false positives from the settings logger!
JsonUtils::GetValueForKey(json, SourceKey, _Source);
JsonUtils::GetValueForKey(json, HiddenKey, _Hidden);
_logSettingIfSet(HiddenKey, _Hidden.has_value());
// Padding was never specified as an integer, but it was a common working mistake.
// Allow it to be permissive.
JsonUtils::GetValueForKey(json, PaddingKey, _Padding, JsonUtils::OptionalConverter<hstring, JsonUtils::PermissiveStringConverter<std::wstring>>{});
_logSettingIfSet(PaddingKey, _Padding.has_value());
JsonUtils::GetValueForKey(json, TabColorKey, _TabColor);
_logSettingIfSet(TabColorKey, _TabColor.has_value());
_logSettingIfSet(HiddenKey, HasHidden());
_logSettingIfSet(PaddingKey, HasPadding());
_logSettingIfSet(TabColorKey, HasTabColor());
// Try to load some legacy keys, to migrate them.
// Done _before_ the MTSM_PROFILE_SETTINGS, which have the updated keys.
JsonUtils::GetValueForKey(json, LegacyShowMarksKey, _ShowMarks);
JsonUtils::GetValueForKey(json, LegacyAutoMarkPromptsKey, _AutoMarkPrompts);
JsonUtils::GetValueForKey(json, LegacyRightClickContextMenuKey, _RightClickContextMenu);
// Normalize legacy keys into canonical _json keys so JSON-backed getters find them.
// Done _before_ the MTSM_PROFILE_SETTINGS logging, which have the updated keys.
static constexpr std::pair<std::string_view, std::string_view> legacyKeyMappings[] = {
{ LegacyShowMarksKey, "showMarksOnScrollbar" },
{ LegacyAutoMarkPromptsKey, "autoMarkPrompts" },
{ LegacyRightClickContextMenuKey, "rightClickContextMenu" },
};
for (const auto& [legacyKey, canonicalKey] : legacyKeyMappings)
{
if (json.isMember(JsonKey(legacyKey)))
{
_json[JsonKey(canonicalKey)] = json[JsonKey(legacyKey)];
}
}
// MTSM settings are now JSON-backed (no backing fields).
// Values are already in _json from the merge step above.
// We only need to log which settings were set in this layer.
#define PROFILE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
_logSettingIfSet(jsonKey, json.isMember(jsonKey) && !json[jsonKey].isNull());
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_LAYER_JSON)
#undef PROFILE_SETTINGS_LAYER_JSON
// Complex/mutable settings — backing fields populated from _json for runtime resolution.
// _json is the source of truth for serialization; backing fields are for resolution lifecycle.
JsonUtils::GetValueForKey(json, "icon", _Icon);
_logSettingIfSet("icon", _Icon.has_value());
JsonUtils::GetValueForKey(json, "bellSound", _BellSound);
_logSettingIfSet("bellSound", _BellSound.has_value());
if (json.isMember(JsonKey(UnfocusedAppearanceKey)))
{
auto unfocusedAppearance{ winrt::make_self<implementation::AppearanceConfig>(weak_ref<Model::Profile>(*this)) };
@@ -332,25 +350,44 @@ Json::Value Profile::ToJson() const
// GH #9962:
// If the settings.json was missing, when we load the dynamic profiles, they are completely empty.
// This caused us to serialize empty profiles "{}" on accident.
const auto writeBasicSettings{ !Source().empty() };
const auto writeBasicSettings{ HasSource() };
// Profile-specific Settings
JsonUtils::SetValueForKey(json, NameKey, writeBasicSettings ? Name() : _Name);
JsonUtils::SetValueForKey(json, GuidKey, writeBasicSettings ? Guid() : _Guid);
JsonUtils::SetValueForKey(json, HiddenKey, writeBasicSettings ? Hidden() : _Hidden);
JsonUtils::SetValueForKey(json, SourceKey, writeBasicSettings ? Source() : _Source);
// Profile-specific Settings (JSON-backed)
if (writeBasicSettings)
{
// Dynamic profiles: write resolved values so defaults are materialized
JsonUtils::SetValueForKey(json, NameKey, Name());
JsonUtils::SetValueForKey(json, GuidKey, Guid());
JsonUtils::SetValueForKey(json, HiddenKey, Hidden());
JsonUtils::SetValueForKey(json, SourceKey, Source());
}
else
{
// User-defined profiles: copy local layer from _json
for (const auto& key : { NameKey, GuidKey, HiddenKey, SourceKey })
{
JsonUtils::CopyKeyIfPresent(_json, json, key);
}
}
// PermissiveStringConverter is unnecessary for serialization
JsonUtils::SetValueForKey(json, PaddingKey, _Padding);
// Padding: copy from _json
JsonUtils::CopyKeyIfPresent(_json, json, PaddingKey);
JsonUtils::SetValueForKey(json, TabColorKey, _TabColor);
// TabColor: nullable — key presence matters (explicit null is valid)
JsonUtils::CopyKeyIfPresent(_json, json, TabColorKey);
// MTSM profile settings: copy from _json (the source of truth)
#define PROFILE_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
JsonUtils::CopyKeyIfPresent(_json, json, jsonKey);
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_TO_JSON)
#undef PROFILE_SETTINGS_TO_JSON
// Complex/mutable settings with backing fields (runtime resolution)
// Read from _json (source of truth), not from backing fields
JsonUtils::CopyKeyIfPresent(_json, json, "icon");
JsonUtils::CopyKeyIfPresent(_json, json, "bellSound");
if (auto fontJSON = winrt::get_self<FontConfig>(_FontInfo)->ToJson(); !fontJSON.empty())
{
json[JsonKey(FontInfoKey)] = std::move(fontJSON);
@@ -364,6 +401,89 @@ Json::Value Profile::ToJson() const
return json;
}
bool Profile::HasSetting(ProfileSettingKey key) const
{
switch (key)
{
#define _PROFILE_HAS_SETTING(type, name, jsonKey, ...) \
case ProfileSettingKey::name: \
return Has##name();
MTSM_PROFILE_SETTINGS(_PROFILE_HAS_SETTING)
#undef _PROFILE_HAS_SETTING
case ProfileSettingKey::_Name:
return HasName();
case ProfileSettingKey::_Guid:
return HasGuid();
case ProfileSettingKey::_Source:
return HasSource();
case ProfileSettingKey::_Hidden:
return HasHidden();
case ProfileSettingKey::_Padding:
return HasPadding();
case ProfileSettingKey::_TabColor:
return HasTabColor();
case ProfileSettingKey::_Icon:
return HasIcon();
case ProfileSettingKey::_BellSound:
return HasBellSound();
default:
return false;
}
}
void Profile::ClearSetting(ProfileSettingKey key)
{
switch (key)
{
#define _PROFILE_CLEAR_SETTING(type, name, jsonKey, ...) \
case ProfileSettingKey::name: \
Clear##name(); \
break;
MTSM_PROFILE_SETTINGS(_PROFILE_CLEAR_SETTING)
#undef _PROFILE_CLEAR_SETTING
case ProfileSettingKey::_Name:
ClearName();
break;
case ProfileSettingKey::_Guid:
ClearGuid();
break;
case ProfileSettingKey::_Source:
ClearSource();
break;
case ProfileSettingKey::_Hidden:
ClearHidden();
break;
case ProfileSettingKey::_Padding:
ClearPadding();
break;
case ProfileSettingKey::_TabColor:
ClearTabColor();
break;
case ProfileSettingKey::_Icon:
ClearIcon();
break;
case ProfileSettingKey::_BellSound:
ClearBellSound();
break;
default:
break;
}
}
std::vector<ProfileSettingKey> Profile::CurrentSettings() const
{
std::vector<ProfileSettingKey> result;
for (auto i = 0; i < static_cast<int>(ProfileSettingKey::SETTINGS_SIZE); i++)
{
const auto key = static_cast<ProfileSettingKey>(i);
if (HasSetting(key))
{
result.push_back(key);
}
}
return result;
}
// Given a commandLine like the following:
// * "C:\WINDOWS\System32\cmd.exe"
// * "pwsh -WorkingDirectory ~"
@@ -502,13 +622,13 @@ void Profile::_logSettingIfSet(const std::string_view& setting, const bool isSet
// Exclude some false positives from userDefaults.json
// NOTE: we can't use the OriginTag here because it hasn't been set yet!
const bool isWinPow = _Guid.has_value() && *_Guid == DEFAULT_WINDOWS_POWERSHELL_GUID; //_Name.has_value() && til::equals_insensitive_ascii(*_Name, L"Windows PowerShell");
const bool isCmd = _Guid.has_value() && *_Guid == DEFAULT_COMMAND_PROMPT_GUID; //_Name.has_value() && til::equals_insensitive_ascii(*_Name, L"Command Prompt");
const bool isACS = _Name.has_value() && til::equals_insensitive_ascii(*_Name, L"Azure Cloud Shell");
const bool isWTDynamicProfile = _Source.has_value() && til::starts_with(*_Source, L"Windows.Terminal");
const bool settingHiddenToFalse = til::equals_insensitive_ascii(setting, HiddenKey) && _Hidden.has_value() && _Hidden == false;
const bool settingCommandlineToWinPow = til::equals_insensitive_ascii(setting, "commandline") && _Commandline.has_value() && til::equals_insensitive_ascii(*_Commandline, L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
const bool settingCommandlineToCmd = til::equals_insensitive_ascii(setting, "commandline") && _Commandline.has_value() && til::equals_insensitive_ascii(*_Commandline, L"%SystemRoot%\\System32\\cmd.exe");
const bool isWinPow = HasGuid() && Guid() == DEFAULT_WINDOWS_POWERSHELL_GUID;
const bool isCmd = HasGuid() && Guid() == DEFAULT_COMMAND_PROMPT_GUID;
const bool isACS = HasName() && til::equals_insensitive_ascii(Name(), L"Azure Cloud Shell");
const bool isWTDynamicProfile = HasSource() && til::starts_with(Source(), L"Windows.Terminal");
const bool settingHiddenToFalse = til::equals_insensitive_ascii(setting, HiddenKey) && HasHidden() && Hidden() == false;
const bool settingCommandlineToWinPow = til::equals_insensitive_ascii(setting, "commandline") && HasCommandline() && til::equals_insensitive_ascii(Commandline(), L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
const bool settingCommandlineToCmd = til::equals_insensitive_ascii(setting, "commandline") && HasCommandline() && til::equals_insensitive_ascii(Commandline(), L"%SystemRoot%\\System32\\cmd.exe");
// clang-format off
if (!(isWinPow && (settingHiddenToFalse || settingCommandlineToWinPow))
&& !(isCmd && (settingHiddenToFalse || settingCommandlineToCmd))
@@ -541,7 +661,7 @@ void Profile::ResolveMediaResources(const Model::MediaResourceResolver& resolver
auto newIcon{ MediaResource::FromString(icon->Path()) };
const std::wstring cmdline{ NormalizeCommandLine(iconSource->Commandline().c_str()) };
newIcon.Resolve(cmdline.c_str() /* c_str: give hstring a chance to find the null terminator */);
iconSource->_Icon = std::move(newIcon);
iconSource->Icon(std::move(newIcon));
}
}

View File

@@ -49,6 +49,7 @@ Author(s):
#include "MTSMSettings.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include <DefaultSettings.h>
#include "MediaResourceSupport.h"
#include "AppearanceConfig.h"
@@ -80,7 +81,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
public:
Profile() noexcept = default;
Profile(guid guid) noexcept;
// Not noexcept: Guid(guid) writes to _json, which allocates.
// The old backing-field constructor was noexcept (POD copy into std::optional),
// but JSON-backed storage requires heap allocation.
Profile(guid guid);
void CreateUnfocusedAppearance();
void DeleteUnfocusedAppearance();
@@ -98,6 +102,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void LayerJson(const Json::Value& json);
Json::Value ToJson() const;
// Generic setting access via SettingKey
bool HasSetting(ProfileSettingKey key) const;
void ClearSetting(ProfileSettingKey key);
std::vector<ProfileSettingKey> CurrentSettings() const;
hstring EvaluatedStartingDirectory() const;
Model::IAppearanceConfig DefaultAppearance();
@@ -122,16 +131,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
WINRT_PROPERTY(OriginTag, Origin, OriginTag::None);
WINRT_PROPERTY(guid, Updates);
// Nullable/optional settings
INHERITABLE_NULLABLE_SETTING(Model::Profile, Microsoft::Terminal::Core::Color, TabColor, nullptr);
INHERITABLE_SETTING(Model::Profile, Model::IAppearanceConfig, UnfocusedAppearance, nullptr);
// Nullable/optional settings (JSON-backed)
INHERITABLE_NULLABLE_SETTING(Model::Profile, Microsoft::Terminal::Core::Color, TabColor, "tabColor", nullptr)
INHERITABLE_MUTABLE_SETTING(Model::Profile, Model::IAppearanceConfig, UnfocusedAppearance, nullptr);
// Settings that cannot be put in the macro because of how they are handled in ToJson/LayerJson
INHERITABLE_SETTING(Model::Profile, hstring, Name, L"Default");
INHERITABLE_SETTING(Model::Profile, hstring, Source);
INHERITABLE_SETTING(Model::Profile, bool, Hidden, false);
INHERITABLE_SETTING(Model::Profile, guid, Guid, _GenerateGuidForProfile(Name(), Source()));
INHERITABLE_SETTING(Model::Profile, hstring, Padding, DEFAULT_PADDING);
// Settings that are JSON-backed but need custom handling in ToJson/LayerJson
INHERITABLE_SETTING(Model::Profile, hstring, Name, "name", L"Default")
INHERITABLE_SETTING(Model::Profile, hstring, Source, "source")
INHERITABLE_SETTING(Model::Profile, bool, Hidden, "hidden", false)
INHERITABLE_SETTING(Model::Profile, hstring, Padding, "padding", DEFAULT_PADDING)
// Guid: hand-written JSON-backed (dynamic default)
_BASE_INHERITABLE_SETTING(Model::Profile, guid, Guid, "guid")
public:
guid Guid() const
{
const auto val{ _getGuidImpl() };
return val ? *val : _GenerateGuidForProfile(Name(), Source());
}
void Guid(const guid& value)
{
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(
_json, "guid", value);
}
winrt::hstring SourceBasePath;
@@ -141,10 +163,33 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_INITIALIZE)
#undef PROFILE_SETTINGS_INITIALIZE
// IMediaResource settings with backing fields for resolution lifecycle.
// _json is the source of truth for serialization. Setters dual-write to both
// the backing field (for runtime resolution) and _json (for auto-save).
INHERITABLE_MEDIA_RESOURCE_SETTING(Model::Profile, Icon, "icon", implementation::MediaResource::FromString(L"\uE756"))
// BellSound: IVector<IMediaResource> with backing field + dual-write setter.
_BASE_INHERITABLE_MUTABLE_SETTING(Model::Profile, std::optional<Windows::Foundation::Collections::IVector<IMediaResource>>, BellSound, nullptr)
public:
Windows::Foundation::Collections::IVector<IMediaResource> BellSound() const
{
const auto val{ _getBellSoundImpl() };
return val ? *val : nullptr;
}
void BellSound(const Windows::Foundation::Collections::IVector<IMediaResource>& value)
{
_BellSound = value;
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(_json, "bellSound", value);
}
private:
Model::IAppearanceConfig _DefaultAppearance{ winrt::make<AppearanceConfig>(weak_ref<Model::Profile>(*this)) };
Model::FontConfig _FontInfo{ winrt::make<FontConfig>(weak_ref<Model::Profile>(*this)) };
// Raw JSON for this layer. Populated by LayerJson(), will become the
// source of truth for settings once the JSON-backed refactor is complete.
Json::Value _json{ Json::ValueType::objectValue };
std::set<std::string> _changeLog;
static std::wstring EvaluateStartingDirectory(const std::wstring& directory);

View File

@@ -31,6 +31,14 @@ namespace SettingsModelUnitTests
TEST_METHOD(TestGenGuidsForProfiles);
TEST_METHOD(TestCorrectOldDefaultShellPaths);
TEST_METHOD(ProfileDefaultsProhibitedSettings);
TEST_METHOD(JsonSyncOnSetAndClear);
TEST_METHOD(SettingKeyEnumAndJsonKeyLookup);
TEST_METHOD(GenericHasAndClearMatchTypedAPIs);
TEST_METHOD(CurrentSettingsReturnsCorrectKeys);
TEST_METHOD(SpecialCasedSettingsJsonBacked);
TEST_METHOD(NullableSettingJsonBacked);
TEST_METHOD(ColorSchemeJsonBacked);
};
void ProfileTests::ProfileGeneratesGuid()
@@ -532,4 +540,424 @@ namespace SettingsModelUnitTests
VERIFY_ARE_NOT_EQUAL(L"Default Profile Source", allProfiles.GetAt(2).Source());
VERIFY_ARE_NOT_EQUAL(L"foo.exe", allProfiles.GetAt(2).Commandline());
}
void ProfileTests::JsonSyncOnSetAndClear()
{
// Verify that setting a value via the typed setter updates the internal
// _json, and clearing it removes the key from _json.
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
// Verify initial value
VERIFY_ARE_EQUAL(1000, profile.HistorySize());
VERIFY_IS_TRUE(profile.HasHistorySize());
// Modify setting; _json should be updated
profile.HistorySize(5000);
VERIFY_ARE_EQUAL(5000, profile.HistorySize());
// Verify ToJson reflects the change
const auto json = profileImpl->ToJson();
VERIFY_ARE_EQUAL(5000, json["historySize"].asInt());
// Clear setting; should fall back to default
profile.ClearHistorySize();
VERIFY_IS_FALSE(profile.HasHistorySize());
// Should now inherit or use default (9001 is the DEFAULT_HISTORY_SIZE)
VERIFY_ARE_EQUAL(DEFAULT_HISTORY_SIZE, profile.HistorySize());
// ToJson should no longer have historySize
const auto json2 = profileImpl->ToJson();
VERIFY_IS_FALSE(json2.isMember("historySize"));
}
void ProfileTests::SettingKeyEnumAndJsonKeyLookup()
{
// Verify that the SettingKey enums map to correct JSON keys.
using namespace implementation;
// Spot-check a few profile setting keys
VERIFY_ARE_EQUAL(std::string_view{ "historySize" }, JsonKeyForSetting(ProfileSettingKey::HistorySize));
VERIFY_ARE_EQUAL(std::string_view{ "snapOnInput" }, JsonKeyForSetting(ProfileSettingKey::SnapOnInput));
VERIFY_ARE_EQUAL(std::string_view{ "commandline" }, JsonKeyForSetting(ProfileSettingKey::Commandline));
VERIFY_ARE_EQUAL(std::string_view{ "tabTitle" }, JsonKeyForSetting(ProfileSettingKey::TabTitle));
// Special-cased settings
VERIFY_ARE_EQUAL(std::string_view{ "name" }, JsonKeyForSetting(ProfileSettingKey::_Name));
VERIFY_ARE_EQUAL(std::string_view{ "guid" }, JsonKeyForSetting(ProfileSettingKey::_Guid));
VERIFY_ARE_EQUAL(std::string_view{ "hidden" }, JsonKeyForSetting(ProfileSettingKey::_Hidden));
VERIFY_ARE_EQUAL(std::string_view{ "padding" }, JsonKeyForSetting(ProfileSettingKey::_Padding));
VERIFY_ARE_EQUAL(std::string_view{ "tabColor" }, JsonKeyForSetting(ProfileSettingKey::_TabColor));
// Global setting keys
VERIFY_ARE_EQUAL(std::string_view{ "initialRows" }, JsonKeyForSetting(GlobalSettingKey::InitialRows));
VERIFY_ARE_EQUAL(std::string_view{ "alwaysOnTop" }, JsonKeyForSetting(GlobalSettingKey::AlwaysOnTop));
// Font setting keys
VERIFY_ARE_EQUAL(std::string_view{ "face" }, JsonKeyForSetting(FontSettingKey::FontFace));
VERIFY_ARE_EQUAL(std::string_view{ "size" }, JsonKeyForSetting(FontSettingKey::FontSize));
// SETTINGS_SIZE should be a valid (but large) enum value
VERIFY_IS_TRUE(static_cast<int>(ProfileSettingKey::SETTINGS_SIZE) > 0);
VERIFY_IS_TRUE(static_cast<int>(GlobalSettingKey::SETTINGS_SIZE) > 0);
VERIFY_IS_TRUE(static_cast<int>(FontSettingKey::SETTINGS_SIZE) > 0);
VERIFY_IS_TRUE(static_cast<int>(AppearanceSettingKey::SETTINGS_SIZE) > 0);
}
void ProfileTests::GenericHasAndClearMatchTypedAPIs()
{
// Verify that HasSetting(key) and ClearSetting(key) match the
// typed HasXxx() and ClearXxx() methods.
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000,
"snapOnInput": false
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
// HasSetting should match HasXxx for set values
VERIFY_ARE_EQUAL(profile.HasHistorySize(), profileImpl->HasSetting(implementation::ProfileSettingKey::HistorySize));
VERIFY_ARE_EQUAL(profile.HasSnapOnInput(), profileImpl->HasSetting(implementation::ProfileSettingKey::SnapOnInput));
// HasSetting should match HasXxx for unset values
VERIFY_ARE_EQUAL(profile.HasTabTitle(), profileImpl->HasSetting(implementation::ProfileSettingKey::TabTitle));
VERIFY_IS_FALSE(profileImpl->HasSetting(implementation::ProfileSettingKey::TabTitle));
// ClearSetting should behave like ClearXxx
profileImpl->ClearSetting(implementation::ProfileSettingKey::HistorySize);
VERIFY_IS_FALSE(profile.HasHistorySize());
VERIFY_IS_FALSE(profileImpl->HasSetting(implementation::ProfileSettingKey::HistorySize));
}
void ProfileTests::CurrentSettingsReturnsCorrectKeys()
{
// Verify that CurrentSettings() returns the keys that are explicitly
// set at the current layer.
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000,
"snapOnInput": false,
"tabTitle": "MyTab"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto currentKeys = profileImpl->CurrentSettings();
// historySize, snapOnInput, and tabTitle should be in the list
auto hasHistorySize = false;
auto hasSnapOnInput = false;
auto hasTabTitle = false;
for (const auto& key : currentKeys)
{
if (key == implementation::ProfileSettingKey::HistorySize)
{
hasHistorySize = true;
}
if (key == implementation::ProfileSettingKey::SnapOnInput)
{
hasSnapOnInput = true;
}
if (key == implementation::ProfileSettingKey::TabTitle)
{
hasTabTitle = true;
}
}
VERIFY_IS_TRUE(hasHistorySize, L"historySize should be in CurrentSettings");
VERIFY_IS_TRUE(hasSnapOnInput, L"snapOnInput should be in CurrentSettings");
VERIFY_IS_TRUE(hasTabTitle, L"tabTitle should be in CurrentSettings");
// Clear one and verify it's removed
profileImpl->ClearSetting(implementation::ProfileSettingKey::TabTitle);
const auto updatedKeys = profileImpl->CurrentSettings();
auto hasTabTitleAfterClear = false;
for (const auto& key : updatedKeys)
{
if (key == implementation::ProfileSettingKey::TabTitle)
{
hasTabTitleAfterClear = true;
}
}
VERIFY_IS_FALSE(hasTabTitleAfterClear, L"tabTitle should NOT be in CurrentSettings after clear");
}
void ProfileTests::SpecialCasedSettingsJsonBacked()
{
// Verify that special-cased settings (Name, Guid, Hidden, Padding)
// are now JSON-backed: setters write to _json, Has/Clear work correctly.
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "TestProfile",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"hidden": true,
"padding": "12"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
// Verify initial values loaded from JSON
VERIFY_ARE_EQUAL(L"TestProfile", profile.Name());
VERIFY_IS_TRUE(profile.HasName());
VERIFY_IS_TRUE(profile.Hidden());
VERIFY_IS_TRUE(profile.HasHidden());
VERIFY_ARE_EQUAL(L"12", profile.Padding());
VERIFY_IS_TRUE(profile.HasPadding());
// Modify Name via setter — should update _json
profile.Name(L"NewName");
VERIFY_ARE_EQUAL(L"NewName", profile.Name());
const auto json1 = profileImpl->ToJson();
VERIFY_ARE_EQUAL(L"NewName", til::u8u16(json1["name"].asString()));
// Clear Name — should fall back to default
profile.ClearName();
VERIFY_IS_FALSE(profile.HasName());
VERIFY_ARE_EQUAL(L"Default", profile.Name());
// Modify Hidden via setter
profile.Hidden(false);
VERIFY_ARE_EQUAL(false, profile.Hidden());
VERIFY_IS_TRUE(profile.HasHidden());
// Clear Hidden — should fall back to default (false)
profile.ClearHidden();
VERIFY_IS_FALSE(profile.HasHidden());
VERIFY_ARE_EQUAL(false, profile.Hidden());
// Modify Padding — should write to _json
profile.Padding(L"24");
VERIFY_ARE_EQUAL(L"24", profile.Padding());
// Test Guid setter
static constexpr winrt::guid testGuid{ 0x11111111, 0x2222, 0x3333, { 0x44, 0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77 } };
profile.Guid(testGuid);
VERIFY_ARE_EQUAL(testGuid, profile.Guid());
}
void ProfileTests::NullableSettingJsonBacked()
{
// Verify that nullable settings (TabColor) are JSON-backed.
// Null is a valid explicit value meaning "no color".
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"tabColor": "#FF0000"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
// Verify initial value
VERIFY_IS_TRUE(profile.HasTabColor());
VERIFY_IS_NOT_NULL(profile.TabColor());
// Set to null explicitly (means "no color")
profile.TabColor(nullptr);
VERIFY_IS_TRUE(profile.HasTabColor()); // still "set" — explicitly null
VERIFY_IS_NULL(profile.TabColor());
// Clear — should inherit from parent (which has no tabColor)
profile.ClearTabColor();
VERIFY_IS_FALSE(profile.HasTabColor());
// ToJson should not have tabColor after clearing
const auto json = profileImpl->ToJson();
VERIFY_IS_FALSE(json.isMember("tabColor"));
}
void ProfileTests::ColorSchemeJsonBacked()
{
// Verify that DarkColorSchemeName/LightColorSchemeName are JSON-backed,
// including round-trip through LayerJson/ToJson, parent inheritance,
// and the polymorphic colorScheme key (string vs object forms).
// Case 1: string form input — sets both dark and light to the same scheme
{
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"colorScheme": "One Half Dark"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto appearance = profile.DefaultAppearance();
const auto appearanceImpl = winrt::get_self<implementation::AppearanceConfig>(appearance);
// Both dark and light should be set to the same value
VERIFY_ARE_EQUAL(L"One Half Dark", appearance.DarkColorSchemeName());
VERIFY_ARE_EQUAL(L"One Half Dark", appearance.LightColorSchemeName());
VERIFY_IS_TRUE(appearanceImpl->HasDarkColorSchemeName());
VERIFY_IS_TRUE(appearanceImpl->HasLightColorSchemeName());
// ToJson should collapse to a string since dark == light
const auto json = profileImpl->ToJson();
VERIFY_IS_TRUE(json.isMember("colorScheme"));
VERIFY_IS_TRUE(json["colorScheme"].isString());
VERIFY_ARE_EQUAL("One Half Dark", json["colorScheme"].asString());
}
// Case 2: object form input — dark and light are different
{
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"colorScheme": {
"dark": "One Half Dark",
"light": "One Half Light"
}
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto appearance = profile.DefaultAppearance();
const auto appearanceImpl = winrt::get_self<implementation::AppearanceConfig>(appearance);
// Each should return its own value
VERIFY_ARE_EQUAL(L"One Half Dark", appearance.DarkColorSchemeName());
VERIFY_ARE_EQUAL(L"One Half Light", appearance.LightColorSchemeName());
// ToJson should produce an object since dark != light
const auto json = profileImpl->ToJson();
VERIFY_IS_TRUE(json.isMember("colorScheme"));
VERIFY_IS_TRUE(json["colorScheme"].isObject());
VERIFY_ARE_EQUAL("One Half Dark", json["colorScheme"]["dark"].asString());
VERIFY_ARE_EQUAL("One Half Light", json["colorScheme"]["light"].asString());
}
// Case 3: setter updates — setting dark should preserve light, and vice versa
{
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"colorScheme": "Campbell"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto appearance = profile.DefaultAppearance();
const auto appearanceImpl = winrt::get_self<implementation::AppearanceConfig>(appearance);
// Change dark only — light should stay "Campbell"
appearance.DarkColorSchemeName(L"Tango Dark");
VERIFY_ARE_EQUAL(L"Tango Dark", appearance.DarkColorSchemeName());
VERIFY_ARE_EQUAL(L"Campbell", appearance.LightColorSchemeName());
// ToJson should produce an object since dark != light
const auto json1 = profileImpl->ToJson();
VERIFY_IS_TRUE(json1["colorScheme"].isObject());
VERIFY_ARE_EQUAL("Tango Dark", json1["colorScheme"]["dark"].asString());
VERIFY_ARE_EQUAL("Campbell", json1["colorScheme"]["light"].asString());
// Change light to match dark — should collapse to string on ToJson
appearance.LightColorSchemeName(L"Tango Dark");
VERIFY_ARE_EQUAL(L"Tango Dark", appearance.LightColorSchemeName());
const auto json2 = profileImpl->ToJson();
VERIFY_IS_TRUE(json2["colorScheme"].isString());
VERIFY_ARE_EQUAL("Tango Dark", json2["colorScheme"].asString());
}
// Case 4: clear — removes colorScheme entirely, falls back to default "Campbell"
{
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"colorScheme": "One Half Dark"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto appearance = profile.DefaultAppearance();
const auto appearanceImpl = winrt::get_self<implementation::AppearanceConfig>(appearance);
VERIFY_IS_TRUE(appearanceImpl->HasDarkColorSchemeName());
// Clear removes colorScheme from this layer
appearanceImpl->ClearDarkColorSchemeName();
VERIFY_IS_FALSE(appearanceImpl->HasDarkColorSchemeName());
VERIFY_IS_FALSE(appearanceImpl->HasLightColorSchemeName());
// Falls back to default "Campbell"
VERIFY_ARE_EQUAL(L"Campbell", appearance.DarkColorSchemeName());
VERIFY_ARE_EQUAL(L"Campbell", appearance.LightColorSchemeName());
// ToJson should not have colorScheme
const auto json = profileImpl->ToJson();
VERIFY_IS_FALSE(json.isMember("colorScheme"));
}
}
}