Skip to content

Commit 0a9cbd0

Browse files
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
1 parent cd8c125 commit 0a9cbd0

22 files changed

+531
-10
lines changed

.github/actions/spelling/allow/allow.txt

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ gje
2727
godbolt
2828
hyperlinking
2929
hyperlinks
30+
Kbds
3031
kje
3132
libfuzzer
3233
liga
@@ -43,6 +44,7 @@ mkmk
4344
mnt
4445
mru
4546
nje
47+
NTMTo
4648
notwrapped
4749
ogonek
4850
overlined

src/cascadia/TerminalApp/AppLogic.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,10 @@ namespace winrt::TerminalApp::implementation
432432
return;
433433
}
434434
}
435+
else
436+
{
437+
_settings.LogSettingChanges(true);
438+
}
435439

436440
if (initialLoad)
437441
{

src/cascadia/TerminalSettingsEditor/MainPage.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
481481

482482
void MainPage::SaveButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/)
483483
{
484+
_settingsClone.LogSettingChanges(false);
484485
_settingsClone.WriteSettingsToDisk();
485486
}
486487

src/cascadia/TerminalSettingsModel/ActionMap.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
570570
const auto action = cmd.ActionAndArgs().Action();
571571
const auto id = action == ShortcutAction::Invalid ? hstring{} : cmd.ID();
572572
_KeyMap.insert_or_assign(keys, id);
573+
_changeLog.emplace(KeysKey);
573574
}
574575

575576
// Method Description:

src/cascadia/TerminalSettingsModel/ActionMap.h

+3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
7373
Json::Value ToJson() const;
7474
Json::Value KeyBindingsToJson() const;
7575
bool FixupsAppliedDuringLoad() const;
76+
void LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const;
7677

7778
// modification
7879
bool RebindKeys(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys);
@@ -138,6 +139,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
138139

139140
til::shared_mutex<std::unordered_map<hstring, std::vector<Model::Command>>> _cwdLocalSnippetsCache{};
140141

142+
std::set<std::string> _changeLog;
143+
141144
friend class SettingsModelUnitTests::KeyBindingsTests;
142145
friend class SettingsModelUnitTests::DeserializationTests;
143146
friend class SettingsModelUnitTests::TerminalSettingsTests;

src/cascadia/TerminalSettingsModel/ActionMapSerialization.cpp

+33-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
7878
// Now check if this is a command block
7979
if (jsonBlock.isMember(JsonKey(CommandsKey)) || jsonBlock.isMember(JsonKey(ActionKey)))
8080
{
81-
AddAction(*Command::FromJson(jsonBlock, warnings, origin), keys);
81+
auto command = Command::FromJson(jsonBlock, warnings, origin);
82+
command->LogSettingChanges(_changeLog);
83+
AddAction(*command, keys);
8284

8385
if (jsonBlock.isMember(JsonKey(KeysKey)))
8486
{
@@ -105,6 +107,28 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
105107

106108
// any existing keybinding with the same keychord in this layer will get overwritten
107109
_KeyMap.insert_or_assign(keys, idJson);
110+
111+
if (!_changeLog.contains(KeysKey.data()))
112+
{
113+
// Log "keys" field, but only if it's one that isn't in userDefaults.json
114+
static constexpr std::array<std::pair<std::wstring_view, std::string_view>, 3> userDefaultKbds{ { { L"Terminal.CopyToClipboard", "ctrl+c" },
115+
{ L"Terminal.PasteFromClipboard", "ctrl+v" },
116+
{ L"Terminal.DuplicatePaneAuto", "alt+shift+d" } } };
117+
bool isUserDefaultKbd = false;
118+
for (const auto& [id, kbd] : userDefaultKbds)
119+
{
120+
const auto keyJson{ jsonBlock.find(&*KeysKey.cbegin(), (&*KeysKey.cbegin()) + KeysKey.size()) };
121+
if (idJson == id && keyJson->asString() == kbd)
122+
{
123+
isUserDefaultKbd = true;
124+
break;
125+
}
126+
}
127+
if (!isUserDefaultKbd)
128+
{
129+
_changeLog.emplace(KeysKey);
130+
}
131+
}
108132
}
109133
}
110134

@@ -156,4 +180,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
156180

157181
return keybindingsList;
158182
}
183+
184+
void ActionMap::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const
185+
{
186+
for (const auto& setting : _changeLog)
187+
{
188+
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), context, setting));
189+
}
190+
}
159191
}

src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp

