From 4d094df5080dcf9e58aa76e8a255ca6ace0ddf60 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 14 May 2025 19:25:20 +0200 Subject: [PATCH 1/5] Fix one source of mouse vanishing (#18911) When the cursor is over the non-client area it wouldn't show the cursor when moving it. Now it works as expected. --- .github/actions/spelling/allow/apis.txt | 1 + src/cascadia/WindowsTerminal/IslandWindow.cpp | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 3137e5624c..6670909587 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -144,6 +144,7 @@ NCHITTEST NCLBUTTONDBLCLK NCMOUSELEAVE NCMOUSEMOVE +NCPOINTERUPDATE NCRBUTTONDBLCLK NIF NIN diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 80932f3322..7e3ef659b6 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -61,7 +61,10 @@ void IslandWindow::HideCursor() noexcept void IslandWindow::ShowCursorMaybe(const UINT message) noexcept { - if (_cursorHidden && (message == WM_ACTIVATE || message == WM_POINTERUPDATE)) + if (_cursorHidden && + (message == WM_ACTIVATE || + message == WM_POINTERUPDATE || + message == WM_NCPOINTERUPDATE)) { _cursorHidden = false; ShowCursor(TRUE); From 4d67453c02c47dfcef89861d03a2c587a039f2ab Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 14 May 2025 10:30:05 -0700 Subject: [PATCH 2/5] Replace New Tab Menu Match Profiles functionality with regex support (#18654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Updates the New Tab Menu's Match Profiles entry to support regex instead of doing a direct match. Also adds validation to ensure the regex is valid. Updated the UI to help make it more clear that this supports regexes and even added a link to some helpful docs. ## Validation Steps Performed ✅ Invalid regex displays a warning ✅ Valid regex works nicely ✅ profile matcher with source=`Windows.Terminal.VisualStudio` still works as expected ## PR Checklist Closes #18553 --- src/buffer/out/UTextAdapter.cpp | 11 --- src/buffer/out/UTextAdapter.h | 2 - src/buffer/out/textBuffer.cpp | 3 +- .../Resources/en-US/Resources.resw | 3 + src/cascadia/TerminalApp/TerminalWindow.cpp | 1 + src/cascadia/TerminalCore/Terminal.cpp | 11 +-- .../TerminalSettingsEditor/NewTabMenu.xaml | 2 + .../Resources/en-US/Resources.resw | 11 +-- .../CascadiaSettings.cpp | 37 ++++++++++ .../TerminalSettingsModel/CascadiaSettings.h | 1 + .../MatchProfilesEntry.cpp | 70 +++++++++++++------ .../MatchProfilesEntry.h | 31 +++++++- .../TerminalWarnings.idl | 1 + src/inc/til/regex.h | 22 ++++++ 14 files changed, 160 insertions(+), 46 deletions(-) create mode 100644 src/inc/til/regex.h diff --git a/src/buffer/out/UTextAdapter.cpp b/src/buffer/out/UTextAdapter.cpp index 717d97812a..1b392f3d23 100644 --- a/src/buffer/out/UTextAdapter.cpp +++ b/src/buffer/out/UTextAdapter.cpp @@ -400,17 +400,6 @@ Microsoft::Console::ICU::unique_utext Microsoft::Console::ICU::UTextFromTextBuff return ut; } -Microsoft::Console::ICU::unique_uregex Microsoft::Console::ICU::CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept -{ -#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1). - const auto re = uregex_open(reinterpret_cast(pattern.data()), gsl::narrow_cast(pattern.size()), flags, nullptr, status); - // ICU describes the time unit as being dependent on CPU performance and "typically [in] the order of milliseconds", - // but this claim seems highly outdated already. On my CPU from 2021, a limit of 4096 equals roughly 600ms. - uregex_setTimeLimit(re, 4096, status); - uregex_setStackLimit(re, 4 * 1024 * 1024, status); - return unique_uregex{ re }; -} - // Returns a half-open [beg,end) range given a text start and end position. // This function is designed to be used with uregex_start64/uregex_end64. til::point_span Microsoft::Console::ICU::BufferRangeFromMatch(UText* ut, URegularExpression* re) diff --git a/src/buffer/out/UTextAdapter.h b/src/buffer/out/UTextAdapter.h index 39903627b5..fe6bacafd4 100644 --- a/src/buffer/out/UTextAdapter.h +++ b/src/buffer/out/UTextAdapter.h @@ -9,10 +9,8 @@ class TextBuffer; namespace Microsoft::Console::ICU { - using unique_uregex = wistd::unique_ptr>; using unique_utext = wil::unique_struct; unique_utext UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept; - unique_uregex CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept; til::point_span BufferRangeFromMatch(UText* ut, URegularExpression* re); } diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 095195133b..11f48ec0f6 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -10,6 +10,7 @@ #include "../../types/inc/CodepointWidthDetector.hpp" #include "../renderer/base/renderer.hpp" #include "../types/inc/utils.hpp" +#include #include "search.h" // BODGY: Misdiagnosis in MSVC 17.11: Referencing global constants in the member @@ -3353,7 +3354,7 @@ std::optional> TextBuffer::SearchText(const std::ws } UErrorCode status = U_ZERO_ERROR; - const auto re = ICU::CreateRegex(needle, icuFlags, &status); + const auto re = til::ICU::CreateRegex(needle, icuFlags, &status); if (status > U_ZERO_ERROR) { return std::nullopt; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 74d09214ba..79a9155134 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -944,4 +944,7 @@ Move right + + An invalid regex was found. + \ No newline at end of file diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index ff3a66acae..9776679691 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -55,6 +55,7 @@ static const std::array settingsLoadWarningsLabels{ USES_RESOURCE(L"UnknownTheme"), USES_RESOURCE(L"DuplicateRemainingProfilesEntry"), USES_RESOURCE(L"InvalidUseOfContent"), + USES_RESOURCE(L"InvalidRegex"), }; static_assert(settingsLoadWarningsLabels.size() == static_cast(SettingsLoadWarnings::WARNINGS_SIZE)); diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 91def44107..68d7178187 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -12,6 +12,7 @@ #include "../../buffer/out/UTextAdapter.h" #include +#include #include using namespace winrt::Microsoft::Terminal::Core; @@ -1375,7 +1376,7 @@ struct URegularExpressionInterner // // An alternative approach would be to not make this method thread-safe and give each // Terminal instance its own cache. I'm not sure which approach would have been better. - ICU::unique_uregex Intern(const std::wstring_view& pattern) + til::ICU::unique_uregex Intern(const std::wstring_view& pattern) { UErrorCode status = U_ZERO_ERROR; @@ -1383,14 +1384,14 @@ struct URegularExpressionInterner const auto guard = _lock.lock_shared(); if (const auto it = _cache.find(pattern); it != _cache.end()) { - return ICU::unique_uregex{ uregex_clone(it->second.re.get(), &status) }; + return til::ICU::unique_uregex{ uregex_clone(it->second.re.get(), &status) }; } } // Even if the URegularExpression creation failed, we'll insert it into the cache, because there's no point in retrying. // (Apart from OOM but in that case this application will crash anyways in 3.. 2.. 1..) - auto re = ICU::CreateRegex(pattern, 0, &status); - ICU::unique_uregex clone{ uregex_clone(re.get(), &status) }; + auto re = til::ICU::CreateRegex(pattern, 0, &status); + til::ICU::unique_uregex clone{ uregex_clone(re.get(), &status) }; std::wstring key{ pattern }; const auto guard = _lock.lock_exclusive(); @@ -1412,7 +1413,7 @@ struct URegularExpressionInterner private: struct CacheValue { - ICU::unique_uregex re; + til::ICU::unique_uregex re; size_t generation = 0; }; diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml index 018a00d532..54acab56e1 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml @@ -449,6 +449,8 @@ FontIconGlyph="" Style="{StaticResource ExpanderSettingContainerStyleWithIcon}"> + Header for a control that adds any remaining profiles to the new tab menu. - Add a group of profiles that match at least one of the defined properties + Add a group of profiles that match at least one of the defined regex properties Additional information for a control that adds a terminal profile matcher to the new tab menu. Presented near "NewTabMenu_AddMatchProfiles". @@ -2121,15 +2121,15 @@ Header for a control that adds a folder to the new tab menu. - Profile name + Profile name (Regex) Header for a text box used to define a regex for the names of profiles to add. - Profile source + Profile source (Regex) Header for a text box used to define a regex for the sources of profiles to add. - Commandline + Commandline (Regex) Header for a text box used to define a regex for the commandlines of profiles to add. @@ -2344,6 +2344,9 @@ This option is managed by enterprise policy and cannot be changed here. This is displayed in concordance with Globals_StartOnUserLogin if the enterprise administrator has taken control of this setting. + + Learn more about regular expressions + None Text displayed when the background image path is not defined. diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index eeb3d7cc2e..9dc2433403 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "CascadiaSettings.h" #include "CascadiaSettings.g.cpp" +#include "MatchProfilesEntry.h" #include "DefaultTerminal.h" #include "FileUtils.h" @@ -429,6 +430,7 @@ void CascadiaSettings::_validateSettings() _validateColorSchemesInCommands(); _validateThemeExists(); _validateProfileEnvironmentVariables(); + _validateRegexes(); } // Method Description: @@ -583,6 +585,41 @@ void CascadiaSettings::_validateProfileEnvironmentVariables() } } +// Returns true if all regexes in the new tab menu are valid, false otherwise +static bool _validateNTMEntries(const IVector& entries) +{ + if (!entries) + { + return true; + } + for (const auto& ntmEntry : entries) + { + if (const auto& folderEntry = ntmEntry.try_as()) + { + if (!_validateNTMEntries(folderEntry.RawEntries())) + { + return false; + } + } + if (const auto& matchProfilesEntry = ntmEntry.try_as()) + { + if (!winrt::get_self(matchProfilesEntry)->ValidateRegexes()) + { + return false; + } + } + } + return true; +} + +void CascadiaSettings::_validateRegexes() +{ + if (!_validateNTMEntries(_globals->NewTabMenu())) + { + _warnings.Append(SettingsLoadWarnings::InvalidRegex); + } +} + // Method Description: // - Helper to get the GUID of a profile, given an optional index and a possible // "profile" value to override that. diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index e91c43885e..a6e50e370a 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -175,6 +175,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _validateColorSchemesInCommands() const; bool _hasInvalidColorScheme(const Model::Command& command) const; void _validateThemeExists(); + void _validateRegexes(); void _researchOnLoad(); diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp index 6946eb98e8..5dc4021954 100644 --- a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp @@ -36,41 +36,71 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto entry = winrt::make_self(); JsonUtils::GetValueForKey(json, NameKey, entry->_Name); + entry->_validateName(); + JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline); + entry->_validateCommandline(); + JsonUtils::GetValueForKey(json, SourceKey, entry->_Source); + entry->_validateSource(); return entry; } + // Returns true if all regexes are valid, false otherwise + bool MatchProfilesEntry::ValidateRegexes() const + { + return !(_invalidName || _invalidCommandline || _invalidSource); + } + +#define DEFINE_VALIDATE_FUNCTION(name) \ + void MatchProfilesEntry::_validate##name() noexcept \ + { \ + _invalid##name = false; \ + if (_##name.empty()) \ + { \ + /* empty field is valid*/ \ + return; \ + } \ + UErrorCode status = U_ZERO_ERROR; \ + _##name##Regex = til::ICU::CreateRegex(_##name, 0, &status); \ + if (U_FAILURE(status)) \ + { \ + _invalid##name = true; \ + _##name##Regex.reset(); \ + } \ + } + + DEFINE_VALIDATE_FUNCTION(Name); + DEFINE_VALIDATE_FUNCTION(Commandline); + DEFINE_VALIDATE_FUNCTION(Source); + bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile) { - // We use an optional here instead of a simple bool directly, since there is no - // sensible default value for the desired semantics: the first property we want - // to match on should always be applied (so one would set "true" as a default), - // but if none of the properties are set, the default return value should be false - // since this entry type is expected to behave like a positive match/whitelist. - // - // The easiest way to deal with this neatly is to use an optional, then for any - // property to match we consider a null value to be "true", and for the return - // value of the function we consider the null value to be "false". - auto isMatching = std::optional{}; + auto isMatch = [](const til::ICU::unique_uregex& regex, std::wstring_view text) { + if (text.empty()) + { + return false; + } + UErrorCode status = U_ZERO_ERROR; + uregex_setText(regex.get(), reinterpret_cast(text.data()), static_cast(text.size()), &status); + const auto match = uregex_matches(regex.get(), 0, &status); + return status == U_ZERO_ERROR && match; + }; - if (!_Name.empty()) + if (!_Name.empty() && isMatch(_NameRegex, profile.Name())) { - isMatching = { isMatching.value_or(true) && _Name == profile.Name() }; + return true; } - - if (!_Source.empty()) + else if (!_Source.empty() && isMatch(_SourceRegex, profile.Source())) { - isMatching = { isMatching.value_or(true) && _Source == profile.Source() }; + return true; } - - if (!_Commandline.empty()) + else if (!_Commandline.empty() && isMatch(_CommandlineRegex, profile.Commandline())) { - isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() }; + return true; } - - return isMatching.value_or(false); + return false; } Model::NewTabMenuEntry MatchProfilesEntry::Copy() const diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h index 815d1be3e8..8849291bbf 100644 --- a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h @@ -17,6 +17,30 @@ Author(s): #include "ProfileCollectionEntry.h" #include "MatchProfilesEntry.g.h" +#include + +// This macro defines the getter and setter for a regex property. +// The setter tries to instantiate the regex immediately and caches +// it if successful. If it fails, it sets a boolean flag to track that +// it failed. +#define DEFINE_MATCH_PROFILE_REGEX_PROPERTY(name) \ +public: \ + hstring name() const noexcept \ + { \ + return _##name; \ + } \ + void name(const hstring& value) noexcept \ + { \ + _##name = value; \ + _validate##name(); \ + } \ + \ +private: \ + void _validate##name() noexcept; \ + \ + hstring _##name; \ + til::ICU::unique_uregex _##name##Regex; \ + bool _invalid##name{ false }; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { @@ -30,11 +54,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value ToJson() const override; static com_ptr FromJson(const Json::Value& json); + bool ValidateRegexes() const; bool MatchesProfile(const Model::Profile& profile); - WINRT_PROPERTY(winrt::hstring, Name); - WINRT_PROPERTY(winrt::hstring, Commandline); - WINRT_PROPERTY(winrt::hstring, Source); + DEFINE_MATCH_PROFILE_REGEX_PROPERTY(Name) + DEFINE_MATCH_PROFILE_REGEX_PROPERTY(Commandline) + DEFINE_MATCH_PROFILE_REGEX_PROPERTY(Source) }; } diff --git a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl index aa02d18ff4..c8923ce34a 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl @@ -25,6 +25,7 @@ namespace Microsoft.Terminal.Settings.Model UnknownTheme, DuplicateRemainingProfilesEntry, InvalidUseOfContent, + InvalidRegex, WARNINGS_SIZE // IMPORTANT: This MUST be the last value in this enum. It's an unused placeholder. }; diff --git a/src/inc/til/regex.h b/src/inc/til/regex.h new file mode 100644 index 0000000000..0ddc84eda6 --- /dev/null +++ b/src/inc/til/regex.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +namespace til::ICU // Terminal Implementation Library. Also: "Today I Learned" +{ + using unique_uregex = wistd::unique_ptr>; + + _TIL_INLINEPREFIX unique_uregex CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept + { +#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1). + const auto re = uregex_open(reinterpret_cast(pattern.data()), gsl::narrow_cast(pattern.size()), flags, nullptr, status); + // ICU describes the time unit as being dependent on CPU performance and "typically [in] the order of milliseconds", + // but this claim seems highly outdated already. On my CPU from 2021, a limit of 4096 equals roughly 600ms. + uregex_setTimeLimit(re, 4096, status); + uregex_setStackLimit(re, 4 * 1024 * 1024, status); + return unique_uregex{ re }; + } +} \ No newline at end of file From 92e8be41fd56c6b152caaf8bd806ac310242c429 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 14 May 2025 10:34:12 -0700 Subject: [PATCH 3/5] Load .wt.json snippets from parent directories (#18904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Adds logic such that local snippets from .wt.json files are imported not only at the current directory, but also in the parent directories. ## References and Relevant Issues Spec: https://github.com/microsoft/terminal/blob/main/doc/specs/%231595%20-%20Suggestions%20UI/Snippets.md ## Validation Steps Performed `D:\test\.wt.json`: contains `Hello --> hello (parent)` `D:\test\inner_dir\.wt.json`: contains `Hello --> hello` - In `D:\test\`... - ✅ `Hello` command outputs `hello (parent)` - ✅ commands from `inner_dir` not shown - In `D:\test\inner_dir\`... - ✅ `Hello` command outputs `hello` - ✅ commands from `D:\test` are shown ## PR Checklist Closes #17805 --- .../TerminalSettingsModel/ActionMap.cpp | 96 +++++++++++++++---- .../TerminalSettingsModel/ActionMap.h | 4 +- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index bebe9ac76b..577903edc0 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -916,14 +916,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation AddAction(*cmd, keys); } - // Update ActionMap's cache of actions for this directory. We'll look for a - // .wt.json in this directory. If it exists, we'll read it, parse it's JSON, - // then take all the sendInput actions in it and store them in our - // _cwdLocalSnippetsCache - std::vector ActionMap::_updateLocalSnippetCache(winrt::hstring currentWorkingDirectory) + // Look for a .wt.json file in the given directory. If it exists, + // read it, parse it's JSON, and retrieve all the sendInput actions. + std::unordered_map ActionMap::_loadLocalSnippets(const std::filesystem::path& currentWorkingDirectory) { // This returns an empty string if we fail to load the file. - std::filesystem::path localSnippetsPath{ std::wstring_view{ currentWorkingDirectory + L"\\.wt.json" } }; + std::filesystem::path localSnippetsPath = currentWorkingDirectory / std::filesystem::path{ ".wt.json" }; const auto data = til::io::read_file_as_utf8_string_if_exists(localSnippetsPath); if (data.empty()) { @@ -943,12 +941,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return {}; } - auto result = std::vector(); + std::unordered_map result; if (auto actions{ root[JsonKey("snippets")] }) { for (const auto& json : actions) { - result.push_back(*Command::FromSnippetJson(json)); + const auto snippet = Command::FromSnippetJson(json); + result.insert_or_assign(snippet->Name(), *snippet); } } return result; @@ -958,34 +957,89 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::hstring currentCommandline, winrt::hstring currentWorkingDirectory) { + // enumerate all the parent directories we want to import snippets from + std::filesystem::path directory{ std::wstring_view{ currentWorkingDirectory } }; + std::vector directories; + while (!directory.empty()) { - // Check if there are any cached commands in this directory. - const auto& cache{ _cwdLocalSnippetsCache.lock_shared() }; - - const auto cacheIterator = cache->find(currentWorkingDirectory); - if (cacheIterator != cache->end()) + directories.push_back(directory); + auto parentPath = directory.parent_path(); + if (directory == parentPath) { - // We found something in the cache! return it. + break; + } + directory = std::move(parentPath); + } + + { + // Check if all the directories are already in the cache + const auto& cache{ _cwdLocalSnippetsCache.lock_shared() }; + if (std::ranges::all_of(directories, [&](auto&& dir) { return cache->contains(dir); })) + { + // Load snippets from directories in reverse order. + // This ensures that we prioritize snippets closer to the cwd. + // The map makes it easy to avoid duplicates. + std::unordered_map localSnippetsMap; + for (auto rit = directories.rbegin(); rit != directories.rend(); ++rit) + { + // register snippets from cache + for (const auto& [name, snippet] : cache->at(*rit)) + { + localSnippetsMap.insert_or_assign(name, snippet); + } + } + + std::vector localSnippets; + localSnippets.reserve(localSnippetsMap.size()); + std::ranges::transform(localSnippetsMap, + std::back_inserter(localSnippets), + [](const auto& kvPair) { return kvPair.second; }); co_return winrt::single_threaded_vector(_filterToSnippets(NameMap(), currentCommandline, - cacheIterator->second)); + localSnippets)); } } // release the lock on the cache // Don't do I/O on the main thread co_await winrt::resume_background(); - auto result = _updateLocalSnippetCache(currentWorkingDirectory); - if (!result.empty()) + // Load snippets from directories in reverse order. + // This ensures that we prioritize snippets closer to the cwd. + // The map makes it easy to avoid duplicates. + const auto& cache{ _cwdLocalSnippetsCache.lock() }; + std::unordered_map localSnippetsMap; + for (auto rit = directories.rbegin(); rit != directories.rend(); ++rit) { - // We found something! Add it to the cache - auto cache{ _cwdLocalSnippetsCache.lock() }; - cache->insert_or_assign(currentWorkingDirectory, result); + const auto& dir = *rit; + if (const auto cacheIterator = cache->find(dir); cacheIterator != cache->end()) + { + // register snippets from cache + for (const auto& [name, snippet] : cache->at(*rit)) + { + localSnippetsMap.insert_or_assign(name, snippet); + } + } + else + { + // we don't have this directory in the cache, so we need to load it + auto result = _loadLocalSnippets(dir); + cache->insert_or_assign(dir, result); + + // register snippets from cache + std::ranges::for_each(result, [&localSnippetsMap](const auto& kvPair) { + localSnippetsMap.insert_or_assign(kvPair.first, kvPair.second); + }); + } } + std::vector localSnippets; + localSnippets.reserve(localSnippetsMap.size()); + std::ranges::transform(localSnippetsMap, + std::back_inserter(localSnippets), + [](const auto& kvPair) { return kvPair.second; }); co_return winrt::single_threaded_vector(_filterToSnippets(NameMap(), currentCommandline, - result)); + localSnippets)); } #pragma endregion } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index 8da139a30d..c0a6e4ac46 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -103,7 +103,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _TryUpdateActionMap(const Model::Command& cmd); void _TryUpdateKeyChord(const Model::Command& cmd, const Control::KeyChord& keys); - std::vector _updateLocalSnippetCache(winrt::hstring currentWorkingDirectory); + static std::unordered_map _loadLocalSnippets(const std::filesystem::path& currentWorkingDirectory); Windows::Foundation::Collections::IMap _AvailableActionsCache{ nullptr }; Windows::Foundation::Collections::IMap _NameMapCache{ nullptr }; @@ -137,7 +137,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // we can give the SUI a view of the key chords and the commands they map to Windows::Foundation::Collections::IMap _ResolvedKeyToActionMapCache{ nullptr }; - til::shared_mutex>> _cwdLocalSnippetsCache{}; + til::shared_mutex>> _cwdLocalSnippetsCache{}; std::set _changeLog; From 9e0ca3aac009ac92e0ec30a91cc4488d2a304290 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 14 May 2025 10:34:41 -0700 Subject: [PATCH 4/5] Add reset cache and settings buttons to SUI (#18907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Adds the ability to reset the settings.json file and application state via the Settings UI. Since these are destructive operations, the destructive styling is applied, as well as a confirmation prompt notifying the user that this occurs immediately. ## Validation Steps Performed ✅ "reset settings" results in the correct contents for settings.json and state.json ✅ "reset cache" results in the correct contents for state.json Closes #947 --- .../TerminalSettingsEditor/Compatibility.cpp | 20 ++++++++- .../TerminalSettingsEditor/Compatibility.h | 14 ++++--- .../TerminalSettingsEditor/Compatibility.idl | 4 +- .../TerminalSettingsEditor/Compatibility.xaml | 42 +++++++++++++++++++ .../TerminalSettingsEditor/MainPage.cpp | 2 +- .../Resources/en-US/Resources.resw | 35 +++++++++++++++- .../TerminalSettingsModel/CascadiaSettings.h | 3 ++ .../CascadiaSettings.idl | 2 + .../CascadiaSettingsSerialization.cpp | 29 ++++++++++--- 9 files changed, 135 insertions(+), 16 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Compatibility.cpp b/src/cascadia/TerminalSettingsEditor/Compatibility.cpp index e95b17fe13..9637992ae8 100644 --- a/src/cascadia/TerminalSettingsEditor/Compatibility.cpp +++ b/src/cascadia/TerminalSettingsEditor/Compatibility.cpp @@ -12,8 +12,8 @@ using namespace winrt::Microsoft::Terminal::Settings::Model; namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { - CompatibilityViewModel::CompatibilityViewModel(Model::GlobalAppSettings globalSettings) : - _GlobalSettings{ globalSettings } + CompatibilityViewModel::CompatibilityViewModel(Model::CascadiaSettings settings) : + _settings{ settings } { INITIALIZE_BINDABLE_ENUM_SETTING(TextMeasurement, TextMeasurement, winrt::Microsoft::Terminal::Control::TextMeasurement, L"Globals_TextMeasurement_", L"Text"); } @@ -23,6 +23,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation return Feature_DebugModeUI::IsEnabled(); } + void CompatibilityViewModel::ResetApplicationState() + { + _settings.ResetApplicationState(); + } + + void CompatibilityViewModel::ResetToDefaultSettings() + { + _settings.ResetToDefaultSettings(); + } + Compatibility::Compatibility() { InitializeComponent(); @@ -32,4 +42,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { _ViewModel = e.Parameter().as(); } + + void Compatibility::ResetApplicationStateButton_Click(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::RoutedEventArgs& /*e*/) + { + _ViewModel.ResetApplicationState(); + ResetCacheFlyout().Hide(); + } } diff --git a/src/cascadia/TerminalSettingsEditor/Compatibility.h b/src/cascadia/TerminalSettingsEditor/Compatibility.h index a95dbc7d2e..54e81f5252 100644 --- a/src/cascadia/TerminalSettingsEditor/Compatibility.h +++ b/src/cascadia/TerminalSettingsEditor/Compatibility.h @@ -13,19 +13,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation struct CompatibilityViewModel : CompatibilityViewModelT, ViewModelHelper { public: - CompatibilityViewModel(Model::GlobalAppSettings globalSettings); + CompatibilityViewModel(Model::CascadiaSettings settings); bool DebugFeaturesAvailable() const noexcept; // DON'T YOU DARE ADD A `WINRT_CALLBACK(PropertyChanged` TO A CLASS DERIVED FROM ViewModelHelper. Do this instead: using ViewModelHelper::PropertyChanged; - PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, AllowHeadless); - PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, DebugFeaturesEnabled); - GETSET_BINDABLE_ENUM_SETTING(TextMeasurement, winrt::Microsoft::Terminal::Control::TextMeasurement, _GlobalSettings.TextMeasurement); + void ResetApplicationState(); + void ResetToDefaultSettings(); + + PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.GlobalSettings(), AllowHeadless); + PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.GlobalSettings(), DebugFeaturesEnabled); + GETSET_BINDABLE_ENUM_SETTING(TextMeasurement, winrt::Microsoft::Terminal::Control::TextMeasurement, _settings.GlobalSettings().TextMeasurement); private: - Model::GlobalAppSettings _GlobalSettings; + Model::CascadiaSettings _settings; }; struct Compatibility : public HasScrollViewer, CompatibilityT @@ -33,6 +36,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Compatibility(); void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); + void ResetApplicationStateButton_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); til::property_changed_event PropertyChanged; WINRT_OBSERVABLE_PROPERTY(Editor::CompatibilityViewModel, ViewModel, PropertyChanged.raise, nullptr); diff --git a/src/cascadia/TerminalSettingsEditor/Compatibility.idl b/src/cascadia/TerminalSettingsEditor/Compatibility.idl index 88c030e1d5..b9f13ffe6e 100644 --- a/src/cascadia/TerminalSettingsEditor/Compatibility.idl +++ b/src/cascadia/TerminalSettingsEditor/Compatibility.idl @@ -9,9 +9,11 @@ namespace Microsoft.Terminal.Settings.Editor { runtimeclass CompatibilityViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged { - CompatibilityViewModel(Microsoft.Terminal.Settings.Model.GlobalAppSettings globalSettings); + CompatibilityViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings); Boolean DebugFeaturesAvailable { get; }; + void ResetApplicationState(); + void ResetToDefaultSettings(); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, AllowHeadless); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, DebugFeaturesEnabled); diff --git a/src/cascadia/TerminalSettingsEditor/Compatibility.xaml b/src/cascadia/TerminalSettingsEditor/Compatibility.xaml index 7345cdd2e9..cb66696312 100644 --- a/src/cascadia/TerminalSettingsEditor/Compatibility.xaml +++ b/src/cascadia/TerminalSettingsEditor/Compatibility.xaml @@ -46,5 +46,47 @@ + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 054c8480cc..6e66fac02a 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -431,7 +431,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else if (clickedItemTag == compatibilityTag) { - contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone.GlobalSettings())); + contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Compatibility/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 46f74160d5..e1e96da833 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -2355,4 +2355,37 @@ None Text displayed when the answerback message is not defined. - \ No newline at end of file + + Reset to default settings + + + Clear cache + + + The cache stores data related to persisting sessions and automatic profile generation. + + + Reset + + + Clear + + + This action is applied immediately and cannot be undone. + + + This action is applied immediately and cannot be undone. + + + Are you sure you want to reset your settings? + + + Are you sure you want to clear your cache? + + + Yes, reset my settings + + + Yes, clear the cache + + diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index a6e50e370a..e650d7c589 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -127,6 +127,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::Windows::Foundation::Collections::IObservableVector AllProfiles() const noexcept; winrt::Windows::Foundation::Collections::IObservableVector ActiveProfiles() const noexcept; Model::ActionMap ActionMap() const noexcept; + void ResetApplicationState() const; + void ResetToDefaultSettings(); void WriteSettingsToDisk(); Json::Value ToJson() const; Model::Profile ProfileDefaults() const; @@ -162,6 +164,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::com_ptr _createNewProfile(const std::wstring_view& name) const; Model::Profile _getProfileForCommandLine(const winrt::hstring& commandLine) const; void _refreshDefaultTerminals(); + void _writeSettingsToDisk(std::string_view contents); void _resolveDefaultProfile() const; void _resolveNewTabMenuProfiles() const; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl index 2fa41941d6..863d2fa751 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl @@ -23,6 +23,8 @@ namespace Microsoft.Terminal.Settings.Model CascadiaSettings(String userJSON, String inboxJSON); CascadiaSettings Copy(); + void ResetApplicationState(); + void ResetToDefaultSettings(); void WriteSettingsToDisk(); void LogSettingChanges(Boolean isJsonLoad); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index ea9bda7616..5fdb390b5a 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -1345,6 +1345,21 @@ winrt::hstring CascadiaSettings::DefaultSettingsPath() return winrt::hstring{ path.native() }; } +void CascadiaSettings::ResetApplicationState() const +{ + auto state = ApplicationState::SharedInstance(); + const auto hash = state.SettingsHash(); + state.Reset(); + state.SettingsHash(hash); + state.Flush(); +} + +void CascadiaSettings::ResetToDefaultSettings() +{ + ApplicationState::SharedInstance().Reset(); + _writeSettingsToDisk(LoadStringResource(IDR_USER_DEFAULTS)); +} + // Method Description: // - Write the current state of CascadiaSettings to our settings file // - Create a backup file with the current contents, if one does not exist @@ -1355,19 +1370,21 @@ winrt::hstring CascadiaSettings::DefaultSettingsPath() // - void CascadiaSettings::WriteSettingsToDisk() { - const auto settingsPath = _settingsPath(); - // write current settings to current settings file Json::StreamWriterBuilder wbuilder; wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons wbuilder.settings_["indentation"] = " "; wbuilder.settings_["precision"] = 6; // prevent values like 1.1000000000000001 - FILETIME lastWriteTime{}; - const auto styledString{ Json::writeString(wbuilder, ToJson()) }; - til::io::write_utf8_string_to_file_atomic(settingsPath, styledString, &lastWriteTime); + _writeSettingsToDisk(Json::writeString(wbuilder, ToJson())); +} - _hash = _calculateHash(styledString, lastWriteTime); +void CascadiaSettings::_writeSettingsToDisk(std::string_view contents) +{ + FILETIME lastWriteTime{}; + til::io::write_utf8_string_to_file_atomic(_settingsPath(), contents, &lastWriteTime); + + _hash = _calculateHash(contents, lastWriteTime); // Persists the default terminal choice // GH#10003 - Only do this if _currentDefaultTerminal was actually initialized. From f769597d8914fbff88db3d72cff1d7b89334d4a8 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 14 May 2025 11:08:07 -0700 Subject: [PATCH 5/5] Persist window layout every few minutes (#18898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If `persistedWindowLayout` is enabled, we now persist the window layout every few minutes (excluding the text buffer). This was done by adding a `SafeDispatcherTimer` to the `WindowEmperor` that calls `PersistState()` every 5 minutes. For `BuildStartupKind`, I split up `Persist` into `PersistAll` and `PersistLayout`. This way, we go through all the same code flow that `Persist` had except for specifically serializing the buffer. ## Validation Steps Performed ✅ (with the timer set to 3 seconds) create a window layout and ensure the layout is restored after forcefully stopping Terminal (aka simulating a "crash") Closes #18838 --- src/cascadia/TerminalApp/IPaneContent.idl | 3 +- src/cascadia/TerminalApp/TerminalPage.cpp | 4 +-- src/cascadia/TerminalApp/TerminalPage.h | 2 +- .../TerminalApp/TerminalPaneContent.cpp | 3 +- src/cascadia/TerminalApp/TerminalWindow.cpp | 4 +-- src/cascadia/TerminalApp/TerminalWindow.h | 2 +- src/cascadia/TerminalApp/TerminalWindow.idl | 2 +- .../WindowsTerminal/WindowEmperor.cpp | 29 ++++++++++++++++--- src/cascadia/WindowsTerminal/WindowEmperor.h | 3 ++ 9 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/cascadia/TerminalApp/IPaneContent.idl b/src/cascadia/TerminalApp/IPaneContent.idl index f4c6ce1395..ea2e900ca6 100644 --- a/src/cascadia/TerminalApp/IPaneContent.idl +++ b/src/cascadia/TerminalApp/IPaneContent.idl @@ -8,7 +8,8 @@ namespace TerminalApp None, Content, MovePane, - Persist, + PersistLayout, + PersistAll }; runtimeclass BellEventArgs diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 581b0ddcf3..135e7cca94 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1953,7 +1953,7 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::PersistState() + void TerminalPage::PersistState(bool serializeBuffer) { // This method may be called for a window even if it hasn't had a tab yet or lost all of them. // We shouldn't persist such windows. @@ -1968,7 +1968,7 @@ namespace winrt::TerminalApp::implementation for (auto tab : _tabs) { auto t = winrt::get_self(tab); - auto tabActions = t->BuildStartupActions(BuildStartupKind::Persist); + auto tabActions = t->BuildStartupActions(serializeBuffer ? BuildStartupKind::PersistAll : BuildStartupKind::PersistLayout); actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 9837f9fe61..b369bd920e 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -113,7 +113,7 @@ namespace winrt::TerminalApp::implementation safe_void_coroutine RequestQuit(); safe_void_coroutine CloseWindow(); - void PersistState(); + void PersistState(bool serializeBuffer); void ToggleFocusMode(); void ToggleFullscreen(); diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.cpp b/src/cascadia/TerminalApp/TerminalPaneContent.cpp index 1ab5cd013c..c85009ea0c 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.cpp +++ b/src/cascadia/TerminalApp/TerminalPaneContent.cpp @@ -141,7 +141,7 @@ namespace winrt::TerminalApp::implementation // "attach existing" rather than a "create" args.ContentId(_control.ContentId()); break; - case BuildStartupKind::Persist: + case BuildStartupKind::PersistAll: { const auto connection = _control.Connection(); const auto id = connection ? connection.SessionId() : winrt::guid{}; @@ -156,6 +156,7 @@ namespace winrt::TerminalApp::implementation } break; } + case BuildStartupKind::PersistLayout: default: break; } diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index 9776679691..e1fd1741d1 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -266,11 +266,11 @@ namespace winrt::TerminalApp::implementation AppLogic::Current()->NotifyRootInitialized(); } - void TerminalWindow::PersistState() + void TerminalWindow::PersistState(bool serializeBuffer) { if (_root) { - _root->PersistState(); + _root->PersistState(serializeBuffer); } } diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index 6e37e17947..fd27617846 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -71,7 +71,7 @@ namespace winrt::TerminalApp::implementation void Create(); - void PersistState(); + void PersistState(bool serializeBuffer); void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args); diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index a8fa0c97c5..baf58317df 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -59,7 +59,7 @@ namespace TerminalApp Boolean ShouldImmediatelyHandoffToElevated(); void HandoffToElevated(); - void PersistState(); + void PersistState(Boolean serializeBuffer); Windows.UI.Xaml.UIElement GetRoot(); diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 381b7f4c8e..cdf34d9a3b 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -314,6 +314,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) _createMessageWindow(windowClassName.c_str()); _setupGlobalHotkeys(); _checkWindowsForNotificationIcon(); + _setupSessionPersistence(_app.Logic().Settings().GlobalSettings().ShouldUsePersistedLayout()); // When the settings change, we'll want to update our global hotkeys // and our notification icon based on the new settings. @@ -323,6 +324,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) _assertIsMainThread(); _setupGlobalHotkeys(); _checkWindowsForNotificationIcon(); + _setupSessionPersistence(args.NewSettings().GlobalSettings().ShouldUsePersistedLayout()); } }); @@ -921,10 +923,22 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c return DefWindowProcW(window, message, wParam, lParam); } -void WindowEmperor::_finalizeSessionPersistence() const +void WindowEmperor::_setupSessionPersistence(bool enabled) { - const auto state = ApplicationState::SharedInstance(); + if (!enabled) + { + _persistStateTimer.Stop(); + return; + } + _persistStateTimer.Interval(std::chrono::minutes(5)); + _persistStateTimer.Tick([&](auto&&, auto&&) { + _persistState(ApplicationState::SharedInstance(), false); + }); + _persistStateTimer.Start(); +} +void WindowEmperor::_persistState(const ApplicationState& state, bool serializeBuffer) const +{ // Calling an `ApplicationState` setter triggers a write to state.json. // With this if condition we avoid an unnecessary write when persistence is disabled. if (state.PersistedWindowLayouts()) @@ -936,12 +950,19 @@ void WindowEmperor::_finalizeSessionPersistence() const { for (const auto& w : _windows) { - w->Logic().PersistState(); + w->Logic().PersistState(serializeBuffer); } } - // Ensure to write the state.json before we TerminateProcess() + // Ensure to write the state.json state.Flush(); +} + +void WindowEmperor::_finalizeSessionPersistence() const +{ + const auto state = ApplicationState::SharedInstance(); + + _persistState(state, true); if (_needsPersistenceCleanup) { diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 3024ab7a2c..bf63214030 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -64,6 +64,8 @@ private: void _registerHotKey(int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; void _unregisterHotKey(int index) noexcept; void _setupGlobalHotkeys(); + void _setupSessionPersistence(bool enabled); + void _persistState(const winrt::Microsoft::Terminal::Settings::Model::ApplicationState& state, bool serializeBuffer) const; void _finalizeSessionPersistence() const; void _checkWindowsForNotificationIcon(); @@ -77,6 +79,7 @@ private: bool _notificationIconShown = false; bool _forcePersistence = false; bool _needsPersistenceCleanup = false; + SafeDispatcherTimer _persistStateTimer; std::optional _currentSystemThemeIsDark; int32_t _windowCount = 0; int32_t _messageBoxCount = 0;