Merge remote-tracking branch 'origin/main' into feature/llm

This commit is contained in:
Dustin L. Howett
2026-05-01 14:13:14 -05:00
28 changed files with 919 additions and 64 deletions

View File

@@ -1104,6 +1104,7 @@ NOSIZE
NOSNAPSHOT
NOTHOUSANDS
NOTICKS
notif
NOTIMEOUTIFNOTHUNG
NOTIMPL
NOTOPMOST

View File

@@ -237,6 +237,7 @@
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="unvirtualizedResources" />
<rescap:Capability Name="appLicensing" />
</Capabilities>
<Extensions>

View File

@@ -237,6 +237,7 @@
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="unvirtualizedResources" />
<rescap:Capability Name="appLicensing" />
</Capabilities>
<Extensions>

View File

@@ -1110,6 +1110,15 @@ int AppCommandlineArgs::ParseArgs(winrt::array_view<const winrt::hstring> 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)

View File

@@ -15,6 +15,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<IPaneContent> TaskbarProgressChanged;
til::typed_event<IPaneContent> ReadOnlyChanged;
til::typed_event<IPaneContent> FocusRequested;
til::typed_event<IPaneContent, winrt::TerminalApp::NotificationEventArgs> NotificationRequested;
til::typed_event<winrt::Windows::Foundation::IInspectable, Microsoft::Terminal::Settings::Model::Command> DispatchCommandRequested;
};

View File

@@ -0,0 +1,129 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "DesktopNotification.h"
#include <WtExeUtils.h>
using namespace winrt::Windows::UI::Notifications;
using namespace winrt::Windows::Data::Xml::Dom;
namespace winrt::TerminalApp::implementation
{
std::atomic<uint64_t> 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<void()> activatedFunc)
{
try
{
if (!ShouldSendNotification())
{
return;
}
// Build the toast XML. We use a simple template with a title and body text.
//
// <toast launch="--from-toast">
// <visual>
// <binding template="ToastGeneric">
// <text>Title</text>
// <text>Message</text>
// </binding>
// </visual>
// </toast>
auto toastXml = ToastNotificationManager::GetTemplateContent(ToastTemplateType::ToastText02);
auto textNodes = toastXml.GetElementsByTagName(L"text");
// First <text> is the title
textNodes.Item(0).InnerText(args.Title);
// Second <text> 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();
}
}
}

View File

@@ -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<void()> activatedFunc);
private:
static std::atomic<uint64_t> _lastNotificationTime;
// Minimum interval between notifications, in milliseconds (GetTickCount64 units).
static constexpr uint64_t MinNotificationIntervalMs = 5'000;
};
}

View File

@@ -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<IPaneContent, Object> TaskbarProgressChanged;
event Windows.Foundation.TypedEventHandler<IPaneContent, Object> ReadOnlyChanged;
event Windows.Foundation.TypedEventHandler<IPaneContent, Object> FocusRequested;
event Windows.Foundation.TypedEventHandler<IPaneContent, NotificationEventArgs> NotificationRequested;
};

View File

