From 75e2b5fae7e2f7caef3c634710cf7aced14ffd98 Mon Sep 17 00:00:00 2001 From: Schuyler Rosefield Date: Mon, 27 Sep 2021 17:18:39 -0400 Subject: [PATCH] Persist window layout cont. save multiple windows (#11083) ## Summary of the Pull Request Continuation of https://github.com/microsoft/terminal/pull/10972 to handle multiple windows, requires that to be merged first. ## References ## PR Checklist * [x] Also closes #766 * [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA * [ ] Tests added/passed * [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx * [x] Schema updated. * [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx ## Detailed Description of the Pull Request / Additional comments Rough changelog: Normally saving is triggered to occur every 30s, or sooner if a window is created/closed. The existing behavior of saving on last close is maintained to bypass that throttling. The automatic saving allows for crash recovery. Additionally all window layouts will be saved upon taking the `quit` action. For loading we will check if we are the first window, that there are any saved layouts, and if the setting is enabled, and then depending on if we were given command line args or startup actions. - create a new window for each saved layout, or - take the first layout for our self and then a new window for each other layout. This also saves the layout when the quit action is taken. Misc changes - A -s,--saved argument was added to the command line to facilitate opening all of the windows with the right settings. This also means that while a terminal session is running you can do wt -s idx to open a copy of window idx. There isn't a stable ordering of which idx each window gets saved as (it is whatever the iteration order of _peasants is), so it is just a cute hack for now. - All position calculation has been moved up to AppHost this does mean we need to awkwardly pass around positions in a couple of unexpected places, but no solution was perfect. - Renamed "Open tabs from a previous session" to "Open windows from a previous session". (not reflected in video below) - Now save runtime tab color and window names - Only enabled for non-elevated windows - Add some change tracking to ApplicationState ## Validation Steps Performed ![output](https://user-images.githubusercontent.com/6185249/131163473-d649d204-a589-41ad-b9d9-c4c0528cb684.gif) --- .github/actions/spelling/allow/microsoft.txt | 1 + src/cascadia/Remoting/GetWindowLayoutArgs.cpp | 5 + src/cascadia/Remoting/GetWindowLayoutArgs.h | 32 ++++ .../Microsoft.Terminal.RemotingLib.vcxproj | 16 +- src/cascadia/Remoting/Monarch.cpp | 37 +++- src/cascadia/Remoting/Monarch.h | 11 +- src/cascadia/Remoting/Monarch.idl | 9 +- src/cascadia/Remoting/Peasant.cpp | 21 +++ src/cascadia/Remoting/Peasant.h | 4 + src/cascadia/Remoting/Peasant.idl | 7 + .../Remoting/QuitAllRequestedArgs.cpp | 5 + src/cascadia/Remoting/QuitAllRequestedArgs.h | 30 +++ src/cascadia/Remoting/WindowManager.cpp | 17 +- src/cascadia/Remoting/WindowManager.h | 4 +- src/cascadia/Remoting/WindowManager.idl | 5 +- .../TerminalApp/AppActionHandlers.cpp | 2 +- .../TerminalApp/AppCommandlineArgs.cpp | 11 ++ src/cascadia/TerminalApp/AppCommandlineArgs.h | 2 + src/cascadia/TerminalApp/AppLogic.cpp | 87 +++++++-- src/cascadia/TerminalApp/AppLogic.h | 11 +- src/cascadia/TerminalApp/AppLogic.idl | 10 +- src/cascadia/TerminalApp/Pane.cpp | 7 +- .../Resources/en-US/Resources.resw | 3 + src/cascadia/TerminalApp/TabManagement.cpp | 4 +- src/cascadia/TerminalApp/TerminalPage.cpp | 126 +++++++------ src/cascadia/TerminalApp/TerminalPage.h | 7 +- src/cascadia/TerminalApp/TerminalPage.idl | 2 +- src/cascadia/TerminalApp/TerminalTab.cpp | 23 ++- .../Resources/en-US/Resources.resw | 4 +- .../TerminalSettingsModel/ActionArgs.h | 6 + .../TerminalSettingsModel/ActionArgs.idl | 2 + .../ApplicationState.cpp | 100 +++++++--- .../TerminalSettingsModel/ApplicationState.h | 12 +- .../ApplicationState.idl | 3 + .../TerminalSettingsModel/FileUtils.cpp | 77 ++++++++ .../TerminalSettingsModel/FileUtils.h | 30 +++ .../UnitTests_Remoting/RemotingTests.cpp | 2 + src/cascadia/WindowsTerminal/AppHost.cpp | 171 +++++++++++++++++- src/cascadia/WindowsTerminal/AppHost.h | 10 +- src/cascadia/WindowsTerminal/BaseWindow.h | 5 + 40 files changed, 788 insertions(+), 133 deletions(-) create mode 100644 src/cascadia/Remoting/GetWindowLayoutArgs.cpp create mode 100644 src/cascadia/Remoting/GetWindowLayoutArgs.h create mode 100644 src/cascadia/Remoting/QuitAllRequestedArgs.cpp create mode 100644 src/cascadia/Remoting/QuitAllRequestedArgs.h diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index 87d7a3d8c..c82254bbb 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -25,6 +25,7 @@ DWINRT enablewttlogging Intelli LKG +LOCKFILE Lxss mfcribbon microsoft diff --git a/src/cascadia/Remoting/GetWindowLayoutArgs.cpp b/src/cascadia/Remoting/GetWindowLayoutArgs.cpp new file mode 100644 index 000000000..f2cc01df4 --- /dev/null +++ b/src/cascadia/Remoting/GetWindowLayoutArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "GetWindowLayoutArgs.h" +#include "GetWindowLayoutArgs.g.cpp" diff --git a/src/cascadia/Remoting/GetWindowLayoutArgs.h b/src/cascadia/Remoting/GetWindowLayoutArgs.h new file mode 100644 index 000000000..06706f60b --- /dev/null +++ b/src/cascadia/Remoting/GetWindowLayoutArgs.h @@ -0,0 +1,32 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- GetWindowLayoutArgs.h + +Abstract: +- This is a helper class for getting the window layout from a peasant. + Depending on if we are running on the monarch or on a peasant we might need + to switch what thread we are executing on. This gives us the option of + either returning the json result synchronously, or as a promise. +--*/ + +#pragma once + +#include "GetWindowLayoutArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct GetWindowLayoutArgs : public GetWindowLayoutArgsT + { + WINRT_PROPERTY(winrt::hstring, WindowLayoutJson, L""); + WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncOperation, WindowLayoutJsonAsync, nullptr) + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(GetWindowLayoutArgs); +} diff --git a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj index a2d6e8ae6..517c2f56f 100644 --- a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj +++ b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj @@ -12,7 +12,6 @@ - @@ -36,6 +35,12 @@ Peasant.idl + + Peasant.idl + + + Monarch.idl + @@ -71,6 +76,12 @@ Peasant.idl + + Peasant.idl + + + Monarch.idl + Create @@ -128,6 +139,5 @@ - - + \ No newline at end of file diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index c16eb52e9..eed247ab4 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -6,6 +6,7 @@ #include "Monarch.h" #include "CommandlineArgs.h" #include "FindTargetWindowArgs.h" +#include "QuitAllRequestedArgs.h" #include "ProposeCommandlineResult.h" #include "Monarch.g.cpp" @@ -135,12 +136,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // - used // Return Value: // - - void Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) + winrt::fire_and_forget Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*args*/) { // Let the process hosting the monarch run any needed logic before // closing all windows. - _QuitAllRequestedHandlers(*this, nullptr); + auto args = winrt::make_self(); + _QuitAllRequestedHandlers(*this, *args); + + if (const auto action = args->BeforeQuitAllAction()) + { + co_await action; + } _quitting.store(true); // Tell all peasants to exit. @@ -994,4 +1001,28 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _forEachPeasant(func, onError); } + + // Method Description: + // - Ask all peasants to return their window layout as json + // Arguments: + // - + // Return Value: + // - The collection of window layouts from each peasant. + Windows::Foundation::Collections::IVector Monarch::GetAllWindowLayouts() + { + std::vector vec; + auto callback = [&](const auto& /*id*/, const auto& p) { + vec.emplace_back(p.GetWindowLayout()); + }; + auto onError = [](auto&& id) { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_GetAllWindowLayouts_Failed", + TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not get a window layout from"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + }; + _forEachPeasant(callback, onError); + + return winrt::single_threaded_vector(std::move(vec)); + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 7ec904499..b965d1d2a 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -59,13 +59,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void SummonAllWindows(); bool DoesQuakeWindowExist(); Windows::Foundation::Collections::IVectorView GetPeasantInfos(); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); private: uint64_t _ourPID; @@ -103,8 +104,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void _renameRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args); - void _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); + winrt::fire_and_forget _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& args); // Method Description: // - Helper for doing something on each and every peasant. @@ -177,6 +178,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } _clearOldMruEntries(peasantsToErase); + + // A peasant died, let the app host know that the number of + // windows has changed. + _WindowClosedHandlers(nullptr, nullptr); } } diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index 4eb77695a..f60b3997a 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -31,6 +31,12 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.IReference WindowID; } + [default_interface] runtimeclass QuitAllRequestedArgs + { + QuitAllRequestedArgs(); + Windows.Foundation.IAsyncAction BeforeQuitAllAction; + } + struct PeasantInfo { UInt64 Id; @@ -52,12 +58,13 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); Boolean DoesQuakeWindowExist(); Windows.Foundation.Collections.IVectorView GetPeasantInfos { get; }; + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; - event Windows.Foundation.TypedEventHandler QuitAllRequested; + event Windows.Foundation.TypedEventHandler QuitAllRequested; }; } diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index a8cb749d6..46fd7ce2e 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -5,6 +5,7 @@ #include "Peasant.h" #include "CommandlineArgs.h" #include "SummonWindowBehavior.h" +#include "GetWindowLayoutArgs.h" #include "Peasant.g.cpp" #include "../../types/inc/utils.hpp" @@ -289,4 +290,24 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } + + // Method Description: + // - Request and return the window layout from the current TerminalPage + // Arguments: + // - + // Return Value: + // - the window layout as a json string + hstring Peasant::GetWindowLayout() + { + auto args = winrt::make_self(); + _GetWindowLayoutRequestedHandlers(nullptr, *args); + if (const auto op = args->WindowLayoutJsonAsync()) + { + // This will fail if called on the UI thread, so the monarch should + // never set WindowLayoutJsonAsync. + auto str = op.get(); + return str; + } + return args->WindowLayoutJson(); + } } diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index f6f884491..fdb20d942 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -36,6 +36,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs(); winrt::Microsoft::Terminal::Remoting::CommandlineArgs InitialArgs(); + + winrt::hstring GetWindowLayout(); + WINRT_PROPERTY(winrt::hstring, WindowName); WINRT_PROPERTY(winrt::hstring, ActiveTabTitle); @@ -49,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); private: Peasant(const uint64_t testPID); diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index 80e24cb2c..ec87c8518 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -30,6 +30,11 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.DateTime ActivatedTime { get; }; }; + [default_interface] runtimeclass GetWindowLayoutArgs { + GetWindowLayoutArgs(); + String WindowLayoutJson; + Windows.Foundation.IAsyncOperation WindowLayoutJsonAsync; + } enum MonitorBehavior { @@ -69,6 +74,7 @@ namespace Microsoft.Terminal.Remoting void RequestHideNotificationIcon(); void RequestQuitAll(); void Quit(); + String GetWindowLayout(); event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; @@ -78,6 +84,7 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler SummonRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; + event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; event Windows.Foundation.TypedEventHandler QuitAllRequested; event Windows.Foundation.TypedEventHandler QuitRequested; }; diff --git a/src/cascadia/Remoting/QuitAllRequestedArgs.cpp b/src/cascadia/Remoting/QuitAllRequestedArgs.cpp new file mode 100644 index 000000000..ed5c39dcf --- /dev/null +++ b/src/cascadia/Remoting/QuitAllRequestedArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "QuitAllRequestedArgs.h" +#include "QuitAllRequestedArgs.g.cpp" diff --git a/src/cascadia/Remoting/QuitAllRequestedArgs.h b/src/cascadia/Remoting/QuitAllRequestedArgs.h new file mode 100644 index 000000000..8c9c26fd2 --- /dev/null +++ b/src/cascadia/Remoting/QuitAllRequestedArgs.h @@ -0,0 +1,30 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- QuitAllRequestedArgs.h + +Abstract: +- This is a helper class for allowing the monarch to run code before telling all + peasants to quit. This way the monarch can raise an event and get back a future + to wait for before continuing. +--*/ + +#pragma once + +#include "QuitAllRequestedArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct QuitAllRequestedArgs : public QuitAllRequestedArgsT + { + WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncAction, BeforeQuitAllAction, nullptr) + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(QuitAllRequestedArgs); +} diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 9a71fcef0..4cafee145 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -271,7 +271,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); _monarch.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); }); _monarch.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); }); - _monarch.QuitAllRequested([this](auto&&, auto&&) { _QuitAllRequestedHandlers(*this, nullptr); }); + _monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers }); _BecameMonarchHandlers(*this, nullptr); } @@ -318,6 +318,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } + _peasant.GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers }); + TraceLoggingWrite(g_hRemotingProvider, "WindowManager_CreateOurPeasant", TraceLoggingUInt64(_peasant.GetID(), "peasantID", "The ID of our new peasant"), @@ -610,4 +612,17 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { winrt::get_self(_peasant)->ActiveTabTitle(title); } + + Windows::Foundation::Collections::IVector WindowManager::GetAllWindowLayouts() + { + if (_monarch) + { + try + { + return _monarch.GetAllWindowLayouts(); + } + CATCH_LOG() + } + return nullptr; + } } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 3d2eaf6c7..379038750 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -50,6 +50,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::fire_and_forget RequestQuitAll(); bool DoesQuakeWindowExist(); void UpdateActiveTabTitle(winrt::hstring title); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); @@ -57,7 +58,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); private: bool _shouldCreateWindow{ false }; diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index cf15fbb42..2fdfd7e34 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -16,6 +16,8 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); void RequestShowNotificationIcon(); void RequestHideNotificationIcon(); + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); + UInt64 GetNumberOfPeasants(); void RequestQuitAll(); void UpdateActiveTabTitle(String title); @@ -25,8 +27,9 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler BecameMonarch; event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; + event Windows.Foundation.TypedEventHandler QuitAllRequested; + event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; - event Windows.Foundation.TypedEventHandler QuitAllRequested; }; } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 1ef4c0c8a..d99726b39 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -78,7 +78,7 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleCloseWindow(const IInspectable& /*sender*/, const ActionEventArgs& args) { - CloseWindow(false); + _CloseRequestedHandlers(nullptr, nullptr); args.Handled(true); } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 7f2f919a4..3baad1aa0 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -187,6 +187,10 @@ void AppCommandlineArgs::_buildParser() _windowTarget, RS_A(L"CmdWindowTargetArgDesc")); + _app.add_option("-s,--saved", + _loadPersistedLayoutIdx, + RS_A(L"CmdSavedLayoutArgDesc")); + // Subcommands _buildNewTabParser(); _buildSplitPaneParser(); @@ -700,6 +704,7 @@ void AppCommandlineArgs::_resetStateToDefault() _swapPaneDirection = FocusDirection::None; _focusPaneTarget = -1; + _loadPersistedLayoutIdx = -1; // DON'T clear _launchMode here! This will get called once for every // subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out @@ -915,6 +920,12 @@ void AppCommandlineArgs::ValidateStartupCommands() } } } +std::optional AppCommandlineArgs::GetPersistedLayoutIdx() const noexcept +{ + return _loadPersistedLayoutIdx >= 0 ? + std::optional{ static_cast(_loadPersistedLayoutIdx) } : + std::nullopt; +} std::optional AppCommandlineArgs::GetLaunchMode() const noexcept { diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 598c3b8ed..de076ec99 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -39,6 +39,7 @@ public: const std::string& GetExitMessage(); bool ShouldExitEarly() const noexcept; + std::optional GetPersistedLayoutIdx() const noexcept; std::optional GetLaunchMode() const noexcept; int ParseArgs(const winrt::Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); @@ -123,6 +124,7 @@ private: std::string _exitMessage; bool _shouldExitEarly{ false }; + int _loadPersistedLayoutIdx{}; std::string _windowTarget{}; // Are you adding more args or attributes here? If they are not reset in _resetStateToDefault, make sure to reset them in FullResetState diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 08032f1c8..72f25b83e 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -600,13 +600,11 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Foundation::Size proposedSize{}; const float scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); - if (_root->ShouldUsePersistedLayout(_settings)) + if (const auto layout = _root->LoadPersistedLayout(_settings)) { - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialSize()) + if (layout.InitialSize()) { - proposedSize = layouts.GetAt(0).InitialSize().Value(); + proposedSize = layout.InitialSize().Value(); // The size is saved as a non-scaled real pixel size, // so we need to scale it appropriately. proposedSize.Height = proposedSize.Height * scale; @@ -704,13 +702,11 @@ namespace winrt::TerminalApp::implementation auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; - if (_root->ShouldUsePersistedLayout(_settings)) + if (const auto layout = _root->LoadPersistedLayout(_settings)) { - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialPosition()) + if (layout.InitialPosition()) { - initialPosition = layouts.GetAt(0).InitialPosition().Value(); + initialPosition = layout.InitialPosition().Value(); } } @@ -1151,10 +1147,22 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - void AppLogic::WindowCloseButtonClicked() + void AppLogic::CloseWindow(LaunchPosition pos) { if (_root) { + // If persisted layout is enabled and we are the last window closing + // we should save our state. + if (_root->ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(pos); + const auto state = ApplicationState::SharedInstance(); + state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + } + } + _root->CloseWindow(false); } } @@ -1168,6 +1176,16 @@ namespace winrt::TerminalApp::implementation return {}; } + bool AppLogic::HasCommandlineArguments() const noexcept + { + return _hasCommandLineArguments; + } + + bool AppLogic::HasSettingsStartupActions() const noexcept + { + return _hasSettingsStartupActions; + } + // Method Description: // - Sets the initial commandline to process on startup, and attempts to // parse it. Commands will be parsed into a list of ShortcutActions that @@ -1191,6 +1209,10 @@ namespace winrt::TerminalApp::implementation // then it contains only the executable name and no other arguments. _hasCommandLineArguments = args.size() > 1; _appArgs.ValidateStartupCommands(); + if (const auto idx = _appArgs.GetPersistedLayoutIdx()) + { + _root->SetPersistedLayoutIdx(idx.value()); + } _root->SetStartupActions(_appArgs.GetStartupActions()); // Check if we were started as a COM server for inbound connections of console sessions @@ -1428,6 +1450,40 @@ namespace winrt::TerminalApp::implementation return _settings.GlobalSettings().ActionMap().GlobalHotkeys(); } + bool AppLogic::ShouldUsePersistedLayout() + { + return _root != nullptr ? _root->ShouldUsePersistedLayout(_settings) : false; + } + + void AppLogic::SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts) + { + std::vector converted; + converted.reserve(layouts.Size()); + + for (const auto& json : layouts) + { + if (json != L"") + { + converted.emplace_back(WindowLayout::FromJson(json)); + } + } + + ApplicationState::SharedInstance().PersistedWindowLayouts(winrt::single_threaded_vector(std::move(converted))); + } + + hstring AppLogic::GetWindowLayoutJson(LaunchPosition position) + { + if (_root != nullptr) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(position); + return WindowLayout::ToJson(layout); + } + } + return L""; + } + void AppLogic::IdentifyWindow() { if (_root) @@ -1459,8 +1515,17 @@ namespace winrt::TerminalApp::implementation } } + void AppLogic::SetPersistedLayoutIdx(const uint32_t idx) + { + if (_root) + { + _root->SetPersistedLayoutIdx(idx); + } + } + void AppLogic::SetNumberOfOpenWindows(const uint64_t num) { + _numOpenWindows = num; if (_root) { _root->SetNumberOfOpenWindows(num); diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 08dc77007..17bd61aaa 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -55,6 +55,8 @@ namespace winrt::TerminalApp::implementation void Quit(); + bool HasCommandlineArguments() const noexcept; + bool HasSettingsStartupActions() const noexcept; int32_t SetStartupCommandline(array_view actions); int32_t ExecuteCommandline(array_view actions, const winrt::hstring& cwd); TerminalApp::FindTargetWindowResult FindTargetWindow(array_view actions); @@ -65,12 +67,16 @@ namespace winrt::TerminalApp::implementation bool Fullscreen() const; bool AlwaysOnTop() const; + bool ShouldUsePersistedLayout(); + hstring GetWindowLayoutJson(Microsoft::Terminal::Settings::Model::LaunchPosition position); + void SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts); void IdentifyWindow(); void RenameFailed(); winrt::hstring WindowName(); void WindowName(const winrt::hstring& name); uint64_t WindowId(); void WindowId(const uint64_t& id); + void SetPersistedLayoutIdx(const uint32_t idx); void SetNumberOfOpenWindows(const uint64_t num); bool IsQuakeWindow() const noexcept; @@ -91,7 +97,7 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); - void WindowCloseButtonClicked(); + void CloseWindow(Microsoft::Terminal::Settings::Model::LaunchPosition position); winrt::TerminalApp::TaskbarState TaskbarState(); @@ -123,6 +129,8 @@ namespace winrt::TerminalApp::implementation HRESULT _settingsLoadedResult = S_OK; bool _loadedInitialSettings = false; + uint64_t _numOpenWindows{ 0 }; + std::shared_mutex _dialogLock; ::TerminalApp::AppCommandlineArgs _appArgs; @@ -175,6 +183,7 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested); FORWARDED_TYPED_EVENT(IsQuakeWindowChanged, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IsQuakeWindowChanged); FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested); + FORWARDED_TYPED_EVENT(CloseRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, CloseRequested); FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index a600891c4..cfe93321a 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -34,6 +34,8 @@ namespace TerminalApp void RunAsUwp(); Boolean IsElevated(); + Boolean HasCommandlineArguments(); + Boolean HasSettingsStartupActions(); Int32 SetStartupCommandline(String[] commands); Int32 ExecuteCommandline(String[] commands, String cwd); String ParseCommandlineMessage { get; }; @@ -55,6 +57,7 @@ namespace TerminalApp void IdentifyWindow(); String WindowName; UInt64 WindowId; + void SetPersistedLayoutIdx(UInt32 idx); void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); @@ -69,10 +72,14 @@ namespace TerminalApp Boolean GetInitialAlwaysOnTop(); Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); void TitlebarClicked(); - void WindowCloseButtonClicked(); + void CloseWindow(Microsoft.Terminal.Settings.Model.LaunchPosition position); TaskbarState TaskbarState{ get; }; + Boolean ShouldUsePersistedLayout(); + String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position); + void SaveWindowLayoutJsons(Windows.Foundation.Collections.IVector layouts); + Boolean GetMinimizeToNotificationArea(); Boolean GetAlwaysShowNotificationIcon(); Boolean GetShowTitleInTitlebar(); @@ -99,6 +106,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler SettingsChanged; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler CloseRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; event Windows.Foundation.TypedEventHandler QuitRequested; } diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 0c5705a7b..310d7ca47 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -106,7 +106,12 @@ NewTerminalArgs Pane::GetTerminalArgsForPane() const if (controlSettings.AppliedColorScheme()) { auto name = controlSettings.AppliedColorScheme().Name(); - args.ColorScheme(name); + // Only save the color scheme if it is different than the profile color + // scheme to not override any other profile appearance choices. + if (_profile.DefaultAppearance().ColorSchemeName() != name) + { + args.ColorScheme(name); + } } return args; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 421816f58..272fa9df6 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -381,6 +381,9 @@ Launch the window in focus mode + + This parameter is an internal implementation detail and should not be used. + Specify a terminal window to run the given commandline in. "0" always refers to the current window. diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index f57e3fe9b..e5d1cdc46 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -501,7 +501,9 @@ namespace winrt::TerminalApp::implementation { // If we are supposed to save state, make sure we clear it out // if the user manually closed all tabs. - if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings)) + // Do this only if we are the last window; the monarch will notice + // we are missing and remove us that way otherwise. + if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) { auto state = ApplicationState::SharedInstance(); state.PersistedWindowLayouts(nullptr); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 74606ab4f..25140362d 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -298,10 +298,37 @@ namespace winrt::TerminalApp::implementation // - true if the ApplicationState should be used. bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const { - // If the setting is enabled, and we are the only window. + // GH#5000 Until there is a separate state file for elevated sessions we should just not + // save at all while in an elevated window. return Feature_PersistedWindowLayout::IsEnabled() && - settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && - _numOpenWindows == 1; + !IsElevated() && + settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout; + } + + // Method Description; + // - Checks if the current window is configured to load a particular layout + // Arguments: + // - settings: The settings to use as this may be called before the page is + // fully initialized. + // Return Value: + // - non-null if there is a particular saved layout to use + std::optional TerminalPage::LoadPersistedLayoutIdx(CascadiaSettings& settings) const + { + return ShouldUsePersistedLayout(settings) ? _loadFromPersistedLayoutIdx : std::nullopt; + } + + WindowLayout TerminalPage::LoadPersistedLayout(CascadiaSettings& settings) const + { + if (const auto idx = LoadPersistedLayoutIdx(settings)) + { + const auto i = idx.value(); + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (layouts && layouts.Size() > i) + { + return layouts.GetAt(i); + } + } + return nullptr; } winrt::fire_and_forget TerminalPage::NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e) @@ -387,30 +414,13 @@ namespace winrt::TerminalApp::implementation { _startupState = StartupState::InStartup; - // If the user selected to save their tab layout, we are the first - // window opened, and wt was not run with any other arguments, then - // we should use the saved settings. - auto firstActionIsDefault = [](ActionAndArgs action) { - if (action.Action() != ShortcutAction::NewTab) - { - return false; - } - - // If no commands were given, we will have default args - if (const auto args = action.Args().try_as()) - { - NewTerminalArgs defaultArgs{}; - return args.TerminalArgs() == nullptr || args.TerminalArgs().Equals(defaultArgs); - } - - return false; - }; - if (ShouldUsePersistedLayout(_settings) && _startupActions.Size() == 1 && firstActionIsDefault(_startupActions.GetAt(0))) + // If we are provided with an index, the cases where we have + // commandline args and startup actions are already handled. + if (const auto layout = LoadPersistedLayout(_settings)) { - auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).TabLayout() && layouts.GetAt(0).TabLayout().Size() > 0) + if (layout.TabLayout().Size() > 0) { - _startupActions = layouts.GetAt(0).TabLayout(); + _startupActions = layout.TabLayout(); } } @@ -1289,12 +1299,19 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Saves the window position and tab layout to the application state + // - This does not create the InitialPosition field, that needs to be + // added externally. // Arguments: // - // Return Value: - // - - void TerminalPage::PersistWindowLayout() + // - the window layout + WindowLayout TerminalPage::GetWindowLayout() { + if (_startupState != StartupState::Initialized) + { + return nullptr; + } + std::vector actions; for (auto tab : _tabs) @@ -1302,7 +1319,7 @@ namespace winrt::TerminalApp::implementation if (auto terminalTab = _GetTerminalTabImpl(tab)) { auto tabActions = terminalTab->BuildStartupActions(); - actions.insert(actions.end(), tabActions.begin(), tabActions.end()); + actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); } else if (tab.try_as()) { @@ -1311,7 +1328,7 @@ namespace winrt::TerminalApp::implementation OpenSettingsArgs args{ SettingsTarget::SettingsUI }; action.Args(args); - actions.push_back(action); + actions.emplace_back(std::move(action)); } } @@ -1324,7 +1341,18 @@ namespace winrt::TerminalApp::implementation SwitchToTabArgs switchToTabArgs{ idx.value() }; action.Args(switchToTabArgs); - actions.push_back(action); + actions.emplace_back(std::move(action)); + } + + // If the user set a custom name, save it + if (_WindowName != L"") + { + ActionAndArgs action; + action.Action(ShortcutAction::RenameWindow); + RenameWindowArgs args{ _WindowName }; + action.Args(args); + + actions.emplace_back(std::move(action)); } WindowLayout layout{}; @@ -1337,33 +1365,7 @@ namespace winrt::TerminalApp::implementation layout.InitialSize(windowSize); - if (_hostingHwnd) - { - // Get the position of the current window. This includes the - // non-client already. - RECT window{}; - GetWindowRect(_hostingHwnd.value(), &window); - - // We want to remove the non-client area so calculate that. - // We don't have access to the (NonClient)IslandWindow directly so - // just replicate the logic. - const auto windowStyle = static_cast(GetWindowLong(_hostingHwnd.value(), GWL_STYLE)); - - auto dpi = GetDpiForWindow(_hostingHwnd.value()); - RECT nonClientArea{}; - LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&nonClientArea, windowStyle, false, 0, dpi)); - - // The nonClientArea adjustment is negative, so subtract that out. - // This way we save the user-visible location of the terminal. - LaunchPosition pos{}; - pos.X = window.left - nonClientArea.left; - pos.Y = window.top; - - layout.InitialPosition(pos); - } - - auto state = ApplicationState::SharedInstance(); - state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + return layout; } // Method Description: @@ -1392,8 +1394,9 @@ namespace winrt::TerminalApp::implementation if (ShouldUsePersistedLayout(_settings)) { - PersistWindowLayout(); - // don't delete the ApplicationState when all of the tabs are removed. + // Don't delete the ApplicationState when all of the tabs are removed. + // If there is still a monarch living they will get the event that + // a window closed and trigger a new save without this window. _maintainStateOnTabClose = true; } @@ -3106,6 +3109,11 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::SetPersistedLayoutIdx(const uint32_t idx) + { + _loadFromPersistedLayoutIdx = idx; + } + void TerminalPage::SetNumberOfOpenWindows(const uint64_t num) { _numOpenWindows = num; diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index a9ff78959..8f776a2d7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -59,6 +59,9 @@ namespace winrt::TerminalApp::implementation void Create(); bool ShouldUsePersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + std::optional LoadPersistedLayoutIdx(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + winrt::Microsoft::Terminal::Settings::Model::WindowLayout LoadPersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout(); winrt::fire_and_forget NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e); @@ -82,7 +85,6 @@ namespace winrt::TerminalApp::implementation bool AlwaysOnTop() const; void SetStartupActions(std::vector& actions); - void PersistWindowLayout(); void SetInboundListener(bool isEmbedding); static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); @@ -111,6 +113,7 @@ namespace winrt::TerminalApp::implementation void WindowId(const uint64_t& value); void SetNumberOfOpenWindows(const uint64_t value); + void SetPersistedLayoutIdx(const uint32_t value); winrt::hstring WindowIdForDisplay() const noexcept; winrt::hstring WindowNameForDisplay() const noexcept; @@ -133,6 +136,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs); TYPED_EVENT(IsQuakeWindowChanged, IInspectable, IInspectable); TYPED_EVENT(SummonWindowRequested, IInspectable, IInspectable); + TYPED_EVENT(CloseRequested, IInspectable, IInspectable); TYPED_EVENT(OpenSystemMenu, IInspectable, IInspectable); TYPED_EVENT(QuitRequested, IInspectable, IInspectable); @@ -166,6 +170,7 @@ namespace winrt::TerminalApp::implementation bool _isAlwaysOnTop{ false }; winrt::hstring _WindowName{}; uint64_t _WindowId{ 0 }; + std::optional _loadFromPersistedLayoutIdx{}; uint64_t _numOpenWindows{ 0 }; bool _maintainStateOnTabClose{ false }; diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index dd2777add..979483d0a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -33,7 +33,6 @@ namespace TerminalApp UInt64 WindowId; String WindowNameForDisplay { get; }; String WindowIdForDisplay { get; }; - void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); @@ -58,6 +57,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RenameWindowRequested; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler CloseRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; } } diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 53c75eb6f..afb637810 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -450,12 +450,25 @@ namespace winrt::TerminalApp::implementation // 1 for the child after the first split. auto state = _rootPane->BuildStartupActions(0, 1); - ActionAndArgs newTabAction{}; - newTabAction.Action(ShortcutAction::NewTab); - NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; - newTabAction.Args(newTabArgs); + { + ActionAndArgs newTabAction{}; + newTabAction.Action(ShortcutAction::NewTab); + NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; + newTabAction.Args(newTabArgs); - state.args.emplace(state.args.begin(), std::move(newTabAction)); + state.args.emplace(state.args.begin(), std::move(newTabAction)); + } + + if (_runtimeTabColor) + { + ActionAndArgs setColorAction{}; + setColorAction.Action(ShortcutAction::SetTabColor); + + SetTabColorArgs setColorArgs{ _runtimeTabColor.value() }; + setColorAction.Args(setColorArgs); + + state.args.emplace_back(std::move(setColorAction)); + } // If we only have one arg, we only have 1 pane so we don't need any // special focus logic diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index a21ae08e3..bf0e85ee8 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -304,8 +304,8 @@ An option to choose from for the "First window preference" setting. Open the default profile. - Open tabs from a previous session - An option to choose from for the "First window preference" setting. Reopen the layout from the last session. + Open windows from a previous session + An option to choose from for the "First window preference" setting. Reopen the layouts from the last session. Launch mode diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 94f7cebea..74315c2ab 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -864,6 +864,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct SetTabColorArgs : public SetTabColorArgsT { SetTabColorArgs() = default; + SetTabColorArgs(Windows::UI::Color tabColor) : + _TabColor{ tabColor } {} ACTION_ARG(Windows::Foundation::IReference, TabColor, nullptr); static constexpr std::string_view ColorKey{ "color" }; @@ -1582,6 +1584,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct RenameWindowArgs : public RenameWindowArgsT { RenameWindowArgs() = default; + RenameWindowArgs(winrt::hstring name) : + _Name{ name } {}; ACTION_ARG(winrt::hstring, Name); static constexpr std::string_view NameKey{ "name" }; @@ -1869,9 +1873,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(NewTabArgs); BASIC_FACTORY(MoveFocusArgs); BASIC_FACTORY(MovePaneArgs); + BASIC_FACTORY(SetTabColorArgs); BASIC_FACTORY(SwapPaneArgs); BASIC_FACTORY(SplitPaneArgs); BASIC_FACTORY(SetColorSchemeArgs); + BASIC_FACTORY(RenameWindowArgs); BASIC_FACTORY(ExecuteCommandlineArgs); BASIC_FACTORY(CloseOtherTabsArgs); BASIC_FACTORY(CloseTabsAfterArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 21b1b4b0b..2ea94e84d 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -213,6 +213,7 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass SetTabColorArgs : IActionArgs { + SetTabColorArgs(Windows.UI.Color tabColor); Windows.Foundation.IReference TabColor { get; }; }; @@ -294,6 +295,7 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass RenameWindowArgs : IActionArgs { + RenameWindowArgs(String name); String Name { get; }; }; diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp index 5a00ba2b4..acd876884 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -60,6 +60,31 @@ using namespace ::Microsoft::Terminal::Settings::Model; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { + winrt::hstring WindowLayout::ToJson(const Model::WindowLayout& layout) + { + JsonUtils::ConversionTrait trait; + auto json = trait.ToJson(layout); + + Json::StreamWriterBuilder wbuilder; + const auto content = Json::writeString(wbuilder, json); + return hstring{ til::u8u16(content) }; + } + + Model::WindowLayout WindowLayout::FromJson(const hstring& str) + { + auto data = til::u16u8(str); + std::string errs; + std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + + Json::Value root; + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + JsonUtils::ConversionTrait trait; + return trait.FromJson(root); + } + // Returns the application-global ApplicationState object. Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance() { @@ -108,6 +133,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { \ auto state = _state.lock(); \ state->name.emplace(value); \ + state->name##Changed = true; \ } \ \ _throttler(); \ @@ -115,34 +141,50 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN + Json::Value ApplicationState::_getRoot(const locked_hfile& file) const noexcept + { + Json::Value root; + try + { + const auto data = ReadUTF8FileLocked(file); + if (data.empty()) + { + return root; + } + + std::string errs; + std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + } + CATCH_LOG() + + return root; + } + // Deserializes the state.json at _path into this ApplicationState. // * ANY errors during app state will result in the creation of a new empty state. // * ANY errors during runtime will result in changes being partially ignored. void ApplicationState::_read() const noexcept try { - const auto data = ReadUTF8FileIfExists(_path).value_or(std::string{}); - if (data.empty()) - { - return; - } - - std::string errs; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; - - Json::Value root; - if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) - { - throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); - } - auto state = _state.lock(); + const auto file = OpenFileReadSharedLocked(_path); + + auto root = _getRoot(file); // GetValueForKey() comes in two variants: // * take a std::optional reference // * return std::optional by value // At the time of writing the former version skips missing fields in the json, // but we want to explicitly clear state fields that were removed from state.json. -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + if (!state->name##Changed) \ + { \ + state->name = JsonUtils::GetValueForKey>(root, key); \ + } MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN } @@ -152,21 +194,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // * Errors are only logged. // * _state->_writeScheduled is set to false, signaling our // setters that _synchronize() needs to be called again. - void ApplicationState::_write() const noexcept + void ApplicationState::_write() noexcept try { - Json::Value root{ Json::objectValue }; - + // re-read the state so that we can only update the properties that were changed. + Json::Value root{}; { - auto state = _state.lock_shared(); -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); + auto state = _state.lock(); + const auto file = OpenFileRWExclusiveLocked(_path); + root = _getRoot(file); + +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + if (state->name##Changed) \ + { \ + JsonUtils::SetValueForKey(root, key, state->name); \ + state->name##Changed = false; \ + } MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN - } - Json::StreamWriterBuilder wbuilder; - const auto content = Json::writeString(wbuilder, root); - WriteUTF8FileAtomic(_path, content); + Json::StreamWriterBuilder wbuilder; + const auto content = Json::writeString(wbuilder, root); + WriteUTF8FileLocked(file, content); + } } CATCH_LOG() } diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index 71c6a576e..9b4f40f28 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -18,6 +18,7 @@ Abstract: #include #include #include +#include "FileUtils.h" #include // This macro generates all getters and setters for ApplicationState. @@ -33,6 +34,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct WindowLayout : WindowLayoutT { + static winrt::hstring ToJson(const Model::WindowLayout& layout); + static Model::WindowLayout FromJson(const winrt::hstring& json); + WINRT_PROPERTY(Windows::Foundation::Collections::IVector, TabLayout, nullptr); WINRT_PROPERTY(winrt::Windows::Foundation::IReference, InitialPosition, nullptr); WINRT_PROPERTY(winrt::Windows::Foundation::IReference, InitialSize, nullptr); @@ -63,12 +67,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation private: struct state_t { -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) std::optional name{ __VA_ARGS__ }; +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ + std::optional name{ __VA_ARGS__ }; \ + bool name##Changed = false; + MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN }; - void _write() const noexcept; + Json::Value _getRoot(const winrt::Microsoft::Terminal::Settings::Model::locked_hfile& file) const noexcept; + void _write() noexcept; void _read() const noexcept; std::filesystem::path _path; diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.idl b/src/cascadia/TerminalSettingsModel/ApplicationState.idl index 91231a112..119a37979 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.idl +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.idl @@ -15,6 +15,9 @@ namespace Microsoft.Terminal.Settings.Model { WindowLayout(); + static String ToJson(WindowLayout layout); + static WindowLayout FromJson(String json); + Windows.Foundation.Collections.IVector TabLayout; Windows.Foundation.IReference InitialPosition; Windows.Foundation.IReference InitialSize; diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.cpp b/src/cascadia/TerminalSettingsModel/FileUtils.cpp index cf273d5a5..599d29a55 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.cpp +++ b/src/cascadia/TerminalSettingsModel/FileUtils.cpp @@ -39,6 +39,83 @@ namespace winrt::Microsoft::Terminal::Settings::Model return baseSettingsPath; } + locked_hfile OpenFileReadSharedLocked(const std::filesystem::path& path) + { + wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) }; + THROW_LAST_ERROR_IF(!file); + // just lock the entire file + OVERLAPPED sOverlapped; + sOverlapped.Offset = 0; + sOverlapped.OffsetHigh = 0; + // Shared lock + THROW_LAST_ERROR_IF(!LockFileEx(file.get(), + 0, // lock shared, wait to return until lock is obtained + 0, // reserved, does nothing + INT_MAX, // lock INT_MAX bytes + 0, // higher-order bytes, if our state file is greater than 2GB I guess this will be a problem + &sOverlapped)); + return { std::move(file), sOverlapped }; + } + + locked_hfile OpenFileRWExclusiveLocked(const std::filesystem::path& path) + { + wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; + THROW_LAST_ERROR_IF(!file); + // just lock the entire file + OVERLAPPED sOverlapped; + sOverlapped.Offset = 0; + sOverlapped.OffsetHigh = 0; + // Shared lock + THROW_LAST_ERROR_IF(!LockFileEx(file.get(), + LOCKFILE_EXCLUSIVE_LOCK, // lock exclusive, wait to return until lock is obtained + 0, // reserved, does nothing + INT_MAX, // lock INT_MAX bytes + 0, // higher-order bytes, if our state file is greater than 2GB I guess this will be a problem + &sOverlapped)); + return { std::move(file), sOverlapped }; + } + + std::string ReadUTF8FileLocked(const locked_hfile& file) + { + const auto fileSize = GetFileSize(file.get(), nullptr); + THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE); + + // By making our buffer just slightly larger we can detect if + // the file size changed and we've failed to read the full file. + std::string buffer(static_cast(fileSize) + 1, '\0'); + DWORD bytesRead = 0; + THROW_IF_WIN32_BOOL_FALSE(ReadFile(file.get(), buffer.data(), gsl::narrow(buffer.size()), &bytesRead, nullptr)); + + // As mentioned before our buffer was allocated oversized. + buffer.resize(bytesRead); + + if (til::starts_with(buffer, Utf8Bom)) + { + // Yeah this memmove()s the entire content. + // But I don't really want to deal with UTF8 BOMs any more than necessary, + // as basically not a single editor writes a BOM for UTF8. + buffer.erase(0, Utf8Bom.size()); + } + + return buffer; + } + + void WriteUTF8FileLocked(const locked_hfile& file, const std::string_view& content) + { + // truncate the file because we want to overwrite it + SetFilePointer(file.get(), 0, nullptr, FILE_BEGIN); + THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get())); + + const auto fileSize = gsl::narrow(content.size()); + DWORD bytesWritten = 0; + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), fileSize, &bytesWritten, nullptr)); + + if (bytesWritten != fileSize) + { + THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write whole file"); + } + } + // Tries to read a file somewhat atomically without locking it. // Strips the UTF8 BOM if it exists. std::string ReadUTF8File(const std::filesystem::path& path) diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.h b/src/cascadia/TerminalSettingsModel/FileUtils.h index c003228c3..187051e7c 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.h +++ b/src/cascadia/TerminalSettingsModel/FileUtils.h @@ -1,9 +1,39 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +#pragma once + namespace winrt::Microsoft::Terminal::Settings::Model { + // I couldn't find a wil helper for this so I made it myself + class locked_hfile + { + public: + wil::unique_hfile file; + OVERLAPPED lockedRegion; + + ~locked_hfile() + { + if (file) + { + // Need to unlock the file before it is closed + UnlockFileEx(file.get(), 0, INT_MAX, 0, &lockedRegion); + } + } + + HANDLE get() const noexcept + { + return file.get(); + } + }; + std::filesystem::path GetBaseSettingsPath(); + + locked_hfile OpenFileReadSharedLocked(const std::filesystem::path& path); + locked_hfile OpenFileRWExclusiveLocked(const std::filesystem::path& path); + std::string ReadUTF8FileLocked(const locked_hfile& file); + void WriteUTF8FileLocked(const locked_hfile& file, const std::string_view& content); + std::string ReadUTF8File(const std::filesystem::path& path); std::optional ReadUTF8FileIfExists(const std::filesystem::path& path); void WriteUTF8File(const std::filesystem::path& path, const std::string_view& content); diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 0a534e5a0..2112c58a6 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -76,6 +76,7 @@ namespace RemotingUnitTests void Summon(const Remoting::SummonWindowBehavior& /*args*/) { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestShowNotificationIcon() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestHideNotificationIcon() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; + winrt::hstring GetWindowLayout() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void RequestQuitAll() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; void Quit() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); }; TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs); @@ -88,6 +89,7 @@ namespace RemotingUnitTests TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, Remoting::GetWindowLayoutArgs); }; class RemotingTests diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index e9d9b2416..c2b8dcda2 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -20,6 +20,7 @@ using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace ::Microsoft::Console; using namespace ::Microsoft::Console::Types; +using namespace std::chrono_literals; // This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx // "If the high-order bit is 1, the key is down; otherwise, it is up." @@ -29,7 +30,8 @@ AppHost::AppHost() noexcept : _app{}, _windowManager{}, _logic{ nullptr }, // don't make one, we're going to take a ref on app's - _window{ nullptr } + _window{ nullptr }, + _getWindowLayoutThrottler{} // this will get set if we become the monarch { _logic = _app.Logic(); // get a ref to app's logic @@ -84,6 +86,12 @@ AppHost::AppHost() noexcept : _window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop()); _window->MakeWindow(); + _windowManager.GetWindowLayoutRequested([this](auto&&, const winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs& args) { + // The peasants are running on separate threads, so they'll need to + // swap what context they are in to the ui thread to get the actual layout. + args.WindowLayoutJsonAsync(_GetWindowLayoutAsync()); + }); + _windowManager.BecameMonarch({ this, &AppHost::_BecomeMonarch }); if (_windowManager.IsMonarch()) { @@ -220,7 +228,47 @@ void AppHost::_HandleCommandlineArgs() // is created. if (_windowManager.IsMonarch()) { - _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); + const auto numPeasants = _windowManager.GetNumberOfPeasants(); + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (_logic.ShouldUsePersistedLayout() && layouts && layouts.Size() > 0) + { + uint32_t startIdx = 0; + // We want to create a window for every saved layout. + // If we are the only window, and no commandline arguments were provided + // then we should just use the current window to load the first layout. + // Otherwise create this window normally with its commandline, and create + // a new window using the first saved layout information. + // The 2nd+ layout will always get a new window. + if (numPeasants == 1 && !_logic.HasCommandlineArguments() && !_logic.HasSettingsStartupActions()) + { + _logic.SetPersistedLayoutIdx(startIdx); + startIdx += 1; + } + + // Create new windows for each of the other saved layouts. + for (const auto size = layouts.Size(); startIdx < size; startIdx += 1) + { + auto newWindowArgs = fmt::format(L"{0} -w new -s {1}", args[0], startIdx); + + STARTUPINFO si; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + wil::unique_process_information pi; + + LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, + newWindowArgs.data(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + false, // bInheritHandles + DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, // doCreationFlags + nullptr, // lpEnvironment + nullptr, // lpStartingDirectory + &si, // lpStartupInfo + &pi // lpProcessInformation + )); + } + } + _logic.SetNumberOfOpenWindows(numPeasants); } _logic.WindowName(peasant.WindowName()); _logic.WindowId(peasant.GetID()); @@ -257,7 +305,16 @@ void AppHost::Initialize() // Register the 'X' button of the window for a warning experience of multiple // tabs opened, this is consistent with Alt+F4 closing - _window->WindowCloseButtonClicked([this]() { _logic.WindowCloseButtonClicked(); }); + _window->WindowCloseButtonClicked([this]() { + const auto pos = _GetWindowLaunchPosition(); + _logic.CloseWindow(pos); + }); + // If the user requests a close in another way handle the same as if the 'X' + // was clicked. + _logic.CloseRequested([this](auto&&, auto&&) { + const auto pos = _GetWindowLaunchPosition(); + _logic.CloseWindow(pos); + }); // Add an event handler to plumb clicks in the titlebar area down to the // application layer. @@ -347,6 +404,24 @@ void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*se _window->Close(); } +LaunchPosition AppHost::_GetWindowLaunchPosition() +{ + // Get the position of the current window. This includes the + // non-client already. + const auto window = _window->GetWindowRect(); + + const auto dpi = _window->GetCurrentDpi(); + const auto nonClientArea = _window->GetNonClientFrame(dpi); + + // The nonClientArea adjustment is negative, so subtract that out. + // This way we save the user-visible location of the terminal. + LaunchPosition pos{}; + pos.X = window.left - nonClientArea.left; + pos.Y = window.top; + + return pos; +} + // Method Description: // - Resize the window we're about to create to the appropriate dimensions, as // specified in the settings. This will be called during the handling of @@ -634,6 +709,31 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send _logic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory()); } +// Method Description: +// - Asynchronously get the window layout from the current page. This is +// done async because we need to switch between the ui thread and the calling +// thread. +// - NB: The peasant calling this must not be running on the UI thread, otherwise +// they will crash since they just call .get on the async operation. +// Arguments: +// - +// Return Value: +// - The window layout as a json string. +winrt::Windows::Foundation::IAsyncOperation AppHost::_GetWindowLayoutAsync() +{ + winrt::apartment_context peasant_thread; + + // Use the main thread since we are accessing controls. + co_await winrt::resume_foreground(_logic.GetRoot().Dispatcher()); + const auto pos = _GetWindowLaunchPosition(); + const auto layoutJson = _logic.GetWindowLayoutJson(pos); + + // go back to give the result to the peasant. + co_await peasant_thread; + + co_return layoutJson; +} + // Method Description: // - Event handler for the WindowManager::FindTargetWindowRequested event. The // manager will ask us how to figure out what the target window is for a set @@ -687,8 +787,13 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s // and subscribe for updates if there are any changes to that number. _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); - _windowManager.WindowCreated([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); - _windowManager.WindowClosed([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); + _windowManager.WindowCreated([this](auto&&, auto&&) { + _getWindowLayoutThrottler.value()(); + _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); + _windowManager.WindowClosed([this](auto&&, auto&&) { + _getWindowLayoutThrottler.value()(); + _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); + }); // These events are coming from peasants that become or un-become quake windows. _windowManager.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequested(); }); @@ -696,6 +801,48 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s // If the monarch receives a QuitAll event it will signal this event to be // ran before each peasant is closed. _windowManager.QuitAllRequested({ this, &AppHost::_QuitAllRequested }); + + // The monarch should be monitoring if it should save the window layout. + if (!_getWindowLayoutThrottler.has_value()) + { + // We want at least some delay to prevent the first save from overwriting + // the data as we try load windows initially. + _getWindowLayoutThrottler.emplace(std::move(std::chrono::seconds(10)), std::move([this]() { _SaveWindowLayoutsRepeat(); })); + _getWindowLayoutThrottler.value()(); + } +} + +winrt::Windows::Foundation::IAsyncAction AppHost::_SaveWindowLayouts() +{ + // Make sure we run on a background thread to not block anything. + co_await winrt::resume_background(); + + if (_logic.ShouldUsePersistedLayout()) + { + const auto layoutJsons = _windowManager.GetAllWindowLayouts(); + _logic.SaveWindowLayoutJsons(layoutJsons); + } + + co_return; +} + +winrt::fire_and_forget AppHost::_SaveWindowLayoutsRepeat() +{ + // Make sure we run on a background thread to not block anything. + co_await winrt::resume_background(); + + co_await _SaveWindowLayouts(); + + // Don't need to save too frequently. + co_await 30s; + + // As long as we are supposed to keep saving, request another save. + // This will be delayed by the throttler so that at most one save happens + // per 10 seconds, if a save is requested by another source simultaneously. + if (_getWindowLayoutThrottler.has_value()) + { + _getWindowLayoutThrottler.value()(); + } } void AppHost::_listenForInboundConnections() @@ -1046,10 +1193,18 @@ void AppHost::_RequestQuitAll(const winrt::Windows::Foundation::IInspectable&, } void AppHost::_QuitAllRequested(const winrt::Windows::Foundation::IInspectable&, - const winrt::Windows::Foundation::IInspectable&) + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args) { - // TODO: GH#9800: For now, nothing needs to be done before the monarch closes all windows. - // Later when we have state saving that should go here. + // Make sure that the current timer is destroyed so that it doesn't attempt + // to run while we are in the middle of quitting. + if (_getWindowLayoutThrottler.has_value()) + { + _getWindowLayoutThrottler.reset(); + } + + // Tell the monarch to wait for the window layouts to save before + // everyone quits. + args.BeforeQuitAllAction(_SaveWindowLayouts()); } void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index db8a0bf1b..d51ba227a 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -4,6 +4,7 @@ #include "pch.h" #include "NonClientIslandWindow.h" #include "NotificationIcon.h" +#include class AppHost { @@ -31,7 +32,12 @@ private: bool _shouldCreateWindow{ false }; bool _useNonClientArea{ false }; + std::optional> _getWindowLayoutThrottler; + winrt::Windows::Foundation::IAsyncAction _SaveWindowLayouts(); + winrt::fire_and_forget _SaveWindowLayoutsRepeat(); + void _HandleCommandlineArgs(); + winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition(); void _HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); void _UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable& sender, @@ -53,6 +59,8 @@ private: void _DispatchCommandline(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::Terminal::Remoting::CommandlineArgs args); + winrt::Windows::Foundation::IAsyncOperation _GetWindowLayoutAsync(); + void _FindTargetWindow(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args); @@ -95,7 +103,7 @@ private: const winrt::Windows::Foundation::IInspectable& args); void _QuitAllRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args); void _CreateNotificationIcon(); void _DestroyNotificationIcon(); diff --git a/src/cascadia/WindowsTerminal/BaseWindow.h b/src/cascadia/WindowsTerminal/BaseWindow.h index 581b38617..167b309bb 100644 --- a/src/cascadia/WindowsTerminal/BaseWindow.h +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -133,6 +133,11 @@ public: return _window.get(); } + UINT GetCurrentDpi() const noexcept + { + return ::GetDpiForWindow(_window.get()); + } + float GetCurrentDpiScale() const noexcept { const auto dpi = ::GetDpiForWindow(_window.get());