Track and log changes to settings (#17678)

Adds functionality throughout the settings model to keep track of which
settings have been set.

There are two entry points:
- AppLogic.cpp: this is where we perform a settings reload by loading
the JSON
- MainPage.cpp: this is where the Save button is clicked in the settings
UI

Both of these entry points call into
`CascadiaSettings::LogSettingChanges()` where we aggregate the list of
changes (specifically, _which_ settings changed, not _what_ their value
is).

Just about all of the settings model objects now have a
`LogSettingChanges(std::set& changes, std::string_view context)` on
them.
- `changes` is where we aggregate all of the changes to. In it being a
set, we don't need to worry about duplicates and can do things like
iterate across all of the profiles.
- `context` prepends a string to the setting. This'll allow us to better
identify where a setting was changes (i.e. "global.X" are global
settings). We also use this to distinguish between settings set in the
~base layer~ profile defaults vs individual profiles.

The change log in each object is modified via two ways:
- `LayerJson()` changes: this is useful for detecting JSON changes! All
we're doing is checking if the setting has a value (due to inheritance,
just about everything is an optional here!). If the value is set, we add
the json key to the change log
- `INHERITABLE_SETTING_WITH_LOGGING` in IInheritable.h: we already use
this macro to define getters and setters. This new macro updates the
setter to check if the value was set to something different. If so, log
it!

 Other notes:
- We're not distinguishing between `defaultAppearance` and
`unfocusedAppearance`
- We are distinguishing between `profileDefaults` and `profile` (any
other profile)
- New Tab Menu Customization:
- we really just care about the entry types. Handled in
`GlobalAppSettings`
- Font:
- We still have support for legacy values here. We still want to track
them, but just use the modern keys.
- `Theme`:
- We don't do inheritance here, so we have to approach it differently.
During the JSON load, we log each setting. However, we don't have
`LayerJson`! So instead, do the work in `CascadiaSettings` and store the
changes there. Note that we don't track any changes made via setters.
This is fine for now since themes aren't even in the settings UI, so we
wouldn't get much use out of it anyways.
- Actions:
- Actions are weird because we can have nested and iterable actions too,
but `ActionsAndArgs` as a whole add a ton of functionality. I handled it
over in `Command::LogSettingChanges` and we generally just serialize it
to JSON to get the keys. It's a lot easier than dealing with the object
model.

Epic: #10000
Auto-Save (ish): #12424
This commit is contained in:
Carlos Zamora 2024-08-23 12:28:19 -07:00 committed by GitHub
parent cd8c12586b
commit 0a9cbd09d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 531 additions and 10 deletions

View File

@ -27,6 +27,7 @@ gje
godbolt
hyperlinking
hyperlinks
Kbds
kje
libfuzzer
liga
@ -43,6 +44,7 @@ mkmk
mnt
mru
nje
NTMTo
notwrapped
ogonek
overlined

View File

@ -432,6 +432,10 @@ namespace winrt::TerminalApp::implementation
return;
}
}
else
{
_settings.LogSettingChanges(true);
}
if (initialLoad)
{

View File

@ -481,6 +481,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void MainPage::SaveButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/)
{
_settingsClone.LogSettingChanges(false);
_settingsClone.WriteSettingsToDisk();
}

View File

@ -570,6 +570,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
const auto action = cmd.ActionAndArgs().Action();
const auto id = action == ShortcutAction::Invalid ? hstring{} : cmd.ID();
_KeyMap.insert_or_assign(keys, id);
_changeLog.emplace(KeysKey);
}
// Method Description:

View File

@ -73,6 +73,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value ToJson() const;
Json::Value KeyBindingsToJson() const;
bool FixupsAppliedDuringLoad() const;
void LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const;
// modification
bool RebindKeys(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys);
@ -138,6 +139,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
til::shared_mutex<std::unordered_map<hstring, std::vector<Model::Command>>> _cwdLocalSnippetsCache{};
std::set<std::string> _changeLog;
friend class SettingsModelUnitTests::KeyBindingsTests;
friend class SettingsModelUnitTests::DeserializationTests;
friend class SettingsModelUnitTests::TerminalSettingsTests;

View File

