gtk: implement bell (#7087)

This PR implements a more lightweight alternative to #5326 that contains
features that I personally think Just Make Sense for the bell.

No configs, no GStreamer stuff, just sane defaults to get us started.
This commit is contained in:
Mitchell Hashimoto
2025-04-14 11:19:19 -07:00
committed by GitHub
10 changed files with 85 additions and 11 deletions

View File

@@ -601,6 +601,7 @@ typedef enum {
GHOSTTY_ACTION_RELOAD_CONFIG,
GHOSTTY_ACTION_CONFIG_CHANGE,
GHOSTTY_ACTION_CLOSE_WINDOW,
GHOSTTY_ACTION_RING_BELL,
} ghostty_action_tag_e;
typedef union {

View File

@@ -930,6 +930,16 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.present_surface => try self.presentSurface(),
.password_input => |v| try self.passwordInput(v),
.ring_bell => {
_ = self.rt_app.performAction(
.{ .surface = self },
.ring_bell,
{},
) catch |err| {
log.warn("apprt failed to ring bell={}", .{err});
};
},
}
}

View File

@@ -244,6 +244,8 @@ pub const Action = union(Key) {
/// Closes the currently focused window.
close_window,
ring_bell,
/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
@@ -287,6 +289,7 @@ pub const Action = union(Key) {
reload_config,
config_change,
close_window,
ring_bell,
};
/// Sync with: ghostty_action_u

View File

@@ -246,6 +246,7 @@ pub const App = struct {
.toggle_maximize,
.prompt_title,
.reset_window_size,
.ring_bell,
=> {
log.info("unimplemented action={}", .{action});
return false;

View File

@@ -484,6 +484,7 @@ pub fn performAction(
.prompt_title => try self.promptTitle(target),
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
.secure_input => self.setSecureInput(target, value),
.ring_bell => try self.ringBell(target),
// Unimplemented
.close_all_windows,
@@ -775,6 +776,13 @@ fn toggleQuickTerminal(self: *App) !bool {
return true;
}
fn ringBell(_: *App, target: apprt.Target) !void {
switch (target) {
.app => {},
.surface => |surface| try surface.rt_surface.ringBell(),
}
}
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
switch (mode) {
.start => self.startQuitTimer(),

View File

@@ -2439,3 +2439,25 @@ pub fn setSecureInput(self: *Surface, value: apprt.action.SecureInput) void {
.toggle => self.is_secure_input = !self.is_secure_input,
}
}
pub fn ringBell(self: *Surface) !void {
const features = self.app.config.@"bell-features";
const window = self.container.window() orelse {
log.warn("failed to ring bell: surface is not attached to any window", .{});
return;
};
// System beep
if (features.system) system: {
const surface = window.window.as(gtk.Native).getSurface() orelse break :system;
surface.beep();
}
// Mark tab as needing attention
if (self.container.tab()) |tab| tab: {
const page = window.notebook.getTabPage(tab) orelse break :tab;
// Need attention if we're not the currently selected tab
if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true));
}
}

View File

@@ -114,9 +114,12 @@ pub fn gotoNthTab(self: *TabView, position: c_int) bool {
return true;
}
pub fn getTabPage(self: *TabView, tab: *Tab) ?*adw.TabPage {
return self.tab_view.getPage(tab.box.as(gtk.Widget));
}
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
return self.tab_view.getPagePosition(page);
return self.tab_view.getPagePosition(self.getTabPage(tab) orelse return null);
}
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
@@ -161,17 +164,16 @@ pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
}
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
_ = self.tab_view.reorderPage(page, position);
_ = self.tab_view.reorderPage(self.getTabPage(tab) orelse return, position);
}
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
const page = self.getTabPage(tab) orelse return;
page.setTitle(title.ptr);
}
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
const page = self.getTabPage(tab) orelse return;
page.setTooltip(tooltip.ptr);
}
@@ -203,8 +205,7 @@ pub fn closeTab(self: *TabView, tab: *Tab) void {
if (n > 1) self.forcing_close = false;
}
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
self.tab_view.closePage(page);
if (self.getTabPage(tab)) |page| self.tab_view.closePage(page);
// If we have no more tabs we close the window
if (self.nPages() == 0) {
@@ -260,6 +261,11 @@ fn adwTabViewCreateWindow(
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void {
const page = self.tab_view.getSelectedPage() orelse return;
// If the tab was previously marked as needing attention
// (e.g. due to a bell character), we now unmark that
page.setNeedsAttention(@intFromBool(false));
const title = page.getTitle();
self.window.setTitle(std.mem.span(title));
}

View File

@@ -81,6 +81,9 @@ pub const Message = union(enum) {
/// The terminal has reported a change in the working directory.
pwd_change: WriteReq,
/// The terminal encountered a bell character.
ring_bell,
pub const ReportTitleStyle = enum {
csi_21_t,

View File

@@ -1861,6 +1861,22 @@ keybind: Keybinds = .{},
/// open terminals.
@"custom-shader-animation": CustomShaderAnimation = .true,
/// The list of enabled features that are activated after encountering
/// a bell character.
///
/// Valid values are:
///
/// * `system` (default)
///
/// Instructs the system to notify the user using built-in system functions.
/// This could result in an audiovisual effect, a notification, or something
/// else entirely. Changing these effects require altering system settings:
/// for instance under the "Sound > Alert Sound" setting in GNOME,
/// or the "Accessibility > System Bell" settings in KDE Plasma.
///
/// Currently only implemented on Linux.
@"bell-features": BellFeatures = .{},
/// Control the in-app notifications that Ghostty shows.
///
/// On Linux (GTK), in-app notifications show up as toasts. Toasts appear
@@ -5691,6 +5707,11 @@ pub const AppNotifications = packed struct {
@"clipboard-copy": bool = true,
};
/// See bell-features
pub const BellFeatures = packed struct {
system: bool = false,
};
/// See mouse-shift-capture
pub const MouseShiftCapture = enum {
false,

View File

@@ -325,9 +325,8 @@ pub const StreamHandler = struct {
try self.terminal.printRepeat(count);
}
pub fn bell(self: StreamHandler) !void {
_ = self;
log.info("BELL", .{});
pub fn bell(self: *StreamHandler) !void {
self.surfaceMessageWriter(.ring_bell);
}
pub fn backspace(self: *StreamHandler) !void {