// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "WindowEmperor.h" #include #include #include #include #include #include #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 commandlineToArgArray(const wchar_t* commandLine) { int argc = 0; const wil::unique_hlocal_ptr argv{ CommandLineToArgvW(commandLine, &argc) }; argc = std::max(argc, 0); std::vector 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(end - beg) }; } // Appends an uint32_t to a byte vector. static void serializeUint32(std::vector& out, uint32_t value) { out.insert(out.end(), reinterpret_cast(&value), reinterpret_cast(&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(end - it) < sizeof(uint32_t)) { throw std::out_of_range("Not enough data for uint32_t"); } val = *reinterpret_cast(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& out, wil::zwstring_view str) { const auto ptr = reinterpret_cast(str.data()); const auto len = str.size() + 1; serializeUint32(out, gsl::narrow(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(len) * sizeof(wchar_t); if (bytes == 0 || static_cast(end - it) < bytes) { throw std::out_of_range("Not enough data for string content"); } str = { reinterpret_cast(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 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::vector out; serializeString(out, args); serializeString(out, env); serializeString(out, cwd); serializeUint32(out, static_cast(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(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(&cds), SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 5000, nullptr)) { return {}; } } Sleep(sleep); } return {}; } 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(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()) { HWND coreHandle = nullptr; if (SUCCEEDED(interop->get_WindowHandle(&coreHandle)) && coreHandle) { SetParent(coreHandle, _window.get()); } } } } } void WindowEmperor::CloseWindow(AppHost* host) const { using namespace winrt::Windows::System; const auto dispatcherQueue = DispatcherQueue::GetForCurrentThread(); dispatcherQueue.TryEnqueue(DispatcherQueuePriority::Low, [hwnd = _window.get(), host]() { PostMessageW(hwnd, WM_CLOSE_TERMINAL_WINDOW, 0, reinterpret_cast(host)); }); } 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::GetModuleFileNameW(nullptr); 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(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(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(); const auto showCmd = gsl::narrow_cast(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) { 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(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(msg.wParam); const auto scanCode = gsl::narrow_cast(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(parsedTarget, 10)) { // Negative window IDs map to WindowingMode::UseNew. if (*opt > 0) { windowId = gsl::narrow_cast(*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 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::weak_ptr mostRecentWeak; if (winrt::guid currentDesktop; VirtualDesktopUtils::GetCurrentVirtualDesktopId(reinterpret_cast(¤tDesktop))) { 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; mostRecentWeak = w; } } } // GetVirtualDesktopId(), as current implemented, should always return on the main thread. _assertIsMainThread(); const auto mostRecent = mostRecentWeak.lock(); auto window = mostRecent.get(); if (!window) { 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(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(lparam); const auto that = static_cast(cs->lpCreateParams); that->_window.reset(window); SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(that)); } return DefWindowProcW(window, message, wparam, lparam); } void WindowEmperor::_createMessageWindow(const wchar_t* className) { const auto instance = reinterpret_cast(&__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(_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(lParam); auto it = _windows.begin(); const auto end = _windows.end(); for (; it != end; ++it) { if (host == it->get()) { // NOTE: The AppHost destructor 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 destruction until after the erase() call. const auto strong = *it; strong->Close(); _windows.erase(it); 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(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(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(lParam); cds->dwData == TERMINAL_HANDOFF_MAGIC) { const auto handoff = deserializeHandoffPayload(static_cast(cds->lpData), static_cast(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(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, true); if (_needsPersistenceCleanup) { // Get the "buffer_{guid}.txt" files that we expect to be there std::unordered_set 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()) { terminalArgs = tabArgs.ContentArgs().try_as(); } else if (const auto paneArgs = args.try_as()) { terminalArgs = paneArgs.ContentArgs().try_as(); } 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(id), displayText.c_str()); } AppendMenuW(menu, MF_POPUP, reinterpret_cast(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(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(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(); _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: // - 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(_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()) { const auto index = gsl::narrow_cast(_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