@ -78,7 +78,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// Now check if this is a command block
if (jsonBlock.isMember(JsonKey(CommandsKey)) || jsonBlock.isMember(JsonKey(ActionKey)))
{
AddAction(*Command::FromJson(jsonBlock, warnings, origin), keys);
auto command = Command::FromJson(jsonBlock, warnings, origin);
command->LogSettingChanges(_changeLog);
AddAction(*command, keys);
if (jsonBlock.isMember(JsonKey(KeysKey)))
{
@ -105,6 +107,28 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// any existing keybinding with the same keychord in this layer will get overwritten
_KeyMap.insert_or_assign(keys, idJson);
if (!_changeLog.contains(KeysKey.data()))
{
// Log "keys" field, but only if it's one that isn't in userDefaults.json
static constexpr std::array<std::pair<std::wstring_view, std::string_view>, 3> userDefaultKbds{ { { L"Terminal.CopyToClipboard", "ctrl+c" },
{ L"Terminal.PasteFromClipboard", "ctrl+v" },
{ L"Terminal.DuplicatePaneAuto", "alt+shift+d" } } };
bool isUserDefaultKbd = false;
for (const auto& [id, kbd] : userDefaultKbds)
{
const auto keyJson{ jsonBlock.find(&*KeysKey.cbegin(), (&*KeysKey.cbegin()) + KeysKey.size()) };
if (idJson == id && keyJson->asString() == kbd)
{
isUserDefaultKbd = true;
break;
}
}
if (!isUserDefaultKbd)
{
_changeLog.emplace(KeysKey);
}
}
}
}
@ -156,4 +180,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return keybindingsList;
}
void ActionMap::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const
{
for (const auto& setting : _changeLog)
{
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), context, setting));
}
}
}

View File

@ -90,27 +90,42 @@ Json::Value AppearanceConfig::ToJson() const
void AppearanceConfig::LayerJson(const Json::Value& json)
{
JsonUtils::GetValueForKey(json, ForegroundKey, _Foreground);
_logSettingIfSet(ForegroundKey, _Foreground.has_value());
JsonUtils::GetValueForKey(json, BackgroundKey, _Background);
_logSettingIfSet(BackgroundKey, _Background.has_value());
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())
{
// to make the UI happy, set ColorSchemeName.
JsonUtils::GetValueForKey(json, ColorSchemeKey, _DarkColorSchemeName);
_LightColorSchemeName = _DarkColorSchemeName;
_logSettingSet(ColorSchemeKey);
}
else if (json["colorScheme"].isObject())
{
// 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");
}
#define APPEARANCE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name);
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_LAYER_JSON)
#undef APPEARANCE_SETTINGS_LAYER_JSON
}
@ -156,3 +171,24 @@ winrt::hstring AppearanceConfig::ExpandedBackgroundImagePath()
return winrt::hstring{ wil::ExpandEnvironmentStringsW<std::wstring>(path.c_str()) };
}
}
void AppearanceConfig::_logSettingSet(const std::string_view& setting)
{
_changeLog.emplace(setting);
}
void AppearanceConfig::_logSettingIfSet(const std::string_view& setting, const bool isSet)
{
if (isSet)
{
_logSettingSet(setting);
}
}
void AppearanceConfig::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const
{
for (const auto& setting : _changeLog)
{
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), context, setting));
}
}

View File

@ -31,6 +31,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static winrt::com_ptr<AppearanceConfig> CopyAppearance(const AppearanceConfig* source, winrt::weak_ref<Profile> sourceProfile);
Json::Value ToJson() const;
void LayerJson(const Json::Value& json);
void LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const;
Model::Profile SourceProfile();
@ -52,5 +53,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
private:
winrt::weak_ref<Profile> _sourceProfile;
std::set<std::string> _changeLog;
void _logSettingSet(const std::string_view& setting);
void _logSettingIfSet(const std::string_view& setting, const bool isSet);
};
}

View File