+37-1
Original file line numberDiff line numberDiff line change
@@ -90,27 +90,42 @@ Json::Value AppearanceConfig::ToJson() const
9090
void AppearanceConfig::LayerJson(const Json::Value& json)
9191
{
9292
JsonUtils::GetValueForKey(json, ForegroundKey, _Foreground);
93+
_logSettingIfSet(ForegroundKey, _Foreground.has_value());
94+
9395
JsonUtils::GetValueForKey(json, BackgroundKey, _Background);
96+
_logSettingIfSet(BackgroundKey, _Background.has_value());
97+
9498
JsonUtils::GetValueForKey(json, SelectionBackgroundKey, _SelectionBackground);
99+
_logSettingIfSet(SelectionBackgroundKey, _SelectionBackground.has_value());
100+
95101
JsonUtils::GetValueForKey(json, CursorColorKey, _CursorColor);
102+
_logSettingIfSet(CursorColorKey, _CursorColor.has_value());
96103

97104
JsonUtils::GetValueForKey(json, LegacyAcrylicTransparencyKey, _Opacity);
98105
JsonUtils::GetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter<float, IntAsFloatPercentConversionTrait>{});
106+
_logSettingIfSet(OpacityKey, _Opacity.has_value());
107+
99108
if (json["colorScheme"].isString())
100109
{
101110
// to make the UI happy, set ColorSchemeName.
102111
JsonUtils::GetValueForKey(json, ColorSchemeKey, _DarkColorSchemeName);
103112
_LightColorSchemeName = _DarkColorSchemeName;
113+
_logSettingSet(ColorSchemeKey);
104114
}
105115
else if (json["colorScheme"].isObject())
106116
{
107117
// to make the UI happy, set ColorSchemeName to whatever the dark value is.
108118
JsonUtils::GetValueForKey(json["colorScheme"], "dark", _DarkColorSchemeName);
109119
JsonUtils::GetValueForKey(json["colorScheme"], "light", _LightColorSchemeName);
120+
121+
_logSettingSet("colorScheme.dark");
122+
_logSettingSet("colorScheme.light");
110123
}
111124

112125
#define APPEARANCE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
113-
JsonUtils::GetValueForKey(json, jsonKey, _##name);
126+
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
127+
_logSettingIfSet(jsonKey, _##name.has_value());
128+
114129
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_LAYER_JSON)
115130
#undef APPEARANCE_SETTINGS_LAYER_JSON
116131
}
@@ -156,3 +171,24 @@ winrt::hstring AppearanceConfig::ExpandedBackgroundImagePath()
156171
return winrt::hstring{ wil::ExpandEnvironmentStringsW<std::wstring>(path.c_str()) };
157172
}
158173
}
174+
175+
void AppearanceConfig::_logSettingSet(const std::string_view& setting)
176+
{
177+
_changeLog.emplace(setting);
178+
}
179+
180+
void AppearanceConfig::_logSettingIfSet(const std::string_view& setting, const bool isSet)
181+
{
182+
if (isSet)
183+
{
184+
_logSettingSet(setting);
185+
}
186+
}
187+
188+
void AppearanceConfig::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const
189+
{
190+
for (const auto& setting : _changeLog)
191+
{
192+
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), context, setting));
193+
}
194+
}

src/cascadia/TerminalSettingsModel/AppearanceConfig.h

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
3131
static winrt::com_ptr<AppearanceConfig> CopyAppearance(const AppearanceConfig* source, winrt::weak_ref<Profile> sourceProfile);
3232
Json::Value ToJson() const;
3333
void LayerJson(const Json::Value& json);
34+
void LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const;
3435

3536
Model::Profile SourceProfile();
3637

@@ -52,5 +53,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
5253

5354
private:
5455
winrt::weak_ref<Profile> _sourceProfile;
56+
std::set<std::string> _changeLog;
57+
58+
void _logSettingSet(const std::string_view& setting);
59+
void _logSettingIfSet(const std::string_view& setting, const bool isSet);
5560
};
5661
}

src/cascadia/TerminalSettingsModel/CascadiaSettings.h

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
4848
std::unordered_map<winrt::hstring, winrt::com_ptr<implementation::ColorScheme>> colorSchemes;
4949
std::unordered_map<winrt::hstring, winrt::hstring> colorSchemeRemappings;
5050
bool fixupsAppliedDuringLoad{ false };
51+
std::set<std::string> themesChangeLog;
5152

5253
void clear();
5354
};
@@ -96,6 +97,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
9697
void _executeGenerator(const IDynamicProfileGenerator& generator);
9798

9899
std::unordered_set<std::wstring_view> _ignoredNamespaces;
100+
std::set<std::string> themesChangeLog;
99101
// See _getNonUserOriginProfiles().
100102
size_t _userProfileCount = 0;
101103
};
@@ -150,6 +152,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
150152

151153
void ExpandCommands();
152154

155+
void LogSettingChanges(bool isJsonLoad) const;
156+
153157
private:
154158
static const std::filesystem::path& _settingsPath();
155159
static const std::filesystem::path& _releaseSettingsPath();
@@ -180,6 +184,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
180184
winrt::com_ptr<implementation::Profile> _baseLayerProfile = winrt::make_self<implementation::Profile>();
181185
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> _allProfiles = winrt::single_threaded_observable_vector<Model::Profile>();
182186
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> _activeProfiles = winrt::single_threaded_observable_vector<Model::Profile>();
187+
std::set<std::string> _themesChangeLog{};
183188

