Compare commits

...

5 Commits

Author SHA1 Message Date
Mike Griese
821cd97610 format 2026-03-06 13:20:45 -06:00
Mike Griese
16099f42bf gottem 2026-03-06 13:19:59 -06:00
Mike Griese
4aff331374 implement the whole thing 2026-03-06 13:17:58 -06:00
Mike Griese
c268521309 just didn't save 2026-03-06 06:51:08 -06:00
Mike Griese
7d02839be2 simplify the base for sending notifications 2026-03-06 06:41:35 -06:00
47 changed files with 489 additions and 4 deletions

View File

@@ -10,6 +10,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<> ConnectionStateChanged;
til::typed_event<IPaneContent> CloseRequested;
til::typed_event<IPaneContent, winrt::TerminalApp::BellEventArgs> BellRequested;
til::typed_event<IPaneContent, winrt::TerminalApp::NotificationEventArgs> NotificationRequested;
til::typed_event<IPaneContent> TitleChanged;
til::typed_event<IPaneContent> TabColorChanged;
til::typed_event<IPaneContent> TaskbarProgressChanged;

View File

@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "DesktopNotification.h"
using namespace winrt::Windows::UI::Notifications;
using namespace winrt::Windows::Data::Xml::Dom;
namespace winrt::TerminalApp::implementation
{
std::atomic<int64_t> DesktopNotification::_lastNotificationTime{ 0 };
bool DesktopNotification::ShouldSendNotification()
{
FILETIME ft{};
GetSystemTimeAsFileTime(&ft);
const auto now = (static_cast<int64_t>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
auto last = _lastNotificationTime.load(std::memory_order_relaxed);
if (now - last < MinNotificationIntervalTicks)
{
return false;
}
// Attempt to update; if another thread beat us, that's fine — we'll skip this one.
_lastNotificationTime.compare_exchange_strong(last, now, std::memory_order_relaxed);
return true;
}
void DesktopNotification::SendNotification(
const DesktopNotificationArgs& args,
std::function<void(uint32_t tabIndex)> activated)
{
try
{
if (!ShouldSendNotification())
{
return;
}
// Build the toast XML. We use a simple template with a title and body text.
//
// <toast launch="__fromToast">
// <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. "__fromToast" is recognized by
// AppCommandlineArgs::ParseArgs as a no-op sentinel.
toastElement.SetAttribute(L"launch", L"__fromToast");
// Set the scenario to "reminder" to ensure the toast shows even in DND,
// and the group/tag to allow replacement of repeated notifications.
toastElement.SetAttribute(L"scenario", L"default");
auto toast = ToastNotification{ toastXml };
// Set the tag and group to enable notification replacement.
// Using the tab index as a tag means repeated output from the same tab
// replaces the previous notification rather than stacking.
toast.Tag(fmt::format(FMT_COMPILE(L"wt-tab-{}"), args.TabIndex));
toast.Group(L"WindowsTerminal");
// When the user activates (clicks) the toast, fire the callback.
if (activated)
{
const auto tabIndex = args.TabIndex;
toast.Activated([activated, tabIndex](const auto& /*sender*/, const auto& /*eventArgs*/) {
activated(tabIndex);
});
}
// For packaged apps, CreateToastNotifier() uses the package identity automatically.
// For unpackaged apps, we need to provide an AUMID, but that case is less common
// and toast notifications may not be supported without additional setup.
auto notifier = ToastNotificationManager::CreateToastNotifier();
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,52 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- DesktopNotification.h
Module Description:
- Helper for sending Windows desktop toast notifications. Used by the
`OutputNotificationStyle::Notification` flag to surface activity
and prompt-return 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;
uint32_t TabIndex{ 0 };
};
class DesktopNotification
{
public:
// 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.
//
// activated: A callback invoked on the background thread when the
// toast is clicked. The uint32_t parameter is the tab index.
static void SendNotification(
const DesktopNotificationArgs& args,
std::function<void(uint32_t tabIndex)> activated);
// Rate-limits toast notifications so we don't spam the user.
// Returns true if a notification is allowed, false if too recent.
static bool ShouldSendNotification();
private:
static std::atomic<int64_t> _lastNotificationTime;
// Minimum interval between notifications, in 100ns ticks (FILETIME units).
// 5 seconds = 5 * 10,000,000
static constexpr int64_t MinNotificationIntervalTicks = 50'000'000LL;
};
}

View File

@@ -16,6 +16,23 @@ namespace TerminalApp
Boolean FlashTaskbar { get; };
};
[flags]
enum OutputNotificationStyle
{
Taskbar = 0x1,
Audible = 0x2,
Tab = 0x4,
Notification = 0x8,
All = 0xffffffff
};
runtimeclass NotificationEventArgs
{
OutputNotificationStyle Style { get; };
String Title { get; };
String Body { get; };
};
interface IPaneContent
{
Windows.UI.Xaml.FrameworkElement GetRoot();
@@ -40,6 +57,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<IPaneContent, Object> CloseRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> ConnectionStateChanged;
event Windows.Foundation.TypedEventHandler<IPaneContent, NotificationEventArgs> NotificationRequested;
event Windows.Foundation.TypedEventHandler<IPaneContent, BellEventArgs> BellRequested;
event Windows.Foundation.TypedEventHandler<IPaneContent, Object> TitleChanged;
event Windows.Foundation.TypedEventHandler<IPaneContent, Object> TabColorChanged;

View File

@@ -923,4 +923,16 @@
<data name="InvalidRegex" xml:space="preserve">
<value>An invalid regular expression was found.</value>
</data>
<data name="NotificationTitle" xml:space="preserve">
<value>Windows Terminal</value>
<comment>Title shown in desktop toast notifications for tab activity.</comment>
</data>
<data name="NotificationMessage_TabActivity" xml:space="preserve">
<value>Tab "{0}" has new activity</value>
<comment>{Locked="{0}"}Message shown in a desktop toast notification when a tab produces output. {0} is the tab title.</comment>
</data>
<data name="NotificationMessage_TabActivityInWindow" xml:space="preserve">
<value>Tab "{0}" in {1} has new activity</value>
<comment>{Locked="{0}"}{Locked="{1}"}Message shown in a desktop toast notification when a tab produces output. {0} is the tab title. {1} is the window name.</comment>
</data>
</root>

View File

@@ -1161,6 +1161,38 @@ namespace winrt::TerminalApp::implementation
}
});
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() })
{
// Other notification styles may be added in the future,
// (see GH#7955 for more details)
//
// Currently the only style is to request a toast
// notification. We'll just check for that flag for now.
const auto style = notifArgs.Style();
if (WI_IsFlagSet(style, winrt::TerminalApp::OutputNotificationStyle::Notification))
{
// Request a desktop toast notification.
// TerminalPage subscribes to this event and handles sending the toast
// and processing its activation (summoning the window + switching tabs).
const auto notifTitle = notifArgs.Title();
const auto notifBody = notifArgs.Body();
if (!notifTitle.empty())
{
tab->TabToastNotificationRequested.raise(notifTitle, notifBody, tab->TabViewIndex());
}
else
{
tab->TabToastNotificationRequested.raise(tab->Title(), L"", tab->TabViewIndex());
}
}
}
});
if (const auto& terminal{ content.try_as<TerminalApp::TerminalPaneContent>() })
{
events.RestartTerminalRequested = terminal.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &Tab::_bubbleRestartTerminalRequested });

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*/, uint32_t /*tabIndex*/>> TabToastNotificationRequested;
til::typed_event<IInspectable, IInspectable> TaskbarProgressChanged;
// The TabViewIndex is the index this Tab object resides in TerminalPage's _tabs vector.
@@ -176,6 +177,7 @@ namespace winrt::TerminalApp::implementation
struct ContentEventTokens
{
winrt::TerminalApp::IPaneContent::BellRequested_revoker BellRequested;
winrt::TerminalApp::IPaneContent::NotificationRequested_revoker NotificationRequested;
winrt::TerminalApp::IPaneContent::TitleChanged_revoker TitleChanged;
winrt::TerminalApp::IPaneContent::TabColorChanged_revoker TabColorChanged;
winrt::TerminalApp::IPaneContent::TaskbarProgressChanged_revoker TaskbarProgressChanged;

View File

@@ -19,6 +19,7 @@
#include "DebugTapConnection.h"
#include "..\TerminalSettingsModel\FileUtils.h"
#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h"
#include "DesktopNotification.h"
#include <shlobj.h>
@@ -150,6 +151,15 @@ namespace winrt::TerminalApp::implementation
}
});
// When a tab requests a desktop toast notification (OutputNotificationStyle::Notification),
// send the toast and handle activation by summoning this window and switching to the tab.
newTabImpl->TabToastNotificationRequested([weakThis{ get_weak() }](const winrt::hstring& title, const winrt::hstring& body, uint32_t tabIndex) {
if (const auto page{ weakThis.get() })
{
page->_SendDesktopNotification(title, body, tabIndex);
}
});
auto tabViewItem = newTabImpl->TabViewItem();
_tabView.TabItems().InsertAt(insertPosition, tabViewItem);
@@ -1185,4 +1195,68 @@ namespace winrt::TerminalApp::implementation
{
return _tabs.Size() > 1;
}
// Method Description:
// - Sends a Windows desktop toast notification for a tab. When the user clicks
// the toast, summon this window and switch to the specified tab.
// Arguments:
// - tabTitle: The title of the tab to display in the notification.
// - tabIndex: The index of the tab to switch to when the toast is activated.
void TerminalPage::_SendDesktopNotification(const winrt::hstring& tabTitle, const winrt::hstring& body, uint32_t tabIndex)
{
// 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 notifTitle;
winrt::hstring message;
if (!body.empty())
{
notifTitle = 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 });
}
notifTitle = RS_(L"NotificationTitle");
}
implementation::DesktopNotificationArgs args;
args.Title = notifTitle;
args.Message = message;
args.TabIndex = tabIndex;
// Capture a weak ref and the dispatcher so we can marshal back to the UI thread
// when the toast is activated.
auto weakThis = get_weak();
auto dispatcher = Dispatcher();
implementation::DesktopNotification::SendNotification(
args,
[weakThis, dispatcher, tabIndex](uint32_t /*activatedTabIndex*/) -> void {
// The toast Activated callback fires on a background thread.
// We need to dispatch to the UI thread to summon the window and switch tabs.
[](auto weakThis, auto dispatcher, auto tabIndex) -> safe_void_coroutine {
co_await wil::resume_foreground(dispatcher);
if (const auto page{ weakThis.get() })
{
// Summon this window (bring to foreground)
page->SummonWindowRequested.raise(nullptr, nullptr);
// Switch to the tab that triggered the notification
page->_SelectTab(tabIndex);
}
}(weakThis, dispatcher, tabIndex);
});
}
}

