diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index b8e6c9ab401..45385fa4e67 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -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.", @@ -94,6 +124,14 @@ } ] }, + "AutoDetectRunningCommand": { + "type": "string", + "enum": [ + "disabled", + "automatic", + "progress" + ] + }, "BuiltinSuggestionSource": { "type": "string", "anyOf": [ @@ -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" } } }, diff --git a/src/cascadia/TerminalApp/BasicPaneEvents.h b/src/cascadia/TerminalApp/BasicPaneEvents.h index e82ff0f49b4..8d4f8ab8102 100644 --- a/src/cascadia/TerminalApp/BasicPaneEvents.h +++ b/src/cascadia/TerminalApp/BasicPaneEvents.h @@ -10,6 +10,7 @@ namespace winrt::TerminalApp::implementation til::typed_event<> ConnectionStateChanged; til::typed_event CloseRequested; til::typed_event BellRequested; + til::typed_event NotificationRequested; til::typed_event TitleChanged; til::typed_event TabColorChanged; til::typed_event TaskbarProgressChanged; diff --git a/src/cascadia/TerminalApp/DesktopNotification.cpp b/src/cascadia/TerminalApp/DesktopNotification.cpp new file mode 100644 index 00000000000..573dc5a461f --- /dev/null +++ b/src/cascadia/TerminalApp/DesktopNotification.cpp @@ -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 DesktopNotification::_lastNotificationTime{ 0 }; + + bool DesktopNotification::ShouldSendNotification() + { + FILETIME ft{}; + GetSystemTimeAsFileTime(&ft); + const auto now = (static_cast(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 activated) + { + try + { + if (!ShouldSendNotification()) + { + return; + } + + // Build the toast XML. We use a simple template with a title and body text. + // + // + // + // + // Title + // Message + // + // + // + auto toastXml = ToastNotificationManager::GetTemplateContent(ToastTemplateType::ToastText02); + auto textNodes = toastXml.GetElementsByTagName(L"text"); + + // First is the title + textNodes.Item(0).InnerText(args.Title); + // Second is the body + textNodes.Item(1).InnerText(args.Message); + + auto toastElement = toastXml.DocumentElement(); + + // When a toast is clicked, Windows launches a new instance of the app + // with the "launch" attribute as command-line arguments. We handle + // toast activation in-process via the Activated event below, so the + // new instance should do nothing. "__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(); + } + } +} diff --git a/src/cascadia/TerminalApp/DesktopNotification.h b/src/cascadia/TerminalApp/DesktopNotification.h new file mode 100644 index 00000000000..55cb228ba6c --- /dev/null +++ b/src/cascadia/TerminalApp/DesktopNotification.h @@ -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 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 _lastNotificationTime; + + // Minimum interval between notifications, in 100ns ticks (FILETIME units). + // 5 seconds = 5 * 10,000,000 + static constexpr int64_t MinNotificationIntervalTicks = 50'000'000LL; + }; +} diff --git a/src/cascadia/TerminalApp/IPaneContent.idl b/src/cascadia/TerminalApp/IPaneContent.idl index f4c6ce13957..17719ad24fb 100644 --- a/src/cascadia/TerminalApp/IPaneContent.idl +++ b/src/cascadia/TerminalApp/IPaneContent.idl @@ -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(); @@ -41,6 +47,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler ConnectionStateChanged; event Windows.Foundation.TypedEventHandler BellRequested; + event Windows.Foundation.TypedEventHandler NotificationRequested; event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler TabColorChanged; event Windows.Foundation.TypedEventHandler TaskbarProgressChanged; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 81ca7d51f83..3458d2dc350 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -923,4 +923,16 @@ An invalid regular expression was found. + + Windows Terminal + Title shown in desktop toast notifications for tab activity. + + + Tab "{0}" has new activity + {Locked="{0}"}Message shown in a desktop toast notification when a tab produces output. {0} is the tab title. + + + Tab "{0}" in {1} has new activity + {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. + diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 4bbf58e50ad..b8f64d44d02 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -123,6 +123,15 @@ namespace winrt::TerminalApp::implementation _bellIndicatorTimer.Stop(); } + // Method Description: + // - Called when the timer for the activity indicator in the tab header fires + // - Removes the activity indicator from the tab header + void Tab::_ActivityIndicatorTimerTick(const Windows::Foundation::IInspectable& /*sender*/, const Windows::Foundation::IInspectable& /*e*/) + { + ShowActivityIndicator(false); + _activityIndicatorTimer.Stop(); + } + // Method Description: // - Initializes a TabViewItem for this Tab instance. // Arguments: @@ -329,6 +338,11 @@ namespace winrt::TerminalApp::implementation { ShowBellIndicator(false); } + // When we gain focus, remove the activity indicator if it is active + if (_tabStatus.ActivityIndicator()) + { + ShowActivityIndicator(false); + } } } @@ -459,6 +473,29 @@ namespace winrt::TerminalApp::implementation _bellIndicatorTimer.Start(); } + void Tab::ShowActivityIndicator(const bool show) + { + ASSERT_UI_THREAD(); + + _tabStatus.ActivityIndicator(show); + } + + // Method Description: + // - Activates the timer for the activity indicator in the tab + // - Called if a notification was raised when the tab already has focus + void Tab::ActivateActivityIndicatorTimer() + { + ASSERT_UI_THREAD(); + + if (!_activityIndicatorTimer) + { + _activityIndicatorTimer.Interval(std::chrono::milliseconds(2000)); + _activityIndicatorTimer.Tick({ get_weak(), &Tab::_ActivityIndicatorTimerTick }); + } + + _activityIndicatorTimer.Start(); + } + // Method Description: // - Gets the title string of the last focused terminal control in our tree. // Returns the empty string if there is no such control. @@ -1068,6 +1105,7 @@ namespace winrt::TerminalApp::implementation dispatcher, til::throttled_func_options{ .delay = std::chrono::milliseconds{ 200 }, + .leading = true, .trailing = true, }, [weakThis]() { @@ -1161,6 +1199,63 @@ namespace winrt::TerminalApp::implementation } }); + events.NotificationRequested = content.NotificationRequested( + winrt::auto_revoke, + [dispatcher, weakThis](TerminalApp::IPaneContent sender, auto notificationArgs) -> safe_void_coroutine { + const auto weakThisCopy = weakThis; + co_await wil::resume_foreground(dispatcher); + if (const auto tab{ weakThisCopy.get() }) + { + // For NotifyOnInactiveOutput (OnlyWhenInactive=true), suppress + // ALL notification styles when the sender is the active pane. + // For NotifyOnNextPrompt (OnlyWhenInactive=false), always fire. + const auto activeContent = tab->GetActiveContent(); + const auto isActivePaneContent = activeContent && activeContent == sender; + + if (notificationArgs.OnlyWhenInactive() && isActivePaneContent && + tab->_focusState != WUX::FocusState::Unfocused) + { + co_return; + } + + const auto style = notificationArgs.Style(); + + if (WI_IsFlagSet(style, OutputNotificationStyle::Taskbar)) + { + // Flash the taskbar button + tab->TabRaiseVisualBell.raise(); + } + + if (WI_IsFlagSet(style, winrt::Microsoft::Terminal::Control::OutputNotificationStyle::Audible)) + { + // Play the notification sound via the pane's bell infrastructure + // (respects BellSound profile setting, uses MediaPlayer, etc.) + if (const auto termContent{ sender.try_as() }) + { + termContent.PlayNotificationSound(); + } + } + + if (WI_IsFlagSet(style, winrt::Microsoft::Terminal::Control::OutputNotificationStyle::Tab)) + { + tab->ShowActivityIndicator(true); + + if (tab->_focusState != WUX::FocusState::Unfocused) + { + tab->ActivateActivityIndicatorTimer(); + } + } + + if (WI_IsFlagSet(style, winrt::Microsoft::Terminal::Control::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). + tab->TabToastNotificationRequested.raise(tab->Title(), tab->TabViewIndex()); + } + } + }); + if (const auto& terminal{ content.try_as() }) { events.RestartTerminalRequested = terminal.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &Tab::_bubbleRestartTerminalRequested }); @@ -1393,6 +1488,11 @@ namespace winrt::TerminalApp::implementation { tab->ShowBellIndicator(false); } + // Also remove the activity indicator + if (tab->_tabStatus.ActivityIndicator()) + { + tab->ShowActivityIndicator(false); + } } }); diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index 65d4e574ede..b4e5d22875e 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -48,6 +48,9 @@ namespace winrt::TerminalApp::implementation void ShowBellIndicator(const bool show); void ActivateBellIndicatorTimer(); + void ShowActivityIndicator(const bool show); + void ActivateActivityIndicatorTimer(); + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; std::optional PreCalculateCanSplit(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, const float splitSize, @@ -121,6 +124,7 @@ namespace winrt::TerminalApp::implementation til::typed_event ActivePaneChanged; til::event> TabRaiseVisualBell; + til::event> TabToastNotificationRequested; til::typed_event TaskbarProgressChanged; // The TabViewIndex is the index this Tab object resides in TerminalPage's _tabs vector. @@ -176,6 +180,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; @@ -210,6 +215,9 @@ namespace winrt::TerminalApp::implementation SafeDispatcherTimer _bellIndicatorTimer; void _BellIndicatorTimerTick(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e); + SafeDispatcherTimer _activityIndicatorTimer; + void _ActivityIndicatorTimerTick(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e); + void _UpdateHeaderControlMaxWidth(); void _CreateContextMenu(); diff --git a/src/cascadia/TerminalApp/TabHeaderControl.xaml b/src/cascadia/TerminalApp/TabHeaderControl.xaml index d44d7a0c7d9..83fc04f7b86 100644 --- a/src/cascadia/TerminalApp/TabHeaderControl.xaml +++ b/src/cascadia/TerminalApp/TabHeaderControl.xaml @@ -32,6 +32,12 @@ FontSize="12" Glyph="" Visibility="{x:Bind TabStatus.BellIndicator, Mode=OneWay}" /> + @@ -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, uint32_t tabIndex) { + if (const auto page{ weakThis.get() }) + { + page->_SendDesktopNotification(title, tabIndex); + } + }); + auto tabViewItem = newTabImpl->TabViewItem(); _tabView.TabItems().InsertAt(insertPosition, tabViewItem); @@ -1185,4 +1195,56 @@ 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, uint32_t tabIndex) + { + // Build the notification message. + // Use the window name if available for context; otherwise just use the tab title. + // Use the raw WindowName (not WindowNameForDisplay) so we don't include + // the "" placeholder in the notification body. + const auto windowName = _WindowProperties ? _WindowProperties.WindowName() : winrt::hstring{}; + winrt::hstring message; + 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 }); + } + + implementation::DesktopNotificationArgs args; + args.Title = RS_(L"NotificationTitle"); + 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); + }); + } } diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index fe141dfa64f..b929f5e9b64 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -173,6 +173,7 @@ TerminalPaneContent.idl + @@ -286,6 +287,7 @@ TerminalPaneContent.idl + diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters index 661065d9ac3..37581283584 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters @@ -30,6 +30,7 @@ fzf + @@ -58,6 +59,7 @@ fzf + diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 4b48cc0e9d9..6c12e4835e6 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -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, uint32_t tabIndex); + #pragma region ActionHandlers // These are all defined in AppActionHandlers.cpp #define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action); diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.cpp b/src/cascadia/TerminalApp/TerminalPaneContent.cpp index b7000d9c2d9..3abf0436e90 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.cpp +++ b/src/cascadia/TerminalApp/TerminalPaneContent.cpp @@ -10,6 +10,7 @@ #include "../../types/inc/utils.hpp" #include "BellEventArgs.g.cpp" +#include "NotificationEventArgs.g.cpp" #include "TerminalPaneContent.g.cpp" using namespace winrt::Windows::Foundation; @@ -34,8 +35,11 @@ namespace winrt::TerminalApp::implementation { _controlEvents._ConnectionStateChanged = _control.ConnectionStateChanged(winrt::auto_revoke, { this, &TerminalPaneContent::_controlConnectionStateChangedHandler }); _controlEvents._WarningBell = _control.WarningBell(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlWarningBellHandler }); + _controlEvents._PromptStarted = _control.PromptStarted(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlPromptStartedHandler }); + _controlEvents._OutputStarted = _control.OutputStarted(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlOutputStartedHandler }); _controlEvents._CloseTerminalRequested = _control.CloseTerminalRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_closeTerminalRequestedHandler }); _controlEvents._RestartTerminalRequested = _control.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_restartTerminalRequestedHandler }); + _controlEvents._OutputIdle = _control.OutputIdle(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlOutputIdleHandler }); _controlEvents._TitleChanged = _control.TitleChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlTitleChanged }); _controlEvents._TabColorChanged = _control.TabColorChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlTabColorChanged }); @@ -261,6 +265,30 @@ namespace winrt::TerminalApp::implementation // has the 'visual' flag set // Arguments: // - + // Method Description: + // - Plays the notification sound using the profile's BellSound setting if + // configured; otherwise falls back to the system "Critical Stop" sound. + // Reused by the warning bell handler, NotifyOnNextPrompt, and + // NotifyOnInactiveOutput (called from Tab after the active-pane check). + void TerminalPaneContent::PlayNotificationSound() + { + if (_profile) + { + auto sounds{ _profile.BellSound() }; + if (sounds && sounds.Size() > 0) + { + winrt::hstring soundPath{ sounds.GetAt(rand() % sounds.Size()).Resolved() }; + winrt::Windows::Foundation::Uri uri{ soundPath }; + _playBellSound(uri); + } + else + { + const auto soundAlias = reinterpret_cast(SND_ALIAS_SYSTEMHAND); + PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY); + } + } + } + void TerminalPaneContent::_controlWarningBellHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*eventArgs*/) { @@ -271,19 +299,7 @@ namespace winrt::TerminalApp::implementation { if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible)) { - // Audible is set, play the sound - auto sounds{ _profile.BellSound() }; - if (sounds && sounds.Size() > 0) - { - winrt::hstring soundPath{ sounds.GetAt(rand() % sounds.Size()).Resolved() }; - winrt::Windows::Foundation::Uri uri{ soundPath }; - _playBellSound(uri); - } - else - { - const auto soundAlias = reinterpret_cast(SND_ALIAS_SYSTEMHAND); - PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY); - } + PlayNotificationSound(); } if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Window)) @@ -298,6 +314,107 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Returns the taskbar state, accounting for auto-detected running command progress. + // VT-set progress (OSC 9;4) takes precedence over auto-detected progress. + uint64_t TerminalPaneContent::TaskbarState() + { + const auto vtState = _control.TaskbarState(); + // VT-set progress takes precedence over auto-detected progress + if (vtState != 0) + { + return vtState; + } + // Auto-detected indeterminate progress (between command start and prompt return) + if (_autoDetectActive) + { + return 3; // TaskbarState::Indeterminate + } + return 0; + } + + // Method Description: + // - Returns the taskbar progress, accounting for auto-detected running command progress. + uint64_t TerminalPaneContent::TaskbarProgress() + { + const auto vtState = _control.TaskbarState(); + // VT-set progress takes precedence + if (vtState != 0) + { + return _control.TaskbarProgress(); + } + return 0; + } + + // Method Description: + // - Raised when a shell integration prompt mark (133;A) is received, indicating + // the command has finished and we're back at a prompt. + // - Checks NotifyOnNextPrompt setting and raises NotificationRequested. + // - If autoDetectRunningCommand is enabled, clears the indeterminate progress ring. + void TerminalPaneContent::_controlPromptStartedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*eventArgs*/) + { + if (_profile) + { + // Check NotifyOnNextPrompt setting and raise a notification. + // Pass OnlyWhenInactive=true so Tab suppresses notifications when + // this pane is active in a focused tab. + const auto notifyStyle = _profile.NotifyOnNextPrompt(); + if (static_cast(notifyStyle) != 0) + { + NotificationRequested.raise(*this, + *winrt::make_self(notifyStyle, true)); + } + + // If autoDetectRunningCommand is enabled, clear the progress ring + const auto autoDetect = _profile.AutoDetectRunningCommand(); + if (autoDetect != AutoDetectRunningCommand::Disabled && _autoDetectActive) + { + _autoDetectActive = false; + TaskbarProgressChanged.raise(*this, nullptr); + } + } + } + + // Method Description: + // - Raised when a shell integration command output mark (133;C) is received, + // indicating a command has started executing. + // - If autoDetectRunningCommand is enabled, shows an indeterminate progress ring. + void TerminalPaneContent::_controlOutputStartedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*eventArgs*/) + { + if (_profile) + { + const auto autoDetect = _profile.AutoDetectRunningCommand(); + if (autoDetect != AutoDetectRunningCommand::Disabled && !_autoDetectActive) + { + _autoDetectActive = true; + TaskbarProgressChanged.raise(*this, nullptr); + } + } + } + + // Method Description: + // - Raised when output stops being produced for ~100ms (debounced). + // Used to detect output in inactive panes for NotifyOnInactiveOutput. + // - Raises NotificationRequested so Tab can show activity indicators. + void TerminalPaneContent::_controlOutputIdleHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*eventArgs*/) + { + if (_profile) + { + const auto notifyStyle = _profile.NotifyOnInactiveOutput(); + if (static_cast(notifyStyle) != 0) + { + // Raise NotificationRequested so Tab can handle Taskbar/Tab/Audible flags. + // Pass OnlyWhenInactive=true so Tab skips notifications for the active pane. + // Note: Audible is handled by Tab (not here) so it can be gated on active state. + NotificationRequested.raise(*this, + *winrt::make_self(notifyStyle, true)); + } + } + } + safe_void_coroutine TerminalPaneContent::_playBellSound(winrt::Windows::Foundation::Uri uri) { auto weakThis{ get_weak() }; diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.h b/src/cascadia/TerminalApp/TerminalPaneContent.h index 0e828cdc1b8..7d4b67b51ec 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.h +++ b/src/cascadia/TerminalApp/TerminalPaneContent.h @@ -4,6 +4,7 @@ #pragma once #include "TerminalPaneContent.g.h" #include "BellEventArgs.g.h" +#include "NotificationEventArgs.g.h" #include "BasicPaneEvents.h" namespace winrt::TerminalApp::implementation @@ -19,6 +20,16 @@ namespace winrt::TerminalApp::implementation til::property FlashTaskbar; }; + struct NotificationEventArgs : public NotificationEventArgsT + { + public: + NotificationEventArgs(winrt::Microsoft::Terminal::Control::OutputNotificationStyle style, bool onlyWhenInactive = false) : + Style(style), OnlyWhenInactive(onlyWhenInactive) {} + + til::property Style; + til::property OnlyWhenInactive; + }; + struct TerminalPaneContent : TerminalPaneContentT, BasicPaneEvents { TerminalPaneContent(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile, @@ -36,6 +47,7 @@ namespace winrt::TerminalApp::implementation void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); void MarkAsDefterm(); + void PlayNotificationSound(); winrt::Microsoft::Terminal::Settings::Model::Profile GetProfile() const { @@ -43,8 +55,8 @@ namespace winrt::TerminalApp::implementation } winrt::hstring Title() { return _control.Title(); } - uint64_t TaskbarState() { return _control.TaskbarState(); } - uint64_t TaskbarProgress() { return _control.TaskbarProgress(); } + uint64_t TaskbarState(); + uint64_t TaskbarProgress(); bool ReadOnly() { return _control.ReadOnly(); } winrt::hstring Icon() const; Windows::Foundation::IReference TabColor() const noexcept; @@ -66,11 +78,15 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Media::Playback::MediaPlayer _bellPlayer{ nullptr }; bool _bellPlayerCreated{ false }; + std::atomic _autoDetectActive{ false }; struct ControlEventTokens { winrt::Microsoft::Terminal::Control::TermControl::ConnectionStateChanged_revoker _ConnectionStateChanged; winrt::Microsoft::Terminal::Control::TermControl::WarningBell_revoker _WarningBell; + winrt::Microsoft::Terminal::Control::TermControl::PromptStarted_revoker _PromptStarted; + winrt::Microsoft::Terminal::Control::TermControl::OutputStarted_revoker _OutputStarted; + winrt::Microsoft::Terminal::Control::TermControl::OutputIdle_revoker _OutputIdle; winrt::Microsoft::Terminal::Control::TermControl::CloseTerminalRequested_revoker _CloseTerminalRequested; winrt::Microsoft::Terminal::Control::TermControl::RestartTerminalRequested_revoker _RestartTerminalRequested; @@ -89,6 +105,12 @@ namespace winrt::TerminalApp::implementation safe_void_coroutine _controlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/); void _controlWarningBellHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& e); + void _controlPromptStartedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& e); + void _controlOutputStartedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& e); + void _controlOutputIdleHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& e); void _controlReadOnlyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& e); void _controlTitleChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.idl b/src/cascadia/TerminalApp/TerminalPaneContent.idl index 53dcf4c3379..36a1612c181 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.idl +++ b/src/cascadia/TerminalApp/TerminalPaneContent.idl @@ -11,6 +11,7 @@ namespace TerminalApp Microsoft.Terminal.Control.TermControl GetTermControl(); void MarkAsDefterm(); + void PlayNotificationSound(); Microsoft.Terminal.Settings.Model.Profile GetProfile(); diff --git a/src/cascadia/TerminalApp/TerminalTabStatus.h b/src/cascadia/TerminalApp/TerminalTabStatus.h index 8d1d015193b..81166b89ae5 100644 --- a/src/cascadia/TerminalApp/TerminalTabStatus.h +++ b/src/cascadia/TerminalApp/TerminalTabStatus.h @@ -17,6 +17,7 @@ namespace winrt::TerminalApp::implementation WINRT_OBSERVABLE_PROPERTY(bool, IsProgressRingActive, PropertyChanged.raise); WINRT_OBSERVABLE_PROPERTY(bool, IsProgressRingIndeterminate, PropertyChanged.raise); WINRT_OBSERVABLE_PROPERTY(bool, BellIndicator, PropertyChanged.raise); + WINRT_OBSERVABLE_PROPERTY(bool, ActivityIndicator, PropertyChanged.raise); WINRT_OBSERVABLE_PROPERTY(bool, IsReadOnlyActive, PropertyChanged.raise); WINRT_OBSERVABLE_PROPERTY(uint32_t, ProgressValue, PropertyChanged.raise); WINRT_OBSERVABLE_PROPERTY(bool, IsInputBroadcastActive, PropertyChanged.raise); diff --git a/src/cascadia/TerminalApp/TerminalTabStatus.idl b/src/cascadia/TerminalApp/TerminalTabStatus.idl index d5664c76c6c..f91199a632b 100644 --- a/src/cascadia/TerminalApp/TerminalTabStatus.idl +++ b/src/cascadia/TerminalApp/TerminalTabStatus.idl @@ -12,6 +12,7 @@ namespace TerminalApp Boolean IsProgressRingActive { get; set; }; Boolean IsProgressRingIndeterminate { get; set; }; Boolean BellIndicator { get; set; }; + Boolean ActivityIndicator { get; set; }; UInt32 ProgressValue { get; set; }; Boolean IsReadOnlyActive { get; set; }; Boolean IsInputBroadcastActive { get; set; }; diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index ee36db25e32..ba0dcfda260 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -54,6 +54,9 @@ #include #include +#include +#include + #include #include #include diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 86747d0f8ff..31b3d8981b1 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -118,6 +118,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnWarningBell = [this] { _terminalWarningBell(); }; _terminal->SetWarningBellCallback(pfnWarningBell); + auto pfnPromptStarted = [this] { _terminalPromptStarted(); }; + _terminal->SetPromptStartedCallback(pfnPromptStarted); + + auto pfnOutputStarted = [this] { _terminalOutputStarted(); }; + _terminal->SetOutputStartedCallback(pfnOutputStarted); + auto pfnTitleChanged = [this](auto&& PH1) { _terminalTitleChanged(std::forward(PH1)); }; _terminal->SetTitleChangedCallback(pfnTitleChanged); @@ -1590,9 +1596,37 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Since this can only ever be triggered by output from the connection, // then the Terminal already has the write lock when calling this // callback. + if (_restoring) + { + return; + } WarningBell.raise(*this, nullptr); } + void ControlCore::_terminalPromptStarted() + { + // Since this can only ever be triggered by output from the connection, + // then the Terminal already has the write lock when calling this + // callback. + if (_restoring) + { + return; + } + PromptStarted.raise(*this, nullptr); + } + + void ControlCore::_terminalOutputStarted() + { + // Since this can only ever be triggered by output from the connection, + // then the Terminal already has the write lock when calling this + // callback. + if (_restoring) + { + return; + } + OutputStarted.raise(*this, nullptr); + } + // Method Description: // - Called for the Terminal's TitleChanged callback. This will re-raise // a new winrt TypedEvent that can be listened to. @@ -1650,6 +1684,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::_terminalTaskbarProgressChanged() { + if (_restoring) + { + return; + } TaskbarProgressChanged.raise(*this, nullptr); } @@ -1667,6 +1705,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - duration - How long the note should be sustained (in microseconds). void ControlCore::_terminalPlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) { + if (_restoring) + { + return; + } // The UI thread might try to acquire the console lock from time to time. // --> Unlock it, so the UI doesn't hang while we're busy. const auto suspension = _terminal->SuspendLock(); @@ -1839,8 +1881,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SerializeMainBuffer(handle); } - void ControlCore::RestoreFromPath(const wchar_t* path) const + void ControlCore::RestoreFromPath(const wchar_t* path) { + // Suppress notifications (bells, prompt-returned, command-started, etc.) + // while we replay persisted buffer content. Without this, restoring a + // session fires the same events that live output would, producing + // unwanted audible bells, tab activity indicators, and taskbar flashes. + _restoring = true; + const auto restoreComplete = wil::scope_exit([&] { _restoring = false; }); + wil::unique_handle file{ CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr) }; // This block of code exists temporarily to fix buffer dumps that were diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 690f6fb465b..071f9dd4f89 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -154,7 +154,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void Close(); void PersistTo(HANDLE handle) const; - void RestoreFromPath(const wchar_t* path) const; + void RestoreFromPath(const wchar_t* path); void ClearQuickFix(); @@ -276,6 +276,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event TitleChanged; til::typed_event WriteToClipboard; til::typed_event<> WarningBell; + til::typed_event<> PromptStarted; + til::typed_event<> OutputStarted; til::typed_event<> TabColorChanged; til::typed_event<> BackgroundColorChanged; til::typed_event ScrollPositionChanged; @@ -324,6 +326,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation #pragma region TerminalCoreCallbacks void _terminalWarningBell(); + void _terminalPromptStarted(); + void _terminalOutputStarted(); void _terminalTitleChanged(std::wstring_view wstr); void _terminalScrollPositionChanged(const int viewTop, const int viewHeight, @@ -420,6 +424,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::optional _lastHoveredCell; uint16_t _lastHoveredId{ 0 }; std::atomic _initializedTerminal{ false }; + std::atomic _restoring{ false }; bool _isReadOnly{ false }; bool _closing{ false }; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index c09fdf2d1aa..9b09a6d7d6a 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -191,6 +191,8 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler WriteToClipboard; event Windows.Foundation.TypedEventHandler WarningBell; + event Windows.Foundation.TypedEventHandler PromptStarted; + event Windows.Foundation.TypedEventHandler OutputStarted; event Windows.Foundation.TypedEventHandler TabColorChanged; event Windows.Foundation.TypedEventHandler BackgroundColorChanged; event Windows.Foundation.TypedEventHandler TaskbarProgressChanged; diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index 19f0d70eca5..4dc8ec754eb 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -29,6 +29,23 @@ namespace Microsoft.Terminal.Control MinGW, }; + [flags] + enum OutputNotificationStyle + { + Taskbar = 0x1, + Audible = 0x2, + Tab = 0x4, + Notification = 0x8, + All = 0xffffffff + }; + + enum AutoDetectRunningCommand + { + Disabled, + Automatic, + Progress + }; + // Class Description: // TerminalSettings encapsulates all settings that control the // TermControl's behavior. In these settings there is both the entirety @@ -77,6 +94,10 @@ namespace Microsoft.Terminal.Control PathTranslationStyle PathTranslationStyle { get; }; + OutputNotificationStyle NotifyOnInactiveOutput { get; }; + OutputNotificationStyle NotifyOnNextPrompt { get; }; + AutoDetectRunningCommand AutoDetectRunningCommand { get; }; + // NOTE! When adding something here, make sure to update ControlProperties.h too! }; } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index dfbc7623c02..2f8a9f45305 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -393,6 +393,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // attached content before we set up the throttled func, and that'll A/V _revokers.coreScrollPositionChanged = _core.ScrollPositionChanged(winrt::auto_revoke, { get_weak(), &TermControl::_ScrollPositionChanged }); _revokers.WarningBell = _core.WarningBell(winrt::auto_revoke, { get_weak(), &TermControl::_coreWarningBell }); + _revokers.PromptStarted = _core.PromptStarted(winrt::auto_revoke, { get_weak(), &TermControl::_corePromptStarted }); + _revokers.OutputStarted = _core.OutputStarted(winrt::auto_revoke, { get_weak(), &TermControl::_coreOutputStarted }); static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast(1.0 / 30.0 * 1000000)); _autoScrollTimer.Interval(AutoScrollUpdateInterval); @@ -3699,6 +3701,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation _playWarningBell->Run(); } + void TermControl::_corePromptStarted(const IInspectable& /*sender*/, const IInspectable& /*args*/) + { + PromptStarted.raise(*this, nullptr); + } + + void TermControl::_coreOutputStarted(const IInspectable& /*sender*/, const IInspectable& /*args*/) + { + OutputStarted.raise(*this, nullptr); + } + hstring TermControl::ReadEntireBuffer() const { return _core.ReadEntireBuffer(); @@ -3820,6 +3832,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::_coreOutputIdle(const IInspectable& /*sender*/, const IInspectable& /*args*/) { _refreshSearch(); + OutputIdle.raise(*this, nullptr); } void TermControl::OwningHwnd(uint64_t owner) diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 3db3cec6730..4c18b64fb1f 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -212,6 +212,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event FocusFollowMouseRequested; til::typed_event Initialized; til::typed_event<> WarningBell; + til::typed_event<> PromptStarted; + til::typed_event<> OutputStarted; + til::typed_event<> OutputIdle; til::typed_event KeySent; til::typed_event CharSent; til::typed_event StringSent; @@ -425,6 +428,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); + void _corePromptStarted(const IInspectable& sender, const IInspectable& args); + void _coreOutputStarted(const IInspectable& sender, const IInspectable& args); void _coreOutputIdle(const IInspectable& sender, const IInspectable& args); winrt::Windows::Foundation::Point _toPosInDips(const Core::Point terminalCellPos); @@ -450,6 +455,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { Control::ControlCore::ScrollPositionChanged_revoker coreScrollPositionChanged; Control::ControlCore::WarningBell_revoker WarningBell; + Control::ControlCore::PromptStarted_revoker PromptStarted; + Control::ControlCore::OutputStarted_revoker OutputStarted; Control::ControlCore::RendererEnteredErrorState_revoker RendererEnteredErrorState; Control::ControlCore::BackgroundColorChanged_revoker BackgroundColorChanged; Control::ControlCore::FontSizeChanged_revoker FontSizeChanged; diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index fb994786321..15d714791e1 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -68,6 +68,9 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler SetTaskbarProgress; event Windows.Foundation.TypedEventHandler RaiseNotice; event Windows.Foundation.TypedEventHandler WarningBell; + event Windows.Foundation.TypedEventHandler PromptStarted; + event Windows.Foundation.TypedEventHandler OutputStarted; + event Windows.Foundation.TypedEventHandler OutputIdle; event Windows.Foundation.TypedEventHandler HidePointerCursor; event Windows.Foundation.TypedEventHandler RestorePointerCursor; event Windows.Foundation.TypedEventHandler TabColorChanged; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index e43180e9aaf..6ca87e44383 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -751,6 +751,11 @@ TerminalInput::OutputType Terminal::SendCharEvent(const wchar_t ch, const WORD s // This changed the scrollbar marks - raise a notification to update them _NotifyScrollEvent(); } + // regardless, start notify that we started command output + if (_pfnOutputStarted) + { + _pfnOutputStarted(); + } } } @@ -1265,6 +1270,16 @@ void Microsoft::Terminal::Core::Terminal::SetClearQuickFixCallback(std::function _pfnClearQuickFix.swap(pfn); } +void Terminal::SetPromptStartedCallback(std::function pfn) noexcept +{ + _pfnPromptStarted.swap(pfn); +} + +void Terminal::SetOutputStartedCallback(std::function pfn) noexcept +{ + _pfnOutputStarted.swap(pfn); +} + // Method Description: // - Stores the search highlighted regions in the terminal void Terminal::SetSearchHighlights(const std::vector& highlights) noexcept diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index e69b42e373b..013f89ec8a1 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -156,7 +156,7 @@ class Microsoft::Terminal::Core::Terminal final : bool IsVtInputEnabled() const noexcept override; void NotifyBufferRotation(const int delta) override; - void NotifyShellIntegrationMark() override; + void NotifyShellIntegrationMark(ShellIntegrationMark mark) override; void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; @@ -232,6 +232,8 @@ class Microsoft::Terminal::Core::Terminal final : void SetSearchMissingCommandCallback(std::function pfn) noexcept; void SetClearQuickFixCallback(std::function pfn) noexcept; void SetWindowSizeChangedCallback(std::function pfn) noexcept; + void SetPromptStartedCallback(std::function pfn) noexcept; + void SetOutputStartedCallback(std::function pfn) noexcept; void SetSearchHighlights(const std::vector& highlights) noexcept; void SetSearchHighlightFocused(size_t focusedIdx) noexcept; void ScrollToSearchHighlight(til::CoordType searchScrollOffset); @@ -340,6 +342,8 @@ class Microsoft::Terminal::Core::Terminal final : std::function _pfnSearchMissingCommand; std::function _pfnClearQuickFix; std::function _pfnWindowSizeChanged; + std::function _pfnPromptStarted; + std::function _pfnOutputStarted; RenderSettings _renderSettings; std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index e87dc765f24..fc9e5e32407 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -400,8 +400,26 @@ void Terminal::NotifyBufferRotation(const int delta) } } -void Terminal::NotifyShellIntegrationMark() +void Terminal::NotifyShellIntegrationMark(ShellIntegrationMark mark) { // Notify the scrollbar that marks have been added so it can refresh the mark indicators _NotifyScrollEvent(); + + switch (mark) + { + case ShellIntegrationMark::Prompt: + if (_pfnPromptStarted) + { + _pfnPromptStarted(); + } + break; + case ShellIntegrationMark::Output: + if (_pfnOutputStarted) + { + _pfnOutputStarted(); + } + break; + default: + break; + } } diff --git a/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp b/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp index 71f8695e2a9..72d22d93ef7 100644 --- a/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp @@ -353,6 +353,10 @@ namespace winrt::Microsoft::Terminal::Settings _AllowVtChecksumReport = profile.AllowVtChecksumReport(); _AllowVtClipboardWrite = profile.AllowVtClipboardWrite(); _PathTranslationStyle = profile.PathTranslationStyle(); + + _NotifyOnInactiveOutput = profile.NotifyOnInactiveOutput(); + _NotifyOnNextPrompt = profile.NotifyOnNextPrompt(); + _AutoDetectRunningCommand = profile.AutoDetectRunningCommand(); } // Method Description: diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp index 661d077b7e8..64328902f8c 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp @@ -40,6 +40,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation INITIALIZE_BINDABLE_ENUM_SETTING_REVERSE_ORDER(CloseOnExitMode, CloseOnExitMode, winrt::Microsoft::Terminal::Settings::Model::CloseOnExitMode, L"Profile_CloseOnExit", L"Content"); INITIALIZE_BINDABLE_ENUM_SETTING(ScrollState, ScrollbarState, winrt::Microsoft::Terminal::Control::ScrollbarState, L"Profile_ScrollbarVisibility", L"Content"); INITIALIZE_BINDABLE_ENUM_SETTING(PathTranslationStyle, PathTranslationStyle, winrt::Microsoft::Terminal::Control::PathTranslationStyle, L"Profile_PathTranslationStyle", L"Content"); + INITIALIZE_BINDABLE_ENUM_SETTING(AutoDetectRunningCommand, AutoDetectRunningCommand, winrt::Microsoft::Terminal::Control::AutoDetectRunningCommand, L"Profile_AutoDetectRunningCommand", L"Content"); _InitializeCurrentBellSounds(); @@ -640,6 +641,170 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation BellStyle(currentStyle); } + // ===================== NotifyOnInactiveOutput ===================== + + hstring ProfileViewModel::NotifyOnInactiveOutputPreview() const + { + using Ons = Control::OutputNotificationStyle; + const auto style = NotifyOnInactiveOutput(); + if (WI_AreAllFlagsSet(style, Ons::Taskbar | Ons::Audible | Ons::Tab | Ons::Notification)) + { + return RS_(L"Profile_OutputNotificationStyleAll/Content"); + } + else if (style == static_cast(0)) + { + return RS_(L"Profile_OutputNotificationStyleNone/Content"); + } + + std::vector resultList; + resultList.reserve(4); + if (WI_IsFlagSet(style, Ons::Taskbar)) + { + resultList.emplace_back(RS_(L"Profile_OutputNotificationStyleTaskbar/Content")); + } + if (WI_IsFlagSet(style, Ons::Audible)) + { + resultList.emplace_back(RS_(L"Profile_OutputNotificationStyleAudible/Content")); + } + if (WI_IsFlagSet(style, Ons::Tab)) + { + resultList.emplace_back(RS_(L"Profile_OutputNotificationStyleTab/Content")); + } + if (WI_IsFlagSet(style, Ons::Notification)) + { + resultList.emplace_back(RS_(L"Profile_OutputNotificationStyleNotification/Content")); + } + + hstring result{}; + for (auto&& entry : resultList) + { + if (result.empty()) + { + result = entry; + } + else + { + result = result + L", " + entry; + } + } + return result; + } + + bool ProfileViewModel::IsNotifyOnInactiveOutputFlagSet(const uint32_t flag) + { + return (WI_EnumValue(NotifyOnInactiveOutput()) & flag) == flag; + } + + void ProfileViewModel::SetNotifyOnInactiveOutputTaskbar(winrt::Windows::Foundation::IReference on) + { + auto currentStyle = NotifyOnInactiveOutput(); + WI_UpdateFlag(currentStyle, Control::OutputNotificationStyle::Taskbar, winrt::unbox_value(on)); + NotifyOnInactiveOutput(currentStyle); + } + + void ProfileViewModel::SetNotifyOnInactiveOutputAudible(winrt::Windows::Foundation::IReference on) + { + auto currentStyle = NotifyOnInactiveOutput(); + WI_UpdateFlag(currentStyle, Control::OutputNotificationStyle::Audible, winrt::unbox_value(on)); + NotifyOnInactiveOutput(currentStyle); + } + + void ProfileViewModel::SetNotifyOnInactiveOutputTab(winrt::Windows::Foundation::IReference on) + { + auto currentStyle = NotifyOnInactiveOutput(); + WI_UpdateFlag(currentStyle, Control::OutputNotificationStyle::Tab, winrt::unbox_value(on)); + NotifyOnInactiveOutput(currentStyle); + } + + void ProfileViewModel::SetNotifyOnInactiveOutputNotification(winrt::Windows::Foundation::IReference on) + { + auto currentStyle = NotifyOnInactiveOutput(); + WI_UpdateFlag(currentStyle, Control::OutputNotificationStyle::Notification, winrt::unbox_value(on)); + NotifyOnInactiveOutput(currentStyle); + } + + // ===================== NotifyOnNextPrompt ===================== + + hstring ProfileViewModel::NotifyOnNextPromptPreview() const + { + using Ons = Control::OutputNotificationStyle; + const auto style = NotifyOnNextPrompt(); + if (WI_AreAllFlagsSet(style, Ons::Taskbar | Ons::Audible | Ons::Tab | Ons::Notification)) + { + return RS_(L"Profile_OutputNotificationStyleAll/Content"); + } + else if (style == static_cast(0)) + { + return RS_(L"Profile_OutputNotificationStyleNone/Content"); + } + + std::vector resultList; + resultList.reserve(4); + if (WI_IsFlagSet(style, Ons::Taskbar)) + { + resultList.emplace_back(RS_(L"Profile_OutputNotificationStyleTaskbar/Content")); + } + if (WI_IsFlagSet(style, Ons::Audible)) + { + resultList.emplace_back(RS_(L"Profile_OutputNotificationStyleAudible/Content")); + } + if (WI_IsFlagSet(style, Ons::Tab)) + { + resultList.emplace_back(RS_(L"Profile_OutputNotificationStyleTab/Content")); + } + if (WI_IsFlagSet(style, Ons::Notification)) + { + resultList.emplace_back(RS_(L"Profile_OutputNotificationStyleNotification/Content")); + } + + hstring result{}; + for (auto&& entry : resultList) + { + if (result.empty()) + { + result = entry; + } + else + { + result = result + L", " + entry; + } + } + return result; + } + + bool ProfileViewModel::IsNotifyOnNextPromptFlagSet(const uint32_t flag) + { + return (WI_EnumValue(NotifyOnNextPrompt()) & flag) == flag; + } + + void ProfileViewModel::SetNotifyOnNextPromptTaskbar(winrt::Windows::Foundation::IReference on) + { + auto currentStyle = NotifyOnNextPrompt(); + WI_UpdateFlag(currentStyle, Control::OutputNotificationStyle::Taskbar, winrt::unbox_value(on)); + NotifyOnNextPrompt(currentStyle); + } + + void ProfileViewModel::SetNotifyOnNextPromptAudible(winrt::Windows::Foundation::IReference on) + { + auto currentStyle = NotifyOnNextPrompt(); + WI_UpdateFlag(currentStyle, Control::OutputNotificationStyle::Audible, winrt::unbox_value(on)); + NotifyOnNextPrompt(currentStyle); + } + + void ProfileViewModel::SetNotifyOnNextPromptTab(winrt::Windows::Foundation::IReference on) + { + auto currentStyle = NotifyOnNextPrompt(); + WI_UpdateFlag(currentStyle, Control::OutputNotificationStyle::Tab, winrt::unbox_value(on)); + NotifyOnNextPrompt(currentStyle); + } + + void ProfileViewModel::SetNotifyOnNextPromptNotification(winrt::Windows::Foundation::IReference on) + { + auto currentStyle = NotifyOnNextPrompt(); + WI_UpdateFlag(currentStyle, Control::OutputNotificationStyle::Notification, winrt::unbox_value(on)); + NotifyOnNextPrompt(currentStyle); + } + // Method Description: // - Construct _CurrentBellSounds by importing the _inherited_ value from the model // - Adds a PropertyChanged handler to each BellSoundViewModel to propagate changes to the model diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h index f120ed74673..09443ac67bd 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -47,6 +47,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void SetBellStyleWindow(winrt::Windows::Foundation::IReference on); void SetBellStyleTaskbar(winrt::Windows::Foundation::IReference on); + // notify on inactive output bits + hstring NotifyOnInactiveOutputPreview() const; + bool IsNotifyOnInactiveOutputFlagSet(const uint32_t flag); + void SetNotifyOnInactiveOutputTaskbar(winrt::Windows::Foundation::IReference on); + void SetNotifyOnInactiveOutputAudible(winrt::Windows::Foundation::IReference on); + void SetNotifyOnInactiveOutputTab(winrt::Windows::Foundation::IReference on); + void SetNotifyOnInactiveOutputNotification(winrt::Windows::Foundation::IReference on); + + // notify on next prompt bits + hstring NotifyOnNextPromptPreview() const; + bool IsNotifyOnNextPromptFlagSet(const uint32_t flag); + void SetNotifyOnNextPromptTaskbar(winrt::Windows::Foundation::IReference on); + void SetNotifyOnNextPromptAudible(winrt::Windows::Foundation::IReference on); + void SetNotifyOnNextPromptTab(winrt::Windows::Foundation::IReference on); + void SetNotifyOnNextPromptNotification(winrt::Windows::Foundation::IReference on); + hstring BellSoundPreview(); void RequestAddBellSound(hstring path); void RequestDeleteBellSound(const Editor::BellSoundViewModel& vm); @@ -146,12 +162,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation OBSERVABLE_PROJECTED_SETTING(_profile, RainbowSuggestions); OBSERVABLE_PROJECTED_SETTING(_profile, PathTranslationStyle); + OBSERVABLE_PROJECTED_SETTING(_profile, NotifyOnInactiveOutput); + OBSERVABLE_PROJECTED_SETTING(_profile, NotifyOnNextPrompt); + OBSERVABLE_PROJECTED_SETTING(_profile, AutoDetectRunningCommand); + WINRT_PROPERTY(bool, IsBaseLayer, false); WINRT_PROPERTY(bool, FocusDeleteButton, false); GETSET_BINDABLE_ENUM_SETTING(AntiAliasingMode, Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode); GETSET_BINDABLE_ENUM_SETTING(CloseOnExitMode, Microsoft::Terminal::Settings::Model::CloseOnExitMode, CloseOnExit); GETSET_BINDABLE_ENUM_SETTING(ScrollState, Microsoft::Terminal::Control::ScrollbarState, ScrollState); GETSET_BINDABLE_ENUM_SETTING(PathTranslationStyle, Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle); + GETSET_BINDABLE_ENUM_SETTING(AutoDetectRunningCommand, Microsoft::Terminal::Control::AutoDetectRunningCommand, AutoDetectRunningCommand); private: Model::Profile _profile; diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl index 1fb13853a64..7298ec00085 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl @@ -50,6 +50,23 @@ namespace Microsoft.Terminal.Settings.Editor void SetBellStyleWindow(Windows.Foundation.IReference on); void SetBellStyleTaskbar(Windows.Foundation.IReference on); + String NotifyOnInactiveOutputPreview { get; }; + Boolean IsNotifyOnInactiveOutputFlagSet(UInt32 flag); + void SetNotifyOnInactiveOutputTaskbar(Windows.Foundation.IReference on); + void SetNotifyOnInactiveOutputAudible(Windows.Foundation.IReference on); + void SetNotifyOnInactiveOutputTab(Windows.Foundation.IReference on); + void SetNotifyOnInactiveOutputNotification(Windows.Foundation.IReference on); + + String NotifyOnNextPromptPreview { get; }; + Boolean IsNotifyOnNextPromptFlagSet(UInt32 flag); + void SetNotifyOnNextPromptTaskbar(Windows.Foundation.IReference on); + void SetNotifyOnNextPromptAudible(Windows.Foundation.IReference on); + void SetNotifyOnNextPromptTab(Windows.Foundation.IReference on); + void SetNotifyOnNextPromptNotification(Windows.Foundation.IReference on); + + IInspectable CurrentAutoDetectRunningCommand; + Windows.Foundation.Collections.IObservableVector AutoDetectRunningCommandList { get; }; + String BellSoundPreview { get; }; Windows.Foundation.Collections.IObservableVector CurrentBellSounds { get; }; void RequestAddBellSound(String path); @@ -140,5 +157,8 @@ 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(Microsoft.Terminal.Control.OutputNotificationStyle, NotifyOnInactiveOutput); + OBSERVABLE_PROJECTED_PROFILE_SETTING(Microsoft.Terminal.Control.OutputNotificationStyle, NotifyOnNextPrompt); + OBSERVABLE_PROJECTED_PROFILE_SETTING(Microsoft.Terminal.Control.AutoDetectRunningCommand, AutoDetectRunningCommand); } } diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml index 3f243c22bca..06d635f528d 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml @@ -196,6 +196,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Type to filter icons Placeholder text for a text box to filter and select an icon. + + + Flash taskbar + An option to choose from for a notification style setting. When selected, the taskbar is flashed. + + + Audible + An option to choose from for a notification style setting. When selected, an audible cue is played. + + + Tab indicator + An option to choose from for a notification style setting. When selected, an activity indicator is shown on the tab. + + + Desktop notification + An option to choose from for a notification style setting. When selected, a desktop toast notification is sent. + + + All + An option label for notification style. Shown when all notification styles are enabled. + + + None + An option label for notification style. Shown when no notification styles are enabled. + + + + Notify on inactive output + Name for a control to select how the app notifies the user when an inactive pane produces output. + + + Notify on inactive output + Header for a control to select how the app notifies the user when an inactive pane produces output. + + + Controls how you are notified when a background pane produces new output. + A description for what the "notify on inactive output" setting does. + + + + Notify on next prompt + Name for a control to select how the app notifies the user when a command finishes and the shell returns to a prompt. + + + Notify on next prompt + Header for a control to select how the app notifies the user when a command finishes and the shell returns to a prompt. + + + Controls how you are notified when a running command finishes and the shell returns to a prompt. Requires shell integration. + A description for what the "notify on next prompt" setting does. + + + + Auto-detect running command + Name for a control to select how the app detects and indicates a running command. + + + Auto-detect running command + Header for a control to select how the app detects and indicates a running command. + + + Controls whether and how the terminal automatically shows progress while a command is running. Requires shell integration. + A description for what the "auto-detect running command" setting does. + + + Disabled + An option to choose from for the "auto-detect running command" setting. When selected, no auto-detection occurs. + + + Automatic + An option to choose from for the "auto-detect running command" setting. When selected, an indeterminate progress ring is shown while a command is running. + + + Progress + An option to choose from for the "auto-detect running command" setting. When selected, the terminal attempts to detect progress percentage from command output. + + + Disabled + An option to choose from for the "auto-detect running command" setting. When selected, no auto-detection occurs. + + + Automatic + An option to choose from for the "auto-detect running command" setting. When selected, an indeterminate progress ring is shown while a command is running. + + + Progress + An option to choose from for the "auto-detect running command" setting. When selected, the terminal attempts to detect progress percentage from command output. + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp index de1bf5185da..d4e45adb969 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp @@ -53,6 +53,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation DEFINE_ENUM_MAP(Microsoft::Terminal::Settings::Model::IntenseStyle, IntenseTextStyle); DEFINE_ENUM_MAP(Microsoft::Terminal::Core::AdjustTextMode, AdjustIndistinguishableColors); DEFINE_ENUM_MAP(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle); + DEFINE_ENUM_MAP(Microsoft::Terminal::Control::AutoDetectRunningCommand, AutoDetectRunningCommand); // Actions DEFINE_ENUM_MAP(Microsoft::Terminal::Settings::Model::ResizeDirection, ResizeDirection); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.h b/src/cascadia/TerminalSettingsModel/EnumMappings.h index 160c9a11b1f..545ac6adca8 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.h +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.h @@ -51,6 +51,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::Windows::Foundation::Collections::IMap IntenseTextStyle(); static winrt::Windows::Foundation::Collections::IMap AdjustIndistinguishableColors(); static winrt::Windows::Foundation::Collections::IMap PathTranslationStyle(); + static winrt::Windows::Foundation::Collections::IMap AutoDetectRunningCommand(); // Actions static winrt::Windows::Foundation::Collections::IMap ResizeDirection(); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.idl b/src/cascadia/TerminalSettingsModel/EnumMappings.idl index 128260a507a..4749ab7c778 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.idl +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.idl @@ -33,6 +33,7 @@ namespace Microsoft.Terminal.Settings.Model static Windows.Foundation.Collections.IMap FontWeight { get; }; static Windows.Foundation.Collections.IMap IntenseTextStyle { get; }; static Windows.Foundation.Collections.IMap PathTranslationStyle { get; }; + static Windows.Foundation.Collections.IMap AutoDetectRunningCommand { get; }; // Actions static Windows.Foundation.Collections.IMap ResizeDirection { get; }; diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 4f99bda187a..d761177d1ce 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -79,36 +79,39 @@ Author(s): // * TerminalSettings.cpp: TerminalSettings::_ApplyProfileSettings // * IControlSettings.idl or ICoreSettings.idl // * ControlProperties.h -#define MTSM_PROFILE_SETTINGS(X) \ - X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \ - X(bool, SnapOnInput, "snapOnInput", true) \ - X(bool, AltGrAliasing, "altGrAliasing", true) \ - X(hstring, AnswerbackMessage, "answerbackMessage") \ - X(hstring, Commandline, "commandline", L"%SystemRoot%\\System32\\cmd.exe") \ - X(Microsoft::Terminal::Control::ScrollbarState, ScrollState, "scrollbarState", Microsoft::Terminal::Control::ScrollbarState::Visible) \ - X(Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, "antialiasingMode", Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \ - X(hstring, StartingDirectory, "startingDirectory") \ - X(IMediaResource, Icon, "icon", implementation::MediaResource::FromString(L"\uE756")) \ - X(bool, SuppressApplicationTitle, "suppressApplicationTitle", false) \ - X(guid, ConnectionType, "connectionType") \ - X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic) \ - X(hstring, TabTitle, "tabTitle") \ - X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \ - X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \ - X(bool, RightClickContextMenu, "rightClickContextMenu", false) \ - X(Windows::Foundation::Collections::IVector, BellSound, "bellSound", nullptr) \ - X(bool, Elevate, "elevate", false) \ - X(bool, AutoMarkPrompts, "autoMarkPrompts", true) \ - X(bool, ShowMarks, "showMarksOnScrollbar", false) \ - X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false) \ - X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \ - X(bool, RainbowSuggestions, "experimental.rainbowSuggestions", false) \ - X(bool, ForceVTInput, "compatibility.input.forceVT", false) \ - X(bool, AllowKittyKeyboardMode, "compatibility.kittyKeyboardMode", true) \ - X(bool, AllowVtChecksumReport, "compatibility.allowDECRQCRA", false) \ - X(bool, AllowVtClipboardWrite, "compatibility.allowOSC52", true) \ - X(bool, AllowKeypadMode, "compatibility.allowDECNKM", false) \ - X(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, "pathTranslationStyle", Microsoft::Terminal::Control::PathTranslationStyle::None) +#define MTSM_PROFILE_SETTINGS(X) \ + X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \ + X(bool, SnapOnInput, "snapOnInput", true) \ + X(bool, AltGrAliasing, "altGrAliasing", true) \ + X(hstring, AnswerbackMessage, "answerbackMessage") \ + X(hstring, Commandline, "commandline", L"%SystemRoot%\\System32\\cmd.exe") \ + X(Microsoft::Terminal::Control::ScrollbarState, ScrollState, "scrollbarState", Microsoft::Terminal::Control::ScrollbarState::Visible) \ + X(Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, "antialiasingMode", Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \ + X(hstring, StartingDirectory, "startingDirectory") \ + X(IMediaResource, Icon, "icon", implementation::MediaResource::FromString(L"\uE756")) \ + X(bool, SuppressApplicationTitle, "suppressApplicationTitle", false) \ + X(guid, ConnectionType, "connectionType") \ + X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic) \ + X(hstring, TabTitle, "tabTitle") \ + X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \ + X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \ + X(bool, RightClickContextMenu, "rightClickContextMenu", false) \ + X(Windows::Foundation::Collections::IVector, BellSound, "bellSound", nullptr) \ + X(bool, Elevate, "elevate", false) \ + X(bool, AutoMarkPrompts, "autoMarkPrompts", true) \ + X(bool, ShowMarks, "showMarksOnScrollbar", false) \ + X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false) \ + X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \ + X(bool, RainbowSuggestions, "experimental.rainbowSuggestions", false) \ + X(bool, ForceVTInput, "compatibility.input.forceVT", false) \ + X(bool, AllowKittyKeyboardMode, "compatibility.kittyKeyboardMode", true) \ + X(bool, AllowVtChecksumReport, "compatibility.allowDECRQCRA", false) \ + X(bool, AllowVtClipboardWrite, "compatibility.allowOSC52", true) \ + X(bool, AllowKeypadMode, "compatibility.allowDECNKM", false) \ + X(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, "pathTranslationStyle", Microsoft::Terminal::Control::PathTranslationStyle::None) \ + X(Microsoft::Terminal::Control::OutputNotificationStyle, NotifyOnInactiveOutput, "notifyOnInactiveOutput", Microsoft::Terminal::Control::OutputNotificationStyle{ 0 }) \ + X(Microsoft::Terminal::Control::OutputNotificationStyle, NotifyOnNextPrompt, "notifyOnNextPrompt", Microsoft::Terminal::Control::OutputNotificationStyle{ 0 }) \ + X(Microsoft::Terminal::Control::AutoDetectRunningCommand, AutoDetectRunningCommand, "autoDetectRunningCommand", Microsoft::Terminal::Control::AutoDetectRunningCommand::Disabled) // Intentionally omitted Profile settings: // * Name diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 65de99b9135..844a8c0a641 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -94,5 +94,9 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(Boolean, AllowVtClipboardWrite); INHERITABLE_PROFILE_SETTING(Microsoft.Terminal.Control.PathTranslationStyle, PathTranslationStyle); + + INHERITABLE_PROFILE_SETTING(Microsoft.Terminal.Control.OutputNotificationStyle, NotifyOnInactiveOutput); + INHERITABLE_PROFILE_SETTING(Microsoft.Terminal.Control.OutputNotificationStyle, NotifyOnNextPrompt); + INHERITABLE_PROFILE_SETTING(Microsoft.Terminal.Control.AutoDetectRunningCommand, AutoDetectRunningCommand); } } diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 9c071945a29..d7d291f2acb 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -119,6 +119,46 @@ JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::BellStyle) } }; +JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Control::OutputNotificationStyle) +{ + static constexpr std::array mappings = { + pair_type{ "none", AllClear }, + pair_type{ "taskbar", ValueType::Taskbar }, + pair_type{ "audible", ValueType::Audible }, + pair_type{ "tab", ValueType::Tab }, + pair_type{ "notification", ValueType::Notification }, + pair_type{ "all", AllSet }, + }; + + auto FromJson(const Json::Value& json) + { + if (json.isBool()) + { + return json.asBool() ? ValueType::Tab : AllClear; + } + return BaseFlagMapper::FromJson(json); + } + + bool CanConvert(const Json::Value& json) + { + return BaseFlagMapper::CanConvert(json) || json.isBool(); + } + + Json::Value ToJson(const ::winrt::Microsoft::Terminal::Control::OutputNotificationStyle& style) + { + return BaseFlagMapper::ToJson(style); + } +}; + +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::AutoDetectRunningCommand) +{ + JSON_MAPPINGS(3) = { + pair_type{ "disabled", ValueType::Disabled }, + pair_type{ "automatic", ValueType::Automatic }, + pair_type{ "progress", ValueType::Progress }, + }; +}; + JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::ConvergedAlignment) { // reduce repetition diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 07e96fcc2e5..ab95bda12e6 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -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. diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h index 91ae711786c..9ed02f4e095 100644 --- a/src/cascadia/inc/ControlProperties.h +++ b/src/cascadia/inc/ControlProperties.h @@ -60,31 +60,34 @@ // --------------------------- Control Settings --------------------------- // All of these settings are defined in IControlSettings. -#define CONTROL_SETTINGS(X) \ - X(winrt::guid, SessionId) \ - X(bool, EnableUnfocusedAcrylic, false) \ - X(winrt::hstring, Padding, DEFAULT_PADDING) \ - X(winrt::hstring, FontFace, L"Consolas") \ - X(float, FontSize, DEFAULT_FONT_SIZE) \ - X(winrt::Windows::UI::Text::FontWeight, FontWeight) \ - X(IFontFeatureMap, FontFeatures) \ - X(IFontAxesMap, FontAxes) \ - X(bool, EnableBuiltinGlyphs, true) \ - X(bool, EnableColorGlyphs, true) \ - X(winrt::hstring, CellWidth) \ - X(winrt::hstring, CellHeight) \ - X(winrt::hstring, Commandline) \ - X(winrt::hstring, StartingDirectory) \ - X(winrt::Microsoft::Terminal::Control::ScrollbarState, ScrollState, winrt::Microsoft::Terminal::Control::ScrollbarState::Visible) \ - X(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \ - X(winrt::Microsoft::Terminal::Control::GraphicsAPI, GraphicsAPI) \ - X(bool, DisablePartialInvalidation, false) \ - X(bool, SoftwareRendering, false) \ - X(winrt::Microsoft::Terminal::Control::TextMeasurement, TextMeasurement) \ - X(winrt::Microsoft::Terminal::Control::AmbiguousWidth, AmbiguousWidth, winrt::Microsoft::Terminal::Control::AmbiguousWidth::Narrow) \ - X(winrt::Microsoft::Terminal::Control::DefaultInputScope, DefaultInputScope, winrt::Microsoft::Terminal::Control::DefaultInputScope::Default) \ - X(bool, UseBackgroundImageForWindow, false) \ - X(bool, ShowMarks, false) \ - X(winrt::Microsoft::Terminal::Control::CopyFormat, CopyFormatting, 0) \ - X(bool, RightClickContextMenu, false) \ - X(winrt::Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, winrt::Microsoft::Terminal::Control::PathTranslationStyle::None) +#define CONTROL_SETTINGS(X) \ + X(winrt::guid, SessionId) \ + X(bool, EnableUnfocusedAcrylic, false) \ + X(winrt::hstring, Padding, DEFAULT_PADDING) \ + X(winrt::hstring, FontFace, L"Consolas") \ + X(float, FontSize, DEFAULT_FONT_SIZE) \ + X(winrt::Windows::UI::Text::FontWeight, FontWeight) \ + X(IFontFeatureMap, FontFeatures) \ + X(IFontAxesMap, FontAxes) \ + X(bool, EnableBuiltinGlyphs, true) \ + X(bool, EnableColorGlyphs, true) \ + X(winrt::hstring, CellWidth) \ + X(winrt::hstring, CellHeight) \ + X(winrt::hstring, Commandline) \ + X(winrt::hstring, StartingDirectory) \ + X(winrt::Microsoft::Terminal::Control::ScrollbarState, ScrollState, winrt::Microsoft::Terminal::Control::ScrollbarState::Visible) \ + X(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \ + X(winrt::Microsoft::Terminal::Control::GraphicsAPI, GraphicsAPI) \ + X(bool, DisablePartialInvalidation, false) \ + X(bool, SoftwareRendering, false) \ + X(winrt::Microsoft::Terminal::Control::TextMeasurement, TextMeasurement) \ + X(winrt::Microsoft::Terminal::Control::AmbiguousWidth, AmbiguousWidth, winrt::Microsoft::Terminal::Control::AmbiguousWidth::Narrow) \ + X(winrt::Microsoft::Terminal::Control::DefaultInputScope, DefaultInputScope, winrt::Microsoft::Terminal::Control::DefaultInputScope::Default) \ + X(bool, UseBackgroundImageForWindow, false) \ + X(bool, ShowMarks, false) \ + X(winrt::Microsoft::Terminal::Control::CopyFormat, CopyFormatting, 0) \ + X(bool, RightClickContextMenu, false) \ + X(winrt::Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, winrt::Microsoft::Terminal::Control::PathTranslationStyle::None) \ + X(winrt::Microsoft::Terminal::Control::OutputNotificationStyle, NotifyOnInactiveOutput, winrt::Microsoft::Terminal::Control::OutputNotificationStyle{ 0 }) \ + X(winrt::Microsoft::Terminal::Control::OutputNotificationStyle, NotifyOnNextPrompt, winrt::Microsoft::Terminal::Control::OutputNotificationStyle{ 0 }) \ + X(winrt::Microsoft::Terminal::Control::AutoDetectRunningCommand, AutoDetectRunningCommand, winrt::Microsoft::Terminal::Control::AutoDetectRunningCommand::Disabled) diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 4f359dba13c..7c66ef9059b 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -424,7 +424,7 @@ void ConhostInternalGetSet::NotifyBufferRotation(const int) { } -void ConhostInternalGetSet::NotifyShellIntegrationMark() +void ConhostInternalGetSet::NotifyShellIntegrationMark(ShellIntegrationMark /*mark*/) { // Not implemented for conhost - shell integration marks are a Terminal app feature. } diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index c80df20ffea..d035aa1c296 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -66,7 +66,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: bool IsVtInputEnabled() const override; void NotifyBufferRotation(const int delta) override; - void NotifyShellIntegrationMark() override; + void NotifyShellIntegrationMark(ShellIntegrationMark mark) override; void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index fb613c4e121..804ad1c804c 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -85,7 +85,16 @@ namespace Microsoft::Console::VirtualTerminal virtual bool ResizeWindow(const til::CoordType width, const til::CoordType height) = 0; virtual void NotifyBufferRotation(const int delta) = 0; - virtual void NotifyShellIntegrationMark() = 0; + + enum class ShellIntegrationMark + { + Prompt, + Command, + Output, + CommandFinished, + Other + }; + virtual void NotifyShellIntegrationMark(ShellIntegrationMark mark) = 0; virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index d49bbfad8fa..f5b45b5b32d 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -3583,7 +3583,7 @@ void AdaptDispatch::DoConEmuAction(const std::wstring_view string) else if (subParam == 12) { _pages.ActivePage().Buffer().StartCommand(); - _api.NotifyShellIntegrationMark(); + _api.NotifyShellIntegrationMark(ITerminalApi::ShellIntegrationMark::Command); } } @@ -3614,7 +3614,7 @@ void AdaptDispatch::DoITerm2Action(const std::wstring_view string) if (action == L"SetMark") { _pages.ActivePage().Buffer().StartPrompt(); - _api.NotifyShellIntegrationMark(); + _api.NotifyShellIntegrationMark(ITerminalApi::ShellIntegrationMark::Prompt); } } @@ -3648,19 +3648,19 @@ void AdaptDispatch::DoFinalTermAction(const std::wstring_view string) case L'A': // FTCS_PROMPT { _pages.ActivePage().Buffer().StartPrompt(); - _api.NotifyShellIntegrationMark(); + _api.NotifyShellIntegrationMark(ITerminalApi::ShellIntegrationMark::Prompt); break; } case L'B': // FTCS_COMMAND_START { _pages.ActivePage().Buffer().StartCommand(); - _api.NotifyShellIntegrationMark(); + _api.NotifyShellIntegrationMark(ITerminalApi::ShellIntegrationMark::Command); break; } case L'C': // FTCS_COMMAND_EXECUTED { _pages.ActivePage().Buffer().StartOutput(); - _api.NotifyShellIntegrationMark(); + _api.NotifyShellIntegrationMark(ITerminalApi::ShellIntegrationMark::Output); break; } case L'D': // FTCS_COMMAND_FINISHED @@ -3681,7 +3681,7 @@ void AdaptDispatch::DoFinalTermAction(const std::wstring_view string) } _pages.ActivePage().Buffer().EndCurrentCommand(error); - _api.NotifyShellIntegrationMark(); + _api.NotifyShellIntegrationMark(ITerminalApi::ShellIntegrationMark::CommandFinished); break; } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 9edc155e999..1db1b6c0c84 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -207,7 +207,7 @@ class TestGetSet final : public ITerminalApi Log::Comment(L"NotifyBufferRotation MOCK called..."); } - void NotifyShellIntegrationMark() override + void NotifyShellIntegrationMark(ShellIntegrationMark /*mark*/) override { Log::Comment(L"NotifyShellIntegrationMark MOCK called..."); } diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index b9c80ce4579..12078e862ac 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -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 diff --git a/tools/runformat.cmd b/tools/runformat.cmd index d9c1d56c73f..9c83fdd004f 100644 --- a/tools/runformat.cmd +++ b/tools/runformat.cmd @@ -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"