184189
// load errors
185190
winrt::Windows::Foundation::Collections::IVector<Model::SettingsLoadWarnings> _warnings = winrt::single_threaded_vector<Model::SettingsLoadWarnings>();

src/cascadia/TerminalSettingsModel/CascadiaSettings.idl

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace Microsoft.Terminal.Settings.Model
2424

2525
CascadiaSettings Copy();
2626
void WriteSettingsToDisk();
27+
void LogSettingChanges(Boolean isJsonLoad);
2728

2829
String Hash { get; };
2930

src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp

+78
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ void ParsedSettings::clear()
109109
profilesByGuid.clear();
110110
colorSchemes.clear();
111111
fixupsAppliedDuringLoad = false;
112+
themesChangeLog.clear();
112113
}
113114

114115
// This is a convenience method used by the CascadiaSettings constructor.
@@ -658,6 +659,12 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source
658659
// versions of these themes overriding the built-in ones.
659660
continue;
660661
}
662+
663+
if (origin != OriginTag::InBox)
664+
{
665+
static std::string themesContext{ "themes" };
666+
theme->LogSettingChanges(settings.themesChangeLog, themesContext);
667+
}
661668
settings.globals->AddTheme(*theme);
662669
}
663670
}
@@ -1222,6 +1229,7 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) :
12221229
_allProfiles = winrt::single_threaded_observable_vector(std::move(allProfiles));
12231230
_activeProfiles = winrt::single_threaded_observable_vector(std::move(activeProfiles));
12241231
_warnings = winrt::single_threaded_vector(std::move(warnings));
1232+
_themesChangeLog = std::move(loader.userSettings.themesChangeLog);
12251233

12261234
_resolveDefaultProfile();
12271235
_resolveNewTabMenuProfiles();
@@ -1596,3 +1604,73 @@ void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector<Model::NewTab
15961604
}
15971605
}
15981606
}
1607+
1608+
void CascadiaSettings::LogSettingChanges(bool isJsonLoad) const
1609+
{
1610+
#ifndef _DEBUG
1611+
// Only do this if we're actually being sampled
1612+
if (!TraceLoggingProviderEnabled(g_hSettingsModelProvider, 0, MICROSOFT_KEYWORD_MEASURES))
1613+
{
1614+
return;
1615+
}
1616+
#endif // !_DEBUG
1617+
1618+
// aggregate setting changes
1619+
std::set<std::string> changes;
1620+
static constexpr std::string_view globalContext{ "global" };
1621+
_globals->LogSettingChanges(changes, globalContext);
1622+
1623+
// Actions are not expected to change when loaded from the settings UI
1624+
static constexpr std::string_view actionContext{ "action" };
1625+
winrt::get_self<implementation::ActionMap>(_globals->ActionMap())->LogSettingChanges(changes, actionContext);
1626+
1627+
static constexpr std::string_view profileContext{ "profile" };
1628+
for (const auto& profile : _allProfiles)
1629+
{
1630+
winrt::get_self<Profile>(profile)->LogSettingChanges(changes, profileContext);
1631+
}
1632+
1633+
static constexpr std::string_view profileDefaultsContext{ "profileDefaults" };
1634+
_baseLayerProfile->LogSettingChanges(changes, profileDefaultsContext);
1635+
1636+
// Themes are not expected to change when loaded from the settings UI
1637+
// DO NOT CALL Theme::LogSettingChanges!!
1638+
// We already collected the changes when we loaded the JSON
1639+
for (const auto& change : _themesChangeLog)
1640+
{
1641+
changes.insert(change);
1642+
}
1643+
1644+
// report changes
1645+
for (const auto& change : changes)
1646+
{
1647+
#ifndef _DEBUG
1648+
// A `isJsonLoad ? "JsonSettingsChanged" : "UISettingsChanged"`
1649+
// would be nice, but that apparently isn't allowed in the macro below.
1650+
// Also, there's guidance to not send too much data all in one event,
1651+
// so we'll be sending a ton of events here.
1652+
if (isJsonLoad)
1653+
{
1654+
TraceLoggingWrite(g_hSettingsModelProvider,
1655+
"JsonSettingsChanged",
1656+
TraceLoggingDescription("Event emitted when settings.json change"),
1657+
TraceLoggingValue(change.data()),
1658+
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
1659+
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
1660+
}
1661+
else
1662+
{
1663+
TraceLoggingWrite(g_hSettingsModelProvider,
1664+
"UISettingsChanged",
1665+
TraceLoggingDescription("Event emitted when settings change via the UI"),
1666+
TraceLoggingValue(change.data()),
1667+
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
1668+
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
1669+
}
1670+
#else
1671+
OutputDebugStringA(isJsonLoad ? "JsonSettingsChanged - " : "UISettingsChanged - ");
1672+
OutputDebugStringA(change.data());
1673+
OutputDebugStringA("\n");
1674+
#endif // !_DEBUG
1675+
}
1676+
}

0 commit comments

Comments
 (0)