@ -48,6 +48,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
std::unordered_map<winrt::hstring, winrt::com_ptr<implementation::ColorScheme>> colorSchemes;
std::unordered_map<winrt::hstring, winrt::hstring> colorSchemeRemappings;
bool fixupsAppliedDuringLoad{ false };
std::set<std::string> themesChangeLog;
void clear();
};
@ -96,6 +97,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _executeGenerator(const IDynamicProfileGenerator& generator);
std::unordered_set<std::wstring_view> _ignoredNamespaces;
std::set<std::string> themesChangeLog;
// See _getNonUserOriginProfiles().
size_t _userProfileCount = 0;
};
@ -150,6 +152,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void ExpandCommands();
void LogSettingChanges(bool isJsonLoad) const;
private:
static const std::filesystem::path& _settingsPath();
static const std::filesystem::path& _releaseSettingsPath();
@ -180,6 +184,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::com_ptr<implementation::Profile> _baseLayerProfile = winrt::make_self<implementation::Profile>();
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> _allProfiles = winrt::single_threaded_observable_vector<Model::Profile>();
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> _activeProfiles = winrt::single_threaded_observable_vector<Model::Profile>();
std::set<std::string> _themesChangeLog{};
// load errors
winrt::Windows::Foundation::Collections::IVector<Model::SettingsLoadWarnings> _warnings = winrt::single_threaded_vector<Model::SettingsLoadWarnings>();

View File

@ -24,6 +24,7 @@ namespace Microsoft.Terminal.Settings.Model
CascadiaSettings Copy();
void WriteSettingsToDisk();
void LogSettingChanges(Boolean isJsonLoad);
String Hash { get; };

View File

@ -109,6 +109,7 @@ void ParsedSettings::clear()
profilesByGuid.clear();
colorSchemes.clear();
fixupsAppliedDuringLoad = false;
themesChangeLog.clear();
}
// This is a convenience method used by the CascadiaSettings constructor.
@ -658,6 +659,12 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source
// versions of these themes overriding the built-in ones.
continue;
}
if (origin != OriginTag::InBox)
{
static std::string themesContext{ "themes" };
theme->LogSettingChanges(settings.themesChangeLog, themesContext);
}
settings.globals->AddTheme(*theme);
}
}
@ -1222,6 +1229,7 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) :
_allProfiles = winrt::single_threaded_observable_vector(std::move(allProfiles));
_activeProfiles = winrt::single_threaded_observable_vector(std::move(activeProfiles));
_warnings = winrt::single_threaded_vector(std::move(warnings));
_themesChangeLog = std::move(loader.userSettings.themesChangeLog);
_resolveDefaultProfile();
_resolveNewTabMenuProfiles();
@ -1596,3 +1604,73 @@ void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector<Model::NewTab
}
}
}
void CascadiaSettings::LogSettingChanges(bool isJsonLoad) const
{
#ifndef _DEBUG
// Only do this if we're actually being sampled
if (!TraceLoggingProviderEnabled(g_hSettingsModelProvider, 0, MICROSOFT_KEYWORD_MEASURES))
{
return;
}
#endif // !_DEBUG
// aggregate setting changes
std::set<std::string> changes;
static constexpr std::string_view globalContext{ "global" };
_globals->LogSettingChanges(changes, globalContext);
// Actions are not expected to change when loaded from the settings UI
static constexpr std::string_view actionContext{ "action" };
winrt::get_self<implementation::ActionMap>(_globals->ActionMap())->LogSettingChanges(changes, actionContext);
static constexpr std::string_view profileContext{ "profile" };
for (const auto& profile : _allProfiles)
{
winrt::get_self<Profile>(profile)->LogSettingChanges(changes, profileContext);
}
static constexpr std::string_view profileDefaultsContext{ "profileDefaults" };
_baseLayerProfile->LogSettingChanges(changes, profileDefaultsContext);
// Themes are not expected to change when loaded from the settings UI
// DO NOT CALL Theme::LogSettingChanges!!
// We already collected the changes when we loaded the JSON
for (const auto& change : _themesChangeLog)
{
changes.insert(change);
}
// report changes
for (const auto& change : changes)
{
#ifndef _DEBUG
// A `isJsonLoad ? "JsonSettingsChanged" : "UISettingsChanged"`
// would be nice, but that apparently isn't allowed in the macro below.
// Also, there's guidance to not send too much data all in one event,
// so we'll be sending a ton of events here.
if (isJsonLoad)
{
TraceLoggingWrite(g_hSettingsModelProvider,
"JsonSettingsChanged",
TraceLoggingDescription("Event emitted when settings.json change"),
TraceLoggingValue(change.data()),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
else
{
TraceLoggingWrite(g_hSettingsModelProvider,
"UISettingsChanged",
TraceLoggingDescription("Event emitted when settings change via the UI"),
TraceLoggingValue(change.data()),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
#else
OutputDebugStringA(isJsonLoad ? "JsonSettingsChanged - " : "UISettingsChanged - ");
OutputDebugStringA(change.data());
OutputDebugStringA("\n");
#endif // !_DEBUG
}
}

View File

@ -741,4 +741,57 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return winrt::single_threaded_vector<Model::Command>(std::move(result));
}
void Command::LogSettingChanges(std::set<std::string>& changes)
{
if (_IterateOn != ExpandCommandType::None)
{
switch (_IterateOn)
{
case ExpandCommandType::Profiles:
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), IterateOnKey, "profiles"));
break;
case ExpandCommandType::ColorSchemes:
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), IterateOnKey, "schemes"));
break;
}
}
if (!_Description.empty())
{
changes.emplace(DescriptionKey);
}
if (IsNestedCommand())
{
changes.emplace(CommandsKey);
}
else
{
const auto json{ ActionAndArgs::ToJson(ActionAndArgs()) };
if (json.isString())
{
// covers actions w/out args
// - "command": "unbound" --> "unbound"
// - "command": "copy" --> "copy"
changes.emplace(fmt::format(FMT_COMPILE("{}"), json.asString()));
}
else
{
// covers actions w/ args
// - "command": { "action": "copy", "singleLine": true } --> "copy.singleLine"
// - "command": { "action": "copy", "singleLine": true, "dismissSelection": true } --> "copy.singleLine", "copy.dismissSelection"
const std::string shortcutActionName{ json[JsonKey("action")].asString() };
auto members = json.getMemberNames();
members.erase(std::remove_if(members.begin(), members.end(), [](const auto& member) { return member == "action"; }), members.end());
for (const auto& actionArg : members)
{
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), shortcutActionName, actionArg));
}
}
}
}
}