@@ -751,6 +751,14 @@
<value>Windows</value>
<comment>This is displayed as a label for the context menu item that holds the submenu of available windows.</comment>
</data>
<data name="NotificationMessage_TabActivity" xml:space="preserve">
<value>Activity in tab "{0}"</value>
<comment>{0} is the tab title. Shown as the body of a desktop notification when tab activity is detected.</comment>
</data>
<data name="NotificationMessage_TabActivityInWindow" xml:space="preserve">
<value>Activity in tab "{0}" (window "{1}")</value>
<comment>{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.</comment>
</data>
<data name="DropPathTabRun.Text" xml:space="preserve">
<value>Open a new tab in given starting directory</value>
</data>
@@ -890,10 +898,10 @@
<value>If set, the command will be appended to the profile's default command instead of replacing it.</value>
</data>
<data name="RestartConnectionText" xml:space="preserve">
<value>Restart connection</value>
<value>Restart session</value>
</data>
<data name="RestartConnectionToolTip" xml:space="preserve">
<value>Restart the active pane connection</value>
<value>Restart the session in the active pane</value>
</data>
<data name="SnippetPaneTitle.Text" xml:space="preserve">
<value>Snippets</value>

View File

@@ -35,7 +35,6 @@ namespace winrt::TerminalApp::implementation
_activePane = nullptr;
_closePaneMenuItem.Visibility(WUX::Visibility::Collapsed);
_restartConnectionMenuItem.Visibility(WUX::Visibility::Collapsed);
auto firstId = _nextPaneId;
@@ -86,6 +85,7 @@ namespace winrt::TerminalApp::implementation
_MakeTabViewItem();
_CreateContextMenu();
_UpdateMenuItemStates();
_headerControl.TabStatus(_tabStatus);
@@ -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<TerminalApp::TerminalPaneContent>() })
@@ -1254,7 +1266,7 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - Set an indicator on the tab if any pane is in a closed connection state.
// - Show/hide the Restart Connection context menu entry depending on active pane's state.
// - Show/hide the Restart Session context menu entry depending on active pane's state.
// Arguments:
// - <none>
// Return Value:
@@ -1271,13 +1283,6 @@ namespace winrt::TerminalApp::implementation
_tabStatus.IsConnectionClosed(isClosed);
}
if (_activePane)
{
_restartConnectionMenuItem.Visibility(_activePane->IsConnectionClosed() ?
WUX::Visibility::Visible :
WUX::Visibility::Collapsed);
}
}
void Tab::_RestartActivePaneConnection()
@@ -1348,6 +1353,22 @@ namespace winrt::TerminalApp::implementation
}
});
}
_UpdateMenuItemStates();
}
void Tab::_UpdateMenuItemStates()
{
// Terminal-specific menu items
const auto content = _activePane ? _activePane->GetContent() : nullptr;
const auto isTerm = content && content.try_as<winrt::TerminalApp::TerminalPaneContent>() != nullptr;
_duplicateTabMenuItem.IsEnabled(isTerm);
_exportTabMenuItem.IsEnabled(isTerm);
_findMenuItem.IsEnabled(isTerm);
_restartConnectionMenuItem.IsEnabled(isTerm);
// Snippets Pane can technically be split
_splitTabMenuItem.IsEnabled(isTerm || (content && content.try_as<winrt::TerminalApp::SnippetsPaneContent>() != nullptr));
}
// Method Description:
@@ -1652,106 +1673,100 @@ namespace winrt::TerminalApp::implementation
Automation::AutomationProperties::SetHelpText(renameTabMenuItem, renameTabToolTip);
}
Controls::MenuFlyoutItem duplicateTabMenuItem;
{
// "Duplicate tab"
Controls::FontIcon duplicateTabSymbol;
duplicateTabSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
duplicateTabSymbol.Glyph(L"\xF5ED");
duplicateTabMenuItem.Click({ get_weak(), &Tab::_duplicateTabClicked });
duplicateTabMenuItem.Text(RS_(L"DuplicateTabText"));
duplicateTabMenuItem.Icon(duplicateTabSymbol);
_duplicateTabMenuItem.Click({ get_weak(), &Tab::_duplicateTabClicked });
_duplicateTabMenuItem.Text(RS_(L"DuplicateTabText"));
_duplicateTabMenuItem.Icon(duplicateTabSymbol);
const auto duplicateTabToolTip = RS_(L"DuplicateTabToolTip");
WUX::Controls::ToolTipService::SetToolTip(duplicateTabMenuItem, box_value(duplicateTabToolTip));
Automation::AutomationProperties::SetHelpText(duplicateTabMenuItem, duplicateTabToolTip);
WUX::Controls::ToolTipService::SetToolTip(_duplicateTabMenuItem, box_value(duplicateTabToolTip));
Automation::AutomationProperties::SetHelpText(_duplicateTabMenuItem, duplicateTabToolTip);
}
Controls::MenuFlyoutItem splitTabMenuItem;
{
// "Split tab"
Controls::FontIcon splitTabSymbol;
splitTabSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
splitTabSymbol.Glyph(L"\xF246"); // ViewDashboard
splitTabMenuItem.Click({ get_weak(), &Tab::_splitTabClicked });
splitTabMenuItem.Text(RS_(L"SplitTabText"));
splitTabMenuItem.Icon(splitTabSymbol);
_splitTabMenuItem.Click({ get_weak(), &Tab::_splitTabClicked });
_splitTabMenuItem.Text(RS_(L"SplitTabText"));
_splitTabMenuItem.Icon(splitTabSymbol);
const auto splitTabToolTip = RS_(L"SplitTabToolTip");
WUX::Controls::ToolTipService::SetToolTip(splitTabMenuItem, box_value(splitTabToolTip));
Automation::AutomationProperties::SetHelpText(splitTabMenuItem, splitTabToolTip);
WUX::Controls::ToolTipService::SetToolTip(_splitTabMenuItem, box_value(splitTabToolTip));
Automation::AutomationProperties::SetHelpText(_splitTabMenuItem, splitTabToolTip);
}
Controls::MenuFlyoutItem closePaneMenuItem = _closePaneMenuItem;
{
// "Close pane"
closePaneMenuItem.Click({ get_weak(), &Tab::_closePaneClicked });
closePaneMenuItem.Text(RS_(L"ClosePaneText"));
_closePaneMenuItem.Click({ get_weak(), &Tab::_closePaneClicked });
_closePaneMenuItem.Text(RS_(L"ClosePaneText"));
const auto closePaneToolTip = RS_(L"ClosePaneToolTip");
WUX::Controls::ToolTipService::SetToolTip(closePaneMenuItem, box_value(closePaneToolTip));
Automation::AutomationProperties::SetHelpText(closePaneMenuItem, closePaneToolTip);
WUX::Controls::ToolTipService::SetToolTip(_closePaneMenuItem, box_value(closePaneToolTip));
Automation::AutomationProperties::SetHelpText(_closePaneMenuItem, closePaneToolTip);
}
Controls::MenuFlyoutItem exportTabMenuItem;
{
// "Export tab"
Controls::FontIcon exportTabSymbol;
exportTabSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
exportTabSymbol.Glyph(L"\xE74E"); // Save
exportTabMenuItem.Click({ get_weak(), &Tab::_exportTextClicked });
exportTabMenuItem.Text(RS_(L"ExportTabText"));
exportTabMenuItem.Icon(exportTabSymbol);
_exportTabMenuItem.Click({ get_weak(), &Tab::_exportTextClicked });
_exportTabMenuItem.Text(RS_(L"ExportTabText"));
_exportTabMenuItem.Icon(exportTabSymbol);
const auto exportTabToolTip = RS_(L"ExportTabToolTip");
WUX::Controls::ToolTipService::SetToolTip(exportTabMenuItem, box_value(exportTabToolTip));
Automation::AutomationProperties::SetHelpText(exportTabMenuItem, exportTabToolTip);
WUX::Controls::ToolTipService::SetToolTip(_exportTabMenuItem, box_value(exportTabToolTip));
Automation::AutomationProperties::SetHelpText(_exportTabMenuItem, exportTabToolTip);
}
Controls::MenuFlyoutItem findMenuItem;
{
// "Find"
Controls::FontIcon findSymbol;
findSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
findSymbol.Glyph(L"\xF78B"); // SearchMedium
findMenuItem.Click({ get_weak(), &Tab::_findClicked });
findMenuItem.Text(RS_(L"FindText"));
findMenuItem.Icon(findSymbol);
_findMenuItem.Click({ get_weak(), &Tab::_findClicked });
_findMenuItem.Text(RS_(L"FindText"));
_findMenuItem.Icon(findSymbol);
const auto findToolTip = RS_(L"FindToolTip");
WUX::Controls::ToolTipService::SetToolTip(findMenuItem, box_value(findToolTip));
Automation::AutomationProperties::SetHelpText(findMenuItem, findToolTip);
WUX::Controls::ToolTipService::SetToolTip(_findMenuItem, box_value(findToolTip));
Automation::AutomationProperties::SetHelpText(_findMenuItem, findToolTip);
}
Controls::MenuFlyoutItem restartConnectionMenuItem = _restartConnectionMenuItem;
{
// "Restart connection"
// "Restart session"
Controls::FontIcon restartConnectionSymbol;
restartConnectionSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
restartConnectionSymbol.Glyph(L"\xE72C");
restartConnectionMenuItem.Click([weakThis](auto&&, auto&&) {
_restartConnectionMenuItem.Click([weakThis](auto&&, auto&&) {
if (auto tab{ weakThis.get() })
{
tab->_RestartActivePaneConnection();
}
});
restartConnectionMenuItem.Text(RS_(L"RestartConnectionText"));
restartConnectionMenuItem.Icon(restartConnectionSymbol);
_restartConnectionMenuItem.Text(RS_(L"RestartConnectionText"));
_restartConnectionMenuItem.Icon(restartConnectionSymbol);
const auto restartConnectionToolTip = RS_(L"RestartConnectionToolTip");
WUX::Controls::ToolTipService::SetToolTip(restartConnectionMenuItem, box_value(restartConnectionToolTip));
Automation::AutomationProperties::SetHelpText(restartConnectionMenuItem, restartConnectionToolTip);
WUX::Controls::ToolTipService::SetToolTip(_restartConnectionMenuItem, box_value(restartConnectionToolTip));
Automation::AutomationProperties::SetHelpText(_restartConnectionMenuItem, restartConnectionToolTip);
}
// Build the menu
@@ -1759,16 +1774,16 @@ namespace winrt::TerminalApp::implementation
Controls::MenuFlyoutSeparator menuSeparator;
contextMenuFlyout.Items().Append(chooseColorMenuItem);
contextMenuFlyout.Items().Append(renameTabMenuItem);
contextMenuFlyout.Items().Append(duplicateTabMenuItem);
contextMenuFlyout.Items().Append(splitTabMenuItem);
contextMenuFlyout.Items().Append(_duplicateTabMenuItem);
contextMenuFlyout.Items().Append(_splitTabMenuItem);
_AppendMoveMenuItems(contextMenuFlyout);
contextMenuFlyout.Items().Append(exportTabMenuItem);
contextMenuFlyout.Items().Append(findMenuItem);
contextMenuFlyout.Items().Append(restartConnectionMenuItem);
contextMenuFlyout.Items().Append(_exportTabMenuItem);
contextMenuFlyout.Items().Append(_findMenuItem);
contextMenuFlyout.Items().Append(_restartConnectionMenuItem);
contextMenuFlyout.Items().Append(menuSeparator);
auto closeSubMenu = _AppendCloseMenuItems(contextMenuFlyout);
closeSubMenu.Items().Append(closePaneMenuItem);
closeSubMenu.Items().Append(_closePaneMenuItem);
// GH#5750 - When the context menu is dismissed with ESC, toss the focus
// back to our control.

View File

@@ -121,6 +121,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<TerminalApp::Tab, IInspectable> ActivePaneChanged;
til::event<winrt::delegate<>> TabRaiseVisualBell;
til::event<winrt::delegate<winrt::hstring /*title*/, winrt::hstring /*body*/, winrt::TerminalApp::IPaneContent /*content*/>> TabToastNotificationRequested;
til::typed_event<IInspectable, IInspectable> TaskbarProgressChanged;
// The TabViewIndex is the index this Tab object resides in TerminalPage's _tabs vector.
@@ -140,11 +141,17 @@ namespace winrt::TerminalApp::implementation
static constexpr double HeaderRenameBoxWidthTitleLength{ std::numeric_limits<double>::infinity() };
winrt::Windows::UI::Xaml::FocusState _focusState{ winrt::Windows::UI::Xaml::FocusState::Unfocused };
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _closeOtherTabsMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _closeTabsAfterMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _duplicateTabMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _splitTabMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _moveToNewWindowMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _moveRightMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _moveLeftMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _exportTabMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _findMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _restartConnectionMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _closeOtherTabsMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _closeTabsAfterMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _closePaneMenuItem{};
winrt::TerminalApp::ShortcutActionDispatch _dispatch;
Microsoft::Terminal::Settings::Model::IActionMapView _actionMap{ nullptr };
winrt::hstring _keyChord{};
@@ -159,9 +166,6 @@ namespace winrt::TerminalApp::implementation
std::shared_ptr<Pane> _activePane{ nullptr };
std::shared_ptr<Pane> _zoomedPane{ nullptr };
Windows::UI::Xaml::Controls::MenuFlyoutItem _closePaneMenuItem;
Windows::UI::Xaml::Controls::MenuFlyoutItem _restartConnectionMenuItem;
winrt::Microsoft::Terminal::Settings::Model::IconStyle _lastIconStyle;
winrt::hstring _lastIconPath{};
std::optional<winrt::Windows::UI::Color> _runtimeTabColor{};
@@ -182,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;
@@ -220,6 +225,7 @@ namespace winrt::TerminalApp::implementation
void _AttachEventHandlersToPane(std::shared_ptr<Pane> pane);
void _UpdateActivePane(std::shared_ptr<Pane> pane);
void _UpdateMenuItemStates();
winrt::hstring _GetActiveTitle() const;
@@ -230,8 +236,6 @@ namespace winrt::TerminalApp::implementation
void _UpdateConnectionClosedState();
void _RestartActivePaneConnection();
void _DuplicateTab();
winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush();
void _MakeTabViewItem();

View File

@@ -16,6 +16,7 @@
#include "TabRowControl.h"
#include "DebugTapConnection.h"
#include "DesktopNotification.h"
#include "..\TerminalSettingsModel\FileUtils.h"
#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h"
@@ -149,6 +150,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);
@@ -1135,4 +1148,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>& 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 "<unnamed window>" 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<winrt::Windows::Foundation::IUnknown>{}(*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);
}
}
});
}
});
}
}

