From ee82baadde3c4ea4c970d4222f977133f593b0c6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Sep 2025 06:44:41 -0700 Subject: [PATCH] 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 --- src/apprt/gtk/class/surface.zig | 52 +++++++++++++++++++++++++++----- src/apprt/gtk/ui/1.2/surface.blp | 1 - 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index fb933073c..344bf8f21 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -51,6 +51,13 @@ pub const Surface = extern struct { pub const Tree = datastruct.SplitTree(Self); 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 name = "bell-ringing"; const impl = gobject.ext.defineProperty( @@ -296,6 +303,19 @@ pub const Surface = extern 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 /// reason. /// @@ -1674,6 +1694,16 @@ pub const Surface = extern struct { } 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(); if (priv.bell_ringing == ringing) return; priv.bell_ringing = ringing; @@ -1858,20 +1888,26 @@ pub const Surface = extern struct { self.as(gtk.Widget).setCursorFromName(name.ptr); } - fn propBellRinging( - self: *Self, - _: *gobject.ParamSpec, - _: ?*anyopaque, - ) callconv(.c) void { + /// Handle bell features that need to happen every time a BEL is received + /// Currently this is audio and system but this could change in the future. + fn ringBell(self: *Self) void { const priv = self.private(); - if (!priv.bell_ringing) return; + + // Emit the signal + signals.bell.impl.emit( + self, + null, + .{}, + null, + ); // Activate actions if they exist _ = self.as(gtk.Widget).activateAction("tab.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; + + // Do our sound if (config.@"bell-features".audio) audio: { const config_path = config.@"bell-audio-path" orelse break :audio; 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_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); - class.bindTemplateCallback("notify_bell_ringing", &propBellRinging); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown); @@ -2884,6 +2919,7 @@ pub const Surface = extern struct { }); // Signals + signals.bell.impl.register(.{}); signals.@"close-request".impl.register(.{}); signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-write".impl.register(.{}); diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index ad971e991..7ed78ecb3 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -169,7 +169,6 @@ template $GhosttySurface: Adw.Bin { "surface", ] - notify::bell-ringing => $notify_bell_ringing(); notify::config => $notify_config(); notify::error => $notify_error(); notify::mouse-hover-url => $notify_mouse_hover_url();