View File

@ -61,6 +61,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
const Json::Value& json,
const OriginTag origin);
Json::Value ToJson() const;
void LogSettingChanges(std::set<std::string>& changes);
bool HasNestedCommands() const;
bool IsNestedCommand() const noexcept;

View File

@ -83,17 +83,25 @@ 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);
#define FONT_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(fontInfoJson, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
MTSM_FONT_SETTINGS(FONT_SETTINGS_LAYER_JSON)
#undef FONT_SETTINGS_LAYER_JSON
}
else
{
// No font object is defined
// 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());
}
}
@ -101,3 +109,41 @@ winrt::Microsoft::Terminal::Settings::Model::Profile FontConfig::SourceProfile()
{
return _sourceProfile.get();
}
void FontConfig::_logSettingSet(const std::string_view& setting)
{
if (setting == "axes" && _FontAxes.has_value())
{
for (const auto& [mapKey, _] : _FontAxes.value())
{
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, til::u16u8(mapKey)));
}
}
else if (setting == "features" && _FontFeatures.has_value())
{
for (const auto& [mapKey, _] : _FontFeatures.value())
{
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, til::u16u8(mapKey)));
}
}
else
{
_changeLog.emplace(setting);
}
}
void FontConfig::_logSettingIfSet(const std::string_view& setting, const bool isSet)
{
if (isSet)
{
_logSettingSet(setting);
}
}
void FontConfig::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const
{
for (const auto& setting : _changeLog)
{
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), context, setting));
}
}

View File

@ -35,6 +35,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static winrt::com_ptr<FontConfig> CopyFontInfo(const FontConfig* source, winrt::weak_ref<Profile> sourceProfile);
Json::Value ToJson() const;
void LayerJson(const Json::Value& json);
void LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const;
Model::Profile SourceProfile();
@ -45,5 +46,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
private:
winrt::weak_ref<Profile> _sourceProfile;
std::set<std::string> _changeLog;
void _logSettingSet(const std::string_view& setting);
void _logSettingIfSet(const std::string_view& setting, const bool isSet);
};
}

