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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2849,6 +2849,78 @@
"description": "Sets the sound played when the application emits a BEL. When set to an array, the terminal will pick one of those sounds at random.",
"$ref": "#/$defs/BellSound"
},
"notifyOnInactiveOutput": {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a setting for duration? Ghostty has this as well. I may only want to see this after, say, 10s and not a command that maybe takes a second and I quickly flip away intending to come back in a few seconds. A longer job is more likely something we'll "walk away from" for a longer period instead of just flipping windows back and forth a bit.

If someone really wanted to be alerted for every command when the window is inactive, they can set this to 0 (or maybe that's the default).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... ghostty calls these settings activity-monitor and activity-monitor-threshold. So, we could name this notifyOnInactiveOutputThreshold and also have it take an int representing seconds (default of 0). Then just place them next to each other in the settings UI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a higher default (IIRC, Ghostty's is 30s when enabled) might solve the other issue you mentioned, but you could also debounce notifications. If they come in within Xs (maybe also configurable), don't notify. Frankly, given the goal is to notify you some task is done - and this would seem to be a long-running task, IMO, since any quick task you're probably coming back to frequently - the "threshold" having a higher default like 30s would probably be sufficient - at least as a start.

"oneOf": [
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string",
"enum": [
"taskbar",
"audible",
"tab",
"notification"
]
}
},
{
"type": "string",
"enum": [
"taskbar",
"audible",
"tab",
"notification",
"all",
"none"
]
}
],
"description": "Controls how you are notified when an inactive tab produces new output."
},
"notifyOnNextPrompt": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string",
"enum": [
"taskbar",
"audible",
"tab",
"notification"
]
}
},
{
"type": "string",
"enum": [
"taskbar",
"audible",
"tab",
"notification",
"all",
"none"
]
}
],
"description": "Controls how you are notified when a new shell prompt is detected. Requires shell integration."
},
"autoDetectRunningCommand": {
"default": "disabled",
"description": "Automatically detect when a command is running and show a progress indicator in the tab and taskbar.",
"enum": [
"disabled",
"automatic",
"progress"
],
"type": "string"
},
"closeOnExit": {
"default": "automatic",
"description": "Sets how the profile reacts to termination or failure to launch. Possible values:\n -\"graceful\" (close when exit is typed or the process exits normally)\n -\"always\" (always close)\n -\"automatic\" (behave as \"graceful\" only for processes launched by terminal, behave as \"always\" otherwise)\n -\"never\" (never close).\ntrue and false are accepted as synonyms for \"graceful\" and \"never\" respectively.",
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalApp/IPaneContent.idl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ namespace TerminalApp

runtimeclass NotificationEventArgs
{
Microsoft.Terminal.Control.OutputNotificationStyle Style { get; };
Boolean OnlyWhenInactive { get; };
String Title { get; };
String Body { get; };
};
Expand Down
76 changes: 74 additions & 2 deletions src/cascadia/TerminalApp/Tab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ namespace winrt::TerminalApp::implementation
_bellIndicatorTimer.Stop();
}

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:
Expand Down Expand Up @@ -329,6 +335,10 @@ namespace winrt::TerminalApp::implementation
{
ShowBellIndicator(false);
}
if (_tabStatus.ActivityIndicator())
{
ShowActivityIndicator(false);
}
}
}

Expand Down Expand Up @@ -459,6 +469,26 @@ namespace winrt::TerminalApp::implementation
_bellIndicatorTimer.Start();
}

void Tab::ShowActivityIndicator(const bool show)
{
ASSERT_UI_THREAD();

_tabStatus.ActivityIndicator(show);
}

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.
Expand Down Expand Up @@ -1068,6 +1098,7 @@ namespace winrt::TerminalApp::implementation
dispatcher,
til::throttled_func_options{
.delay = std::chrono::milliseconds{ 200 },
.leading = true,
.trailing = true,
},
[weakThis]() {
Expand Down Expand Up @@ -1173,8 +1204,45 @@ namespace winrt::TerminalApp::implementation
co_await wil::resume_foreground(dispatcher);
if (const auto tab{ weakThisCopy.get() })
{
const auto title = notifArgs.Title().empty() ? tab->Title() : notifArgs.Title();
tab->TabToastNotificationRequested.raise(title, notifArgs.Body(), sender);
const auto activeContent = tab->GetActiveContent();
const auto isActivePaneContent = activeContent && activeContent == sender;

if (notifArgs.OnlyWhenInactive() && isActivePaneContent &&
tab->_focusState != WUX::FocusState::Unfocused)
{
co_return;
}

const auto style = notifArgs.Style();

if (WI_IsFlagSet(style, OutputNotificationStyle::Taskbar))
{
tab->TabRaiseVisualBell.raise();
}

if (WI_IsFlagSet(style, winrt::Microsoft::Terminal::Control::OutputNotificationStyle::Audible))
{
if (const auto termContent{ sender.try_as<TerminalApp::TerminalPaneContent>() })
{
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))
{
const auto title = notifArgs.Title().empty() ? tab->Title() : notifArgs.Title();
tab->TabToastNotificationRequested.raise(title, notifArgs.Body(), sender);
}
}
});

Expand Down Expand Up @@ -1405,6 +1473,10 @@ namespace winrt::TerminalApp::implementation
{
tab->ShowBellIndicator(false);
}
if (tab->_tabStatus.ActivityIndicator())
{
tab->ShowActivityIndicator(false);
}
}
});

Expand Down
6 changes: 6 additions & 0 deletions src/cascadia/TerminalApp/Tab.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<winrt::Microsoft::Terminal::Settings::Model::SplitDirection> PreCalculateCanSplit(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType,
const float splitSize,
Expand Down Expand Up @@ -212,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();
Expand Down
6 changes: 6 additions & 0 deletions src/cascadia/TerminalApp/TabHeaderControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
FontSize="12"
Glyph="&#xEA8F;"
Visibility="{x:Bind TabStatus.BellIndicator, Mode=OneWay}" />
<FontIcon x:Name="HeaderActivityIndicator"
Margin="0,0,8,0"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="8"
Glyph="&#xF127;"
Visibility="{x:Bind TabStatus.ActivityIndicator, Mode=OneWay}" />
<FontIcon x:Name="HeaderZoomIcon"
Margin="0,0,8,0"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Expand Down
4 changes: 2 additions & 2 deletions src/cascadia/TerminalApp/TabManagement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1281,8 +1281,8 @@ namespace winrt::TerminalApp::implementation
{
// The toast Activated callback runs on a background thread.
// Marshal to the UI thread for tab focus and window summon.
page->Dispatcher().RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakPage{ page->get_weak() }, weakTab, weakContent]() {
if (const auto p{ weakPage.get() })
page->Dispatcher().RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakThis, weakTab, weakContent]() {
if (const auto p{ weakThis.get() })
{
if (const auto t{ weakTab.get() })
{
Expand Down
Loading
Loading