View File

@@ -174,6 +174,7 @@
<DependentUpon>TerminalPaneContent.idl</DependentUpon>
</ClInclude>
<ClInclude Include="Toast.h" />
<ClInclude Include="DesktopNotification.h" />
<ClInclude Include="TerminalSettingsCache.h" />
<ClInclude Include="SuggestionsControl.h">
<DependentUpon>SuggestionsControl.xaml</DependentUpon>
@@ -287,6 +288,7 @@
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
<ClCompile Include="Toast.cpp" />
<ClCompile Include="DesktopNotification.cpp" />
<ClCompile Include="TerminalSettingsCache.cpp" />
<ClCompile Include="SuggestionsControl.cpp">
<DependentUpon>SuggestionsControl.xaml</DependentUpon>

View File

@@ -168,6 +168,7 @@ namespace winrt::TerminalApp::implementation
void OpenSettingsUI(const winrt::hstring& startingPage = {});
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<IInspectable, IInspectable> IdentifyWindowsRequested;
til::typed_event<IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs> RenameWindowRequested;
til::typed_event<IInspectable, IInspectable> SummonWindowRequested;
til::typed_event<IInspectable, winrt::TerminalApp::Tab> FocusTabRequested;
til::typed_event<IInspectable, winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs> WindowSizeChanged;
til::typed_event<IInspectable, IInspectable> OpenSystemMenu;
@@ -580,6 +582,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>& tab, const winrt::TerminalApp::IPaneContent& content);
// Terminal Chat related members and functions
winrt::Microsoft::Terminal::Query::Extension::ILMProvider _lmProvider{ nullptr };
winrt::Microsoft::Terminal::Settings::Model::LLMProvider _currentProvider{ winrt::Microsoft::Terminal::Settings::Model::LLMProvider::None };