View File

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

View File

@@ -30,6 +30,7 @@
<ClCompile Include="fzf/fzf.cpp">
<Filter>fzf</Filter>
</ClCompile>
<ClCompile Include="DesktopNotification.cpp" />
<ClCompile Include="Toast.cpp" />
<ClCompile Include="LanguageProfileNotifier.cpp" />
<ClCompile Include="TerminalSettingsCache.cpp" />
@@ -58,6 +59,7 @@
<ClInclude Include="fzf/fzf.h">
<Filter>fzf</Filter>
</ClInclude>
<ClInclude Include="DesktopNotification.h" />
<ClInclude Include="Toast.h" />
<ClInclude Include="LanguageProfileNotifier.h" />
<ClInclude Include="WindowsPackageManagerFactory.h" />

View File

@@ -570,6 +570,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, uint32_t tabIndex);
#pragma region ActionHandlers
// These are all defined in AppActionHandlers.cpp
#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action);

View File

@@ -42,6 +42,7 @@ namespace winrt::TerminalApp::implementation
_controlEvents._SetTaskbarProgress = _control.SetTaskbarProgress(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlSetTaskbarProgress });
_controlEvents._ReadOnlyChanged = _control.ReadOnlyChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlReadOnlyChanged });
_controlEvents._FocusFollowMouseRequested = _control.FocusFollowMouseRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlFocusFollowMouseRequested });
_controlEvents._ShowNotification = _control.ShowNotification(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlShowNotification });
}
void TerminalPaneContent::_removeControlEvents()
{
@@ -182,6 +183,15 @@ namespace winrt::TerminalApp::implementation
FocusRequested.raise(*this, nullptr);
}
void TerminalPaneContent::_controlShowNotification(const IInspectable& /*sender*/, const ShowNotificationEventArgs& args)
{
auto notifArgs = winrt::make<implementation::NotificationEventArgs>(
OutputNotificationStyle::Notification,
args.Title(),
args.Body());
NotificationRequested.raise(*this, notifArgs);
}
// Method Description:
// - Called when our attached control is closed. Triggers listeners to our close
// event, if we're a leaf pane.