View File

@ -128,13 +128,16 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::FromJson(const Json::Value&
void GlobalAppSettings::LayerJson(const Json::Value& json, const OriginTag origin)
{
JsonUtils::GetValueForKey(json, DefaultProfileKey, _UnparsedDefaultProfile);
// GH#8076 - when adding enum values to this key, we also changed it from
// "useTabSwitcher" to "tabSwitcherMode". Continue supporting
// "useTabSwitcher", but prefer "tabSwitcherMode"
JsonUtils::GetValueForKey(json, LegacyUseTabSwitcherModeKey, _TabSwitcherMode);
#define GLOBAL_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name);
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_LAYER_JSON)
#undef GLOBAL_SETTINGS_LAYER_JSON
@ -152,6 +155,25 @@ void GlobalAppSettings::LayerJson(const Json::Value& json, const OriginTag origi
LayerActionsFrom(json, origin, true);
JsonUtils::GetValueForKey(json, LegacyReloadEnvironmentVariablesKey, _legacyReloadEnvironmentVariables);
if (json[LegacyReloadEnvironmentVariablesKey.data()])
{
_logSettingSet(LegacyReloadEnvironmentVariablesKey);
}
// Remove settings included in userDefaults
static constexpr std::array<std::pair<std::string_view, std::string_view>, 2> userDefaultSettings{ { { "copyOnSelect", "false" },
{ "copyFormatting", "false" } } };
for (const auto& [setting, val] : userDefaultSettings)
{
if (const auto settingJson{ json.find(&*setting.cbegin(), (&*setting.cbegin()) + setting.size()) })
{
if (settingJson->asString() == val)
{
// false positive!
_changeLog.erase(std::string{ setting });
}
}
}
}
void GlobalAppSettings::LayerActionsFrom(const Json::Value& json, const OriginTag origin, const bool withKeybindings)
@ -317,3 +339,85 @@ bool GlobalAppSettings::ShouldUsePersistedLayout() const
{
return FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && !IsolatedMode();
}
void GlobalAppSettings::_logSettingSet(const std::string_view& setting)
{
if (setting == "theme")
{
if (_Theme.has_value())
{
// ThemePair always has a Dark/Light value,
// so we need to check if they were explicitly set
if (_Theme->DarkName() == _Theme->LightName())
{
_changeLog.emplace(setting);
}
else
{
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, "dark"));
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, "light"));
}
}
}
else if (setting == "newTabMenu")
{
if (_NewTabMenu.has_value())
{
for (const auto& entry : *_NewTabMenu)
{
std::string entryType;
switch (entry.Type())
{
case NewTabMenuEntryType::Profile:
entryType = "profile";
break;
case NewTabMenuEntryType::Separator:
entryType = "separator";
break;
case NewTabMenuEntryType::Folder:
entryType = "folder";
break;
case NewTabMenuEntryType::RemainingProfiles:
entryType = "remainingProfiles";
break;
case NewTabMenuEntryType::MatchProfiles:
entryType = "matchProfiles";
break;
case NewTabMenuEntryType::Action:
entryType = "action";
break;
case NewTabMenuEntryType::Invalid:
// ignore invalid
continue;
}
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, entryType));
}
}
}
else
{
_changeLog.emplace(setting);
}
}
void GlobalAppSettings::_logSettingIfSet(const std::string_view& setting, const bool isSet)
{
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;
if (!settingCopyFormattingToDefault && !settingNTMToDefault)
{
_logSettingSet(setting);
}
}
}
void GlobalAppSettings::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const
{
for (const auto& setting : _changeLog)
{
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), context, setting));
}
}

View File

