Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,36 @@
}
]
},
"OutputNotificationStyle": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string",
"enum": [
"taskbar",
"audible",
"tab",
"notification"
]
}
},
{
"type": "string",
"enum": [
"taskbar",
"audible",
"tab",
"notification",
"all",
"none"
]
}
]
},
"BellSound": {
"default": "",
"description": "Sets the file location of the sound played when the application emits a BEL character. If the path is invalid no sound will be played. This property also accepts an array of sounds and the terminal will pick one at random.",
Expand All @@ -94,6 +124,14 @@
}
]
},
"AutoDetectRunningCommand": {
"type": "string",
"enum": [
"disabled",
"automatic",
"progress"
]
},
"BuiltinSuggestionSource": {
"type": "string",
"anyOf": [
Expand Down Expand Up @@ -3174,6 +3212,21 @@
"mingw"
],
"type": "string"
},
"notifyOnInactiveOutput": {
"default": "none",
"description": "Controls how the terminal notifies you when a background pane produces output. Supported values include `taskbar`, `audible`, `tab`, and `notification`. Can be set to true (equivalent to `tab`), false/none (disabled), a single string, or an array of strings.",
"$ref": "#/$defs/OutputNotificationStyle"
},
"notifyOnNextPrompt": {
"default": "none",
"description": "Controls how the terminal notifies you when a running command finishes and the shell returns to a prompt. Requires shell integration. Supported values include `taskbar`, `audible`, `tab`, and `notification`. Can be set to true (equivalent to `tab`), false/none (disabled), a single string, or an array of strings.",
"$ref": "#/$defs/OutputNotificationStyle"
},
"autoDetectRunningCommand": {
"default": "disabled",
"description": "Controls automatic detection of running commands via shell integration marks. When set to `automatic`, an indeterminate progress indicator is shown on the tab while a command is executing. When set to `progress`, the terminal will also attempt to detect progress percentages from command output. Requires shell integration.",
"$ref": "#/$defs/AutoDetectRunningCommand"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/BasicPaneEvents.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
104 changes: 104 additions & 0 deletions src/cascadia/TerminalApp/DesktopNotification.cpp
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
52 changes: 52 additions & 0 deletions src/cascadia/TerminalApp/DesktopNotification.h
Original file line number Diff line number Diff line change
@@ -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;
};
}
7 changes: 7 additions & 0 deletions src/cascadia/TerminalApp/IPaneContent.idl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ namespace TerminalApp
Boolean FlashTaskbar { get; };
};

runtimeclass NotificationEventArgs
{
Microsoft.Terminal.Control.OutputNotificationStyle Style { get; };
Boolean OnlyWhenInactive { get; };
};

interface IPaneContent
{
Windows.UI.Xaml.FrameworkElement GetRoot();
Expand All @@ -41,6 +47,7 @@ namespace TerminalApp

event Windows.Foundation.TypedEventHandler<Object, Object> ConnectionStateChanged;
event Windows.Foundation.TypedEventHandler<IPaneContent, BellEventArgs> BellRequested;
event Windows.Foundation.TypedEventHandler<IPaneContent, NotificationEventArgs> NotificationRequested;
event Windows.Foundation.TypedEventHandler<IPaneContent, Object> TitleChanged;
event Windows.Foundation.TypedEventHandler<IPaneContent, Object> TabColorChanged;
event Windows.Foundation.TypedEventHandler<IPaneContent, Object> TaskbarProgressChanged;
Expand Down
12 changes: 12 additions & 0 deletions src/cascadia/TerminalApp/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Loading
Loading