View File

@@ -5,6 +5,7 @@
#include "TerminalPaneContent.g.h"
#include "BellEventArgs.g.h"
#include "BasicPaneEvents.h"
#include "NotificationEventArgs.g.h"
namespace winrt::TerminalApp::implementation
{
@@ -19,6 +20,20 @@ namespace winrt::TerminalApp::implementation
til::property<bool> FlashTaskbar;
};
struct NotificationEventArgs : public NotificationEventArgsT<NotificationEventArgs>
{
public:
NotificationEventArgs(
winrt::TerminalApp::OutputNotificationStyle style,
const winrt::hstring& title = {},
const winrt::hstring& body = {}) :
Style(style), Title(title), Body(body) {}
til::property<winrt::TerminalApp::OutputNotificationStyle> Style;
til::property<winrt::hstring> Title;
til::property<winrt::hstring> Body;
};
struct TerminalPaneContent : TerminalPaneContentT<TerminalPaneContent>, BasicPaneEvents
{
TerminalPaneContent(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
@@ -79,6 +94,7 @@ namespace winrt::TerminalApp::implementation
winrt::Microsoft::Terminal::Control::TermControl::SetTaskbarProgress_revoker _SetTaskbarProgress;
winrt::Microsoft::Terminal::Control::TermControl::ReadOnlyChanged_revoker _ReadOnlyChanged;
winrt::Microsoft::Terminal::Control::TermControl::FocusFollowMouseRequested_revoker _FocusFollowMouseRequested;
winrt::Microsoft::Terminal::Control::TermControl::ShowNotification_revoker _ShowNotification;
} _controlEvents;
void _setupControlEvents();
@@ -96,6 +112,7 @@ namespace winrt::TerminalApp::implementation
void _controlSetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
void _controlReadOnlyChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
void _controlFocusFollowMouseRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
void _controlShowNotification(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Control::ShowNotificationEventArgs& args);
void _closeTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
void _restartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);

