terminal/src/cascadia/WindowsTerminal/WindowEmperor.cpp
Carlos Zamora 16f7ab4185
Add firstWindowPreference value for layout only (#19341)
## Summary of the Pull Request
Updates the "firstWindowPreference" global setting to take 3 values:
"defaultProfile", "persistedLayout", and "persistedLayoutAndContent".

The legacy "persistedWindowLayout" is being interpreted as
"persistedLayoutAndContent".

The tricky part here is that we need to maintain support for the legacy
value as persisting the layout and content, even though the value's name
suggests that it should just support the layout and no content. To get
around this, I added "persistedLayout" and "persistedLayoutAndContent".

The enum map is manually constructed for `FirstWindowPreference` to
exclude the deprecated value. This prevents the legacy value from
leaking into the settings UI.

Functionally, the change to serialize the contents is simple.
`WindowEmperor::_persistState()`'s second parameter is used to serialize
the buffer. Rather than having it set to `true`, we set it to
`GlobalSettings().FirstWindowPreference() ==
FirstWindowPreference::PersistedLayoutAndContent`.

## Validation Steps Performed
 "persistedWindowLayout" is changed to "persistedLayoutAndContent"

 Closes #18757
2025-10-27 20:55:54 +00:00

1405 lines
50 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "WindowEmperor.h"
#include <CoreWindow.h>
#include <LibraryResources.h>
#include <ScopedResourceLoader.h>
#include <WtExeUtils.h>
#include <til/hash.h>
#include <wil/token_helpers.h>
#include "AppHost.h"
#include "resource.h"
#include "VirtualDesktopUtils.h"
#include "../../types/inc/User32Utils.hpp"
#include "../../types/inc/utils.hpp"
using namespace winrt;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Windows::Foundation;
using namespace ::Microsoft::Console;
using namespace std::chrono_literals;
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
#ifdef _WIN64
static constexpr ULONG_PTR TERMINAL_HANDOFF_MAGIC = 0x4c414e494d524554; // 'TERMINAL'
#else
static constexpr ULONG_PTR TERMINAL_HANDOFF_MAGIC = 0x4d524554; // 'TERM'
#endif
extern "C" IMAGE_DOS_HEADER __ImageBase;
// A convenience function around CommandLineToArgv.
static std::vector<winrt::hstring> commandlineToArgArray(const wchar_t* commandLine)
{
int argc = 0;
const wil::unique_hlocal_ptr<LPWSTR> argv{ CommandLineToArgvW(commandLine, &argc) };
argc = std::max(argc, 0);
std::vector<winrt::hstring> args;
args.reserve(argc);
for (int i = 0; i < argc; i++)
{
args.emplace_back(argv.get()[i]);
}
return args;
}
// Returns the length of a double-null encoded string *excluding* the trailing double-null character.
static wil::zwstring_view stringFromDoubleNullTerminated(const wchar_t* beg)
{
auto end = beg;
for (; *end; end += wcsnlen(end, SIZE_T_MAX) + 1)
{
}
return { beg, gsl::narrow_cast<size_t>(end - beg) };
}
// Appends an uint32_t to a byte vector.
static void serializeUint32(std::vector<uint8_t>& out, uint32_t value)
{
out.insert(out.end(), reinterpret_cast<const uint8_t*>(&value), reinterpret_cast<const uint8_t*>(&value) + sizeof(value));
}
// Parses an uint32_t from the input iterator. Performs bounds-checks.
// Returns an iterator that points past it.
static const uint8_t* deserializeUint32(const uint8_t* it, const uint8_t* end, uint32_t& val)
{
if (static_cast<size_t>(end - it) < sizeof(uint32_t))
{
throw std::out_of_range("Not enough data for uint32_t");
}
val = *reinterpret_cast<const uint32_t*>(it);
return it + sizeof(uint32_t);
}
// Writes a null-terminated string to `out`: A uint32_t length prefix,
// *including null byte*, followed by the string data, followed by the null-terminator.
static void serializeString(std::vector<uint8_t>& out, wil::zwstring_view str)
{
const auto ptr = reinterpret_cast<const uint8_t*>(str.data());
const auto len = str.size() + 1;
serializeUint32(out, gsl::narrow<uint32_t>(len));
out.insert(out.end(), ptr, ptr + len * sizeof(wchar_t));
}
// Counter-part to `serializeString`. Performs bounds-checks.
// Returns an iterator that points past it.
static const uint8_t* deserializeString(const uint8_t* it, const uint8_t* end, wil::zwstring_view& str)
{
uint32_t len;
it = deserializeUint32(it, end, len);
const auto bytes = static_cast<size_t>(len) * sizeof(wchar_t);
if (bytes == 0 || static_cast<size_t>(end - it) < bytes)
{
throw std::out_of_range("Not enough data for string content");
}
str = { reinterpret_cast<const wchar_t*>(it), len - 1 };
return it + bytes;
}
struct Handoff
{
wil::zwstring_view args;
wil::zwstring_view env;
wil::zwstring_view cwd;
uint32_t show;
};
// Serializes all relevant parameters to a byte blob for a WM_COPYDATA message.
// This allows us to hand off an invocation to an existing instance of the Terminal.
static std::vector<uint8_t> serializeHandoffPayload(int nCmdShow)
{
const auto args = GetCommandLineW();
const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() };
const auto env = stringFromDoubleNullTerminated(envMem.get());
const auto cwd = wil::GetCurrentDirectoryW<std::wstring>();
std::vector<uint8_t> out;
serializeString(out, args);
serializeString(out, env);
serializeString(out, cwd);
serializeUint32(out, static_cast<uint32_t>(nCmdShow));
return out;
}
// Counterpart to serializeHandoffPayload.
static Handoff deserializeHandoffPayload(const uint8_t* beg, const uint8_t* end)
{
Handoff result{};
auto it = beg;
it = deserializeString(it, end, result.args);
it = deserializeString(it, end, result.env);
it = deserializeString(it, end, result.cwd);
it = deserializeUint32(it, end, result.show);
return result;
}
// Either acquires unique ownership over the given `className` mutex,
// or attempts to pass the commandline to the existing instance.
static wil::unique_mutex acquireMutexOrAttemptHandoff(const wchar_t* className, int nCmdShow)
{
// If the process that owns the mutex has not finished creating the window yet,
// FindWindowW will return nullptr. We'll retry a few times just in case.
// At the 1.5x growth rate, this will retry up to ~30s in total.
for (DWORD sleep = 50; sleep < 10000; sleep += sleep / 2)
{
wil::unique_mutex mutex{ CreateMutexW(nullptr, TRUE, className) };
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
mutex.reset();
}
if (mutex)
{
return mutex;
}
// I found that FindWindow() with no other filters is substantially faster than
// using FindWindowEx() and restricting the search to just HWND_MESSAGE windows.
// In both cases it's quite fast though at ~1us/op vs ~3us/op (good old Win32 <3).
if (const auto hwnd = FindWindowW(className, nullptr))
{
auto payload = serializeHandoffPayload(nCmdShow);
const COPYDATASTRUCT cds{
.dwData = TERMINAL_HANDOFF_MAGIC,
.cbData = gsl::narrow<DWORD>(payload.size()),
.lpData = payload.data(),
};
// Allow the existing instance to gain foreground rights.
DWORD processId = 0;
if (GetWindowThreadProcessId(hwnd, &processId) && processId)
{
AllowSetForegroundWindow(processId);
}
if (SendMessageTimeoutW(hwnd, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(&cds), SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 5000, nullptr))
{
return {};
}
}
Sleep(sleep);
}
return {};
}
static constexpr bool IsInputKey(WORD vkey) noexcept
{
return vkey != VK_CONTROL &&
vkey != VK_LCONTROL &&
vkey != VK_RCONTROL &&
vkey != VK_MENU &&
vkey != VK_LMENU &&
vkey != VK_RMENU &&
vkey != VK_SHIFT &&
vkey != VK_LSHIFT &&
vkey != VK_RSHIFT &&
vkey != VK_LWIN &&
vkey != VK_RWIN &&
vkey != VK_SNAPSHOT;
}
HWND WindowEmperor::GetMainWindow() const noexcept
{
_assertIsMainThread();
return _window.get();
}
AppHost* WindowEmperor::GetWindowById(uint64_t id) const noexcept
{
_assertIsMainThread();
for (const auto& window : _windows)
{
if (window->Logic().WindowProperties().WindowId() == id)
{
return window.get();
}
}
return nullptr;
}
AppHost* WindowEmperor::GetWindowByName(std::wstring_view name) const noexcept
{
_assertIsMainThread();
for (const auto& window : _windows)
{
if (window->Logic().WindowProperties().WindowName() == name)
{
return window.get();
}
}
return nullptr;
}
void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args)
{
_assertIsMainThread();
uint64_t id = args.Id();
bool needsNewId = id == 0;
uint64_t newId = 0;
for (const auto& host : _windows)
{
const auto existingId = host->Logic().WindowProperties().WindowId();
newId = std::max(newId, existingId);
needsNewId |= existingId == id;
}
if (needsNewId)
{
args.Id(newId + 1);
}
auto host = std::make_shared<AppHost>(this, _app.Logic(), std::move(args));
host->Initialize();
_windowCount += 1;
_windows.emplace_back(std::move(host));
if (_windowCount == 1)
{
// The first CoreWindow is created implicitly by XAML and parented to the
// first XAML island. We parent it to our initial window for 2 reasons:
// * On Windows 10 the CoreWindow will show up as a visible window on the taskbar
// due to a WinUI bug, and this will hide it, because our initial window is hidden.
// * When we DestroyWindow() the island it will destroy the CoreWindow,
// and it's not possible to recreate it. That's also a WinUI bug.
//
// Note that this must be done after the first window (= first island) is created.
if (const auto coreWindow = winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread())
{
if (const auto interop = coreWindow.try_as<ICoreWindowInterop>())
{
HWND coreHandle = nullptr;
if (SUCCEEDED(interop->get_WindowHandle(&coreHandle)) && coreHandle)
{
SetParent(coreHandle, _window.get());
}
}
}
}
}
AppHost* WindowEmperor::_mostRecentWindow() const noexcept
{
int64_t max = INT64_MIN;
AppHost* mostRecent = nullptr;
for (const auto& w : _windows)
{
const auto lastActivatedTime = w->GetLastActivatedTime();
if (lastActivatedTime > max)
{
max = lastActivatedTime;
mostRecent = w.get();
}
}
return mostRecent;
}
void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
{
std::wstring windowClassName;
windowClassName.reserve(64); // "Windows Terminal Preview Admin 0123456789012345 0123456789012345"
#if defined(WT_BRANDING_RELEASE)
windowClassName.append(L"Windows Terminal");
#elif defined(WT_BRANDING_PREVIEW)
windowClassName.append(L"Windows Terminal Preview");
#elif defined(WT_BRANDING_CANARY)
windowClassName.append(L"Windows Terminal Canary");
#else
windowClassName.append(L"Windows Terminal Dev");
#endif
if (Utils::IsRunningElevated())
{
windowClassName.append(L" Admin");
}
if (!IsPackaged())
{
const auto path = wil::QueryFullProcessImageNameW<std::wstring>();
const auto hash = til::hash(path);
#ifdef _WIN64
fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:016x}"), hash);
#else
fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:08x}"), hash);
#endif
}
{
wil::unique_handle processToken{ GetCurrentProcessToken() };
const auto userTokenInfo{ wil::get_token_information<TOKEN_USER>(processToken.get()) };
const auto sidLength{ GetLengthSid(userTokenInfo->User.Sid) };
const auto hash{ til::hash(userTokenInfo->User.Sid, sidLength) };
#ifdef _WIN64
fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:016x}"), hash);
#else
fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:08x}"), hash);
#endif
}
// Windows Terminal is a single-instance application. Either acquire ownership
// over the mutex, or hand off the command line to the existing instance.
const auto mutex = acquireMutexOrAttemptHandoff(windowClassName.c_str(), nCmdShow);
if (!mutex)
{
// The command line has been handed off. We can now exit.
// We do it with TerminateProcess() primarily to avoid WinUI shutdown issues.
TerminateProcess(GetCurrentProcess(), gsl::narrow_cast<UINT>(0));
__assume(false);
}
_app = winrt::TerminalApp::App{};
_app.Logic().ReloadSettings();
_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.
_app.Logic().SettingsChanged([this](auto&&, const TerminalApp::SettingsLoadEventArgs& args) {
if (SUCCEEDED(args.Result()))
{
_assertIsMainThread();
_setupGlobalHotkeys();
_checkWindowsForNotificationIcon();
_setupSessionPersistence(args.NewSettings().GlobalSettings().ShouldUsePersistedLayout());
}
});
{
const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() };
const auto env = stringFromDoubleNullTerminated(envMem.get());
const auto cwd = wil::GetCurrentDirectoryW<std::wstring>();
const auto showCmd = gsl::narrow_cast<uint32_t>(nCmdShow);
// Restore persisted windows.
const auto state = ApplicationState::SharedInstance();
const auto layouts = state.PersistedWindowLayouts();
if (layouts && layouts.Size() > 0)
{
_needsPersistenceCleanup = true;
uint32_t startIdx = 0;
for (const auto layout : layouts)
{
hstring args[] = { L"wt", L"-w", L"new", L"-s", winrt::to_hstring(startIdx) };
_dispatchCommandlineCommon(args, cwd, env, showCmd);
startIdx += 1;
}
}
const auto args = commandlineToArgArray(GetCommandLineW());
if (args.size() == 2 && args[1] == L"-Embedding")
{
// We were launched for ConPTY handoff. We have no windows and also don't want to exit.
//
// TODO: Here we could start a timer and exit after, say, 5 seconds
// if no windows are created. But that's a minor concern.
}
else
{
// Create another window if needed: There aren't any yet, OR we got an explicit command line.
if (_windows.empty() || args.size() != 1)
{
_dispatchCommandlineCommon(args, cwd, env, showCmd);
}
// If we created no windows, e.g. because the args are "/?" we can just exit now.
_postQuitMessageIfNeeded();
}
}
// ALWAYS change the _real_ CWD of the Terminal to system32,
// so that we don't lock the directory we were spawned in.
if (std::wstring system32; SUCCEEDED_LOG(wil::GetSystemDirectoryW(system32)))
{
LOG_IF_WIN32_BOOL_FALSE(SetCurrentDirectoryW(system32.c_str()));
}
{
TerminalConnection::ConptyConnection::NewConnection([this](TerminalConnection::ConptyConnection conn) {
TerminalApp::CommandlineArgs args;
args.ShowWindowCommand(conn.ShowWindow());
args.Connection(std::move(conn));
_dispatchCommandline(std::move(args));
_summonWindow(SummonWindowSelectionArgs{
.SummonBehavior = nullptr,
});
});
TerminalConnection::ConptyConnection::StartInboundListener();
}
// Main message loop. It pumps all windows.
bool loggedInteraction = false;
MSG msg{};
while (GetMessageW(&msg, nullptr, 0, 0))
{
// This is `if (WM_KEYDOWN || WM_KEYUP || WM_SYSKEYDOWN || WM_SYSKEYUP)`.
// It really doesn't need to be written that obtuse, but it at
// least nicely mirrors our `keyDown = msg.message & 1` logic.
// FYI: For the key-down/up messages the lowest bit indicates if it's up.
if ((msg.message & ~1) == WM_KEYDOWN || (msg.message & ~1) == WM_SYSKEYDOWN)
{
if (!loggedInteraction)
{
#if defined(WT_BRANDING_RELEASE)
constexpr uint8_t branding = 3;
#elif defined(WT_BRANDING_PREVIEW)
constexpr uint8_t branding = 2;
#elif defined(WT_BRANDING_CANARY)
constexpr uint8_t branding = 1;
#else
constexpr uint8_t branding = 0;
#endif
const uint8_t distribution = IsPackaged() ? 2 :
_app.Logic().Settings().IsPortableMode() ? 1 :
0;
TraceLoggingWrite(
g_hWindowsTerminalProvider,
"SessionBecameInteractive",
TraceLoggingDescription("Event emitted when the session was interacted with"),
TraceLoggingValue(branding, "Branding"),
TraceLoggingValue(distribution, "Distribution"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
loggedInteraction = true;
}
const bool keyDown = (msg.message & 1) == 0;
if (
// GH#638: The Xaml input stack doesn't allow an application to suppress the "caret browsing"
// dialog experience triggered when you press F7. Official recommendation from the Xaml
// team is to catch F7 before we hand it off.
(msg.wParam == VK_F7 && keyDown) ||
// GH#6421: System XAML will never send an Alt KeyUp event. Same thing here.
(msg.wParam == VK_MENU && !keyDown) ||
// GH#7125: System XAML will show a system dialog on Alt Space.
// We want to handle it ourselves and there's no way to suppress it.
// It almost seems like there's a pattern here...
(msg.wParam == VK_SPACE && msg.message == WM_SYSKEYDOWN))
{
_dispatchSpecialKey(msg);
continue;
}
if (msg.message == WM_KEYDOWN)
{
const auto vkey = static_cast<WORD>(msg.wParam);
// Hide the cursor only when the key pressed is an input key (ignore modifier keys).
if (IsInputKey(vkey))
{
IslandWindow::HideCursor();
}
}
}
if (IslandWindow::IsCursorHidden())
{
IslandWindow::ShowCursorMaybe(msg.message);
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
_finalizeSessionPersistence();
if (_notificationIconShown)
{
Shell_NotifyIconW(NIM_DELETE, &_notificationIcon);
}
// There's a mysterious crash in XAML on Windows 10 if you just let _app get destroyed (GH#15410).
// We also need to ensure that all UI threads exit before WindowEmperor leaves the scope on the main thread (MSFT:46744208).
// Both problems can be solved and the shutdown accelerated by using TerminateProcess.
// std::exit(), etc., cannot be used here, because those use ExitProcess for unpackaged applications.
TerminateProcess(GetCurrentProcess(), gsl::narrow_cast<UINT>(0));
__assume(false);
}
void WindowEmperor::_dispatchSpecialKey(const MSG& msg) const
{
// Each CoreInput window is a child of our IslandWindow.
// We can figure out the IslandWindow HWND via GetAncestor().
const auto hwnd = GetAncestor(msg.hwnd, GA_ROOT);
AppHost* window = nullptr;
for (const auto& h : _windows)
{
const auto w = h->GetWindow();
if (w && w->GetHandle() == hwnd)
{
window = h.get();
break;
}
}
// Fallback.
if (!window)
{
window = _mostRecentWindow();
if (!window)
{
return;
}
}
const auto vkey = gsl::narrow_cast<uint32_t>(msg.wParam);
const auto scanCode = gsl::narrow_cast<uint8_t>(msg.lParam >> 16);
const bool keyDown = (msg.message & 1) == 0;
window->OnDirectKeyEvent(vkey, scanCode, keyDown);
}
void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs args)
{
_assertIsMainThread();
const auto exitCode = args.ExitCode();
if (const auto msg = args.ExitMessage(); !msg.empty())
{
_showMessageBox(msg, exitCode != 0);
return;
}
if (exitCode != 0)
{
return;
}
const auto parsedTarget = args.TargetWindow();
WindowingMode windowingBehavior = WindowingMode::UseNew;
uint64_t windowId = 0;
winrt::hstring windowName;
// Figure out the windowing behavior the caller wants
// and get the sanitized window ID (if any) and window name (if any).
if (parsedTarget.empty())
{
windowingBehavior = _app.Logic().Settings().GlobalSettings().WindowingBehavior();
}
else if (const auto opt = til::parse_signed<int64_t>(parsedTarget, 10))
{
// Negative window IDs map to WindowingMode::UseNew.
if (*opt > 0)
{
windowId = gsl::narrow_cast<uint64_t>(*opt);
}
else if (*opt == 0)
{
windowingBehavior = WindowingMode::UseExisting;
}
}
else if (parsedTarget == L"last")
{
windowingBehavior = WindowingMode::UseExisting;
}
// A window name of "new" maps to WindowingMode::UseNew.
else if (parsedTarget != L"new")
{
windowName = parsedTarget;
}
AppHost* window = nullptr;
// Map from the windowing behavior, ID, and name to a window.
if (windowId)
{
window = GetWindowById(windowId);
}
else if (!windowName.empty())
{
window = GetWindowByName(windowName);
}
else
{
switch (windowingBehavior)
{
case WindowingMode::UseAnyExisting:
window = _mostRecentWindow();
break;
case WindowingMode::UseExisting:
_dispatchCommandlineCurrentDesktop(std::move(args));
return;
default:
break;
}
}
if (window)
{
window->DispatchCommandline(std::move(args));
}
else
{
winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) };
request.WindowName(std::move(windowName));
CreateNewWindow(std::move(request));
}
}
void WindowEmperor::_dispatchCommandlineCommon(winrt::array_view<const winrt::hstring> args, wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand)
{
winrt::TerminalApp::CommandlineArgs c;
c.Commandline(args);
c.CurrentDirectory(currentDirectory);
c.CurrentEnvironment(envString);
c.ShowWindowCommand(showWindowCommand);
_dispatchCommandline(std::move(c));
}
// This is an implementation-detail of _dispatchCommandline().
safe_void_coroutine WindowEmperor::_dispatchCommandlineCurrentDesktop(winrt::TerminalApp::CommandlineArgs args)
{
std::shared_ptr<AppHost> mostRecent;
AppHost* window = nullptr;
if (winrt::guid currentDesktop; VirtualDesktopUtils::GetCurrentVirtualDesktopId(reinterpret_cast<GUID*>(&currentDesktop)))
{
int64_t max = INT64_MIN;
for (const auto& w : _windows)
{
const auto lastActivatedTime = w->GetLastActivatedTime();
const auto desktopId = co_await w->GetVirtualDesktopId();
if (desktopId == currentDesktop && lastActivatedTime > max)
{
max = lastActivatedTime;
mostRecent = w;
}
}
// GetVirtualDesktopId(), as current implemented, should always return on the main thread.
_assertIsMainThread();
window = mostRecent.get();
}
else
{
// If virtual desktops have never been used, and in turn Explorer never set them up,
// GetCurrentVirtualDesktopId will return false. In this case just use the current (only) desktop.
window = _mostRecentWindow();
}
if (window)
{
window->DispatchCommandline(std::move(args));
}
else
{
CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs{ 0, std::move(args) });
}
}
bool WindowEmperor::_summonWindow(const SummonWindowSelectionArgs& args) const
{
_assertIsMainThread();
AppHost* window = nullptr;
if (args.WindowID)
{
for (const auto& w : _windows)
{
if (w->Logic().WindowProperties().WindowId() == args.WindowID)
{
window = w.get();
break;
}
}
}
else if (!args.WindowName.empty())
{
for (const auto& w : _windows)
{
if (w->Logic().WindowProperties().WindowName() == args.WindowName)
{
window = w.get();
break;
}
}
}
else
{
window = _mostRecentWindow();
}
if (!window)
{
return false;
}
window->HandleSummon(args.SummonBehavior);
return true;
}
void WindowEmperor::_summonAllWindows() const
{
_assertIsMainThread();
TerminalApp::SummonWindowBehavior args;
args.ToggleVisibility(false);
for (const auto& window : _windows)
{
window->HandleSummon(args);
}
}
#pragma region WindowProc
static WindowEmperor* GetThisFromHandle(HWND const window) noexcept
{
const auto data = GetWindowLongPtrW(window, GWLP_USERDATA);
return reinterpret_cast<WindowEmperor*>(data);
}
[[nodiscard]] LRESULT __stdcall WindowEmperor::_wndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
{
if (const auto that = GetThisFromHandle(window))
{
return that->_messageHandler(window, message, wparam, lparam);
}
if (WM_NCCREATE == message)
{
const auto cs = reinterpret_cast<CREATESTRUCT*>(lparam);
const auto that = static_cast<WindowEmperor*>(cs->lpCreateParams);
that->_window.reset(window);
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(that));
}
return DefWindowProcW(window, message, wparam, lparam);
}
void WindowEmperor::_createMessageWindow(const wchar_t* className)
{
const auto instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
const auto icon = LoadIconW(instance, MAKEINTRESOURCEW(IDI_APPICON));
const WNDCLASS wc{
.lpfnWndProc = &_wndProc,
.hInstance = instance,
.hIcon = icon,
.lpszClassName = className,
};
RegisterClassW(&wc);
WM_TASKBARCREATED = RegisterWindowMessageW(L"TaskbarCreated");
// NOTE: This cannot be a HWND_MESSAGE window as otherwise we don't
// receive any HWND_BROADCAST messages, like WM_QUERYENDSESSION.
// NOTE: Before CreateWindowExW() returns it invokes our WM_NCCREATE
// message handler, which then stores the HWND in this->_window.
WINRT_VERIFY(CreateWindowExW(
/* dwExStyle */ 0,
/* lpClassName */ className,
/* lpWindowName */ L"Windows Terminal",
/* dwStyle */ 0,
/* X */ 0,
/* Y */ 0,
/* nWidth */ 0,
/* nHeight */ 0,
/* hWndParent */ nullptr,
/* hMenu */ nullptr,
/* hInstance */ instance,
/* lpParam */ this));
_notificationIcon.cbSize = sizeof(NOTIFYICONDATA);
_notificationIcon.hWnd = _window.get();
_notificationIcon.uID = 1;
_notificationIcon.uFlags = NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP | NIF_ICON;
_notificationIcon.uCallbackMessage = WM_NOTIFY_FROM_NOTIFICATION_AREA;
_notificationIcon.hIcon = icon;
_notificationIcon.uVersion = NOTIFYICON_VERSION_4;
// AppName happens to be in the ContextMenu's Resources, see GH#12264
const ScopedResourceLoader loader{ L"TerminalApp/ContextMenu" };
const auto appNameLoc = loader.GetLocalizedString(L"AppName");
StringCchCopy(_notificationIcon.szTip, ARRAYSIZE(_notificationIcon.szTip), appNameLoc.c_str());
}
// Posts a WM_QUIT as soon as we have no reason to exist anymore.
// That basically means no windows and no message boxes.
void WindowEmperor::_postQuitMessageIfNeeded() const
{
if (
_messageBoxCount <= 0 &&
_windowCount <= 0 &&
!_app.Logic().Settings().GlobalSettings().AllowHeadless())
{
PostQuitMessage(0);
}
}
safe_void_coroutine WindowEmperor::_showMessageBox(winrt::hstring message, bool error)
{
// Prevent the main loop from exiting until the message box is closed.
// Once the loop exits, the app exits, and the message box will be closed.
_messageBoxCount += 1;
const auto decrement = wil::scope_exit([hwnd = _window.get()]() noexcept {
PostMessageW(hwnd, WM_MESSAGE_BOX_CLOSED, 0, 0);
});
// We must yield to a background thread, because MessageBoxW() is a blocking call, and we can't
// block the main thread. That would prevent us from servicing WM_COPYDATA messages and similar.
co_await winrt::resume_background();
const auto messageTitle = error ? IDS_ERROR_DIALOG_TITLE : IDS_HELP_DIALOG_TITLE;
const auto messageIcon = error ? MB_ICONERROR : MB_ICONWARNING;
// The dialog cannot have our _window as the parent, because that one is always hidden/invisible.
// TODO:GH#4134: polish this dialog more, to make the text more like msiexec /?
MessageBoxW(nullptr, message.c_str(), GetStringResource(messageTitle).c_str(), MB_OK | messageIcon);
}
LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM const wParam, LPARAM const lParam) noexcept
{
try
{
switch (message)
{
case WM_CLOSE_TERMINAL_WINDOW:
{
const auto globalSettings = _app.Logic().Settings().GlobalSettings();
// Keep the last window in the array so that we can persist it on exit.
// We check for AllowHeadless(), as that being true prevents us from ever quitting in the first place.
// (= If we avoided closing the last window you wouldn't be able to reach a headless state.)
const auto shouldKeepWindow =
_windows.size() == 1 &&
globalSettings.ShouldUsePersistedLayout() &&
!globalSettings.AllowHeadless();
if (!shouldKeepWindow)
{
// Did the window counter get out of sync? It shouldn't.
assert(_windowCount == gsl::narrow_cast<int32_t>(_windows.size()));
// !!! NOTE !!!
// At least theoretically the lParam pointer may be invalid.
// We should only access it if we find it in our _windows array.
const auto host = reinterpret_cast<AppHost*>(lParam);
auto it = _windows.begin();
const auto end = _windows.end();
for (; it != end; ++it)
{
if (host == it->get())
{
// NOTE: AppHost::Close is highly non-trivial.
//
// It _may_ call into XAML, which _may_ pump the message loop, which would then recursively
// re-enter this function, which _may_ then handle another WM_CLOSE_TERMINAL_WINDOW,
// which would change the _windows array, and invalidate our iterator and crash.
//
// We can prevent this by deferring Close() until after the erase() call.
const auto strong = *it;
_windows.erase(it);
strong->Close();
break;
}
}
}
// Counterpart specific to CreateNewWindow().
_windowCount -= 1;
_postQuitMessageIfNeeded();
return 0;
}
case WM_MESSAGE_BOX_CLOSED:
// Counterpart specific to _showMessageBox().
_messageBoxCount -= 1;
_postQuitMessageIfNeeded();
return 0;
case WM_IDENTIFY_ALL_WINDOWS:
for (const auto& host : _windows)
{
host->Logic().IdentifyWindow();
}
return 0;
case WM_NOTIFY_FROM_NOTIFICATION_AREA:
switch (LOWORD(lParam))
{
case NIN_SELECT:
case NIN_KEYSELECT:
{
SummonWindowSelectionArgs args;
args.SummonBehavior.MoveToCurrentDesktop(false);
args.SummonBehavior.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace);
args.SummonBehavior.ToggleVisibility(false);
std::ignore = _summonWindow(std::move(args));
break;
}
case WM_CONTEXTMENU:
_notificationAreaMenuRequested(wParam);
break;
default:
break;
}
return 0;
case WM_WINDOWPOSCHANGING:
{
// No, we really don't want this window to be visible. It hides the buggy CoreWindow that XAML creates.
// We can't make it a HWND_MESSAGE window, because then we don't get WM_QUERYENDSESSION messages.
const auto wp = reinterpret_cast<WINDOWPOS*>(lParam);
wp->flags &= ~SWP_SHOWWINDOW;
return 0;
}
case WM_MENUCOMMAND:
_notificationAreaMenuClicked(wParam, lParam);
return 0;
case WM_SETTINGCHANGE:
// Currently, we only support checking when the OS theme changes. In that case, wParam is 0.
// GH#15102: Re-evaluate when we decide to reload env vars.
//
// ImmersiveColorSet seems to be the notification that the OS theme
// changed. If that happens, let the app know, so it can hot-reload
// themes, color schemes that might depend on the OS theme
if (wParam == 0 && lParam != 0 && wcscmp(reinterpret_cast<const wchar_t*>(lParam), L"ImmersiveColorSet") == 0)
{
// GH#15732: Don't update the settings, unless the theme
// _actually_ changed. ImmersiveColorSet gets sent more often
// than just on a theme change. It notably gets sent when the PC
// is locked, or the UAC prompt opens.
const auto isCurrentlyDark = Theme::IsSystemInDarkTheme();
if (isCurrentlyDark != _currentSystemThemeIsDark)
{
_currentSystemThemeIsDark = isCurrentlyDark;
_app.Logic().ReloadSettings();
}
}
return 0;
case WM_COPYDATA:
if (const auto cds = reinterpret_cast<COPYDATASTRUCT*>(lParam); cds->dwData == TERMINAL_HANDOFF_MAGIC)
{
const auto handoff = deserializeHandoffPayload(static_cast<const uint8_t*>(cds->lpData), static_cast<const uint8_t*>(cds->lpData) + cds->cbData);
const auto argv = commandlineToArgArray(handoff.args.c_str());
_dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show);
}
return 0;
case WM_HOTKEY:
_hotkeyPressed(static_cast<long>(wParam));
return 0;
case WM_QUERYENDSESSION:
// For WM_QUERYENDSESSION and WM_ENDSESSION, refer to:
// https://docs.microsoft.com/en-us/windows/win32/rstmgr/guidelines-for-applications
// ENDSESSION_CLOSEAPP: The application is using a file that must be replaced,
// the system is being serviced, or system resources are exhausted.
RegisterApplicationRestart(nullptr, RESTART_NO_CRASH | RESTART_NO_HANG);
return TRUE;
case WM_ENDSESSION:
_finalizeSessionPersistence();
_skipPersistence = true;
PostQuitMessage(0);
return 0;
default:
// We'll want to receive this message when explorer.exe restarts
// so that we can re-add our icon to the notification area.
// This unfortunately isn't a switch case because we register the
// message at runtime.
if (message == WM_TASKBARCREATED)
{
_notificationIconShown = false;
_checkWindowsForNotificationIcon();
return 0;
}
}
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
return DefWindowProcW(window, message, wParam, lParam);
}
void WindowEmperor::_setupSessionPersistence(bool enabled)
{
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())
{
state.PersistedWindowLayouts(nullptr);
}
if (_app.Logic().Settings().GlobalSettings().ShouldUsePersistedLayout())
{
for (const auto& w : _windows)
{
w->Logic().PersistState(serializeBuffer);
}
}
// Ensure to write the state.json
state.Flush();
}
void WindowEmperor::_finalizeSessionPersistence() const
{
if (_skipPersistence)
{
// We received WM_ENDSESSION and persisted the state.
// We don't need to persist it again.
return;
}
const auto state = ApplicationState::SharedInstance();
_persistState(state, _app.Logic().Settings().GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedLayoutAndContent);
if (_needsPersistenceCleanup)
{
// Get the "buffer_{guid}.txt" files that we expect to be there
std::unordered_set<winrt::guid> sessionIds;
if (const auto layouts = state.PersistedWindowLayouts())
{
for (const auto& windowLayout : layouts)
{
for (const auto& actionAndArgs : windowLayout.TabLayout())
{
const auto args = actionAndArgs.Args();
NewTerminalArgs terminalArgs{ nullptr };
if (const auto tabArgs = args.try_as<NewTabArgs>())
{
terminalArgs = tabArgs.ContentArgs().try_as<NewTerminalArgs>();
}
else if (const auto paneArgs = args.try_as<SplitPaneArgs>())
{
terminalArgs = paneArgs.ContentArgs().try_as<NewTerminalArgs>();
}
if (terminalArgs)
{
sessionIds.emplace(terminalArgs.SessionId());
}
}
}
}
// Remove the "buffer_{guid}.txt" files that shouldn't be there
// e.g. "buffer_FD40D746-163E-444C-B9B2-6A3EA2B26722.txt"
{
const std::filesystem::path settingsDirectory{ std::wstring_view{ CascadiaSettings::SettingsDirectory() } };
const auto filter = settingsDirectory / L"buffer_*";
WIN32_FIND_DATAW ffd;
// This could also use std::filesystem::directory_iterator.
// I was just slightly bothered by how it doesn't have a O(1) .filename()
// function, even though the underlying Win32 APIs provide it for free.
// Both work fine.
const wil::unique_hfind handle{ FindFirstFileExW(filter.c_str(), FindExInfoBasic, &ffd, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH) };
if (!handle)
{
return;
}
do
{
const auto nameLen = wcsnlen_s(&ffd.cFileName[0], ARRAYSIZE(ffd.cFileName));
const std::wstring_view name{ &ffd.cFileName[0], nameLen };
if (nameLen != 47)
{
continue;
}
wchar_t guidStr[39];
guidStr[0] = L'{';
memcpy(&guidStr[1], name.data() + 7, 36 * sizeof(wchar_t));
guidStr[37] = L'}';
guidStr[38] = L'\0';
const auto id = Utils::GuidFromString(&guidStr[0]);
if (!sessionIds.contains(id))
{
std::filesystem::remove(settingsDirectory / name);
}
} while (FindNextFileW(handle.get(), &ffd));
}
}
}
void WindowEmperor::_notificationAreaMenuRequested(const WPARAM wParam)
{
const auto menu = CreatePopupMenu();
if (!menu)
{
assert(false);
return;
}
static constexpr MENUINFO mi{
.cbSize = sizeof(MENUINFO),
.fMask = MIM_STYLE | MIM_APPLYTOSUBMENUS | MIM_MENUDATA,
.dwStyle = MNS_NOTIFYBYPOS,
};
SetMenuInfo(menu, &mi);
// The "Focus Terminal" menu item.
AppendMenuW(menu, MF_STRING, 0, RS_(L"NotificationIconFocusTerminal").c_str());
AppendMenuW(menu, MF_SEPARATOR, 0, L"");
// A submenu to focus a specific window. Lists all windows that we manage.
if (const auto submenu = CreatePopupMenu())
{
static constexpr MENUINFO submenuInfo{
.cbSize = sizeof(MENUINFO),
.fMask = MIM_MENUDATA,
.dwStyle = MNS_NOTIFYBYPOS,
};
SetMenuInfo(submenu, &submenuInfo);
std::wstring displayText;
displayText.reserve(64);
for (const auto& host : _windows)
{
const auto logic = host->Logic();
const auto props = logic.WindowProperties();
const auto id = props.WindowId();
displayText.clear();
fmt::format_to(std::back_inserter(displayText), L"#{}", id);
if (const auto title = logic.Title(); !title.empty())
{
fmt::format_to(std::back_inserter(displayText), L": {}", title);
}
if (const auto name = props.WindowName(); !name.empty())
{
fmt::format_to(std::back_inserter(displayText), L" [{}]", name);
}
AppendMenuW(submenu, MF_STRING, gsl::narrow_cast<UINT_PTR>(id), displayText.c_str());
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(submenu), RS_(L"NotificationIconWindowSubmenu").c_str());
}
// We'll need to set our window to the foreground before calling
// TrackPopupMenuEx or else the menu won't dismiss when clicking away.
SetForegroundWindow(_window.get());
// User can select menu items with the left and right buttons.
const auto rightAlign = GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0;
const UINT uFlags = TPM_RIGHTBUTTON | (rightAlign ? TPM_RIGHTALIGN : TPM_LEFTALIGN);
TrackPopupMenuEx(menu, uFlags, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam), _window.get(), nullptr);
if (_currentWindowMenu)
{
// This will recursively destroy the submenu(s) as well.
DestroyMenu(_currentWindowMenu);
}
_currentWindowMenu = menu;
}
void WindowEmperor::_notificationAreaMenuClicked(const WPARAM wParam, const LPARAM lParam) const
{
const auto menu = reinterpret_cast<HMENU>(lParam);
const auto menuItemIndex = LOWORD(wParam);
const auto windowId = GetMenuItemID(menu, menuItemIndex);
// _notificationAreaMenuRequested constructs each menu item with an ID
// that is either 0 for "Focus Terminal" or >0 for a specific window ID.
// This works well for us because valid window IDs are always >0.
SummonWindowSelectionArgs args;
args.WindowID = windowId;
args.SummonBehavior.ToggleVisibility(false);
args.SummonBehavior.MoveToCurrentDesktop(false);
args.SummonBehavior.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace);
std::ignore = _summonWindow(std::move(args));
}
#pragma endregion
#pragma region GlobalHotkeys
void WindowEmperor::_hotkeyPressed(const long hotkeyIndex)
{
if (hotkeyIndex < 0 || static_cast<size_t>(hotkeyIndex) > _hotkeys.size())
{
return;
}
const auto& summonArgs = til::at(_hotkeys, hotkeyIndex);
// desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false
// desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false
// desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true
SummonWindowSelectionArgs args;
args.WindowName = summonArgs.Name();
args.OnCurrentDesktop = summonArgs.Desktop() == DesktopBehavior::OnCurrent;
args.SummonBehavior.MoveToCurrentDesktop(summonArgs.Desktop() == DesktopBehavior::ToCurrent);
args.SummonBehavior.ToggleVisibility(summonArgs.ToggleVisibility());
args.SummonBehavior.DropdownDuration(summonArgs.DropdownDuration());
switch (summonArgs.Monitor())
{
case MonitorBehavior::Any:
args.SummonBehavior.ToMonitor(TerminalApp::MonitorBehavior::InPlace);
break;
case MonitorBehavior::ToCurrent:
args.SummonBehavior.ToMonitor(TerminalApp::MonitorBehavior::ToCurrent);
break;
case MonitorBehavior::ToMouse:
args.SummonBehavior.ToMonitor(TerminalApp::MonitorBehavior::ToMouse);
break;
}
if (_summonWindow(args))
{
return;
}
hstring argv[] = { L"wt", L"-w", summonArgs.Name() };
if (argv[2].empty())
{
argv[2] = L"new";
}
const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() };
const auto env = stringFromDoubleNullTerminated(envMem.get());
const auto cwd = wil::GetCurrentDirectoryW<std::wstring>();
_dispatchCommandlineCommon(argv, cwd, env, SW_SHOWDEFAULT);
}
void WindowEmperor::_registerHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept
{
const auto vkey = hotkey.Vkey();
auto hotkeyFlags = MOD_NOREPEAT;
{
const auto modifiers = hotkey.Modifiers();
WI_SetFlagIf(hotkeyFlags, MOD_WIN, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows));
WI_SetFlagIf(hotkeyFlags, MOD_ALT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu));
WI_SetFlagIf(hotkeyFlags, MOD_CONTROL, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control));
WI_SetFlagIf(hotkeyFlags, MOD_SHIFT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift));
}
// TODO GH#8888: We should display a warning of some kind if this fails.
// This can fail if something else already bound this hotkey.
LOG_IF_WIN32_BOOL_FALSE(::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey));
}
// Method Description:
// - Call UnregisterHotKey once for each previously registered hotkey.
// Return Value:
// - <none>
void WindowEmperor::_unregisterHotKey(const int index) noexcept
{
LOG_IF_WIN32_BOOL_FALSE(::UnregisterHotKey(_window.get(), index));
}
void WindowEmperor::_setupGlobalHotkeys()
{
// Unregister all previously registered hotkeys.
//
// RegisterHotKey(), will not unregister hotkeys automatically.
// If a hotkey with a given HWND and ID combination already exists
// then a duplicate one will be added, which we don't want.
// (Additionally we want to remove hotkeys that were removed from the settings.)
for (auto i = 0, count = gsl::narrow_cast<int>(_hotkeys.size()); i < count; ++i)
{
_unregisterHotKey(i);
}
_hotkeys.clear();
// Re-register all current hotkeys.
for (const auto& [keyChord, cmd] : _app.Logic().GlobalHotkeys())
{
if (auto summonArgs = cmd.ActionAndArgs().Args().try_as<Settings::Model::GlobalSummonArgs>())
{
const auto index = gsl::narrow_cast<int>(_hotkeys.size());
_registerHotKey(index, keyChord);
_hotkeys.emplace_back(summonArgs);
}
}
}
#pragma endregion
#pragma region NotificationIcon
void WindowEmperor::_checkWindowsForNotificationIcon()
{
// We need to check some conditions to show the notification icon.
//
// * If there's a Quake window somewhere, we'll want to keep the
// notification icon.
// * There's two settings - MinimizeToNotificationArea and
// AlwaysShowNotificationIcon. If either one of them are true, we want to
// make sure there's a notification icon.
//
// If both are false, we want to remove our icon from the notification area.
// When we remove our icon from the notification area, we'll also want to
// re-summon any hidden windows, but right now we're not keeping track of
// who's hidden, so just summon them all. Tracking the work to do a "summon
// all minimized" in GH#10448
//
// To avoid races between us thinking the settings updated, and the windows
// themselves getting the new settings, only ask the app logic for the
// RequestsTrayIcon setting value, and combine that with the result of each
// window (which won't change during a settings reload).
const auto globals = _app.Logic().Settings().GlobalSettings();
auto needsIcon = globals.AlwaysShowNotificationIcon() || globals.MinimizeToNotificationArea();
if (!needsIcon)
{
for (const auto& host : _windows)
{
needsIcon |= host->Logic().IsQuakeWindow();
}
}
if (_notificationIconShown == needsIcon)
{
return;
}
if (needsIcon)
{
Shell_NotifyIconW(NIM_ADD, &_notificationIcon);
Shell_NotifyIconW(NIM_SETVERSION, &_notificationIcon);
}
else
{
Shell_NotifyIconW(NIM_DELETE, &_notificationIcon);
// If we no longer want the tray icon, but we did have one, then quick
// re-summon all our windows, so they don't get lost when the icon
// disappears forever.
_summonAllWindows();
}
_notificationIconShown = needsIcon;
}
#pragma endregion