From e4e3f08efca9d0ffba330eee12edbcb16897ddcb Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 29 Apr 2026 17:24:45 -0700 Subject: [PATCH] Add toast notification infrastructure (#20010) ## Summary of the Pull Request Adds the infrastructure for toast notifications. Breakdown: - `DesktopNotification`: - `DesktopNotificationArgs` includes the struct to group all notification-related data together. - `SendNotification()` actually sends it - `AppCommandlineArgs.cpp`: added a check for the `--from-toast` no-op sentinel; ensures no new window is created - Most of the other changes are just bubbling up the notification from the `TerminalPaneContent` to `TerminalPage` - `TabManagement.cpp`: `_SendDesktopNotification()` does the final packaging of the notification before calling the `DesktopNotification` API This supports finding the right tab when it's been reordered or even moved to a new window! This also has expanded to support finding the right pane, which is resilient to pane swaps/closing too. When the pane can't be found, we just fallback to the tab. If the pane is already focused, we don't send a notification. This simply adds the infrastructure! Looks like nothing can actually take advantage of it yet, but it's been tested with the changes in #20011. Heavily based on #19935 Co-authored by @zadjii-msft --- .github/actions/spelling/expect/expect.txt | 1 + .../TerminalApp/AppCommandlineArgs.cpp | 9 ++ src/cascadia/TerminalApp/BasicPaneEvents.h | 1 + .../TerminalApp/DesktopNotification.cpp | 129 +++++++++++++++++ .../TerminalApp/DesktopNotification.h | 37 +++++ src/cascadia/TerminalApp/IPaneContent.idl | 7 + .../Resources/en-US/Resources.resw | 8 + src/cascadia/TerminalApp/Tab.cpp | 12 ++ src/cascadia/TerminalApp/Tab.h | 2 + src/cascadia/TerminalApp/TabManagement.cpp | 137 ++++++++++++++++++ .../TerminalApp/TerminalAppLib.vcxproj | 2 + src/cascadia/TerminalApp/TerminalPage.h | 4 + .../TerminalApp/TerminalPaneContent.h | 11 ++ src/cascadia/TerminalApp/TerminalWindow.cpp | 9 ++ src/cascadia/TerminalApp/TerminalWindow.h | 2 + src/cascadia/TerminalApp/TerminalWindow.idl | 3 + src/cascadia/TerminalApp/pch.h | 3 + src/cascadia/WindowsTerminal/AppHost.cpp | 9 ++ src/cascadia/WindowsTerminal/AppHost.h | 4 + .../WindowsTerminal/WindowEmperor.cpp | 28 +++- src/cascadia/WindowsTerminal/WindowEmperor.h | 1 + 21 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 src/cascadia/TerminalApp/DesktopNotification.cpp create mode 100644 src/cascadia/TerminalApp/DesktopNotification.h diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 29d56325c5..102af4d88f 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1104,6 +1104,7 @@ NOSIZE NOSNAPSHOT NOTHOUSANDS NOTICKS +notif NOTIMEOUTIFNOTHUNG NOTIMPL NOTOPMOST diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index c41eea5431..950984019b 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -1068,6 +1068,15 @@ int AppCommandlineArgs::ParseArgs(winrt::array_view args) return 0; } + // When a toast notification is clicked, Windows may launch a new instance + // with "--from-toast" as the argument. This is a no-op sentinel — the + // in-process Activated handler on the toast already handled activation. + // See DesktopNotification.cpp for more details. + if (args.size() == 2 && args[1] == L"--from-toast") + { + return 0; + } + auto commands = ::TerminalApp::AppCommandlineArgs::BuildCommands(args); for (auto& cmdBlob : commands) diff --git a/src/cascadia/TerminalApp/BasicPaneEvents.h b/src/cascadia/TerminalApp/BasicPaneEvents.h index e82ff0f49b..bd9bfb3916 100644 --- a/src/cascadia/TerminalApp/BasicPaneEvents.h +++ b/src/cascadia/TerminalApp/BasicPaneEvents.h @@ -15,6 +15,7 @@ namespace winrt::TerminalApp::implementation til::typed_event TaskbarProgressChanged; til::typed_event ReadOnlyChanged; til::typed_event FocusRequested; + til::typed_event NotificationRequested; til::typed_event DispatchCommandRequested; }; diff --git a/src/cascadia/TerminalApp/DesktopNotification.cpp b/src/cascadia/TerminalApp/DesktopNotification.cpp new file mode 100644 index 0000000000..d1e8a198c7 --- /dev/null +++ b/src/cascadia/TerminalApp/DesktopNotification.cpp @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "DesktopNotification.h" + +#include + +using namespace winrt::Windows::UI::Notifications; +using namespace winrt::Windows::Data::Xml::Dom; + +namespace winrt::TerminalApp::implementation +{ + std::atomic DesktopNotification::_lastNotificationTime{ 0 }; + + // Method Description: + // - Rate-limits toast notifications so we don't spam the user. + // Return Value: + // - Returns true if a notification is allowed, false if too recent. + bool DesktopNotification::ShouldSendNotification() + { + const auto now = GetTickCount64(); + auto last = _lastNotificationTime.load(std::memory_order_relaxed); + + // Subtraction wraps cleanly modulo 2^64, so the delta is correct even + // across the (~584 million year) GetTickCount64 rollover. + if (now - last < MinNotificationIntervalMs) + { + return false; + } + + // Attempt to update; if another thread beat us, that's fine — we'll skip this one. + return _lastNotificationTime.compare_exchange_strong(last, now, std::memory_order_relaxed); + } + + // Method Description: + // - Sends a toast notification with the given title and message. + // - When the user clicks the toast, the `Activated` callback fires + // with the tabIndex that was passed in, so the caller can switch + // to the correct tab and summon the window. + // Arguments: + // - args: The title, message, and tab index to include in the notification. + // - activated: A callback invoked on the background thread when the + // toast is clicked. The uint32_t parameter is the tab index. + void DesktopNotification::SendNotification(const DesktopNotificationArgs& args, std::function activatedFunc) + { + try + { + if (!ShouldSendNotification()) + { + return; + } + + // Build the toast XML. We use a simple template with a title and body text. + // + // + // + // + // Title + // Message + // + // + // + auto toastXml = ToastNotificationManager::GetTemplateContent(ToastTemplateType::ToastText02); + auto textNodes = toastXml.GetElementsByTagName(L"text"); + + // First is the title + textNodes.Item(0).InnerText(args.Title); + // Second is the body + textNodes.Item(1).InnerText(args.Message); + + auto toastElement = toastXml.DocumentElement(); + + // When a toast is clicked, Windows launches a new instance of the app + // with the "launch" attribute as command-line arguments. We handle + // toast activation in-process via the Activated event below, so the + // new instance should do nothing. "--from-toast" is recognized by + // AppCommandlineArgs::ParseArgs as a no-op sentinel. + toastElement.SetAttribute(L"launch", L"--from-toast"); + + toastElement.SetAttribute(L"scenario", L"default"); + + auto toast = ToastNotification{ toastXml }; + + // Set the tag and group to enable notification replacement. + // Repeated notifications with the same tag replace the previous one + // rather than stacking in the notification center. + toast.Tag(args.Tag); + toast.Group(L"WindowsTerminal"); + + // When the user activates (clicks) the toast, fire the callback. + if (activatedFunc) + { + toast.Activated([activatedFunc](const auto& /*sender*/, const auto& /*eventArgs*/) { + activatedFunc(); + }); + } + + // For packaged apps, CreateToastNotifier() uses the package identity automatically. + // For unpackaged apps, we must pass the explicit AUMID that was registered + // at startup via SetCurrentProcessExplicitAppUserModelID. + winrt::Windows::UI::Notifications::ToastNotifier notifier{ nullptr }; + if (IsPackaged()) + { + notifier = ToastNotificationManager::CreateToastNotifier(); + } + else + { + // Retrieve the AUMID that was set by WindowEmperor at startup. + wil::unique_cotaskmem_string aumid; + if (SUCCEEDED(GetCurrentProcessExplicitAppUserModelID(&aumid))) + { + notifier = ToastNotificationManager::CreateToastNotifier(aumid.get()); + } + } + if (notifier) + { + notifier.Show(toast); + } + } + catch (...) + { + // Toast notification is a best-effort feature. If it fails (e.g., notifications + // are disabled, or the app is unpackaged without proper AUMID setup), we silently + // ignore the error. + LOG_CAUGHT_EXCEPTION(); + } + } +} diff --git a/src/cascadia/TerminalApp/DesktopNotification.h b/src/cascadia/TerminalApp/DesktopNotification.h new file mode 100644 index 0000000000..a3bab6dddd --- /dev/null +++ b/src/cascadia/TerminalApp/DesktopNotification.h @@ -0,0 +1,37 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- DesktopNotification.h + +Module Description: +- Helper for sending Windows desktop toast notifications. Used to surface + terminal activity events to the user via the Windows notification center. +--*/ + +#pragma once +#include "pch.h" + +namespace winrt::TerminalApp::implementation +{ + struct DesktopNotificationArgs + { + winrt::hstring Title; + winrt::hstring Message; + winrt::hstring Tag; + }; + + class DesktopNotification + { + public: + static bool ShouldSendNotification(); + static void SendNotification(const DesktopNotificationArgs& args, std::function activatedFunc); + + private: + static std::atomic _lastNotificationTime; + + // Minimum interval between notifications, in milliseconds (GetTickCount64 units). + static constexpr uint64_t MinNotificationIntervalMs = 5'000; + }; +} diff --git a/src/cascadia/TerminalApp/IPaneContent.idl b/src/cascadia/TerminalApp/IPaneContent.idl index f4c6ce1395..8128776247 100644 --- a/src/cascadia/TerminalApp/IPaneContent.idl +++ b/src/cascadia/TerminalApp/IPaneContent.idl @@ -16,6 +16,12 @@ namespace TerminalApp Boolean FlashTaskbar { get; }; }; + runtimeclass NotificationEventArgs + { + String Title { get; }; + String Body { get; }; + }; + interface IPaneContent { Windows.UI.Xaml.FrameworkElement GetRoot(); @@ -46,6 +52,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler TaskbarProgressChanged; event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusRequested; + event Windows.Foundation.TypedEventHandler NotificationRequested; }; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 9de52e45c3..3e0a711711 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -745,6 +745,14 @@ Windows This is displayed as a label for the context menu item that holds the submenu of available windows. + + Activity in tab "{0}" + {0} is the tab title. Shown as the body of a desktop notification when tab activity is detected. + + + Activity in tab "{0}" (window "{1}") + {0} is the tab title, {1} is the window name. Shown as the body of a desktop notification when tab activity is detected and the window has a name. + Open a new tab in given starting directory diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 3d3db9a425..236ab16382 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -1166,6 +1166,18 @@ namespace winrt::TerminalApp::implementation events.RestartTerminalRequested = terminal.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &Tab::_bubbleRestartTerminalRequested }); } + events.NotificationRequested = content.NotificationRequested( + winrt::auto_revoke, + [dispatcher, weakThis](TerminalApp::IPaneContent sender, auto notifArgs) -> safe_void_coroutine { + const auto weakThisCopy = weakThis; + co_await wil::resume_foreground(dispatcher); + if (const auto tab{ weakThisCopy.get() }) + { + const auto title = notifArgs.Title().empty() ? tab->Title() : notifArgs.Title(); + tab->TabToastNotificationRequested.raise(title, notifArgs.Body(), sender); + } + }); + if (_tabStatus.IsInputBroadcastActive()) { if (const auto& termContent{ content.try_as() }) diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index 70dec43426..1a9d43dd7b 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -121,6 +121,7 @@ namespace winrt::TerminalApp::implementation til::typed_event ActivePaneChanged; til::event> TabRaiseVisualBell; + til::event> TabToastNotificationRequested; til::typed_event TaskbarProgressChanged; // The TabViewIndex is the index this Tab object resides in TerminalPage's _tabs vector. @@ -185,6 +186,7 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::IPaneContent::ConnectionStateChanged_revoker ConnectionStateChanged; winrt::TerminalApp::IPaneContent::ReadOnlyChanged_revoker ReadOnlyChanged; winrt::TerminalApp::IPaneContent::FocusRequested_revoker FocusRequested; + winrt::TerminalApp::IPaneContent::NotificationRequested_revoker NotificationRequested; // These events literally only apply if the content is a TermControl. winrt::Microsoft::Terminal::Control::TermControl::KeySent_revoker KeySent; diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 21e31fb6dd..9c2621f712 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -17,6 +17,7 @@ #include "TabRowControl.h" #include "DebugTapConnection.h" +#include "DesktopNotification.h" #include "..\TerminalSettingsModel\FileUtils.h" #include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" @@ -150,6 +151,18 @@ namespace winrt::TerminalApp::implementation } }); + // When a tab requests a desktop toast notification, send the toast + // and handle activation by summoning this window and switching to the tab. + newTabImpl->TabToastNotificationRequested([weakThis{ get_weak() }, weakTab{ newTabImpl->get_weak() }](const winrt::hstring& title, const winrt::hstring& body, const winrt::TerminalApp::IPaneContent& content) { + if (const auto page{ weakThis.get() }) + { + if (const auto tab{ weakTab.get() }) + { + page->_SendDesktopNotification(title, body, tab, content); + } + } + }); + auto tabViewItem = newTabImpl->TabViewItem(); _tabView.TabItems().InsertAt(insertPosition, tabViewItem); @@ -1185,4 +1198,128 @@ namespace winrt::TerminalApp::implementation { return _tabs.Size() > 1; } + + // Method Description: + // - Attempts to find and focus the given tab in this window. + // Arguments: + // - tab: The tab to focus. + // Return Value: + // - true if the tab was found and focused, false otherwise. + bool TerminalPage::FocusTab(const winrt::TerminalApp::Tab& tab) + { + if (const auto tabIndex{ _GetTabIndex(tab) }) + { + _SelectTab(tabIndex.value()); + return true; + } + return false; + } + + // Method Description: + // - Sends a desktop toast notification with the given title and body. + // When the toast is activated (clicked), the window is summoned and + // the originating tab is focused. + // Arguments: + // - tabTitle: The title to display in the notification. + // - body: The body text. If empty, a standard tab-activity message is built. + // - tab: The tab to switch to when the toast is activated. + void TerminalPage::_SendDesktopNotification(const winrt::hstring& tabTitle, const winrt::hstring& body, const winrt::com_ptr& tab, const winrt::TerminalApp::IPaneContent& content) + { + // Don't send a notification if the window is focused and the requesting + // pane is the active pane. The user is already looking at it. + if (_activated && tab == _GetFocusedTabImpl()) + { + if (const auto activePane{ tab->GetActivePane() }) + { + if (activePane->GetContent() == content) + { + return; + } + } + } + + // Build the notification message. + // If a custom body is provided (e.g. from OSC 777), use the title/body directly. + // Otherwise, build the standard tab-activity notification message. + winrt::hstring notificationTitle; + winrt::hstring message; + if (!body.empty()) + { + notificationTitle = tabTitle; + message = body; + } + else + { + // Use the window name if available for context; otherwise just use the tab title. + // Use the raw WindowName (not WindowNameForDisplay) so we don't include + // the "" placeholder in the notification body. + const auto windowName = _WindowProperties ? _WindowProperties.WindowName() : winrt::hstring{}; + if (!windowName.empty()) + { + message = RS_fmt(L"NotificationMessage_TabActivityInWindow", std::wstring_view{ tabTitle }, std::wstring_view{ windowName }); + } + else + { + message = RS_fmt(L"NotificationMessage_TabActivity", std::wstring_view{ tabTitle }); + } + notificationTitle = CascadiaSettings::ApplicationDisplayName(); + } + + // Use the Tab object's identity hash as a stable toast tag. + // This survives tab reordering and cross-window moves. + const auto tabHash = std::hash{}(*tab); + const hstring tabTag{ fmt::format(FMT_COMPILE(L"wt-tab-{:016x}"), tabHash) }; + + const implementation::DesktopNotificationArgs args{ + .Title = notificationTitle, + .Message = message, + .Tag = tabTag + }; + + implementation::DesktopNotification::SendNotification(args, [weakThis{ get_weak() }, weakTab{ tab->get_weak() }, weakContent{ winrt::make_weak(content) }]() { + if (const auto page{ weakThis.get() }) + { + // The toast Activated callback runs on a background thread. + // Marshal to the UI thread for tab focus and window summon. + page->Dispatcher().RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakPage{ page->get_weak() }, weakTab, weakContent]() { + if (const auto p{ weakPage.get() }) + { + if (const auto t{ weakTab.get() }) + { + // Try to find and focus the tab in this window first. + if (const auto tabIndex{ p->_GetTabIndex(*t) }) + { + p->SummonWindowRequested.raise(nullptr, nullptr); + p->_SelectTab(tabIndex.value()); + + // Focus the specific pane that raised the notification. + if (const auto paneContent{ weakContent.get() }) + { + const auto rootPane = t->GetRootPane(); + rootPane->WalkTree([&](const auto& pane) { + if (pane->GetContent() == paneContent) + { + rootPane->FocusPane(pane); + } + }); + } + } + else + { + // The tab may have moved to another window. + // Raise FocusTabRequested so the emperor can + // search all windows for it. + p->FocusTabRequested.raise(nullptr, *t); + } + } + else + { + // Tab was closed. Just summon this window. + p->SummonWindowRequested.raise(nullptr, nullptr); + } + } + }); + } + }); + } } diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index fe141dfa64..371dbd1746 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -174,6 +174,7 @@ TerminalPaneContent.idl + SuggestionsControl.xaml @@ -287,6 +288,7 @@ + SuggestionsControl.xaml diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index b1d26ee69d..29f0cd23e1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -168,6 +168,7 @@ namespace winrt::TerminalApp::implementation void OpenSettingsUI(); void WindowActivated(const bool activated); + bool FocusTab(const winrt::TerminalApp::Tab& tab); bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); @@ -192,6 +193,7 @@ namespace winrt::TerminalApp::implementation til::typed_event IdentifyWindowsRequested; til::typed_event RenameWindowRequested; til::typed_event SummonWindowRequested; + til::typed_event FocusTabRequested; til::typed_event WindowSizeChanged; til::typed_event OpenSystemMenu; @@ -571,6 +573,8 @@ namespace winrt::TerminalApp::implementation void _activePaneChanged(winrt::TerminalApp::Tab tab, Windows::Foundation::IInspectable args); safe_void_coroutine _doHandleSuggestions(Microsoft::Terminal::Settings::Model::SuggestionsArgs realArgs); + void _SendDesktopNotification(const winrt::hstring& tabTitle, const winrt::hstring& body, const winrt::com_ptr& tab, const winrt::TerminalApp::IPaneContent& content); + #pragma region ActionHandlers // These are all defined in AppActionHandlers.cpp #define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action); diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.h b/src/cascadia/TerminalApp/TerminalPaneContent.h index 0e828cdc1b..b2f38ba249 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.h +++ b/src/cascadia/TerminalApp/TerminalPaneContent.h @@ -4,6 +4,7 @@ #pragma once #include "TerminalPaneContent.g.h" #include "BellEventArgs.g.h" +#include "NotificationEventArgs.g.h" #include "BasicPaneEvents.h" namespace winrt::TerminalApp::implementation @@ -19,6 +20,16 @@ namespace winrt::TerminalApp::implementation til::property FlashTaskbar; }; + struct NotificationEventArgs : public NotificationEventArgsT + { + public: + NotificationEventArgs(const winrt::hstring& title = {}, const winrt::hstring& body = {}) : + Title(title), Body(body) {} + + til::property Title; + til::property Body; + }; + struct TerminalPaneContent : TerminalPaneContentT, BasicPaneEvents { TerminalPaneContent(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile, diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index b2fa349e40..0fafac603c 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -1205,6 +1205,15 @@ namespace winrt::TerminalApp::implementation } } + bool TerminalWindow::FocusTab(const winrt::TerminalApp::Tab& tab) + { + if (_root) + { + return _root->FocusTab(tab); + } + return false; + } + void TerminalWindow::WindowName(const winrt::hstring& name) { const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow(); diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index 2f1aad5a7a..08ddeabafc 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -92,6 +92,7 @@ namespace winrt::TerminalApp::implementation bool ShowTabsFullscreen() const; bool AutoHideWindow(); void IdentifyWindow(); + bool FocusTab(const winrt::TerminalApp::Tab& tab); std::optional LoadPersistedLayoutIdx() const; winrt::Microsoft::Terminal::Settings::Model::WindowLayout LoadPersistedLayout(); @@ -221,6 +222,7 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(SetTaskbarProgress, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, SetTaskbarProgress); FORWARDED_TYPED_EVENT(IdentifyWindowsRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IdentifyWindowsRequested); FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested); + FORWARDED_TYPED_EVENT(FocusTabRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::Tab, _root, FocusTabRequested); FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); FORWARDED_TYPED_EVENT(ShowWindowChanged, Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs, _root, ShowWindowChanged); diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index b900522cbf..c12f73e137 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -4,6 +4,7 @@ import "IPaneContent.idl"; import "TerminalPage.idl"; import "ShortcutActionDispatch.idl"; +import "Tab.idl"; namespace TerminalApp { @@ -74,6 +75,7 @@ namespace TerminalApp Boolean ShowTabsFullscreen { get; }; void IdentifyWindow(); + Boolean FocusTab(TerminalApp.Tab tab); void SetPersistedLayoutIdx(UInt32 idx); void RequestExitFullscreen(); @@ -126,6 +128,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler FocusTabRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; event Windows.Foundation.TypedEventHandler QuitRequested; event Windows.Foundation.TypedEventHandler SystemMenuChangeRequested; diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index ee36db25e3..ba0dcfda26 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -54,6 +54,9 @@ #include #include +#include +#include + #include #include #include diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 522e90c7d2..0fa2f61ae4 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -265,6 +265,7 @@ void AppHost::Initialize() _revokers.IsQuakeWindowChanged = _windowLogic.IsQuakeWindowChanged(winrt::auto_revoke, { this, &AppHost::_IsQuakeWindowChanged }); _revokers.SummonWindowRequested = _windowLogic.SummonWindowRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowRequested }); + _revokers.FocusTabRequested = _windowLogic.FocusTabRequested(winrt::auto_revoke, { this, &AppHost::_FocusTabRequested }); _revokers.OpenSystemMenu = _windowLogic.OpenSystemMenu(winrt::auto_revoke, { this, &AppHost::_OpenSystemMenu }); _revokers.QuitRequested = _windowLogic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll }); _revokers.ShowWindowChanged = _windowLogic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged }); @@ -1064,6 +1065,14 @@ void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspecta HandleSummon(std::move(summonArgs)); } +void AppHost::_FocusTabRequested(const winrt::Windows::Foundation::IInspectable&, + const winrt::TerminalApp::Tab& tab) +{ + // The tab may have moved to another window. Ask the emperor to + // search all windows and focus the tab wherever it currently lives. + _windowManager->FocusTabInAnyWindow(tab); +} + void AppHost::_OpenSystemMenu(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 379f876b94..ffc16d916b 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -91,6 +91,9 @@ private: void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); + void _FocusTabRequested(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::TerminalApp::Tab& tab); + void _OpenSystemMenu(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); @@ -151,6 +154,7 @@ private: winrt::TerminalApp::TerminalWindow::IdentifyWindowsRequested_revoker IdentifyWindowsRequested; winrt::TerminalApp::TerminalWindow::IsQuakeWindowChanged_revoker IsQuakeWindowChanged; winrt::TerminalApp::TerminalWindow::SummonWindowRequested_revoker SummonWindowRequested; + winrt::TerminalApp::TerminalWindow::FocusTabRequested_revoker FocusTabRequested; winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu; winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested; winrt::TerminalApp::TerminalWindow::ShowWindowChanged_revoker ShowWindowChanged; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 55244118c9..917f492be9 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -775,6 +775,25 @@ bool WindowEmperor::_summonWindow(const SummonWindowSelectionArgs& args) const return true; } +void WindowEmperor::FocusTabInAnyWindow(const winrt::TerminalApp::Tab& tab) const +{ + _assertIsMainThread(); + + for (const auto& w : _windows) + { + if (w->Logic().FocusTab(tab)) + { + winrt::TerminalApp::SummonWindowBehavior summonArgs; + summonArgs.MoveToCurrentDesktop(false); + summonArgs.DropdownDuration(0); + summonArgs.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace); + summonArgs.ToggleVisibility(false); + w->HandleSummon(std::move(summonArgs)); + return; + } + } +} + void WindowEmperor::_summonAllWindows() const { _assertIsMainThread(); @@ -1033,7 +1052,14 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c { 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); + // When a toast notification is clicked, Windows launches a new + // wt.exe with "--from-toast". That instance hands off here via + // WM_COPYDATA. We already handle activation in-process via the + // toast's Activated event, so just ignore this handoff. + if (argv.size() != 2 || argv[1] != L"--from-toast") + { + _dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show); + } } return 0; case WM_HOTKEY: diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 5e2801276c..42c84c38c9 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -35,6 +35,7 @@ public: AppHost* GetWindowByName(std::wstring_view name) const noexcept; void CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args); void HandleCommandlineArgs(int nCmdShow); + void FocusTabInAnyWindow(const winrt::TerminalApp::Tab& tab) const; private: struct SummonWindowSelectionArgs