View File

@@ -54,6 +54,9 @@
#include <winrt/Windows.Media.Playback.h>
#include <winrt/Windows.Management.Deployment.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

@@ -139,6 +139,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
auto pfnSearchMissingCommand = [this](auto&& PH1, auto&& PH2) { _terminalSearchMissingCommand(std::forward<decltype(PH1)>(PH1), std::forward<decltype(PH2)>(PH2)); };
_terminal->SetSearchMissingCommandCallback(pfnSearchMissingCommand);
auto pfnShowNotification = [this](auto&& PH1, auto&& PH2) { _terminalShowNotification(std::forward<decltype(PH1)>(PH1), std::forward<decltype(PH2)>(PH2)); };
_terminal->SetShowNotificationCallback(pfnShowNotification);
auto pfnClearQuickFix = [this] { ClearQuickFix(); };
_terminal->SetClearQuickFixCallback(pfnClearQuickFix);
@@ -1685,6 +1688,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
SearchMissingCommand.raise(*this, make<implementation::SearchMissingCommandEventArgs>(hstring{ missingCommand }, bufferRow));
}
void ControlCore::_terminalShowNotification(std::wstring_view title, std::wstring_view body)
{
ShowNotification.raise(*this, make<implementation::ShowNotificationEventArgs>(hstring{ title }, hstring{ body }));
}
void ControlCore::OpenCWD()
{
const auto workingDirectory = WorkingDirectory();

View File

@@ -293,6 +293,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
til::typed_event<IInspectable, Control::OpenHyperlinkEventArgs> OpenHyperlink;
til::typed_event<IInspectable, Control::CompletionsChangedEventArgs> CompletionsChanged;
til::typed_event<IInspectable, Control::SearchMissingCommandEventArgs> SearchMissingCommand;
til::typed_event<IInspectable, Control::ShowNotificationEventArgs> ShowNotification;
til::typed_event<> RefreshQuickFixUI;
til::typed_event<IInspectable, Control::WindowSizeChangedEventArgs> WindowSizeChanged;
@@ -334,6 +335,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const int velocity,
const std::chrono::microseconds duration);
void _terminalSearchMissingCommand(std::wstring_view missingCommand, const til::CoordType& bufferRow);
void _terminalShowNotification(std::wstring_view title, std::wstring_view body);
void _terminalWindowSizeChanged(int32_t width, int32_t height);
void _terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength);

View File

@@ -197,6 +197,7 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, Object> RendererEnteredErrorState;
event Windows.Foundation.TypedEventHandler<Object, ShowWindowArgs> ShowWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, SearchMissingCommandEventArgs> SearchMissingCommand;
event Windows.Foundation.TypedEventHandler<Object, ShowNotificationEventArgs> ShowNotification;
event Windows.Foundation.TypedEventHandler<Object, Object> RefreshQuickFixUI;
event Windows.Foundation.TypedEventHandler<Object, WindowSizeChangedEventArgs> WindowSizeChanged;

