diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 07eb75ab82d..428ff6779b6 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -117,6 +117,7 @@ uiatextrange UIs und unregister +urxvt versioned vsdevcmd walkthrough diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index aa9f67b0b3f..bdf083b0423 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -461,4 +461,35 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation co_await winrt::resume_background(); _monarch.RequestSendContent(args); } + + // Attempt to summon an existing window. This static function does NOT + // pre-register as the monarch. This is used for activations from a + // notification, where this process should NEVER become its own window. + bool WindowManager::SummonForNotification(const uint64_t windowId) + { + auto monarch = create_instance(Monarch_clsid, + CLSCTX_LOCAL_SERVER); + + if (monarch == nullptr) + { + return false; + } + SummonWindowSelectionArgs args{}; + args.WindowID(windowId); + + // Summon the window... + // * On its current desktop + // * Without a dropdown + // * On the monitor it is already on + // * Do not toggle, just make visible. + const Remoting::SummonWindowBehavior summonArgs{}; + summonArgs.MoveToCurrentDesktop(false); + summonArgs.DropdownDuration(0); + summonArgs.ToMonitor(Remoting::MonitorBehavior::InPlace); + summonArgs.ToggleVisibility(false); + + args.SummonBehavior(summonArgs); + monarch.SummonWindow(args); + return true; + } } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 015ab9f3472..99ff73f29b3 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -47,6 +47,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::fire_and_forget RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex, Windows::Foundation::IReference windowBounds); winrt::fire_and_forget RequestSendContent(Remoting::RequestReceiveContentArgs args); + static bool SummonForNotification(const uint64_t windowId); + TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index 5dd72b7f19a..40ea94d7034 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -29,6 +29,8 @@ namespace Microsoft.Terminal.Remoting void RequestMoveContent(String window, String content, UInt32 tabIndex, Windows.Foundation.IReference bounds); void RequestSendContent(RequestReceiveContentArgs args); + static Boolean SummonForNotification(UInt64 windowId); + event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler WindowCreated; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 26a80fa7457..68020a99c8d 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -45,6 +46,9 @@ using namespace ::Microsoft::Console; using namespace ::Microsoft::Terminal::Core; using namespace std::chrono_literals; +using namespace winrt::Windows::UI::Notifications; +using namespace winrt::Windows::Data::Xml::Dom; + #define HOOKUP_ACTION(action) _actionDispatch->action({ this, &TerminalPage::_Handle##action }); namespace winrt @@ -1671,6 +1675,7 @@ namespace winrt::TerminalApp::implementation { term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler }); } + winrt::weak_ref weakTerm{ term }; term.ContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { if (const auto& page{ weak.get() }) @@ -1684,6 +1689,7 @@ namespace winrt::TerminalApp::implementation page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), true); } }); + term.SendNotification({ get_weak(), &TerminalPage::_SendNotificationHandler }); } // Method Description: @@ -2958,6 +2964,110 @@ namespace winrt::TerminalApp::implementation _ShowWindowChangedHandlers(*this, args); } + // Method Description: + // - Handler for a control's SendNotification event. `args` will contain the + // title and body of the notification requested by the client application. + // - This will only actually send a notification when the sender is + // - in an inactive window OR + // - in an inactive tab. + winrt::fire_and_forget TerminalPage::_SendNotificationHandler(const IInspectable sender, + const Microsoft::Terminal::Control::SendNotificationArgs args) + { + // This never works as expected when we're an elevated instance. The + // notification will end up launching an unelevated instance to handle + // it, and there's no good way to get back to the elevated one. + // Possibly revisit after GH #13276. + // + // We're using CanDragDrop, because TODO! I bet this works with UAC disabled + if (!CanDragDrop()) + { + co_return; + } + + auto weakThis = get_weak(); + + co_await resume_foreground(Dispatcher()); + auto page{ weakThis.get() }; + if (page) + { + // If the window is inactive, we always want to send the notification. + // + // Otherwise, we only want to send the notification for panes in inactive tabs. + if (_activated) + { + auto foundControl = false; + if (const auto activeTab{ _GetFocusedTabImpl() }) + { + activeTab->GetRootPane()->WalkTree([&](auto&& pane) { + if (const auto& term{ pane->GetTerminalControl() }) + { + if (term == sender) + { + foundControl = true; + return; + } + } + }); + } + + // The control that sent this is in the active tab. We + // should only send the notification if the window was + // inactive. + if (foundControl) + { + co_return; + } + } + + _sendNotification(args.Title(), args.Body()); + } + } + + // Actually write the payload to a XML doc and load it into a ToastNotification. + void TerminalPage::_sendNotification(const std::wstring_view title, + const std::wstring_view body) + { + // ToastNotificationManager::CreateToastNotifier doesn't work in + // unpackaged scenarios without an AUMID. We probably don't have one if + // we're unpackaged. Unpackaged isn't a wholly supported scenario + // anyways, so let's just bail. + + if (!IsPackaged()) + { + return; + } + + static winrt::hstring xmlTemplate{ L"\ + \ + \ + \ + \ + \ + \ + \ + " }; + + XmlDocument doc; + doc.LoadXml(xmlTemplate); + // Populate with text and values + auto payload{ fmt::format(L"window={}&tabIndex=0", WindowProperties().WindowId()) }; + doc.DocumentElement().SetAttribute(L"launch", payload); + doc.SelectSingleNode(L"//text[1]").InnerText(title); + doc.SelectSingleNode(L"//text[2]").InnerText(body); + + // Construct the notification + ToastNotification notification{ doc }; + + // lazy-init + if (!_toastNotifier) + { + _toastNotifier = ToastNotificationManager::CreateToastNotifier(); + } + + // And show it! + _toastNotifier.Show(notification); + } + // Method Description: // - Paste text from the Windows Clipboard to the focused terminal void TerminalPage::_PasteText() diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 90caedf4a24..1ca416f34ab 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -287,6 +287,9 @@ namespace winrt::TerminalApp::implementation __declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath(); bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility); + // todo! maybe move to TerminalWindow + winrt::Windows::UI::Notifications::ToastNotifier _toastNotifier{ nullptr }; + winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); void _ShowAboutDialog(); @@ -543,6 +546,9 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender); winrt::com_ptr _senderOrFocusedTab(const IInspectable& sender); + winrt::fire_and_forget _SendNotificationHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::SendNotificationArgs args); + + void _sendNotification(const std::wstring_view title, const std::wstring_view body); #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/pch.h b/src/cascadia/TerminalApp/pch.h index 8d0338108e3..6638459e618 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -27,6 +27,8 @@ #include #include +#include +#include #include #include #include @@ -35,6 +37,7 @@ #include #include #include +#include #include #include #include diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 68702b27500..f8aef9c8395 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -126,6 +126,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnCompletionsChanged = [=](auto&& menuJson, auto&& replaceLength) { _terminalCompletionsChanged(menuJson, replaceLength); }; _terminal->CompletionsChangedCallback(pfnCompletionsChanged); + auto pfnSendNotification = std::bind(&ControlCore::_terminalSendNotification, this, std::placeholders::_1, std::placeholders::_2); + _terminal->SetSendNotificationCallback(pfnSendNotification); + // MSFT 33353327: Initialize the renderer in the ctor instead of Initialize(). // We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go. // If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach @@ -1585,6 +1588,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation _midiAudio.PlayNote(reinterpret_cast(_owningHwnd), noteNumber, velocity, std::chrono::duration_cast(duration)); } + void ControlCore::_terminalSendNotification(const std::wstring_view title, + const std::wstring_view body) + { + const auto e = winrt::make_self(title, body); + _SendNotificationHandlers(*this, *e); + } + bool ControlCore::HasSelection() const { const auto lock = _terminal->LockForReading(); diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 6b513f2bf86..e141614e50e 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -280,6 +280,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(RestartTerminalRequested, IInspectable, IInspectable); TYPED_EVENT(Attached, IInspectable, IInspectable); + + TYPED_EVENT(SendNotification, IInspectable, Control::SendNotificationArgs); // clang-format on private: @@ -372,6 +374,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::fire_and_forget _terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength); + void _terminalSendNotification(const std::wstring_view title, + const std::wstring_view body); #pragma endregion MidiAudio _midiAudio; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 01223fb1a14..ce4283335a8 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -190,5 +190,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler CompletionsChanged; + event Windows.Foundation.TypedEventHandler SendNotification; + }; } diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index 93e147feaa8..5a659797699 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -20,3 +20,4 @@ #include "KeySentEventArgs.g.cpp" #include "CharSentEventArgs.g.cpp" #include "StringSentEventArgs.g.cpp" +#include "SendNotificationArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 1c135cdf4f6..bf8bfdb0a8d 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -20,6 +20,7 @@ #include "KeySentEventArgs.g.h" #include "CharSentEventArgs.g.h" #include "StringSentEventArgs.g.h" +#include "SendNotificationArgs.g.h" namespace winrt::Microsoft::Terminal::Control::implementation { @@ -251,6 +252,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(winrt::hstring, Text); }; + + struct SendNotificationArgs : public SendNotificationArgsT + { + public: + SendNotificationArgs(const std::wstring_view title, + const std::wstring_view body) : + _Title(title), + _Body(body) + { + } + + WINRT_PROPERTY(winrt::hstring, Title); + WINRT_PROPERTY(winrt::hstring, Body); + }; } namespace winrt::Microsoft::Terminal::Control::factory_implementation diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 3caea5a0f38..881dba23671 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -121,4 +121,10 @@ namespace Microsoft.Terminal.Control { String Text { get; }; } + + runtimeclass SendNotificationArgs + { + String Title { get; }; + String Body { get; }; + } } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 5978149cd36..f3efdbb4665 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -104,6 +104,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation _revokers.PasteFromClipboard = _interactivity.PasteFromClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubblePasteFromClipboard }); + // Re-raise the event with us as the sender. + _core.SendNotification([weakThis = get_weak()](auto s, auto e) { + if (auto self{ weakThis.get() }) + { + self->_SendNotificationHandlers(*self, e); + } + }); + // Initialize the terminal only once the swapchainpanel is loaded - that // way, we'll be able to query the real pixel size it got on layout _layoutUpdatedRevoker = SwapChainPanel().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 7dc74d7136b..38ccf9b7d65 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -194,6 +194,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(KeySent, IInspectable, Control::KeySentEventArgs); TYPED_EVENT(CharSent, IInspectable, Control::CharSentEventArgs); TYPED_EVENT(StringSent, IInspectable, Control::StringSentEventArgs); + TYPED_EVENT(SendNotification, IInspectable, Control::SendNotificationArgs); // clang-format on WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, BackgroundBrush, _PropertyChangedHandlers, nullptr); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 66727690082..c1100506dbc 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -58,6 +58,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler TabColorChanged; event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusFollowMouseRequested; + event Windows.Foundation.TypedEventHandler SendNotification; event Windows.Foundation.TypedEventHandler CompletionsChanged; diff --git a/src/cascadia/TerminalCore/ICoreSettings.idl b/src/cascadia/TerminalCore/ICoreSettings.idl index 1f3c78aa09b..5ffff637001 100644 --- a/src/cascadia/TerminalCore/ICoreSettings.idl +++ b/src/cascadia/TerminalCore/ICoreSettings.idl @@ -28,6 +28,7 @@ namespace Microsoft.Terminal.Core Windows.Foundation.IReference StartingTabColor; Boolean AutoMarkPrompts; + Boolean AllowNotifications; }; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 3b086e3c143..136e6419a5a 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -95,6 +95,7 @@ void Terminal::UpdateSettings(ICoreSettings settings) _startingTitle = settings.StartingTitle(); _trimBlockSelection = settings.TrimBlockSelection(); _autoMarkPrompts = settings.AutoMarkPrompts(); + _allowNotifications = settings.AllowNotifications(); _getTerminalInput().ForceDisableWin32InputMode(settings.ForceVTInput()); @@ -1160,6 +1161,11 @@ void Terminal::SetPlayMidiNoteCallback(std::function pfn) noexcept +{ + _pfnSendNotification.swap(pfn); +} + void Terminal::BlinkCursor() noexcept { if (_selectionMode != SelectionInteractionMode::Mark) diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 80c5e3cc905..358e50d0188 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -163,6 +163,7 @@ class Microsoft::Terminal::Core::Terminal final : void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; + void SendNotification(const std::wstring_view title, const std::wstring_view body) override; #pragma endregion void ClearMark(); @@ -237,6 +238,7 @@ class Microsoft::Terminal::Core::Terminal final : void SetShowWindowCallback(std::function pfn) noexcept; void SetPlayMidiNoteCallback(std::function pfn) noexcept; void CompletionsChangedCallback(std::function pfn) noexcept; + void SetSendNotificationCallback(std::function pfn) noexcept; void BlinkCursor() noexcept; void SetCursorOn(const bool isOn) noexcept; @@ -345,6 +347,7 @@ class Microsoft::Terminal::Core::Terminal final : std::function _pfnShowWindowChanged; std::function _pfnPlayMidiNote; std::function _pfnCompletionsChanged; + std::function _pfnSendNotification; RenderSettings _renderSettings; std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine; @@ -363,6 +366,7 @@ class Microsoft::Terminal::Core::Terminal final : bool _suppressApplicationTitle = false; bool _trimBlockSelection = false; bool _autoMarkPrompts = false; + bool _allowNotifications = true; size_t _taskbarState = 0; size_t _taskbarProgress = 0; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 9a5ea2221ac..57949cc2d50 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -511,3 +511,13 @@ void Terminal::NotifyBufferRotation(const int delta) _NotifyScrollEvent(); } } + +void Terminal::SendNotification(const std::wstring_view title, + const std::wstring_view body) +{ + // Only send notifications if enabled in the settings + if (_pfnSendNotification && _allowNotifications) + { + _pfnSendNotification(title, body); + } +} diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index d0dc6bfcca5..1da8566b717 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -98,7 +98,8 @@ Author(s): X(bool, AutoMarkPrompts, "experimental.autoMarkPrompts", false) \ X(bool, ShowMarks, "experimental.showMarksOnScrollbar", false) \ X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false) \ - X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) + X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \ + X(bool, AllowNotifications, "allowNotifications", true) // Intentionally omitted Profile settings: // * Name diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 4aa9af3422a..d9a1b13abd3 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -94,6 +94,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(Boolean, RightClickContextMenu); INHERITABLE_PROFILE_SETTING(Boolean, RepositionCursorWithMouse); + INHERITABLE_PROFILE_SETTING(Boolean, AllowNotifications); INHERITABLE_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index 580b06089fd..a09a657ffd8 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -339,6 +339,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _RightClickContextMenu = profile.RightClickContextMenu(); _RepositionCursorWithMouse = profile.RepositionCursorWithMouse(); + _AllowNotifications = profile.AllowNotifications(); _ReloadEnvironmentVariables = profile.ReloadEnvironmentVariables(); } diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 009e6503da0..9dc05e1aaf2 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -167,6 +167,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, bool, ShowMarks, false); INHERITABLE_SETTING(Model::TerminalSettings, bool, RightClickContextMenu, false); INHERITABLE_SETTING(Model::TerminalSettings, bool, RepositionCursorWithMouse, false); + INHERITABLE_SETTING(Model::TerminalSettings, bool, AllowNotifications, true); INHERITABLE_SETTING(Model::TerminalSettings, bool, ReloadEnvironmentVariables, true); diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 6a0e0ed698d..184ca9b2584 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -23,6 +23,8 @@ using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace ::Microsoft::Console; using namespace ::Microsoft::Console::Types; using namespace std::chrono_literals; +using namespace winrt::Windows::ApplicationModel; +using namespace winrt::Windows::UI::Notifications; // This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx // "If the high-order bit is 1, the key is down; otherwise, it is up." diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 60b96103124..9a79fbde0bf 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -71,6 +71,8 @@ class AppHost : public std::enable_shared_from_this void _HandleCommandlineArgs(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); void _HandleSessionRestore(const bool startedForContent); + // bool _HandleLaunchArgs(); + winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition(); void _HandleCreateWindow(const HWND hwnd, const til::rect& proposedRect); diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 5b1e7e496aa..e9d54e0618e 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -14,6 +14,8 @@ using namespace winrt; using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::ApplicationModel; +using namespace winrt::Windows::UI::Notifications; using namespace ::Microsoft::Console; using namespace std::chrono_literals; using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; @@ -58,8 +60,89 @@ void _buildArgsFromCommandline(std::vector& args) } } +// Method Description: +// - Attempt to handle activated event args, which are a kind of "modern" +// activation, which we use for supporting toast notifications. +// - If we do find we were activated from a toast notification, we'll unpack the +// arguments from the toast. Then, we'll try to open up a connection to the +// monarch and ask the monarch to activate the right window. +// Arguments: +// - +// Return Value: +// - +bool WindowEmperor::_handleLaunchArgs() +try +{ + // AppInstance::GetActivatedEventArgs will throw when unpackaged. + if (!IsPackaged()) + { + return false; + } + // If someone clicks on a notification, then a fresh instance of + // windowsterminal.exe will spawn. We certainly don't want to create a new + // window for that - we only want to activate the window that created the + // actual notification. In the toast arg's payload will be the window id + // that sent the notification. We'll ask the window manager to try and + // activate that window ID, without even bothering to register as the + // monarch ourselves (if we can't find a monarch, then there are no windows + // running, so whoever sent it must have died.) + + const auto activatedArgs = AppInstance::GetActivatedEventArgs(); + if (activatedArgs != nullptr && + activatedArgs.Kind() == Activation::ActivationKind::ToastNotification) + { + if (const auto& toastArgs{ activatedArgs.try_as() }) + { + // Obtain the arguments from the notification + const auto args = toastArgs.Argument(); + + // Args is gonna look like + // + // "window=id&foo=bar&..." + // + // We need to first split on &, then split those pairs on = + + // tabIndex code here is left as reference for parsing multiple + // arguments, despite it not being used currently. + uint32_t window; + // uint32_t tabIndex = 0; + + const std::wstring_view argsView{ args }; + const auto pairs = Utils::SplitString(argsView, L'&'); + for (const auto& pair : pairs) + { + const auto pairParts = Utils::SplitString(pair, L'='); + if (pairParts.size() == 2) + { + if (til::at(pairParts, 0) == L"window") + { + window = std::wcstoul(pairParts[1].data(), nullptr, 10); + } + // else if (pairParts[0] == L"tabIndex") + // { + // // convert a wide string to a uint + // tabIndex = std::wcstoul(pairParts[1].data(), nullptr, 10); + // } + } + } + return winrt::Microsoft::Terminal::Remoting::WindowManager::SummonForNotification(window); + } + } + + return false; +} +CATCH_LOG_RETURN_HR(false) + void WindowEmperor::HandleCommandlineArgs(int nCmdShow) { + // Before handling any commandline arguments, check if this was a toast + // invocation. If it was, we can go ahead and totally ignore everything + // else. + if (_handleLaunchArgs()) + { + TerminateProcess(GetCurrentProcess(), 0u); + } + std::vector args; _buildArgsFromCommandline(args); const auto cwd{ wil::GetCurrentDirectoryW() }; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 9f44a276c3c..a256243053d 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -29,6 +29,7 @@ class WindowEmperor : public std::enable_shared_from_this void HandleCommandlineArgs(int nCmdShow); private: + bool _handleLaunchArgs(); void _createNewWindowThread(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); [[nodiscard]] static LRESULT __stdcall _wndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; diff --git a/src/cascadia/WindowsTerminal/pch.h b/src/cascadia/WindowsTerminal/pch.h index 5011dbeba61..6424df901bd 100644 --- a/src/cascadia/WindowsTerminal/pch.h +++ b/src/cascadia/WindowsTerminal/pch.h @@ -59,7 +59,14 @@ Module Name: // * Media for ScaleTransform // * ApplicationModel for finding the path to wt.exe // * Primitives for Popup (used by GetOpenPopupsForXamlRoot) +// * XML, Notifications, Activation: for notification activations +#include +#include +#include +#include +#include #include +#include #include #include #include diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h index 1b61775becc..3d88bacb522 100644 --- a/src/cascadia/inc/ControlProperties.h +++ b/src/cascadia/inc/ControlProperties.h @@ -49,7 +49,8 @@ X(bool, DetectURLs, true) \ X(bool, VtPassthrough, false) \ X(bool, AutoMarkPrompts) \ - X(bool, RepositionCursorWithMouse, false) + X(bool, RepositionCursorWithMouse, false) \ + X(bool, AllowNotifications, true) // --------------------------- Control Settings --------------------------- // All of these settings are defined in IControlSettings. diff --git a/src/features.xml b/src/features.xml index 4a555d7d4e6..9eadb86dec5 100644 --- a/src/features.xml +++ b/src/features.xml @@ -187,4 +187,15 @@ + + Feature_Notifications + Enables OSC777 notifications + 16654 + AlwaysDisabled + + Dev + Canary + + + diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 45ea6052e21..0889d1c7799 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -76,6 +76,8 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; + void SendNotification(const std::wstring_view /*title*/, const std::wstring_view /*body*/) noexcept override{}; + private: Microsoft::Console::IIoProvider& _io; }; diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 1b847d32ba0..7fb5e022a6c 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -137,6 +137,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool DoITerm2Action(const std::wstring_view string) = 0; virtual bool DoFinalTermAction(const std::wstring_view string) = 0; + virtual bool DoUrxvtAction(const std::wstring_view string) = 0; virtual bool DoVsCodeAction(const std::wstring_view string) = 0; diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index 3375a968b04..bfb21102037 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -87,5 +87,7 @@ namespace Microsoft::Console::VirtualTerminal virtual void MarkCommandFinish(std::optional error) = 0; virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0; + + virtual void SendNotification(const std::wstring_view title, const std::wstring_view body) = 0; }; } diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index fb504c20200..95870b4a956 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -3863,6 +3863,41 @@ bool AdaptDispatch::DoVsCodeAction(const std::wstring_view string) return false; } +bool AdaptDispatch::DoUrxvtAction(const std::wstring_view string) +{ + // This is not implemented in conhost. + if (_api.IsConsolePty()) + { + // Flush the frame manually, to make sure marks end up on the right line, like the alt buffer sequence. + _renderer.TriggerFlush(false); + return false; + } + + if constexpr (!Feature_Notifications::IsEnabled()) + { + return false; + } + + const auto parts = Utils::SplitString(string, L';'); + + if (parts.size() < 1) + { + return false; + } + + const auto action = til::at(parts, 0); + + if (action == L"notify") + { + const std::wstring_view title = parts.size() > 1 ? til::at(parts, 1) : L""; + const std::wstring_view body = parts.size() > 2 ? til::at(parts, 2) : L""; + _api.SendNotification(title, body); + return true; + } + + return false; +} + // Method Description: // - DECDLD - Downloads one or more characters of a dynamically redefinable // character set (DRCS) with a specified pixel pattern. The pixel array is diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 0a24d01577d..049162d716b 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -139,6 +139,7 @@ namespace Microsoft::Console::VirtualTerminal bool DoITerm2Action(const std::wstring_view string) override; bool DoFinalTermAction(const std::wstring_view string) override; + bool DoUrxvtAction(const std::wstring_view string) override; bool DoVsCodeAction(const std::wstring_view string) override; diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 122b48820bb..f42616ecc51 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -133,6 +133,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool DoVsCodeAction(const std::wstring_view /*string*/) override { return false; } + bool DoUrxvtAction(const std::wstring_view /*string*/) override { return false; } + StringHandler DownloadDRCS(const VTInt /*fontNumber*/, const VTParameter /*startChar*/, const DispatchTypes::DrcsEraseControl /*eraseControl*/, diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index a1e775d3dfb..62f91eafcff 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -238,6 +238,11 @@ class TestGetSet final : public ITerminalApi VERIFY_ARE_EQUAL(_expectedReplaceLength, replaceLength); } + void SendNotification(const std::wstring_view /*title*/, const std::wstring_view /*body*/) override + { + Log::Comment(L"SendNotification MOCK called..."); + } + void PrepData() { PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter. diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 9d247bfc4fe..f3bfdf4ab8f 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -883,6 +883,11 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, success = _dispatch->DoVsCodeAction(string); break; } + case OscActionCodes::UrxvtAction: + { + success = _dispatch->DoUrxvtAction(string); + break; + } default: // If no functions to call, overall dispatch was a failure. success = false; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 32105133bf3..bb97e388fbe 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -218,6 +218,7 @@ namespace Microsoft::Console::VirtualTerminal ResetCursorColor = 112, FinalTermAction = 133, VsCodeAction = 633, + UrxvtAction = 777, ITerm2Action = 1337, };