gtk: some bell features need to happen on receipt of every BEL (#8962)

Some bell features should be triggered on the receipt of every BEL
character, namely `audio` and `system`. However, Ghostty was setting a
boolean to `true` upon the receipt of the first BEL. Subsequent BEL
characters would be ignored until that boolean was reset to `false`,
usually by keyboard/mouse activity.

This PR fixes the problem by ensuring that the `audio` and `system`
features are triggered every time a BEL is received. Other features
continue to be triggered only when the `bell-ringing` boolean state
changes.

Fixes #8957
This commit is contained in:
Mitchell Hashimoto
2025-09-30 06:44:41 -07:00
parent e974d58615
commit ee82baadde
2 changed files with 44 additions and 9 deletions

View File

@@ -51,6 +51,13 @@ pub const Surface = extern struct {
pub const Tree = datastruct.SplitTree(Self); pub const Tree = datastruct.SplitTree(Self);
pub const properties = struct { pub const properties = struct {
/// This property is set to true when the bell is ringing. Note that
/// this property will only emit a changed signal when there is a
/// full state change. If a bell is ringing and another bell event
/// comes through, the change notification will NOT be emitted.
///
/// If you need to know every scenario the bell is triggered,
/// listen to the `bell` signal instead.
pub const @"bell-ringing" = struct { pub const @"bell-ringing" = struct {
pub const name = "bell-ringing"; pub const name = "bell-ringing";
const impl = gobject.ext.defineProperty( const impl = gobject.ext.defineProperty(
@@ -296,6 +303,19 @@ pub const Surface = extern struct {
}; };
pub const signals = struct { pub const signals = struct {
/// Emitted whenever the bell event is received. Unlike the
/// `bell-ringing` property, this is emitted every time the event
/// is received and not just on state changes.
pub const bell = struct {
pub const name = "bell";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted whenever the surface would like to be closed for any /// Emitted whenever the surface would like to be closed for any
/// reason. /// reason.
/// ///
@@ -1674,6 +1694,16 @@ pub const Surface = extern struct {
} }
pub fn setBellRinging(self: *Self, ringing: bool) void { pub fn setBellRinging(self: *Self, ringing: bool) void {
// Prevent duplicate change notifications if the signals we emit
// in this function cause this state to change again.
self.as(gobject.Object).freezeNotify();
defer self.as(gobject.Object).thawNotify();
// Logic around bell reaction happens on every event even if we're
// already in the ringing state.
if (ringing) self.ringBell();
// Property change only happens on actual state change
const priv = self.private(); const priv = self.private();
if (priv.bell_ringing == ringing) return; if (priv.bell_ringing == ringing) return;
priv.bell_ringing = ringing; priv.bell_ringing = ringing;
@@ -1858,20 +1888,26 @@ pub const Surface = extern struct {
self.as(gtk.Widget).setCursorFromName(name.ptr); self.as(gtk.Widget).setCursorFromName(name.ptr);
} }
fn propBellRinging( /// Handle bell features that need to happen every time a BEL is received
self: *Self, /// Currently this is audio and system but this could change in the future.
_: *gobject.ParamSpec, fn ringBell(self: *Self) void {
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private(); const priv = self.private();
if (!priv.bell_ringing) return;
// Emit the signal
signals.bell.impl.emit(
self,
null,
.{},
null,
);
// Activate actions if they exist // Activate actions if they exist
_ = self.as(gtk.Widget).activateAction("tab.ring-bell", null); _ = self.as(gtk.Widget).activateAction("tab.ring-bell", null);
_ = self.as(gtk.Widget).activateAction("win.ring-bell", null); _ = self.as(gtk.Widget).activateAction("win.ring-bell", null);
// Do our sound
const config = if (priv.config) |c| c.get() else return; const config = if (priv.config) |c| c.get() else return;
// Do our sound
if (config.@"bell-features".audio) audio: { if (config.@"bell-features".audio) audio: {
const config_path = config.@"bell-audio-path" orelse break :audio; const config_path = config.@"bell-audio-path" orelse break :audio;
const path, const required = switch (config_path) { const path, const required = switch (config_path) {
@@ -2859,7 +2895,6 @@ pub const Surface = extern struct {
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
class.bindTemplateCallback("notify_bell_ringing", &propBellRinging);
class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown);
class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown); class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown);
@@ -2884,6 +2919,7 @@ pub const Surface = extern struct {
}); });
// Signals // Signals
signals.bell.impl.register(.{});
signals.@"close-request".impl.register(.{}); signals.@"close-request".impl.register(.{});
signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{}); signals.@"clipboard-write".impl.register(.{});

View File

@@ -169,7 +169,6 @@ template $GhosttySurface: Adw.Bin {
"surface", "surface",
] ]
notify::bell-ringing => $notify_bell_ringing();
notify::config => $notify_config(); notify::config => $notify_config();
notify::error => $notify_error(); notify::error => $notify_error();
notify::mouse-hover-url => $notify_mouse_hover_url(); notify::mouse-hover-url => $notify_mouse_hover_url();