View File

@@ -20,6 +20,7 @@
#include "CharSentEventArgs.g.h"
#include "StringSentEventArgs.g.h"
#include "SearchMissingCommandEventArgs.g.h"
#include "ShowNotificationEventArgs.g.h"
#include "WindowSizeChangedEventArgs.g.h"
namespace winrt::Microsoft::Terminal::Control::implementation
@@ -252,6 +253,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
til::property<til::CoordType> BufferRow;
};
struct ShowNotificationEventArgs : public ShowNotificationEventArgsT<ShowNotificationEventArgs>
{
public:
ShowNotificationEventArgs(const winrt::hstring& title, const winrt::hstring& body) :
Title(title),
Body(body) {}
til::property<winrt::hstring> Title;
til::property<winrt::hstring> Body;
};
struct WindowSizeChangedEventArgs : public WindowSizeChangedEventArgsT<WindowSizeChangedEventArgs>
{
public:

View File

@@ -160,6 +160,12 @@ namespace Microsoft.Terminal.Control
Int32 BufferRow { get; };
}
runtimeclass ShowNotificationEventArgs
{
String Title { get; };
String Body { get; };
}
runtimeclass WindowSizeChangedEventArgs
{
Int32 Width;

View File

@@ -328,6 +328,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_revokers.CompletionsChanged = _core.CompletionsChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleCompletionsChanged });
_revokers.RestartTerminalRequested = _core.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleRestartTerminalRequested });
_revokers.SearchMissingCommand = _core.SearchMissingCommand(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleSearchMissingCommand });
_revokers.ShowNotification = _core.ShowNotification(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleShowNotification });
_revokers.WindowSizeChanged = _core.WindowSizeChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleWindowSizeChanged });
_revokers.WriteToClipboard = _core.WriteToClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleWriteToClipboard });

View File

@@ -231,6 +231,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
BUBBLED_FORWARDED_TYPED_EVENT(RestartTerminalRequested, IInspectable, IInspectable);
BUBBLED_FORWARDED_TYPED_EVENT(WriteToClipboard, IInspectable, Control::WriteToClipboardEventArgs);
BUBBLED_FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs);
BUBBLED_FORWARDED_TYPED_EVENT(ShowNotification, IInspectable, Control::ShowNotificationEventArgs);
// clang-format on
@@ -469,6 +470,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
Control::ControlCore::CompletionsChanged_revoker CompletionsChanged;
Control::ControlCore::RestartTerminalRequested_revoker RestartTerminalRequested;
Control::ControlCore::SearchMissingCommand_revoker SearchMissingCommand;
Control::ControlCore::ShowNotification_revoker ShowNotification;
Control::ControlCore::RefreshQuickFixUI_revoker RefreshQuickFixUI;
Control::ControlCore::WindowSizeChanged_revoker WindowSizeChanged;

View File

@@ -81,6 +81,7 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, CharSentEventArgs> CharSent;
event Windows.Foundation.TypedEventHandler<Object, StringSentEventArgs> StringSent;
event Windows.Foundation.TypedEventHandler<Object, SearchMissingCommandEventArgs> SearchMissingCommand;
event Windows.Foundation.TypedEventHandler<Object, ShowNotificationEventArgs> ShowNotification;
Microsoft.UI.Xaml.Controls.CommandBarFlyout ContextMenu { get; };

View File

@@ -124,6 +124,7 @@ namespace Microsoft.Terminal.Core
Boolean AllowKittyKeyboardMode { get; };
Boolean AllowVtChecksumReport { get; };
Boolean AllowVtClipboardWrite { get; };
Boolean AllowOscNotifications { get; };
Boolean TrimBlockSelection { get; };
Boolean DetectURLs { get; };

View File