@ -72,10 +72,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
bool LegacyReloadEnvironmentVariables() const noexcept { return _legacyReloadEnvironmentVariables; }
void LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const;
INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L"");
#define GLOBAL_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
INHERITABLE_SETTING(Model::GlobalAppSettings, type, name, ##__VA_ARGS__)
INHERITABLE_SETTING_WITH_LOGGING(Model::GlobalAppSettings, type, name, jsonKey, ##__VA_ARGS__)
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_INITIALIZE)
#undef GLOBAL_SETTINGS_INITIALIZE
@ -89,9 +91,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::guid _defaultProfile{};
bool _legacyReloadEnvironmentVariables{ true };
winrt::com_ptr<implementation::ActionMap> _actionMap{ winrt::make_self<implementation::ActionMap>() };
std::set<std::string> _changeLog;
std::vector<SettingsLoadWarnings> _keybindingsWarnings;
Windows::Foundation::Collections::IMap<winrt::hstring, Model::ColorScheme> _colorSchemes{ winrt::single_threaded_map<winrt::hstring, Model::ColorScheme>() };
Windows::Foundation::Collections::IMap<winrt::hstring, Model::Theme> _themes{ winrt::single_threaded_map<winrt::hstring, Model::Theme>() };
void _logSettingSet(const std::string_view& setting);
void _logSettingIfSet(const std::string_view& setting, const bool isSet);
};
}

View File

@ -185,6 +185,27 @@ public: \
_##name = 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; \
}
// 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")

View File

@ -175,15 +175,22 @@ void Profile::LayerJson(const Json::Value& json)
JsonUtils::GetValueForKey(json, NameKey, _Name);
JsonUtils::GetValueForKey(json, UpdatesKey, _Updates);
JsonUtils::GetValueForKey(json, GuidKey, _Guid);
JsonUtils::GetValueForKey(json, HiddenKey, _Hidden);
// 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());
JsonUtils::GetValueForKey(json, IconKey, _Icon);
_logSettingIfSet(IconKey, _Icon.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());
// Try to load some legacy keys, to migrate them.
// Done _before_ the MTSM_PROFILE_SETTINGS, which have the updated keys.
@ -191,7 +198,8 @@ void Profile::LayerJson(const Json::Value& json)
JsonUtils::GetValueForKey(json, LegacyAutoMarkPromptsKey, _AutoMarkPrompts);
#define PROFILE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name);
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_LAYER_JSON)
#undef PROFILE_SETTINGS_LAYER_JSON
@ -208,6 +216,8 @@ void Profile::LayerJson(const Json::Value& json)
unfocusedAppearance->LayerJson(json[JsonKey(UnfocusedAppearanceKey)]);
_UnfocusedAppearance = *unfocusedAppearance;
_logSettingSet(UnfocusedAppearanceKey);
}
}
@ -518,3 +528,58 @@ std::wstring Profile::NormalizeCommandLine(LPCWSTR commandLine)
return normalized;
}
void Profile::_logSettingSet(const std::string_view& setting)
{
_changeLog.emplace(setting);
}
void Profile::_logSettingIfSet(const std::string_view& setting, const bool isSet)
{
if (isSet)
{
// make sure this matches defaults.json.
static constexpr winrt::guid DEFAULT_WINDOWS_POWERSHELL_GUID{ 0x61c54bbd, 0xc2c6, 0x5271, { 0x96, 0xe7, 0x00, 0x9a, 0x87, 0xff, 0x44, 0xbf } };
static constexpr winrt::guid DEFAULT_COMMAND_PROMPT_GUID{ 0x0caa0dad, 0x35be, 0x5f56, { 0xa8, 0xff, 0xaf, 0xce, 0xee, 0xaa, 0x61, 0x01 } };
// 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");
// clang-format off
if (!(isWinPow && (settingHiddenToFalse || settingCommandlineToWinPow))
&& !(isCmd && (settingHiddenToFalse || settingCommandlineToCmd))
&& !(isACS && settingHiddenToFalse)
&& !(isWTDynamicProfile && settingHiddenToFalse))
{
// clang-format on
_logSettingSet(setting);
}
}
}
void Profile::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const
{
for (const auto& setting : _changeLog)
{
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), context, setting));
}
std::string fontContext{ fmt::format(FMT_COMPILE("{}.{}"), context, FontInfoKey) };
winrt::get_self<implementation::FontConfig>(_FontInfo)->LogSettingChanges(changes, fontContext);
// We don't want to distinguish between "profile.defaultAppearance.*" and "profile.unfocusedAppearance.*" settings,
// but we still want to aggregate all of the appearance settings from both appearances.
// Log them as "profile.appearance.*"
std::string appContext{ fmt::format(FMT_COMPILE("{}.{}"), context, "appearance") };
winrt::get_self<implementation::AppearanceConfig>(_DefaultAppearance)->LogSettingChanges(changes, appContext);
if (_UnfocusedAppearance)
{
winrt::get_self<implementation::AppearanceConfig>(*_UnfocusedAppearance)->LogSettingChanges(changes, appContext);
}
}

