// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "CascadiaSettings.h" #include "CascadiaSettings.g.cpp" #include "DefaultTerminal.h" #include "FileUtils.h" #include #include #include #include #include using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings; using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; using namespace winrt::Microsoft::Terminal::Control; using namespace winrt::Windows::Foundation::Collections; using namespace Microsoft::Console; // Creating a child of a profile requires us to copy certain // required attributes. This method handles those attributes. // // NOTE however that it doesn't call _FinalizeInheritance() for you! Don't forget that! // // At the time of writing only one caller needs to call _FinalizeInheritance(), // which is why this unsafety wasn't further abstracted away. winrt::com_ptr Model::implementation::CreateChild(const winrt::com_ptr& parent) { auto profile = winrt::make_self(); profile->Origin(OriginTag::User); profile->Name(parent->Name()); profile->Guid(parent->Guid()); profile->Hidden(parent->Hidden()); profile->InsertParent(parent); return profile; } Model::CascadiaSettings CascadiaSettings::Copy() const { const auto settings{ winrt::make_self() }; // user settings { std::vector allProfiles; std::vector activeProfiles; allProfiles.reserve(_allProfiles.Size()); activeProfiles.reserve(_activeProfiles.Size()); // Clone the graph of profiles. // _baseLayerProfile is part of the graph // and thus needs to be handled here as well. { std::vector> sourceProfiles; std::vector> targetProfiles; sourceProfiles.reserve(allProfiles.size()); targetProfiles.reserve(allProfiles.size()); for (const auto& profile : _allProfiles) { winrt::com_ptr profileImpl; profileImpl.copy_from(winrt::get_self(profile)); sourceProfiles.emplace_back(std::move(profileImpl)); } // Profiles are basically a directed acyclic graph. Cloning it without creating duplicated nodes, // requires us to "intern" visited profiles. Thus the "visited" map contains a cache of // previously cloned profiles/sub-graphs. It maps from source-profile-pointer to cloned-profile. std::unordered_map> visited; // I'm just gonna estimate that each profile has 3 parents at most on average: // * base layer // * fragment // * inbox defaults visited.reserve(sourceProfiles.size() * 3); // _baseLayerProfile is part of the profile graph. // In order to get a reference to the clone, we need to copy it explicitly. settings->_baseLayerProfile = _baseLayerProfile->CopyInheritanceGraph(visited); Profile::CopyInheritanceGraphs(visited, sourceProfiles, targetProfiles); for (const auto& profile : targetProfiles) { allProfiles.emplace_back(*profile); if (!profile->Hidden()) { activeProfiles.emplace_back(*profile); } } } settings->_globals = _globals->Copy(); settings->_allProfiles = winrt::single_threaded_observable_vector(std::move(allProfiles)); settings->_activeProfiles = winrt::single_threaded_observable_vector(std::move(activeProfiles)); } // load errors { std::vector warnings{ _warnings.Size() }; _warnings.GetMany(0, warnings); settings->_warnings = winrt::single_threaded_vector(std::move(warnings)); settings->_loadError = _loadError; settings->_deserializationErrorMessage = _deserializationErrorMessage; } // defterm settings->_currentDefaultTerminal = _currentDefaultTerminal; return *settings; } // Method Description: // - Finds a profile that matches the given GUID. If there is no profile in this // settings object that matches, returns nullptr. // Arguments: // - guid: the GUID of the profile to return. // Return Value: // - a strong reference to the profile matching the given guid, or nullptr // if there is no match. Model::Profile CascadiaSettings::FindProfile(const winrt::guid& guid) const noexcept { for (const auto& profile : _allProfiles) { if (profile.Guid() == guid) { return profile; } } return nullptr; } // Method Description: // - Returns an iterable collection of all of our Profiles. // Arguments: // - // Return Value: // - an iterable collection of all of our Profiles. IObservableVector CascadiaSettings::AllProfiles() const noexcept { return _allProfiles; } // Method Description: // - Returns an iterable collection of all of our non-hidden Profiles. // Arguments: // - // Return Value: // - an iterable collection of all of our Profiles. IObservableVector CascadiaSettings::ActiveProfiles() const noexcept { return _activeProfiles; } // Method Description: // - Returns the globally configured keybindings // Arguments: // - // Return Value: // - the globally configured keybindings Model::ActionMap CascadiaSettings::ActionMap() const noexcept { return _globals->ActionMap(); } // Method Description: // - Get a reference to our global settings // Arguments: // - // Return Value: // - a reference to our global settings Model::GlobalAppSettings CascadiaSettings::GlobalSettings() const { return *_globals; } // Method Description: // - Get a reference to our profiles.defaults object // Arguments: // - // Return Value: // - a reference to our profile.defaults object Model::Profile CascadiaSettings::ProfileDefaults() const { return *_baseLayerProfile; } // Method Description: // - Create a new profile based off the default profile settings. // Arguments: // - // Return Value: // - a reference to the new profile Model::Profile CascadiaSettings::CreateNewProfile() { if (_allProfiles.Size() == std::numeric_limits::max()) { // Shouldn't really happen return nullptr; } std::wstring newName; for (uint32_t candidateIndex = 0, count = _allProfiles.Size() + 1; candidateIndex < count; candidateIndex++) { // There is a theoretical unsigned integer wraparound, which is OK newName = fmt::format(L"Profile {}", count + candidateIndex); if (std::none_of(begin(_allProfiles), end(_allProfiles), [&](auto&& profile) { return profile.Name() == newName; })) { break; } } const auto newProfile = _createNewProfile(newName); _allProfiles.Append(*newProfile); _activeProfiles.Append(*newProfile); return *newProfile; } // Method Description: // - Duplicate a new profile based off another profile's settings // - This differs from Profile::Copy because it also copies over settings // that were not defined in the json (for example, settings that were // defined in one of the parents) // - This will not duplicate settings that were defined in profiles.defaults // however, because we do not want the json blob generated from the new profile // to contain those settings // Arguments: // - source: the Profile object we are duplicating (must not be null) // Return Value: // - a reference to the new profile Model::Profile CascadiaSettings::DuplicateProfile(const Model::Profile& source) { THROW_HR_IF_NULL(E_INVALIDARG, source); auto newName = fmt::format(L"{} ({})", source.Name(), RS_(L"CopySuffix")); // Check if this name already exists and if so, append a number for (uint32_t candidateIndex = 0, count = _allProfiles.Size() + 1; candidateIndex < count; ++candidateIndex) { if (std::none_of(begin(_allProfiles), end(_allProfiles), [&](auto&& profile) { return profile.Name() == newName; })) { break; } // There is a theoretical unsigned integer wraparound, which is OK newName = fmt::format(L"{} ({} {})", source.Name(), RS_(L"CopySuffix"), candidateIndex + 2); } const auto duplicated = _createNewProfile(newName); static constexpr auto isProfilesDefaultsOrigin = [](const auto& profile) -> bool { return profile && profile.Origin() != OriginTag::ProfilesDefaults; }; static constexpr auto isProfilesDefaultsOriginSub = [](const auto& sub) -> bool { return sub && isProfilesDefaultsOrigin(sub.SourceProfile()); }; #define NEEDS_DUPLICATION(settingName) source.Has##settingName() || isProfilesDefaultsOrigin(source.settingName##OverrideSource()) #define NEEDS_DUPLICATION_SUB(source, settingName) source.Has##settingName() || isProfilesDefaultsOriginSub(source.settingName##OverrideSource()) #define DUPLICATE_SETTING_MACRO(settingName) \ if (NEEDS_DUPLICATION(settingName)) \ { \ duplicated->settingName(source.settingName()); \ } #define DUPLICATE_SETTING_MACRO_SUB(source, target, settingName) \ if (NEEDS_DUPLICATION_SUB(source, settingName)) \ { \ target.settingName(source.settingName()); \ } // If the source is hidden and the Settings UI creates a // copy of it we don't want the copy to be hidden as well. // --> Don't do DUPLICATE_SETTING_MACRO(Hidden); DUPLICATE_SETTING_MACRO(Icon); DUPLICATE_SETTING_MACRO(CloseOnExit); DUPLICATE_SETTING_MACRO(TabTitle); DUPLICATE_SETTING_MACRO(TabColor); DUPLICATE_SETTING_MACRO(SuppressApplicationTitle); DUPLICATE_SETTING_MACRO(UseAcrylic); DUPLICATE_SETTING_MACRO(ScrollState); DUPLICATE_SETTING_MACRO(Padding); DUPLICATE_SETTING_MACRO(Commandline); DUPLICATE_SETTING_MACRO(StartingDirectory); DUPLICATE_SETTING_MACRO(AntialiasingMode); DUPLICATE_SETTING_MACRO(ForceFullRepaintRendering); DUPLICATE_SETTING_MACRO(SoftwareRendering); DUPLICATE_SETTING_MACRO(HistorySize); DUPLICATE_SETTING_MACRO(SnapOnInput); DUPLICATE_SETTING_MACRO(AltGrAliasing); DUPLICATE_SETTING_MACRO(BellStyle); { const auto font = source.FontInfo(); const auto target = duplicated->FontInfo(); DUPLICATE_SETTING_MACRO_SUB(font, target, FontFace); DUPLICATE_SETTING_MACRO_SUB(font, target, FontSize); DUPLICATE_SETTING_MACRO_SUB(font, target, FontWeight); DUPLICATE_SETTING_MACRO_SUB(font, target, FontFeatures); DUPLICATE_SETTING_MACRO_SUB(font, target, FontAxes); } { const auto appearance = source.DefaultAppearance(); const auto target = duplicated->DefaultAppearance(); DUPLICATE_SETTING_MACRO_SUB(appearance, target, ColorSchemeName); DUPLICATE_SETTING_MACRO_SUB(appearance, target, Foreground); DUPLICATE_SETTING_MACRO_SUB(appearance, target, Background); DUPLICATE_SETTING_MACRO_SUB(appearance, target, SelectionBackground); DUPLICATE_SETTING_MACRO_SUB(appearance, target, CursorColor); DUPLICATE_SETTING_MACRO_SUB(appearance, target, PixelShaderPath); DUPLICATE_SETTING_MACRO_SUB(appearance, target, IntenseTextStyle); DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImagePath); DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImageOpacity); DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImageStretchMode); DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImageAlignment); DUPLICATE_SETTING_MACRO_SUB(appearance, target, RetroTerminalEffect); DUPLICATE_SETTING_MACRO_SUB(appearance, target, CursorShape); DUPLICATE_SETTING_MACRO_SUB(appearance, target, CursorHeight); DUPLICATE_SETTING_MACRO_SUB(appearance, target, AdjustIndistinguishableColors); DUPLICATE_SETTING_MACRO_SUB(appearance, target, Opacity); } // UnfocusedAppearance is treated as a single setting, // but requires a little more legwork to duplicate properly if (NEEDS_DUPLICATION(UnfocusedAppearance)) { // It is alright to simply call CopyAppearance here instead of needing a separate function // like DuplicateAppearance since UnfocusedAppearance is treated as a single setting. const auto unfocusedAppearance = AppearanceConfig::CopyAppearance( winrt::get_self(source.UnfocusedAppearance()), winrt::weak_ref(*duplicated)); // Make sure to add the default appearance of the duplicated profile as a parent to the duplicate's UnfocusedAppearance winrt::com_ptr defaultAppearance; defaultAppearance.copy_from(winrt::get_self(duplicated->DefaultAppearance())); unfocusedAppearance->InsertParent(defaultAppearance); duplicated->UnfocusedAppearance(*unfocusedAppearance); } if (source.HasConnectionType()) { duplicated->ConnectionType(source.ConnectionType()); } _allProfiles.Append(*duplicated); _activeProfiles.Append(*duplicated); return *duplicated; } // Method Description: // - Gets our list of warnings we found during loading. These are things that we // knew were bad when we called `_ValidateSettings` last. // Return Value: // - a reference to our list of warnings. IVectorView CascadiaSettings::Warnings() const { return _warnings.GetView(); } winrt::Windows::Foundation::IReference CascadiaSettings::GetLoadingError() const { return _loadError; } winrt::hstring CascadiaSettings::GetSerializationErrorMessage() const { return _deserializationErrorMessage; } // As used by CreateNewProfile and DuplicateProfile this function // creates a new Profile instance with a random UUID and a given name. winrt::com_ptr CascadiaSettings::_createNewProfile(const std::wstring_view& name) const { // Technically there's Utils::CreateV5Uuid which we could use, but I wanted // truly globally unique UUIDs for profiles created through the settings UI. GUID guid{}; LOG_IF_FAILED(CoCreateGuid(&guid)); auto profile = CreateChild(_baseLayerProfile); profile->_FinalizeInheritance(); profile->Guid(guid); profile->Name(winrt::hstring{ name }); return profile; } // Method Description: // - Attempts to validate this settings structure. If there are critical errors // found, they'll be thrown as a SettingsLoadError. Non-critical errors, such // as not finding the default profile, will only result in an error. We'll add // all these warnings to our list of warnings, and the application can chose // to display these to the user. // Arguments: // - // Return Value: // - void CascadiaSettings::_validateSettings() { _validateAllSchemesExist(); _validateMediaResources(); _validateKeybindings(); _validateColorSchemesInCommands(); } // Method Description: // - Ensures that every profile has a valid "color scheme" set. If any profile // has a colorScheme set to a value which is _not_ the name of an actual color // scheme, we'll set the color table of the profile to something reasonable. // Arguments: // - // Return Value: // - // - Appends a SettingsLoadWarnings::UnknownColorScheme to our list of warnings if // we find any such duplicate. void CascadiaSettings::_validateAllSchemesExist() { const auto colorSchemes = _globals->ColorSchemes(); bool foundInvalidScheme = false; for (const auto& profile : _allProfiles) { for (const auto& appearance : std::array{ profile.DefaultAppearance(), profile.UnfocusedAppearance() }) { if (appearance && !colorSchemes.HasKey(appearance.ColorSchemeName())) { // Clear the user set color scheme. We'll just fallback instead. appearance.ClearColorSchemeName(); foundInvalidScheme = true; } } } if (foundInvalidScheme) { _warnings.Append(SettingsLoadWarnings::UnknownColorScheme); } } // Method Description: // - Ensures that all specified images resources (icons and background images) are valid URIs. // This does not verify that the icon or background image files are encoded as an image. // Arguments: // - // Return Value: // - // - Appends a SettingsLoadWarnings::InvalidBackgroundImage to our list of warnings if // we find any invalid background images. // - Appends a SettingsLoadWarnings::InvalidIconImage to our list of warnings if // we find any invalid icon images. void CascadiaSettings::_validateMediaResources() { bool invalidBackground{ false }; bool invalidIcon{ false }; for (auto profile : _allProfiles) { if (const auto path = profile.DefaultAppearance().ExpandedBackgroundImagePath(); !path.empty()) { // Attempt to convert the path to a URI, the ctor will throw if it's invalid/unparseable. // This covers file paths on the machine, app data, URLs, and other resource paths. try { winrt::Windows::Foundation::Uri imagePath{ path }; } catch (...) { // reset background image path profile.DefaultAppearance().ClearBackgroundImagePath(); invalidBackground = true; } } if (profile.UnfocusedAppearance()) { if (const auto path = profile.UnfocusedAppearance().ExpandedBackgroundImagePath(); !path.empty()) { // Attempt to convert the path to a URI, the ctor will throw if it's invalid/unparseable. // This covers file paths on the machine, app data, URLs, and other resource paths. try { winrt::Windows::Foundation::Uri imagePath{ path }; } catch (...) { // reset background image path profile.UnfocusedAppearance().ClearBackgroundImagePath(); invalidBackground = true; } } } // Anything longer than 2 wchar_t's _isn't_ an emoji or symbol, // so treat it as an invalid path. if (const auto icon = profile.Icon(); icon.size() > 2) { const auto iconPath{ wil::ExpandEnvironmentStringsW(icon.c_str()) }; try { winrt::Windows::Foundation::Uri imagePath{ iconPath }; } catch (...) { profile.ClearIcon(); invalidIcon = true; } } } if (invalidBackground) { _warnings.Append(SettingsLoadWarnings::InvalidBackgroundImage); } if (invalidIcon) { _warnings.Append(SettingsLoadWarnings::InvalidIcon); } } // Method Description: // - Helper to get the GUID of a profile, given an optional index and a possible // "profile" value to override that. // - First, we'll try looking up the profile for the given index. This will // either get us the GUID of the Nth profile, or the GUID of the default // profile. // - Then, if there was a Profile set in the NewTerminalArgs, we'll use that to // try and look the profile up by either GUID or name. // Arguments: // - index: if provided, the index in the list of profiles to get the GUID for. // If omitted, instead use the default profile's GUID // - newTerminalArgs: An object that may contain a profile name or GUID to // actually use. If the Profile value is not a guid, we'll treat it as a name, // and attempt to look the profile up by name instead. // Return Value: // - the GUID of the profile corresponding to this combination of index and NewTerminalArgs Model::Profile CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs& newTerminalArgs) const { if (newTerminalArgs) { if (const auto name = newTerminalArgs.Profile(); !name.empty()) { if (auto profile = GetProfileByName(name)) { return profile; } } if (const auto index = newTerminalArgs.ProfileIndex()) { if (auto profile = GetProfileByIndex(gsl::narrow(index.Value()))) { return profile; } } if (const auto commandLine = newTerminalArgs.Commandline(); !commandLine.empty()) { if (auto profile = _getProfileForCommandLine(commandLine)) { return profile; } } } if constexpr (Feature_ShowProfileDefaultsInSettings::IsEnabled()) { // If the user has access to the "Defaults" profile, and no profile was otherwise specified, // what we do is dependent on whether there was a commandline. // If there was a commandline (case 1), we we'll launch in the "Defaults" profile. // If there wasn't a commandline or there wasn't a NewTerminalArgs (case 2), we'll // launch in the user's actual default profile. // Case 2 above could be the result of a "nt" or "sp" invocation that doesn't specify anything. // TODO GH#10952: Detect the profile based on the commandline (add matching support) return (!newTerminalArgs || newTerminalArgs.Commandline().empty()) ? FindProfile(GlobalSettings().DefaultProfile()) : ProfileDefaults(); } else { // For compatibility with the stable version's behavior, return the default by GUID in all other cases. return FindProfile(GlobalSettings().DefaultProfile()); } } // The method does some crude command line matching for our console hand-off support. // If you have hand-off enabled and start PowerShell from the start menu we might be called with // "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~" // This function then checks all known user profiles for one that's compatible with the commandLine. // In this case we might have a profile with the command line // "C:\Program Files\PowerShell\7\pwsh.exe" // This function will then match this profile return it. // // If no matching profile could be found a nullptr will be returned. Model::Profile CascadiaSettings::_getProfileForCommandLine(const winrt::hstring& commandLine) const { // We're going to cache all the command lines we got, as // _normalizeCommandLine is a relatively heavy operation. std::call_once(_commandLinesCacheOnce, [this]() { _commandLinesCache.reserve(_allProfiles.Size()); for (const auto& profile : _allProfiles) { if (profile.ConnectionType() != winrt::guid{}) { continue; } const auto cmd = profile.Commandline(); if (cmd.empty()) { continue; } try { _commandLinesCache.emplace_back(_normalizeCommandLine(cmd.c_str()), profile); } CATCH_LOG() } // We're trying to find the command line with the longest common prefix below. // Given the commandLine "foo.exe -bar -baz" and these two user profiles: // * "foo.exe" // * "foo.exe -bar" // we want to choose the second one. By sorting the _commandLinesCache in a descending order // by command line length, we can return from this function the moment we found a matching // profile as there cannot possibly be any other profile anymore with a longer command line. std::stable_sort(_commandLinesCache.begin(), _commandLinesCache.end(), [](const auto& lhs, const auto& rhs) { return lhs.first.size() > rhs.first.size(); }); }); try { const auto needle = _normalizeCommandLine(commandLine.c_str()); // til::starts_with(string, prefix) will always return false if prefix.size() > string.size(). // --> Using binary search we can safely skip all items in _commandLinesCache where .first.size() > needle.size(). const auto end = _commandLinesCache.end(); auto it = std::lower_bound(_commandLinesCache.begin(), end, needle, [&](const auto& lhs, const auto& rhs) { return lhs.first.size() > rhs.size(); }); // `it` is now at a position where it->first.size() <= needle.size(). // Hopefully we'll now find a command line with matching prefix. for (; it != end; ++it) { const auto& prefix = it->first; const auto length = gsl::narrow(prefix.size()); if (CompareStringOrdinal(needle.data(), length, prefix.data(), length, TRUE) == CSTR_EQUAL) { return it->second; } } } catch (...) { LOG_CAUGHT_EXCEPTION(); } return nullptr; } // Given a commandLine like the following: // * "C:\WINDOWS\System32\cmd.exe" // * "pwsh -WorkingDirectory ~" // * "C:\Program Files\PowerShell\7\pwsh.exe" // * "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~" // // This function returns: // * "C:\Windows\System32\cmd.exe" // * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~" // * "C:\Program Files\PowerShell\7\pwsh.exe" // * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~" // // The resulting strings are then used for comparisons in _getProfileForCommandLine(). // For instance a resulting string of // "C:\Program Files\PowerShell\7\pwsh.exe" // is considered a compatible profile with // "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~" // as it shares the same (normalized) prefix. std::wstring CascadiaSettings::_normalizeCommandLine(LPCWSTR commandLine) { // Turn "%SystemRoot%\System32\cmd.exe" into "C:\WINDOWS\System32\cmd.exe". // We do this early, as environment variables might occur anywhere in the commandLine. std::wstring normalized; THROW_IF_FAILED(wil::ExpandEnvironmentStringsW(commandLine, normalized)); // One of the most important things this function does is to strip quotes. // That way the commandLine "foo.exe -bar" and "\"foo.exe\" \"-bar\"" appear identical. // We'll abuse CommandLineToArgvW for that as it's close to what CreateProcessW uses. int argc = 0; wil::unique_hlocal_ptr argv{ CommandLineToArgvW(normalized.c_str(), &argc) }; THROW_LAST_ERROR_IF(!argc); // The given commandLine should start with an executable name or path. // For instance given the following argv arrays: // * {"C:\WINDOWS\System32\cmd.exe"} // * {"pwsh", "-WorkingDirectory", "~"} // * {"C:\Program", "Files\PowerShell\7\pwsh.exe"} // ^^^^ // Notice how there used to be a space in the path, which was split by ExpandEnvironmentStringsW(). // CreateProcessW() supports such atrocities, so we got to do the same. // * {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"} // // This loop tries to resolve relative paths, as well as executable names in %PATH% // into absolute paths and normalizes them. The results for the above would be: // * "C:\Windows\System32\cmd.exe" // * "C:\Program Files\PowerShell\7\pwsh.exe" // * "C:\Program Files\PowerShell\7\pwsh.exe" // * "C:\Program Files\PowerShell\7\pwsh.exe" for (;;) { // CreateProcessW uses RtlGetExePath to get the lpPath for SearchPathW. // The difference between the behavior of SearchPathW if lpPath is nullptr and what RtlGetExePath returns // seems to be mostly whether SafeProcessSearchMode is respected and the support for relative paths. // Windows Terminal makes the use relative paths rather impractical which is why we simply dropped the call to RtlGetExePath. const auto status = wil::SearchPathW(nullptr, argv[0], L".exe", normalized); if (status == S_OK) { std::filesystem::path path{ std::move(normalized) }; // ExpandEnvironmentStringsW() might have returned a string that's not in the canonical capitalization. // For instance %SystemRoot% is set to C:\WINDOWS on my system (ugh), even though the path is actually C:\Windows. // We need to fix this as case-sensitive path comparisons will fail otherwise (Windows supports case-sensitive file systems). // If we fail to resolve the path for whatever reason (pretty unlikely given that SearchPathW found it) // we fall back to leaving the path as is. Better than throwing a random exception and making this unusable. { std::error_code ec; auto canonicalPath = std::filesystem::canonical(path, ec); if (!ec) { path = std::move(canonicalPath); } } // std::filesystem::path has no way to extract the internal path. // So about that.... I own you, computer. Give me that path. normalized = std::move(const_cast(path.native())); break; } // If the file path couldn't be found by SearchPathW this could be the result of us being given a commandLine // like "C:\foo bar\baz.exe -arg" which is resolved to the argv array {"C:\foo", "bar\baz.exe", "-arg"}. // Just like CreateProcessW() we thus try to concatenate arguments until we successfully resolve a valid path. // Of course we can only do that if we have at least 2 remaining arguments in argv. // All other error types aren't handled at the moment. if (argc < 2 || status != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { break; } // As described in the comment right above, we concatenate arguments in an attempt to resolve a valid path. // The code below turns argv from {"C:\foo", "bar\baz.exe", "-arg"} into {"C:\foo bar\baz.exe", "-arg"}. // The code abuses the fact that CommandLineToArgvW allocates all arguments back-to-back on the heap separated by '\0'. argv[1][-1] = L' '; --argc; } // We've (hopefully) finished resolving the path to the executable. // We're now going to append all remaining arguments to the resulting string. // If argv is {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"}, // then we'll get "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~" if (argc > 1) { // normalized contains a canonical form of argv[0] at this point. // -1 allows us to include the \0 between argv[0] and argv[1] in the call to append(). const auto beg = argv[1] - 1; const auto lastArg = argv[argc - 1]; const auto end = lastArg + wcslen(lastArg); normalized.append(beg, end); } return normalized; } // Method Description: // - Helper to get a profile given a name that could be a guid or an actual name. // Arguments: // - name: a guid string _or_ the name of a profile // Return Value: // - the GUID of the profile corresponding to this name Model::Profile CascadiaSettings::GetProfileByName(const winrt::hstring& name) const { // First, try and parse the "name" as a GUID. If it's a // GUID, and the GUID of one of our profiles, then use that as the // profile GUID instead. If it's not, then try looking it up as a // name of a profile. If it's still not that, then just ignore it. if (!name.empty()) { // Do a quick heuristic check - is the profile 38 chars long (the // length of a GUID string), and does it start with '{'? Because if // it doesn't, it's _definitely_ not a GUID. if (name.size() == 38 && name[0] == L'{') { const auto newGUID{ Utils::GuidFromString(name.c_str()) }; if (auto profile = FindProfile(newGUID)) { return profile; } } // Here, we were unable to use the profile string as a GUID to // lookup a profile. Instead, try using the string to look the // Profile up by name. for (auto profile : _allProfiles) { if (profile.Name() == name) { return profile; } } } return nullptr; } // Method Description: // - Helper to get the profile at the given index in the list of profiles. // - Returns a nullptr if the index is out of bounds. // Arguments: // - index: The profile index in ActiveProfiles() // Return Value: // - the Nth profile Model::Profile CascadiaSettings::GetProfileByIndex(uint32_t index) const { return index < _activeProfiles.Size() ? _activeProfiles.GetAt(index) : nullptr; } // Method Description: // - If there were any warnings we generated while parsing the user's // keybindings, add them to the list of warnings here. If there were warnings // generated in this way, we'll add a AtLeastOneKeybindingWarning, which will // act as a header for the other warnings // - GH#3522 // With variable args to keybindings, it's possible that a user // set a keybinding without all the required args for an action. // Display a warning if an action didn't have a required arg. // This will also catch other keybinding warnings, like from GH#4239. // Arguments: // - // Return Value: // - void CascadiaSettings::_validateKeybindings() const { const auto keybindingWarnings = _globals->KeybindingsWarnings(); if (!keybindingWarnings.empty()) { _warnings.Append(SettingsLoadWarnings::AtLeastOneKeybindingWarning); for (auto warning : keybindingWarnings) { _warnings.Append(warning); } } } // Method Description: // - Ensures that every "setColorScheme" command has a valid "color scheme" set. // Arguments: // - // Return Value: // - // - Appends a SettingsLoadWarnings::InvalidColorSchemeInCmd to our list of warnings if // we find any command with an invalid color scheme. void CascadiaSettings::_validateColorSchemesInCommands() const { bool foundInvalidScheme{ false }; for (const auto& nameAndCmd : _globals->ActionMap().NameMap()) { if (_hasInvalidColorScheme(nameAndCmd.Value())) { foundInvalidScheme = true; break; } } if (foundInvalidScheme) { _warnings.Append(SettingsLoadWarnings::InvalidColorSchemeInCmd); } } bool CascadiaSettings::_hasInvalidColorScheme(const Model::Command& command) const { bool invalid{ false }; if (command.HasNestedCommands()) { for (const auto& nested : command.NestedCommands()) { if (_hasInvalidColorScheme(nested.Value())) { invalid = true; break; } } } else if (const auto& actionAndArgs = command.ActionAndArgs()) { if (const auto& realArgs = actionAndArgs.Args().try_as()) { const auto cmdImpl{ winrt::get_self(command) }; // no need to validate iterable commands on color schemes // they will be expanded to commands with a valid scheme name if (cmdImpl->IterateOn() != ExpandCommandType::ColorSchemes && !_globals->ColorSchemes().HasKey(realArgs.SchemeName())) { invalid = true; } } } return invalid; } // Method Description: // - Lookup the color scheme for a given profile. If the profile doesn't exist, // or the scheme name listed in the profile doesn't correspond to a scheme, // this will return `nullptr`. // Arguments: // - profileGuid: the GUID of the profile to find the scheme for. // Return Value: // - a non-owning pointer to the scheme. Model::ColorScheme CascadiaSettings::GetColorSchemeForProfile(const Model::Profile& profile) const { if (!profile) { return nullptr; } const auto schemeName = profile.DefaultAppearance().ColorSchemeName(); return _globals->ColorSchemes().TryLookup(schemeName); } // Method Description: // - updates all references to that color scheme with the new name // Arguments: // - oldName: the original name for the color scheme // - newName: the new name for the color scheme // Return Value: // - void CascadiaSettings::UpdateColorSchemeReferences(const winrt::hstring& oldName, const winrt::hstring& newName) { // update profiles.defaults, if necessary if (_baseLayerProfile && _baseLayerProfile->DefaultAppearance().HasColorSchemeName() && _baseLayerProfile->DefaultAppearance().ColorSchemeName() == oldName) { _baseLayerProfile->DefaultAppearance().ColorSchemeName(newName); } // update all profiles referencing this color scheme for (const auto& profile : _allProfiles) { const auto defaultAppearance = profile.DefaultAppearance(); if (defaultAppearance.HasColorSchemeName() && defaultAppearance.ColorSchemeName() == oldName) { defaultAppearance.ColorSchemeName(newName); } if (profile.UnfocusedAppearance()) { if (profile.UnfocusedAppearance().HasColorSchemeName() && profile.UnfocusedAppearance().ColorSchemeName() == oldName) { profile.UnfocusedAppearance().ColorSchemeName(newName); } } } } winrt::hstring CascadiaSettings::ApplicationDisplayName() { try { const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; return package.DisplayName(); } CATCH_LOG(); return RS_(L"ApplicationDisplayNameUnpackaged"); } winrt::hstring CascadiaSettings::ApplicationVersion() { try { const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; const auto version{ package.Id().Version() }; winrt::hstring formatted{ wil::str_printf(L"%u.%u.%u.%u", version.Major, version.Minor, version.Build, version.Revision) }; return formatted; } CATCH_LOG(); // Get the product version the old-fashioned way from the localized version compartment. // // We explicitly aren't using VS_FIXEDFILEINFO here, because our build pipeline puts // a non-standard version number into the localized version field. // For instance the fixed file info might contain "1.12.2109.13002", // while the localized field might contain "1.11.210830001-release1.11". try { struct LocalizationInfo { WORD language, codepage; }; // Use the current module instance handle for TerminalApp.dll, nullptr for WindowsTerminal.exe auto filename{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; auto size{ GetFileVersionInfoSizeExW(0, filename.c_str(), nullptr) }; THROW_LAST_ERROR_IF(size == 0); auto versionBuffer{ std::make_unique(size) }; THROW_IF_WIN32_BOOL_FALSE(GetFileVersionInfoExW(0, filename.c_str(), 0, size, versionBuffer.get())); // Get the list of Version localizations LocalizationInfo* pVarLocalization{ nullptr }; UINT varLen{ 0 }; THROW_IF_WIN32_BOOL_FALSE(VerQueryValueW(versionBuffer.get(), L"\\VarFileInfo\\Translation", reinterpret_cast(&pVarLocalization), &varLen)); THROW_HR_IF(E_UNEXPECTED, varLen < sizeof(*pVarLocalization)); // there must be at least one translation // Get the product version from the localized version compartment // We're using String/ProductVersion here because our build pipeline puts more rich information in it (like the branch name) // than in the unlocalized numeric version fields. WCHAR* pProductVersion{ nullptr }; UINT versionLen{ 0 }; const auto localizedVersionName{ wil::str_printf(L"\\StringFileInfo\\%04x%04x\\ProductVersion", pVarLocalization->language ? pVarLocalization->language : 0x0409, // well-known en-US LCID pVarLocalization->codepage) }; THROW_IF_WIN32_BOOL_FALSE(VerQueryValueW(versionBuffer.get(), localizedVersionName.c_str(), reinterpret_cast(&pProductVersion), &versionLen)); return { pProductVersion }; } CATCH_LOG(); return RS_(L"ApplicationVersionUnknown"); } // Method Description: // - Determines if we're on an OS platform that supports // the default terminal handoff functionality. // Arguments: // - // Return Value: // - True if OS supports default terminal. False otherwise. bool CascadiaSettings::IsDefaultTerminalAvailable() noexcept { if (!IsPackaged()) { return false; } OSVERSIONINFOEXW osver{}; osver.dwOSVersionInfoSize = sizeof(osver); osver.dwBuildNumber = 22000; DWORDLONG dwlConditionMask = 0; VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL); return VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE; } bool CascadiaSettings::IsDefaultTerminalSet() noexcept { return DefaultTerminal::HasCurrent(); } // Method Description: // - Returns an iterable collection of all available terminals. // Arguments: // - // Return Value: // - an iterable collection of all available terminals that could be the default. IObservableVector CascadiaSettings::DefaultTerminals() noexcept { _refreshDefaultTerminals(); return _defaultTerminals; } // Method Description: // - Returns the currently selected default terminal application. // Arguments: // - // Return Value: // - the selected default terminal application Settings::Model::DefaultTerminal CascadiaSettings::CurrentDefaultTerminal() noexcept { _refreshDefaultTerminals(); return _currentDefaultTerminal; } // Method Description: // - Sets the current default terminal application // Arguments: // - terminal - Terminal from `DefaultTerminals` list to set as default // Return Value: // - void CascadiaSettings::CurrentDefaultTerminal(const Model::DefaultTerminal& terminal) { _currentDefaultTerminal = terminal; } // This function is implicitly called by DefaultTerminals/CurrentDefaultTerminal(). // It reloads the selection of available, installed terminals and caches them. // WinUI requires us that the `SelectedItem` of a collection is member of the list given to `ItemsSource`. // It's thus important that _currentDefaultTerminal is a member of _defaultTerminals. // Right now this is implicitly the case thanks to DefaultTerminal::Available(), // but in the future it might be worthwhile to change the code to use list indices instead. void CascadiaSettings::_refreshDefaultTerminals() { if (!_defaultTerminals) { auto [defaultTerminals, defaultTerminal] = DefaultTerminal::Available(); _defaultTerminals = winrt::single_threaded_observable_vector(std::move(defaultTerminals)); _currentDefaultTerminal = std::move(defaultTerminal); } } void CascadiaSettings::ExportFile(winrt::hstring path, winrt::hstring content) { try { WriteUTF8FileAtomic({ path.c_str() }, til::u16u8(content)); } CATCH_LOG(); }