@@ -251,6 +251,7 @@ void Terminal::SetOptionalFeatures(winrt::Microsoft::Terminal::Core::ICoreSettin
auto features = til::enumset<ITermDispatch::OptionalFeature>{};
features.set(ITermDispatch::OptionalFeature::ChecksumReport, settings.AllowVtChecksumReport());
features.set(ITermDispatch::OptionalFeature::ClipboardWrite, settings.AllowVtClipboardWrite());
features.set(ITermDispatch::OptionalFeature::DesktopNotification, settings.AllowOscNotifications());
engine.Dispatch().SetOptionalFeatures(features);
}
@@ -1260,6 +1261,11 @@ void Microsoft::Terminal::Core::Terminal::SetSearchMissingCommandCallback(std::f
_pfnSearchMissingCommand.swap(pfn);
}
void Microsoft::Terminal::Core::Terminal::SetShowNotificationCallback(std::function<void(std::wstring_view, std::wstring_view)> pfn) noexcept
{
_pfnShowNotification.swap(pfn);
}
void Microsoft::Terminal::Core::Terminal::SetClearQuickFixCallback(std::function<void()> pfn) noexcept
{
_pfnClearQuickFix.swap(pfn);

View File

@@ -162,6 +162,8 @@ public:
void SearchMissingCommand(const std::wstring_view command) override;
void ShowNotification(const std::wstring_view title, const std::wstring_view body) override;
#pragma endregion
void ClearMark();
@@ -230,6 +232,7 @@ public:
void SetPlayMidiNoteCallback(std::function<void(const int, const int, const std::chrono::microseconds)> pfn) noexcept;
void CompletionsChangedCallback(std::function<void(std::wstring_view, unsigned int)> pfn) noexcept;
void SetSearchMissingCommandCallback(std::function<void(std::wstring_view, const til::CoordType)> pfn) noexcept;
void SetShowNotificationCallback(std::function<void(std::wstring_view, std::wstring_view)> pfn) noexcept;
void SetClearQuickFixCallback(std::function<void()> pfn) noexcept;
void SetWindowSizeChangedCallback(std::function<void(int32_t, int32_t)> pfn) noexcept;
void SetSearchHighlights(const std::vector<til::point_span>& highlights) noexcept;
@@ -338,6 +341,7 @@ private:
std::function<void(const int, const int, const std::chrono::microseconds)> _pfnPlayMidiNote;
std::function<void(std::wstring_view, unsigned int)> _pfnCompletionsChanged;
std::function<void(std::wstring_view, const til::CoordType)> _pfnSearchMissingCommand;
std::function<void(std::wstring_view, std::wstring_view)> _pfnShowNotification;
std::function<void()> _pfnClearQuickFix;
std::function<void(int32_t, int32_t)> _pfnWindowSizeChanged;

View File

@@ -364,6 +364,14 @@ void Terminal::SearchMissingCommand(const std::wstring_view command)
}
}
void Terminal::ShowNotification(const std::wstring_view title, const std::wstring_view body)
{
if (_pfnShowNotification)
{
_pfnShowNotification(title, body);
}
}
void Terminal::NotifyBufferRotation(const int delta)
{
// Update our selection, so it doesn't move as the buffer is cycled

View File

@@ -352,6 +352,7 @@ namespace winrt::Microsoft::Terminal::Settings
_AllowKittyKeyboardMode = profile.AllowKittyKeyboardMode();
_AllowVtChecksumReport = profile.AllowVtChecksumReport();
_AllowVtClipboardWrite = profile.AllowVtClipboardWrite();
_AllowOscNotifications = profile.AllowOscNotifications();
_PathTranslationStyle = profile.PathTranslationStyle();
}

View File

@@ -142,6 +142,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
OBSERVABLE_PROJECTED_SETTING(_profile, AllowKittyKeyboardMode);
OBSERVABLE_PROJECTED_SETTING(_profile, AllowVtChecksumReport);
OBSERVABLE_PROJECTED_SETTING(_profile, AllowVtClipboardWrite);
OBSERVABLE_PROJECTED_SETTING(_profile, AllowOscNotifications);
OBSERVABLE_PROJECTED_SETTING(_profile, AnswerbackMessage);
OBSERVABLE_PROJECTED_SETTING(_profile, RainbowSuggestions);
OBSERVABLE_PROJECTED_SETTING(_profile, PathTranslationStyle);

View File

@@ -140,5 +140,6 @@ namespace Microsoft.Terminal.Settings.Editor
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, RainbowSuggestions);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Microsoft.Terminal.Control.PathTranslationStyle, PathTranslationStyle);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AllowVtClipboardWrite);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AllowOscNotifications);
}
}

View File

@@ -81,6 +81,16 @@
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- Allow OSC 777 Desktop Notifications -->
<local:SettingContainer x:Name="AllowOscNotifications"
x:Uid="Profile_AllowOscNotifications"
ClearSettingValue="{x:Bind Profile.ClearAllowOscNotifications}"
HasSettingValue="{x:Bind Profile.HasAllowOscNotifications, Mode=OneWay}"
SettingOverrideSource="{x:Bind Profile.AllowOscNotificationsOverrideSource, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind Profile.AllowOscNotifications, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- Answerback Message -->
<local:SettingContainer x:Name="AnswerbackMessage"
x:Uid="Profile_AnswerbackMessage"

