-
Notifications
You must be signed in to change notification settings - Fork 8.3k
/
CascadiaSettingsSerialization.cpp
1703 lines (1496 loc) · 68.3 KB
/
CascadiaSettingsSerialization.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "CascadiaSettings.h"
#include <LibraryResources.h>
#include <fmt/chrono.h>
#include <shlobj.h>
#include <til/latch.h>
#include <til/io.h>
#include "resource.h"
#include "AzureCloudShellGenerator.h"
#include "PowershellCoreProfileGenerator.h"
#include "VisualStudioGenerator.h"
#include "WslDistroGenerator.h"
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
#include "SshHostGenerator.h"
#endif
#include "ApplicationState.h"
#include "DefaultTerminal.h"
#include "FileUtils.h"
#include "ProfileEntry.h"
#include "FolderEntry.h"
#include "MatchProfilesEntry.h"
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::ApplicationModel::AppExtensions;
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::wstring_view SettingsFilename{ L"settings.json" };
static constexpr std::wstring_view DefaultsFilename{ L"defaults.json" };
static constexpr std::string_view ProfilesKey{ "profiles" };
static constexpr std::string_view DefaultSettingsKey{ "defaults" };
static constexpr std::string_view ProfilesListKey{ "list" };
static constexpr std::string_view SchemesKey{ "schemes" };
static constexpr std::string_view ThemesKey{ "themes" };
constexpr std::wstring_view systemThemeName{ L"system" };
constexpr std::wstring_view darkThemeName{ L"dark" };
constexpr std::wstring_view lightThemeName{ L"light" };
constexpr std::wstring_view legacySystemThemeName{ L"legacySystem" };
constexpr std::wstring_view legacyDarkThemeName{ L"legacyDark" };
constexpr std::wstring_view legacyLightThemeName{ L"legacyLight" };
static constexpr std::array builtinThemes{
systemThemeName,
lightThemeName,
darkThemeName,
legacySystemThemeName,
legacyLightThemeName,
legacyDarkThemeName,
};
static constexpr std::wstring_view jsonExtension{ L".json" };
static constexpr std::wstring_view FragmentsSubDirectory{ L"\\Fragments" };
static constexpr std::wstring_view FragmentsPath{ L"\\Microsoft\\Windows Terminal\\Fragments" };
static constexpr std::wstring_view AppExtensionHostName{ L"com.microsoft.windows.terminal.settings" };
// 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 } };
// Function Description:
// - Extracting the value from an async task (like talking to the app catalog) when we are on the
// UI thread causes C++/WinRT to complain quite loudly (and halt execution!)
// This templated function extracts the result from a task with chicanery.
template<typename TTask>
static auto extractValueFromTaskWithoutMainThreadAwait(TTask&& task) -> decltype(task.get())
{
std::optional<decltype(task.get())> finalVal;
til::latch latch{ 1 };
const auto _ = [&]() -> safe_void_coroutine {
const auto cleanup = wil::scope_exit([&]() {
latch.count_down();
});
co_await winrt::resume_background();
finalVal.emplace(co_await task);
}();
latch.wait();
return finalVal.value();
}
// Concatenates the two given strings (!) and returns them as a path.
// You better make sure there's a path separator at the end of lhs or at the start of rhs.
static std::filesystem::path buildPath(const std::wstring_view& lhs, const std::wstring_view& rhs)
{
std::wstring buffer;
buffer.reserve(lhs.size() + rhs.size());
buffer.append(lhs);
buffer.append(rhs);
return { std::move(buffer) };
}
void ParsedSettings::clear()
{
globals = {};
baseLayerProfile = {};
profiles.clear();
profilesByGuid.clear();
colorSchemes.clear();
fixupsAppliedDuringLoad = false;
themesChangeLog.clear();
}
// This is a convenience method used by the CascadiaSettings constructor.
// It runs some basic settings layering without relying on external programs or files.
// This makes it suitable for most unit tests.
SettingsLoader SettingsLoader::Default(const std::string_view& userJSON, const std::string_view& inboxJSON)
{
SettingsLoader loader{ userJSON, inboxJSON };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
loader.FixupUserSettings();
return loader;
}
// The SettingsLoader class is an internal implementation detail of CascadiaSettings.
// Member methods aren't safe against misuse and you need to ensure to call them in a specific order.
// See CascadiaSettings::LoadAll() for a specific usage example.
//
// This constructor only handles parsing the two given JSON strings.
// At a minimum you should do at least everything that SettingsLoader::Default does.
SettingsLoader::SettingsLoader(const std::string_view& userJSON, const std::string_view& inboxJSON)
{
_parse(OriginTag::InBox, {}, inboxJSON, inboxSettings);
try
{
_parse(OriginTag::User, {}, userJSON, userSettings);
}
catch (const JsonUtils::DeserializationError& e)
{
_rethrowSerializationExceptionWithLocationInfo(e, userJSON);
}
if (const auto sources = userSettings.globals->DisabledProfileSources())
{
_ignoredNamespaces.reserve(sources.Size());
for (auto&& id : sources)
{
_ignoredNamespaces.emplace(std::move(id));
}
}
// Apply DisabledProfileSources policy setting. Pick whatever policy is set first.
// In most cases HKCU settings take precedence over HKLM settings, but the inverse is true for policies.
for (const auto key : { HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER })
{
wchar_t buffer[512]; // "640K ought to be enough for anyone"
DWORD bufferSize = sizeof(buffer);
if (RegGetValueW(key, LR"(Software\Policies\Microsoft\Windows Terminal)", L"DisabledProfileSources", RRF_RT_REG_MULTI_SZ, nullptr, buffer, &bufferSize) == 0)
{
for (auto p = buffer; *p;)
{
const auto len = wcslen(p);
_ignoredNamespaces.emplace(p, gsl::narrow_cast<uint32_t>(len));
p += len + 1;
}
break;
}
}
// See member description of _userProfileCount.
_userProfileCount = userSettings.profiles.size();
}
// Generate dynamic profiles and add them to the list of "inbox" profiles
// (meaning profiles specified by the application rather by the user).
void SettingsLoader::GenerateProfiles()
{
_executeGenerator(PowershellCoreProfileGenerator{});
_executeGenerator(WslDistroGenerator{});
_executeGenerator(AzureCloudShellGenerator{});
_executeGenerator(VisualStudioGenerator{});
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
_executeGenerator(SshHostGenerator{});
#endif
}
// A new settings.json gets a special treatment:
// 1. The default profile is a PowerShell 7+ one, if one was generated,
// and falls back to the standard PowerShell 5 profile otherwise.
// 2. cmd.exe gets a localized name.
void SettingsLoader::ApplyRuntimeInitialSettings()
{
// 1.
{
const auto preferredPowershellProfile = PowershellCoreProfileGenerator::GetPreferredPowershellProfileName();
auto guid = DEFAULT_WINDOWS_POWERSHELL_GUID;
for (const auto& profile : inboxSettings.profiles)
{
if (profile->Name() == preferredPowershellProfile)
{
guid = profile->Guid();
break;
}
}
userSettings.globals->DefaultProfile(guid);
}
// 2.
{
for (const auto& profile : userSettings.profiles)
{
if (profile->Guid() == DEFAULT_COMMAND_PROMPT_GUID)
{
profile->Name(RS_(L"CommandPromptDisplayName"));
break;
}
}
}
}
// Adds profiles from .inboxSettings as parents of matching profiles in .userSettings.
// That way the user profiles will get appropriate defaults from the generators (like icons and such).
// If a matching profile doesn't exist yet in .userSettings, one will be created.
// Additionally, produces a final view of the color schemes from the inbox + user settings
void SettingsLoader::MergeInboxIntoUserSettings()
{
for (const auto& profile : inboxSettings.profiles)
{
_addUserProfileParent(profile);
}
}
// Searches AppData/ProgramData and app extension directories for settings JSON files.
// If such JSON files are found, they're read and their contents added to .userSettings.
//
// Of course it would be more elegant to add fragments to .inboxSettings first and then have MergeInboxIntoUserSettings
// merge them. Unfortunately however the "updates" key in fragment profiles make this impossible:
// The targeted profile might be one that got created as part of SettingsLoader::MergeInboxIntoUserSettings.
// Additionally the GUID in "updates" will conflict with existing GUIDs in .inboxSettings.
void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
{
ParsedSettings fragmentSettings;
const auto parseAndLayerFragmentFiles = [&](const std::filesystem::path& path, const winrt::hstring& source) {
for (const auto& fragmentExt : std::filesystem::directory_iterator{ path })
{
if (fragmentExt.path().extension() == jsonExtension)
{
try
{
const auto content = til::io::read_file_as_utf8_string_if_exists(fragmentExt.path());
if (!content.empty())
{
_parseFragment(source, content, fragmentSettings);
}
}
CATCH_LOG();
}
}
};
for (const auto& rfid : std::array{ FOLDERID_LocalAppData, FOLDERID_ProgramData })
{
wil::unique_cotaskmem_string folder;
THROW_IF_FAILED(SHGetKnownFolderPath(rfid, 0, nullptr, &folder));
const auto fragmentPath = buildPath(folder.get(), FragmentsPath);
if (std::filesystem::is_directory(fragmentPath))
{
for (const auto& fragmentExtFolder : std::filesystem::directory_iterator{ fragmentPath })
{
const auto filename = fragmentExtFolder.path().filename();
const auto& source = filename.native();
if (!_ignoredNamespaces.contains(std::wstring_view{ source }) && fragmentExtFolder.is_directory())
{
parseAndLayerFragmentFiles(fragmentExtFolder.path(), winrt::hstring{ source });
}
}
}
}
// Search through app extensions.
// Gets the catalog of extensions with the name "com.microsoft.windows.terminal.settings".
//
// GH#12305: Open() can throw an 0x80070490 "Element not found.".
// It's unclear to me under which circumstances this happens as no one on the team
// was able to reproduce the user's issue, even if the application was run unpackaged.
// The error originates from `CallerIdentity::GetCallingProcessAppId` which returns E_NOT_SET.
// A comment can be found, reading:
// > Gets the "strong" AppId from the process token. This works for UWAs and Centennial apps,
// > strongly named processes where the AppId is stored securely in the process token. [...]
// > E_NOT_SET is returned for processes without strong AppIds.
IVectorView<AppExtension> extensions;
try
{
const auto catalog = AppExtensionCatalog::Open(AppExtensionHostName);
extensions = extractValueFromTaskWithoutMainThreadAwait(catalog.FindAllAsync());
}
CATCH_LOG();
if (!extensions)
{
return;
}
for (const auto& ext : extensions)
{
const auto packageName = ext.Package().Id().FamilyName();
if (_ignoredNamespaces.contains(std::wstring_view{ packageName }))
{
continue;
}
// Likewise, getting the public folder from an extension is an async operation.
auto foundFolder = extractValueFromTaskWithoutMainThreadAwait(ext.GetPublicFolderAsync());
if (!foundFolder)
{
continue;
}
// the StorageFolder class has its own methods for obtaining the files within the folder
// however, all those methods are Async methods
// you may have noticed that we need to resort to clunky implementations for async operations
// (they are in extractValueFromTaskWithoutMainThreadAwait)
// so for now we will just take the folder path and access the files that way
const auto path = buildPath(foundFolder.Path(), FragmentsSubDirectory);
if (std::filesystem::is_directory(path))
{
parseAndLayerFragmentFiles(path, packageName);
}
}
}
// See FindFragmentsAndMergeIntoUserSettings.
// This function does the same, but for a single given JSON blob and source
// and at the time of writing is used for unit tests only.
void SettingsLoader::MergeFragmentIntoUserSettings(const winrt::hstring& source, const std::string_view& content)
{
ParsedSettings fragmentSettings;
_parseFragment(source, content, fragmentSettings);
}
// Call this method before passing SettingsLoader to the CascadiaSettings constructor.
// It layers all remaining objects onto each other (those that aren't covered
// by MergeInboxIntoUserSettings/FindFragmentsAndMergeIntoUserSettings).
void SettingsLoader::FinalizeLayering()
{
for (const auto& colorScheme : inboxSettings.colorSchemes)
{
_addOrMergeUserColorScheme(colorScheme.second);
}
// Layer default globals -> user globals
userSettings.globals->AddLeastImportantParent(inboxSettings.globals);
// Actions are currently global, so if we want to conditionally light up a bunch of
// actions, this is the time to do it.
if (userSettings.globals->EnableColorSelection())
{
const auto json = _parseJson(LoadStringResource(IDR_ENABLE_COLOR_SELECTION));
const auto globals = GlobalAppSettings::FromJson(json.root, OriginTag::InBox);
userSettings.globals->AddLeastImportantParent(globals);
}
userSettings.globals->_FinalizeInheritance();
// Layer default profile defaults -> user profile defaults
userSettings.baseLayerProfile->AddLeastImportantParent(inboxSettings.baseLayerProfile);
userSettings.baseLayerProfile->_FinalizeInheritance();
// Layer user profile defaults -> user profiles
for (const auto& profile : userSettings.profiles)
{
profile->AddMostImportantParent(userSettings.baseLayerProfile);
// This completes the parenting process that was started in _addUserProfileParent().
profile->_FinalizeInheritance();
if (profile->Origin() == OriginTag::None)
{
// If you add more fields here, make sure to do the same in
// implementation::CreateChild().
profile->Origin(OriginTag::User);
profile->Name(profile->Name());
profile->Hidden(profile->Hidden());
}
}
}
// Let's say a user doesn't know that they need to write `"hidden": true` in
// order to prevent a profile from showing up (and a settings UI doesn't exist).
// Naturally they would open settings.json and try to remove the profile object.
// This section of code recognizes if a profile was seen before and marks it as
// `"hidden": true` by default and thus ensures the behavior the user expects:
// Profiles won't show up again after they've been removed from settings.json.
//
// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::DisableDeletedProfiles()
{
const auto& state = winrt::get_self<ApplicationState>(ApplicationState::SharedInstance());
auto generatedProfileIds = state->GeneratedProfiles();
auto newGeneratedProfiles = false;
for (const auto& profile : _getNonUserOriginProfiles())
{
if (generatedProfileIds.emplace(profile->Guid()).second)
{
newGeneratedProfiles = true;
}
else
{
profile->Deleted(true);
profile->Hidden(true);
}
}
if (newGeneratedProfiles)
{
state->GeneratedProfiles(generatedProfileIds);
}
return newGeneratedProfiles;
}
bool winrt::Microsoft::Terminal::Settings::Model::implementation::SettingsLoader::RemapColorSchemeForProfile(const winrt::com_ptr<winrt::Microsoft::Terminal::Settings::Model::implementation::Profile>& profile)
{
bool modified{ false };
const IAppearanceConfig appearances[] = {
profile->DefaultAppearance(),
profile->UnfocusedAppearance()
};
for (auto&& appearance : appearances)
{
if (appearance)
{
if (auto schemeName{ appearance.LightColorSchemeName() }; !schemeName.empty())
{
if (auto found{ userSettings.colorSchemeRemappings.find(schemeName) }; found != userSettings.colorSchemeRemappings.end())
{
appearance.LightColorSchemeName(found->second);
modified = true;
}
}
if (auto schemeName{ appearance.DarkColorSchemeName() }; !schemeName.empty())
{
if (auto found{ userSettings.colorSchemeRemappings.find(schemeName) }; found != userSettings.colorSchemeRemappings.end())
{
appearance.DarkColorSchemeName(found->second);
modified = true;
}
}
}
}
return modified;
}
// Runs migrations and fixups on user settings.
// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::FixupUserSettings()
{
struct CommandlinePatch
{
winrt::guid guid{};
std::wstring_view before;
std::wstring_view after;
};
static constexpr std::array commandlinePatches{
CommandlinePatch{ DEFAULT_COMMAND_PROMPT_GUID, L"cmd.exe", L"%SystemRoot%\\System32\\cmd.exe" },
CommandlinePatch{ DEFAULT_WINDOWS_POWERSHELL_GUID, L"powershell.exe", L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" },
};
static constexpr std::array iconsToClearFromVisualStudioProfiles{
std::wstring_view{ L"ms-appx:///ProfileIcons/{61c54bbd-c2c6-5271-96e7-009a87ff44bf}.png" },
std::wstring_view{ L"ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png" },
};
auto fixedUp = userSettings.fixupsAppliedDuringLoad;
fixedUp = userSettings.globals->FixupsAppliedDuringLoad() || fixedUp;
fixedUp = RemapColorSchemeForProfile(userSettings.baseLayerProfile) || fixedUp;
for (const auto& profile : userSettings.profiles)
{
fixedUp = RemapColorSchemeForProfile(profile) || fixedUp;
if (profile->HasCommandline())
{
for (const auto& patch : commandlinePatches)
{
if (profile->Guid() == patch.guid && til::equals_insensitive_ascii(profile->Commandline(), patch.before))
{
profile->ClearCommandline();
// GH#12842:
// With the commandline field on the user profile gone, it's actually unknown what
// commandline it'll inherit, since a user profile can have multiple parents. We have to
// make sure we restore the correct commandline in case we don't inherit the expected one.
if (profile->Commandline() != patch.after)
{
profile->Commandline(winrt::hstring{ patch.after });
}
fixedUp = true;
break;
}
}
}
if (profile->HasIcon() && profile->HasSource() && profile->Source() == VisualStudioGenerator::Namespace)
{
for (auto&& icon : iconsToClearFromVisualStudioProfiles)
{
if (profile->Icon() == icon)
{
profile->ClearIcon();
fixedUp = true;
break;
}
}
}
}
// Terminal 1.19: Migrate the global
// `compatibility.reloadEnvironmentVariables` to being a per-profile
// setting. If the user had it disabled in 1.18, then set the
// profiles.defaults value to false to match.
if (!userSettings.globals->LegacyReloadEnvironmentVariables())
{
// migrate the user's opt-out to the profiles.defaults
userSettings.baseLayerProfile->ReloadEnvironmentVariables(false);
fixedUp = true;
}
// Terminal 1.23: Migrate the global
// `experimental.input.forceVT` to being a per-profile setting.
if (userSettings.globals->LegacyForceVTInput())
{
// migrate the user's opt-out to the profiles.defaults
userSettings.baseLayerProfile->ForceVTInput(true);
fixedUp = true;
}
return fixedUp;
}
// Give a string of length N and a position of [0,N) this function returns
// the line/column within the string, similar to how text editors do it.
// Newlines are considered part of the current line (as per POSIX).
std::pair<size_t, size_t> SettingsLoader::_lineAndColumnFromPosition(const std::string_view& string, const size_t position)
{
size_t line = 1;
size_t column = 0;
for (;;)
{
const auto p = string.find('\n', column);
if (p >= position)
{
break;
}
column = p + 1;
line++;
}
return { line, position - column + 1 };
}
// Formats a JSON exception for humans to read and throws that.
void SettingsLoader::_rethrowSerializationExceptionWithLocationInfo(const JsonUtils::DeserializationError& e, const std::string_view& settingsString)
{
std::string jsonValueAsString;
try
{
jsonValueAsString = e.jsonValue.asString();
if (e.jsonValue.isString())
{
jsonValueAsString = fmt::format("\"{}\"", jsonValueAsString);
}
}
catch (...)
{
jsonValueAsString = "array or object";
}
const auto [line, column] = _lineAndColumnFromPosition(settingsString, static_cast<size_t>(e.jsonValue.getOffsetStart()));
fmt::memory_buffer msg;
fmt::format_to(std::back_inserter(msg), "* Line {}, Column {}", line, column);
if (e.key)
{
fmt::format_to(std::back_inserter(msg), " ({})", *e.key);
}
fmt::format_to(std::back_inserter(msg), "\n Have: {}\n Expected: {}\0", jsonValueAsString, e.expectedType);
throw SettingsTypedDeserializationException{ msg.data() };
}
// Simply parses the given content to a Json::Value.
Json::Value SettingsLoader::_parseJSON(const std::string_view& content)
{
Json::Value json;
std::string errs;
const std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder{}.newCharReader() };
if (!reader->parse(content.data(), content.data() + content.size(), &json, &errs))
{
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
return json;
}
// A helper method similar to Json::Value::operator[], but compatible with std::string_view.
const Json::Value& SettingsLoader::_getJSONValue(const Json::Value& json, const std::string_view& key) noexcept
{
if (json.isObject())
{
if (const auto val = json.find(key.data(), key.data() + key.size()))
{
return *val;
}
}
return Json::Value::nullSingleton();
}
// We treat userSettings.profiles as an append-only array and will
// append profiles into the userSettings as necessary in this function.
// _userProfileCount stores the number of profiles that were in userJSON during construction.
//
// Thus no matter how many profiles are added later on, the following condition holds true:
// The userSettings.profiles in the range [0, _userProfileCount) contain all profiles specified by the user.
// In turn all profiles in the range [_userProfileCount, ∞) contain newly generated/added profiles.
// std::span{ userSettings.profiles }.subspan(_userProfileCount) gets us the latter range.
std::span<const winrt::com_ptr<Profile>> SettingsLoader::_getNonUserOriginProfiles() const
{
return std::span{ userSettings.profiles }.subspan(_userProfileCount);
}
// Parses the given JSON string ("content") and fills a ParsedSettings instance with it.
// This function is to be used for user settings files.
void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings)
{
const auto json = _parseJson(content);
settings.clear();
{
settings.globals = GlobalAppSettings::FromJson(json.root, origin);
for (const auto& schemeJson : json.colorSchemes)
{
if (const auto scheme = ColorScheme::FromJson(schemeJson))
{
scheme->Origin(origin);
settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
}
}
}
{
for (const auto& themeJson : json.themes)
{
if (const auto theme = Theme::FromJson(themeJson))
{
const auto& name{ theme->Name() };
if (origin != OriginTag::InBox &&
(std::ranges::find(builtinThemes, name) != builtinThemes.end()))
{
// If the theme didn't come from the in-box themes, and its
// name was one of the reserved names, then just ignore it.
// Themes don't support layering - we don't want the user
// 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);
}
}
}
{
settings.baseLayerProfile = Profile::FromJson(json.profileDefaults);
// Remove the `guid` member from the default settings.
// That will hyper-explode, so just don't let them do that.
settings.baseLayerProfile->ClearGuid();
settings.baseLayerProfile->Origin(OriginTag::ProfilesDefaults);
}
{
const auto size = json.profilesList.size();
settings.profiles.reserve(size);
settings.profilesByGuid.reserve(size);
for (const auto& profileJson : json.profilesList)
{
auto profile = _parseProfile(origin, source, profileJson);
// GH#9962: Discard Guid-less, Name-less profiles.
if (profile->HasGuid())
{
_appendProfile(std::move(profile), profile->Guid(), settings);
}
}
}
}
// Just like _parse, but is to be used for fragment files, which don't support anything but color
// schemes and profiles. Additionally this function supports profiles which specify an "updates" key.
void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings)
{
auto json = _parseJson(content);
settings.clear();
{
settings.globals = winrt::make_self<GlobalAppSettings>();
for (const auto& schemeJson : json.colorSchemes)
{
try
{
if (const auto scheme = ColorScheme::FromJson(schemeJson))
{
scheme->Origin(OriginTag::Fragment);
// Don't add the color scheme to the Fragment's GlobalSettings; that will
// cause layering issues later. Add them to a staging area for later processing.
// (search for STAGED COLORS to find the next step)
settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
}
}
CATCH_LOG()
}
// Parse out actions from the fragment. Manually opt-out of keybinding
// parsing - fragments shouldn't be allowed to bind actions to keys
// directly. We may want to revisit circa GH#2205
settings.globals->LayerActionsFrom(json.root, OriginTag::Fragment, false);
}
{
const auto size = json.profilesList.size();
settings.profiles.reserve(size);
settings.profilesByGuid.reserve(size);
for (const auto& profileJson : json.profilesList)
{
try
{
auto profile = _parseProfile(OriginTag::Fragment, source, profileJson);
// GH#9962: Discard Guid-less, Name-less profiles, but...
// allow ones with an Updates field, as those are special for fragments.
// We need to make sure to only call Guid() if HasGuid() is true,
// as Guid() will dynamically generate a return value otherwise.
const auto guid = profile->HasGuid() ? profile->Guid() : profile->Updates();
if (guid != winrt::guid{})
{
_appendProfile(std::move(profile), guid, settings);
}
}
CATCH_LOG()
}
}
for (const auto& fragmentProfile : settings.profiles)
{
if (const auto updates = fragmentProfile->Updates(); updates != winrt::guid{})
{
if (const auto it = userSettings.profilesByGuid.find(updates); it != userSettings.profilesByGuid.end())
{
it->second->AddMostImportantParent(fragmentProfile);
}
}
else
{
_addUserProfileParent(fragmentProfile);
}
}
// STAGED COLORS are processed here: we merge them into the partially-loaded
// settings directly so that we can resolve conflicts between user-generated
// color schemes and fragment-originated ones.
for (const auto& fragmentColorScheme : settings.colorSchemes)
{
_addOrMergeUserColorScheme(fragmentColorScheme.second);
}
// Add the parsed fragment globals as a parent of the user's settings.
// Later, in FinalizeInheritance, this will result in the action map from
// the fragments being applied before the user's own settings.
userSettings.globals->AddLeastImportantParent(settings.globals);
}
SettingsLoader::JsonSettings SettingsLoader::_parseJson(const std::string_view& content)
{
auto root = content.empty() ? Json::Value{ Json::ValueType::objectValue } : _parseJSON(content);
const auto& colorSchemes = _getJSONValue(root, SchemesKey);
const auto& themes = _getJSONValue(root, ThemesKey);
const auto& profilesObject = _getJSONValue(root, ProfilesKey);
const auto& profileDefaults = _getJSONValue(profilesObject, DefaultSettingsKey);
const auto& profilesList = profilesObject.isArray() ? profilesObject : _getJSONValue(profilesObject, ProfilesListKey);
return JsonSettings{ std::move(root), colorSchemes, profileDefaults, profilesList, themes };
}
// Just a common helper function between _parse and _parseFragment.
// Parses a profile and ensures it has a Guid if possible.
winrt::com_ptr<Profile> SettingsLoader::_parseProfile(const OriginTag origin, const winrt::hstring& source, const Json::Value& profileJson)
{
auto profile = Profile::FromJson(profileJson);
profile->Origin(origin);
// The Guid() generation below depends on the value of Source().
// --> Provide one if we got one.
if (!source.empty())
{
profile->Source(source);
}
// If none exists. the Guid() getter generates one from Name() and optionally Source().
// We want to ensure that every profile has a GUID no matter what, not just to
// cache the value, but also to make them consistently identifiable later on.
if (!profile->HasGuid() && profile->HasName())
{
profile->Guid(profile->Guid());
}
return profile;
}
// Adds a profile to the ParsedSettings instance. Takes ownership of the profile.
// It ensures no duplicate GUIDs are added to the ParsedSettings instance.
void SettingsLoader::_appendProfile(winrt::com_ptr<Profile>&& profile, const winrt::guid& guid, ParsedSettings& settings)
{
// FYI: The static_cast ensures we don't move the profile into
// `profilesByGuid`, even though we still need it later for `profiles`.
if (settings.profilesByGuid.emplace(guid, static_cast<const winrt::com_ptr<Profile>&>(profile)).second)
{
settings.profiles.emplace_back(profile);
}
else
{
duplicateProfile = true;
}
}
// If the given ParsedSettings instance contains a profile with the given profile's GUID,
// the profile is added as a parent. Otherwise a new child profile is created.
void SettingsLoader::_addUserProfileParent(const winrt::com_ptr<implementation::Profile>& profile)
{
if (const auto [it, inserted] = userSettings.profilesByGuid.emplace(profile->Guid(), nullptr); !inserted)
{
// If inserted is false, we got a matching user profile with identical GUID.
// --> The generated profile is a parent of the existing user profile.
it->second->AddLeastImportantParent(profile);
}
else
{
// If inserted is true, then this is a generated profile that doesn't exist
// in the user's settings (which makes this branch somewhat unlikely).
//
// When a user modifies a profile they shouldn't modify the (static/constant)
// inbox profile of course. That's why we need to create a child.
// And since we previously added the (now) parent profile into profilesByGuid
// we'll have to replace it->second with the (new) child profile.
//
// These additional things are required to complete a (user) profile:
// * A call to _FinalizeInheritance()
// * Every profile should at least have Origin(), Name() and Hidden() set
// They're handled by SettingsLoader::FinalizeLayering() and detected by
// the missing Origin(). Setting these fields as late as possible ensures
// that we pick up the correct, inherited values of all of the child's parents.
//
// If you add more fields here, make sure to do the same in
// implementation::CreateChild().
auto child = winrt::make_self<Profile>();
child->AddLeastImportantParent(profile);
child->Guid(profile->Guid());
// If profile is a dynamic/generated profile, a fragment's
// Source() should have no effect on this user profile.
if (profile->HasSource())
{
child->Source(profile->Source());
}
it->second = child;
userSettings.profiles.emplace_back(std::move(child));
}
}
void SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& newScheme)
{
// On entry, all the user color schemes have been loaded. Therefore, any insertions of inbox or fragment schemes
// will fail; we can leverage this to detect when they are equivalent and delete the user's duplicate copies.
// If the user has changed the otherwise "duplicate" scheme, though, we will move it aside.
if (const auto [it, inserted] = userSettings.colorSchemes.emplace(newScheme->Name(), newScheme); !inserted)
{
// This scheme was not inserted because one already existed.
auto existingScheme{ it->second };
if (existingScheme->Origin() == OriginTag::User) // we only want to impose ordering on User schemes
{
it->second = newScheme; // Stomp the user's existing scheme with the one we just got (to make sure the right Origin is set)
userSettings.fixupsAppliedDuringLoad = true; // Make sure we save the settings.
if (!existingScheme->IsEquivalentForSettingsMergePurposes(newScheme))
{
hstring newName{ fmt::format(FMT_COMPILE(L"{} (modified)"), existingScheme->Name()) };
int differentiator = 2;
while (userSettings.colorSchemes.contains(newName))
{
newName = hstring{ fmt::format(FMT_COMPILE(L"{} (modified {})"), existingScheme->Name(), differentiator++) };
}
// Rename the user's scheme.
existingScheme->Name(newName);
userSettings.colorSchemeRemappings.emplace(newScheme->Name(), newName);
// And re-add it to the end.
userSettings.colorSchemes.emplace(newName, std::move(existingScheme));
}
}
}
}
// As the name implies it executes a generator.
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator)
{
const auto generatorNamespace = generator.GetNamespace();
if (_ignoredNamespaces.contains(generatorNamespace))
{
return;
}
const auto previousSize = inboxSettings.profiles.size();
try
{
generator.GenerateProfiles(inboxSettings.profiles);
}
CATCH_LOG_MSG("Dynamic Profile Namespace: \"%.*s\"", gsl::narrow<int>(generatorNamespace.size()), generatorNamespace.data())
// If the generator produced some profiles we're going to give them default attributes.
// By setting the Origin/Source/etc. here, we deduplicate some code and ensure they aren't missing accidentally.
if (inboxSettings.profiles.size() > previousSize)
{
const winrt::hstring source{ generatorNamespace };
for (const auto& profile : std::span(inboxSettings.profiles).subspan(previousSize))
{
profile->Origin(OriginTag::Generated);
profile->Source(source);
}
}
}
// Method Description:
// - Creates a CascadiaSettings from whatever's saved on disk, or instantiates
// a new one with the default values. If we're running as a packaged app,
// it will load the settings from our packaged localappdata. If we're
// running as an unpackaged application, it will read it from the path
// we've set under localappdata.
// - Loads both the settings from the defaults.json and the user's settings.json
// - Also runs and dynamic profile generators. If any of those generators create
// new profiles, we'll write the user settings back to the file, with the new
// profiles inserted into their list of profiles.
// Return Value:
// - a unique_ptr containing a new CascadiaSettings object.
Model::CascadiaSettings CascadiaSettings::LoadAll()
try
{
FILETIME lastWriteTime{};
auto settingsString = til::io::read_file_as_utf8_string_if_exists(_settingsPath(), false, &lastWriteTime);
auto firstTimeSetup = settingsString.empty();
// If it's the firstTimeSetup and a preview build, then try to
// read settings.json from the Release stable file path if it exists.
// Otherwise use default settings file provided from original settings file
bool releaseSettingExists = false;
if (firstTimeSetup && !IsPortableMode())
{
#if defined(WT_BRANDING_PREVIEW) || defined(WT_BRANDING_CANARY)
{
try
{
settingsString = til::io::read_file_as_utf8_string_if_exists(_releaseSettingsPath());