View File

@@ -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<bool> FlashTaskbar;
};
struct NotificationEventArgs : public NotificationEventArgsT<NotificationEventArgs>
{
public:
NotificationEventArgs(const winrt::hstring& title = {}, const winrt::hstring& body = {}) :
Title(title), Body(body) {}
til::property<winrt::hstring> Title;
til::property<winrt::hstring> Body;
};
struct TerminalPaneContent : TerminalPaneContentT<TerminalPaneContent>, BasicPaneEvents
{
TerminalPaneContent(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,

View File

@@ -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();

View File

@@ -92,6 +92,7 @@ namespace winrt::TerminalApp::implementation
bool ShowTabsFullscreen() const;
bool AutoHideWindow();
void IdentifyWindow();
bool FocusTab(const winrt::TerminalApp::Tab& tab);
std::optional<uint32_t> 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);

View File

@@ -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<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, TerminalApp.Tab> FocusTabRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
event Windows.Foundation.TypedEventHandler<Object, TerminalApp.SystemMenuChangeArgs> SystemMenuChangeRequested;

View File

@@ -55,6 +55,9 @@
#include <winrt/Windows.Management.Deployment.h>
#include <winrt/Windows.Data.Json.h>
#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>
#include <winrt/Microsoft.UI.Xaml.Controls.h>
#include <winrt/Microsoft.UI.Xaml.Controls.Primitives.h>
#include <winrt/Microsoft.UI.Xaml.XamlTypeInfo.h>

View File

@@ -309,8 +309,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const auto isOnOriginalPosition = _lastMouseClickPosNoSelection == pixelPosition;
// Rounded coordinates for text selection.
// Don't round in VT mouse mode; cell-level precision matters more
const auto round = !_core->IsVtMouseModeEnabled();
// Don't round in VT mouse mode; cell-level precision matters more.
// Only round for single-click: for double/triple-click, rounding
// can push the position to the next cell, selecting the wrong word.
const auto round = multiClickMapper == 1 && !_core->IsVtMouseModeEnabled();
_core->LeftClickOnTerminal(_getTerminalPosition(til::point{ pixelPosition }, round),
multiClickMapper,
altEnabled,

View File

@@ -705,7 +705,7 @@
<comment>When enabled, input will go to all panes in this tab simultaneously</comment>
</data>
<data name="RestartConnectionKey" xml:space="preserve">
<value>Restart connection</value>
<value>Restart session</value>
</data>
<data name="OpenScratchpadKey" xml:space="preserve">
<value>Open scratchpad</value>

View File

@@ -1843,6 +1843,15 @@ namespace SettingsModelUnitTests
VERIFY_ARE_EQUAL(settings->ProfileDefaults().HasTabTitle(), copyImpl->ProfileDefaults().HasTabTitle());
VERIFY_ARE_NOT_EQUAL(settings->ProfileDefaults().TabTitle(), copyImpl->ProfileDefaults().TabTitle());
// Verify HasXxx independence: setting a previously-inherited value on the clone
// should make HasXxx true on the clone but remain false on the original.
// SnapOnInput is not set in the JSON, so both should inherit the default.
VERIFY_IS_FALSE(settings->AllProfiles().GetAt(0).HasSnapOnInput());
VERIFY_IS_FALSE(copyImpl->AllProfiles().GetAt(0).HasSnapOnInput());
copyImpl->AllProfiles().GetAt(0).SnapOnInput(false);
VERIFY_IS_FALSE(settings->AllProfiles().GetAt(0).HasSnapOnInput());
VERIFY_IS_TRUE(copyImpl->AllProfiles().GetAt(0).HasSnapOnInput());
Log::Comment(L"Test empty profiles.defaults");
static constexpr std::string_view emptyPDJson{ R"(
{

View File

@@ -31,6 +31,10 @@ namespace SettingsModelUnitTests
TEST_METHOD(TestGenGuidsForProfiles);
TEST_METHOD(TestCorrectOldDefaultShellPaths);
TEST_METHOD(ProfileDefaultsProhibitedSettings);
TEST_METHOD(SettingInheritanceFallback);
TEST_METHOD(ClearSettingRestoresInheritance);
TEST_METHOD(HasSettingAtSpecificLayer);
};
void ProfileTests::ProfileGeneratesGuid()
@@ -532,4 +536,130 @@ namespace SettingsModelUnitTests
VERIFY_ARE_NOT_EQUAL(L"Default Profile Source", allProfiles.GetAt(2).Source());
VERIFY_ARE_NOT_EQUAL(L"foo.exe", allProfiles.GetAt(2).Commandline());
}
void ProfileTests::SettingInheritanceFallback()
{
// Verify that when no layer defines a setting, the default value is used.
// Also verify that when only user defaults defines it, profiles inherit from there.
static constexpr std::string_view userSettings{ R"({
"profiles": {
"defaults": {
"historySize": 5000
},
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"snapOnInput": false
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userSettings);
const auto allProfiles = settings->AllProfiles();
VERIFY_ARE_EQUAL(2u, allProfiles.Size());
// profile0: historySize inherited from defaults
VERIFY_ARE_EQUAL(5000, allProfiles.GetAt(0).HistorySize());
// profile0: snapOnInput not set anywhere, falls back to default (true)
VERIFY_ARE_EQUAL(true, allProfiles.GetAt(0).SnapOnInput());
// profile1: historySize inherited from defaults
VERIFY_ARE_EQUAL(5000, allProfiles.GetAt(1).HistorySize());
// profile1: snapOnInput explicitly set to false
VERIFY_ARE_EQUAL(false, allProfiles.GetAt(1).SnapOnInput());
}
void ProfileTests::ClearSettingRestoresInheritance()
{
// Verify that clearing a setting at the profile layer causes it to
// fall back to the parent's value.
static constexpr std::string_view parentString{ R"({
"name": "parent",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000,
"tabTitle": "ParentTitle"
})" };
static constexpr std::string_view childString{ R"({
"name": "child",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 2000,
"tabTitle": "ChildTitle"
})" };
const auto parentJson = VerifyParseSucceeded(parentString);
const auto childJson = VerifyParseSucceeded(childString);
auto parent = implementation::Profile::FromJson(parentJson);
auto child = parent->CreateChild();
child->LayerJson(childJson);
// Verify child has its own values
VERIFY_ARE_EQUAL(2000, child->HistorySize());
VERIFY_ARE_EQUAL(L"ChildTitle", child->TabTitle());
VERIFY_IS_TRUE(child->HasHistorySize());
VERIFY_IS_TRUE(child->HasTabTitle());
// Clear historySize on child: should fall back to parent
child->ClearHistorySize();
VERIFY_IS_FALSE(child->HasHistorySize());
VERIFY_ARE_EQUAL(1000, child->HistorySize());
// Clear tabTitle on child: should fall back to parent
child->ClearTabTitle();
VERIFY_IS_FALSE(child->HasTabTitle());
VERIFY_ARE_EQUAL(L"ParentTitle", child->TabTitle());
}
void ProfileTests::HasSettingAtSpecificLayer()
{
// Verify that HasXxx() correctly reports whether a setting is defined
// at the current layer vs inherited from a parent.
static constexpr std::string_view userSettings{ R"({
"profiles": {
"defaults": {
"historySize": 5000,
"tabTitle": "DefaultTitle"
},
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 9001
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userSettings);
const auto allProfiles = settings->AllProfiles();
VERIFY_ARE_EQUAL(2u, allProfiles.Size());
// profile0: historySize is explicitly set
VERIFY_IS_TRUE(allProfiles.GetAt(0).HasHistorySize());
VERIFY_ARE_EQUAL(9001, allProfiles.GetAt(0).HistorySize());
// profile0: tabTitle is NOT set at this layer (inherited from defaults)
VERIFY_IS_FALSE(allProfiles.GetAt(0).HasTabTitle());
VERIFY_ARE_EQUAL(L"DefaultTitle", allProfiles.GetAt(0).TabTitle());
// profile1: historySize is NOT set at this layer (inherited from defaults)
VERIFY_IS_FALSE(allProfiles.GetAt(1).HasHistorySize());
VERIFY_ARE_EQUAL(5000, allProfiles.GetAt(1).HistorySize());
// ProfileDefaults: historySize is set
VERIFY_IS_TRUE(settings->ProfileDefaults().HasHistorySize());
VERIFY_ARE_EQUAL(5000, settings->ProfileDefaults().HistorySize());
}
}

View File

@@ -60,6 +60,12 @@ namespace SettingsModelUnitTests
TEST_METHOD(ProfileWithInvalidIcon);
TEST_METHOD(ModifyProfileSettingAndRoundtrip);
TEST_METHOD(ModifyGlobalSettingAndRoundtrip);
TEST_METHOD(ModifyColorSchemeAndRoundtrip);
TEST_METHOD(FixupUserSettingsDetectsChanges);
TEST_METHOD(FixupCommandlinePatching);
private:
// Method Description:
// - deserializes and reserializes a json string representing a settings object model of type T
@@ -1325,4 +1331,288 @@ namespace SettingsModelUnitTests
// what was written in the settings file.
VERIFY_ARE_EQUAL(R"(c:\this_icon_had_better_not_exist.tiff)", newResult["profiles"]["list"][0]["icon"].asString());
}
void SerializationTests::ModifyProfileSettingAndRoundtrip()
{
// Load settings, modify a profile setting via setter, serialize,
// and verify the JSON output reflects the change.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000,
"commandline": "cmd.exe"
}
]
})" };
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(settingsJson) };
// Verify initial value
VERIFY_ARE_EQUAL(1000, settings->AllProfiles().GetAt(0).HistorySize());
// Modify the setting
settings->AllProfiles().GetAt(0).HistorySize(5000);
VERIFY_ARE_EQUAL(5000, settings->AllProfiles().GetAt(0).HistorySize());
// Serialize and verify the change is reflected in JSON
const auto result{ settings->ToJson() };
VERIFY_ARE_EQUAL(5000, result["profiles"]["list"][0]["historySize"].asInt());
// Verify other settings are preserved
VERIFY_ARE_EQUAL("cmd.exe", result["profiles"]["list"][0]["commandline"].asString());
// Also verify: modify a setting that wasn't previously set
settings->AllProfiles().GetAt(0).TabTitle(L"NewTitle");
const auto result2{ settings->ToJson() };
VERIFY_ARE_EQUAL("NewTitle", result2["profiles"]["list"][0]["tabTitle"].asString());
}
void SerializationTests::ModifyGlobalSettingAndRoundtrip()
{
// Load settings, modify a global setting, serialize, verify JSON.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"initialRows": 30,
"alwaysOnTop": false,
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
}
]
})" };
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(settingsJson) };
// Verify initial values
VERIFY_ARE_EQUAL(30, settings->GlobalSettings().InitialRows());
VERIFY_ARE_EQUAL(false, settings->GlobalSettings().AlwaysOnTop());
// Modify global settings
settings->GlobalSettings().InitialRows(50);
settings->GlobalSettings().AlwaysOnTop(true);
// Verify in-memory changes
VERIFY_ARE_EQUAL(50, settings->GlobalSettings().InitialRows());
VERIFY_ARE_EQUAL(true, settings->GlobalSettings().AlwaysOnTop());
// Serialize and verify
const auto result{ settings->ToJson() };
VERIFY_ARE_EQUAL(50, result["initialRows"].asInt());
VERIFY_ARE_EQUAL(true, result["alwaysOnTop"].asBool());
}
void SerializationTests::ModifyColorSchemeAndRoundtrip()
{
// Load settings with a user color scheme, modify it, serialize, verify.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
}
],
"schemes": [
{
"name": "MyScheme",
"foreground": "#CCCCCC",
"background": "#0C0C0C",
"cursorColor": "#FFFFFF",
"black": "#0C0C0C",
"red": "#C50F1F",
"green": "#13A10E",
"yellow": "#C19C00",
"blue": "#0037DA",
"purple": "#881798",
"cyan": "#3A96DD",
"white": "#CCCCCC",
"brightBlack": "#767676",
"brightRed": "#E74856",
"brightGreen": "#16C60C",
"brightYellow": "#F9F1A5",
"brightBlue": "#3B78FF",
"brightPurple": "#B4009E",
"brightCyan": "#61D6D6",
"brightWhite": "#F2F2F2"
}
]
})" };
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(settingsJson) };
// Find and modify the color scheme
const auto schemes = settings->GlobalSettings().ColorSchemes();
VERIFY_IS_TRUE(schemes.HasKey(L"MyScheme"));
auto myScheme = schemes.Lookup(L"MyScheme");
const auto origForeground = myScheme.Foreground();
myScheme.Foreground(til::color{ 0xAA, 0xBB, 0xCC });
// Serialize and verify the change persists
const auto result{ settings->ToJson() };
const auto& schemesJson = result["schemes"];
bool found = false;
for (const auto& scheme : schemesJson)
{
if (scheme["name"].asString() == "MyScheme")
{
VERIFY_ARE_EQUAL("#AABBCC", scheme["foreground"].asString());
found = true;
break;
}
}
VERIFY_IS_TRUE(found, L"MyScheme should be present in serialized output");
}
void SerializationTests::FixupUserSettingsDetectsChanges()
{
// Verify that FixupUserSettings returns true when settings need
// to be written back (e.g., migration), and false when clean.
static constexpr std::string_view cleanSettingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"commandline": "cmd.exe"
}
]
})" };
// Load, fixup, serialize. Reload and verify fixup returns false.
implementation::SettingsLoader loader1{ cleanSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader1.MergeInboxIntoUserSettings();
loader1.FinalizeLayering();
loader1.FixupUserSettings();
const auto settings1 = winrt::make_self<implementation::CascadiaSettings>(std::move(loader1));
const auto result1{ settings1->ToJson() };
// Reload from the serialized output (should be stable)
implementation::SettingsLoader loader2{ toString(result1), implementation::LoadStringResource(IDR_DEFAULTS) };
loader2.MergeInboxIntoUserSettings();
loader2.FinalizeLayering();
const auto fixupNeeded = loader2.FixupUserSettings();
// After a clean roundtrip, no further fixups should be needed
VERIFY_IS_FALSE(fixupNeeded, L"A clean roundtrip should not require further fixups");
}
void SerializationTests::FixupCommandlinePatching()
{
// Verify that FixupUserSettings patches "cmd.exe" to the full path
// for the Command Prompt profile, and "powershell.exe" for the
// Windows PowerShell profile, and returns true to indicate changes.
// Case 1: CMD profile with short commandline should be patched
static constexpr std::string_view cmdSettingsJson{ R"(
{
"defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"profiles": [
{
"name": "Command Prompt",
"guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"commandline": "cmd.exe"
}
]
})" };
{
implementation::SettingsLoader loader{ cmdSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
const auto fixupNeeded = loader.FixupUserSettings();
VERIFY_IS_TRUE(fixupNeeded, L"FixupUserSettings should return true when cmd.exe is patched");
const auto settings = winrt::make_self<implementation::CascadiaSettings>(std::move(loader));
const auto cmdProfile = settings->FindProfile(Utils::GuidFromString(L"{0caa0dad-35be-5f56-a8ff-afceeeaa6101}"));
VERIFY_IS_NOT_NULL(cmdProfile);
VERIFY_ARE_EQUAL(L"%SystemRoot%\\System32\\cmd.exe", cmdProfile.Commandline());
}
// Case 2: PowerShell profile with short commandline should be patched
static constexpr std::string_view psSettingsJson{ R"(
{
"defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"profiles": [
{
"name": "Windows PowerShell",
"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"commandline": "powershell.exe"
}
]
})" };
{
implementation::SettingsLoader loader{ psSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
const auto fixupNeeded = loader.FixupUserSettings();
VERIFY_IS_TRUE(fixupNeeded, L"FixupUserSettings should return true when powershell.exe is patched");
const auto settings = winrt::make_self<implementation::CascadiaSettings>(std::move(loader));
const auto psProfile = settings->FindProfile(Utils::GuidFromString(L"{61c54bbd-c2c6-5271-96e7-009a87ff44bf}"));
VERIFY_IS_NOT_NULL(psProfile);
VERIFY_ARE_EQUAL(L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", psProfile.Commandline());
}
// Case 3: CMD profile with the full path should NOT trigger fixup
static constexpr std::string_view cleanCmdSettingsJson{ R"(
{
"defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"profiles": [
{
"name": "Command Prompt",
"guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}"
}
]
})" };
{
implementation::SettingsLoader loader{ cleanCmdSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
const auto fixupNeeded = loader.FixupUserSettings();
VERIFY_IS_FALSE(fixupNeeded, L"FixupUserSettings should return false when no patching is needed");
const auto settings = winrt::make_self<implementation::CascadiaSettings>(std::move(loader));
const auto cmdProfile = settings->FindProfile(Utils::GuidFromString(L"{0caa0dad-35be-5f56-a8ff-afceeeaa6101}"));
VERIFY_IS_NOT_NULL(cmdProfile);
// Should still resolve to the full path via inbox defaults
VERIFY_ARE_EQUAL(L"%SystemRoot%\\System32\\cmd.exe", cmdProfile.Commandline());
}
// Case 4: A non-builtin profile with "cmd.exe" should NOT be patched
static constexpr std::string_view customCmdSettingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "My Custom CMD",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"commandline": "cmd.exe"
}
]
})" };
{
implementation::SettingsLoader loader{ customCmdSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
loader.FixupUserSettings();
const auto settings = winrt::make_self<implementation::CascadiaSettings>(std::move(loader));
const auto customProfile = settings->FindProfile(Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"));
VERIFY_IS_NOT_NULL(customProfile);
// Custom profile should keep "cmd.exe" unchanged
VERIFY_ARE_EQUAL(L"cmd.exe", customProfile.Commandline());
}
}
}

View File

@@ -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&)
{

View File

@@ -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;

View File

@@ -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<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);
// 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:

View File

@@ -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