View File

@@ -573,6 +573,14 @@
<value>Allow OSC 52 (Manipulate Selection Data) to write to the clipboard</value>
<comment>{Locked="OSC 52"}{Locked="Manipulate Selection Data"}Header for a control to toggle support for applications to change the contents of the Windows system clipboard.</comment>
</data>
<data name="Profile_AllowOscNotifications.Header" xml:space="preserve">
<value>Allow OSC 777 (Desktop Notification) to show toast notifications</value>
<comment>{Locked="OSC 777"}{Locked="Desktop Notification"}Header for a control to toggle support for applications to display desktop toast notifications via the OSC 777 escape sequence.</comment>
</data>
<data name="Profile_AllowOscNotifications.HelpText" xml:space="preserve">
<value>When enabled, applications can send the OSC 777 escape sequence to trigger a desktop notification with a custom title and body.</value>
<comment>{Locked="OSC 777"}Help text for the OSC 777 desktop notification toggle.</comment>
</data>
<data name="Globals_AllowHeadless.Header" xml:space="preserve">
<value>Allow Windows Terminal to run in the background</value>
<comment>Header for a control to toggle support for Windows Terminal to run in the background.</comment>

View File

@@ -107,6 +107,7 @@ Author(s):
X(bool, AllowKittyKeyboardMode, "compatibility.kittyKeyboardMode", true) \
X(bool, AllowVtChecksumReport, "compatibility.allowDECRQCRA", false) \
X(bool, AllowVtClipboardWrite, "compatibility.allowOSC52", true) \
X(bool, AllowOscNotifications, "compatibility.allowOSC777", true) \
X(bool, AllowKeypadMode, "compatibility.allowDECNKM", false) \
X(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, "pathTranslationStyle", Microsoft::Terminal::Control::PathTranslationStyle::None)

View File

@@ -92,6 +92,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_PROFILE_SETTING(Boolean, AllowVtChecksumReport);
INHERITABLE_PROFILE_SETTING(Boolean, AllowKeypadMode);
INHERITABLE_PROFILE_SETTING(Boolean, AllowVtClipboardWrite);
INHERITABLE_PROFILE_SETTING(Boolean, AllowOscNotifications);
INHERITABLE_PROFILE_SETTING(Microsoft.Terminal.Control.PathTranslationStyle, PathTranslationStyle);
}

View File

@@ -353,6 +353,23 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
#endif
}
// Toast notification activations launch a new unelevated instance of the
// app with "__fromToast" as the sole command-line argument. It will always
// start a new instance of our exe.
//
// However, we're also able to just handle the .Activated event on the toast
// itself, so we don't care about this process we're spawning. So before we
// do _anything_ else, if we were created for a toast, just immediately
// bail.
const auto args = commandlineToArgArray(GetCommandLineW());
{
if (args.size() == 2 && args[1] == L"__fromToast")
{
TerminateProcess(GetCurrentProcess(), 0);
__assume(false);
}
}
// 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);
@@ -406,8 +423,6 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
}
}
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.

View File

@@ -56,7 +56,8 @@
X(bool, RepositionCursorWithMouse, false) \
X(bool, RainbowSuggestions) \
X(bool, AllowVtChecksumReport) \
X(bool, AllowVtClipboardWrite, true)
X(bool, AllowVtClipboardWrite, true) \
X(bool, AllowOscNotifications)
// --------------------------- Control Settings ---------------------------
// All of these settings are defined in IControlSettings.

View File

@@ -437,3 +437,7 @@ void ConhostInternalGetSet::SearchMissingCommand(std::wstring_view /*missingComm
{
// Not implemented for conhost.
}
void ConhostInternalGetSet::ShowNotification(std::wstring_view /*title*/, std::wstring_view /*body*/)
{
// Not implemented for conhost.
}

View File

@@ -72,6 +72,8 @@ public:
void SearchMissingCommand(std::wstring_view missingCommand) override;
void ShowNotification(std::wstring_view title, std::wstring_view body) override;
private:
Microsoft::Console::IIoProvider& _io;
};

View File