View File

@ -108,6 +108,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _FinalizeInheritance() override;
void LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const;
// Special fields
hstring Icon() const;
void Icon(const hstring& value);
@ -131,7 +133,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
#define PROFILE_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
INHERITABLE_SETTING(Model::Profile, type, name, ##__VA_ARGS__)
INHERITABLE_SETTING_WITH_LOGGING(Model::Profile, type, name, jsonKey, ##__VA_ARGS__)
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_INITIALIZE)
#undef PROFILE_SETTINGS_INITIALIZE
@ -140,12 +142,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Model::FontConfig _FontInfo{ winrt::make<FontConfig>(weak_ref<Model::Profile>(*this)) };
std::optional<winrt::hstring> _evaluatedIcon{ std::nullopt };
std::set<std::string> _changeLog;
static std::wstring EvaluateStartingDirectory(const std::wstring& directory);
static guid _GenerateGuidForProfile(const std::wstring_view& name, const std::wstring_view& source) noexcept;
winrt::hstring _evaluateIcon() const;
void _logSettingSet(const std::string_view& setting);
void _logSettingIfSet(const std::string_view& setting, const bool isSet);
friend class SettingsModelUnitTests::DeserializationTests;
friend class SettingsModelUnitTests::ProfileTests;

View File

@ -292,6 +292,53 @@ winrt::com_ptr<Theme> Theme::FromJson(const Json::Value& json)
return result;
}
void Theme::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context)
{
#pragma warning(push)
#pragma warning(disable : 5103) // pasting '{' and 'winrt' does not result in a valid preprocessing token
#define GENERATE_SET_CHECK_AND_JSON_KEYS(type, name, jsonKey, ...) \
const bool is##name##Set = _##name != nullptr; \
std::string_view outer##name##JsonKey = jsonKey;
MTSM_THEME_SETTINGS(GENERATE_SET_CHECK_AND_JSON_KEYS)
#define LOG_IF_SET(type, name, jsonKey, ...) \
if (obj.name() != type{##__VA_ARGS__ }) \
changes.emplace(fmt::format(FMT_COMPILE("{}.{}.{}"), context, outerJsonKey, jsonKey));
if (isWindowSet)
{
const auto obj = _Window;
const auto outerJsonKey = outerWindowJsonKey;
MTSM_THEME_WINDOW_SETTINGS(LOG_IF_SET)
}
if (isSettingsSet)
{
const auto obj = _Settings;
const auto outerJsonKey = outerSettingsJsonKey;
MTSM_THEME_SETTINGS_SETTINGS(LOG_IF_SET)
}
if (isTabRowSet)
{
const auto obj = _TabRow;
const auto outerJsonKey = outerTabRowJsonKey;
MTSM_THEME_TABROW_SETTINGS(LOG_IF_SET)
}
if (isTabSet)
{
const auto obj = _Tab;
const auto outerJsonKey = outerTabJsonKey;
MTSM_THEME_TAB_SETTINGS(LOG_IF_SET)
}
#undef LOG_IF_SET
#undef GENERATE_SET_CHECK_AND_JSON_KEYS
#pragma warning(pop)
}
// Method Description:
// - Create a new serialized JsonObject from an instance of this class
// Arguments:

View File

@ -97,8 +97,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
hstring ToString();
static com_ptr<Theme> FromJson(const Json::Value& json);
void LayerJson(const Json::Value& json);
Json::Value ToJson() const;
void LogSettingChanges(std::set<std::string>& changes, const std::string_view& context);
winrt::Windows::UI::Xaml::ElementTheme RequestedTheme() const noexcept;