@@ -29,6 +29,7 @@ public:
{
ChecksumReport,
ClipboardWrite,
DesktopNotification,
};
#pragma warning(push)
@@ -164,6 +165,8 @@ public:
virtual void DoWTAction(const std::wstring_view string) = 0;
virtual void DoDesktopNotification(const std::wstring_view string) = 0;
virtual StringHandler DefineSixelImage(const VTInt macroParameter,
const DispatchTypes::SixelBackground backgroundSelect,
const VTParameter backgroundColor) = 0; // SIXEL

View File

@@ -90,5 +90,7 @@ namespace Microsoft::Console::VirtualTerminal
virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0;
virtual void SearchMissingCommand(const std::wstring_view command) = 0;
virtual void ShowNotification(const std::wstring_view title, const std::wstring_view body) = 0;
};
}

View File

@@ -3800,6 +3800,36 @@ void AdaptDispatch::DoWTAction(const std::wstring_view string)
}
}
// Method Description:
// - OSC 777 - Handles desktop notification requests.
// The format is: OSC 777;notify;title;body ST
// - Not actually used in conhost
// Arguments:
// - string: contains the parameters that define the notification
void AdaptDispatch::DoDesktopNotification(const std::wstring_view string)
{
if (!_optionalFeatures.test(OptionalFeature::DesktopNotification))
{
return;
}
const auto parts = Utils::SplitString(string, L';');
if (parts.size() < 1)
{
return;
}
const auto action = til::at(parts, 0);
if (action == L"notify")
{
const auto title = parts.size() >= 2 ? til::at(parts, 1) : std::wstring_view{};
const auto body = parts.size() >= 3 ? til::at(parts, 2) : std::wstring_view{};
_api.ShowNotification(title, body);
}
}
// Method Description:
// - SIXEL - Defines an image transmitted in sixel format via the returned
// StringHandler function.

View File

@@ -161,6 +161,8 @@ namespace Microsoft::Console::VirtualTerminal
void DoWTAction(const std::wstring_view string) override;
void DoDesktopNotification(const std::wstring_view string) override;
StringHandler DefineSixelImage(const VTInt macroParameter,
const DispatchTypes::SixelBackground backgroundSelect,
const VTParameter backgroundColor) override; // SIXEL

View File

@@ -151,6 +151,8 @@ public:
void DoWTAction(const std::wstring_view /*string*/) override {}
void DoDesktopNotification(const std::wstring_view /*string*/) override {}
StringHandler DefineSixelImage(const VTInt /*macroParameter*/,
const DispatchTypes::SixelBackground /*backgroundSelect*/,
const VTParameter /*backgroundColor*/) override { return nullptr; }; // SIXEL

View File

@@ -224,6 +224,11 @@ public:
Log::Comment(L"SearchMissingCommand MOCK called...");
}
void ShowNotification(const std::wstring_view /*title*/, const std::wstring_view /*body*/) override
{
Log::Comment(L"ShowNotification MOCK called...");
}
void PrepData()
{
PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter.

View File

@@ -900,6 +900,11 @@ bool OutputStateMachineEngine::ActionOscDispatch(const size_t parameter, const s
_dispatch->DoWTAction(string);
break;
}
case OscActionCodes::DesktopNotification:
{
_dispatch->DoDesktopNotification(string);
break;
}
default:
break;
}

View File

@@ -225,6 +225,7 @@ namespace Microsoft::Console::VirtualTerminal
ResetHighlightColor = 117,
FinalTermAction = 133,
VsCodeAction = 633,
DesktopNotification = 777,
ITerm2Action = 1337,
WTAction = 9001,
};

View File

@@ -417,8 +417,15 @@ function Invoke-CodeFormat() {
)
$clangFormatPath = & 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -find "**\x64\bin\clang-format.exe"
If ([String]::IsNullOrEmpty($clangFormatPath)) {
# try again with prerelease versions of Visual Studio,
# and just take the first
$clangFormatPath = & 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -prerelease -find "**\clang-format.exe" | Select-Object -First 1
}
If ([String]::IsNullOrEmpty($clangFormatPath)) {
Write-Error "No Visual Studio-supplied version of clang-format could be found."
return -1
}
$root = Find-OpenConsoleRoot

View File

@@ -2,4 +2,4 @@
rem run clang-format on c++ files
powershell -noprofile "import-module %OPENCON_TOOLS%\openconsole.psm1; Invoke-CodeFormat"
pwsh -noprofile "import-module %OPENCON_TOOLS%\openconsole.psm1; Invoke-CodeFormat"