Skip to content

Commit ab32939

Browse files
committed
gtk: implement audio bell
1 parent 0532f67 commit ab32939

File tree

9 files changed

+144
-3
lines changed

9 files changed

+144
-3
lines changed

include/ghostty.h

+1
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ typedef enum {
597597
GHOSTTY_ACTION_COLOR_CHANGE,
598598
GHOSTTY_ACTION_RELOAD_CONFIG,
599599
GHOSTTY_ACTION_CONFIG_CHANGE,
600+
GHOSTTY_ACTION_BELL,
600601
} ghostty_action_tag_e;
601602

602603
typedef union {

src/Surface.zig

+14
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
943943
.present_surface => try self.presentSurface(),
944944

945945
.password_input => |v| try self.passwordInput(v),
946+
947+
.bell => self.bell(),
946948
}
947949
}
948950

@@ -4694,3 +4696,15 @@ fn presentSurface(self: *Surface) !void {
46944696
{},
46954697
);
46964698
}
4699+
4700+
fn bell(self: *Surface) void {
4701+
self.rt_app.performAction(
4702+
.{ .surface = self },
4703+
.bell,
4704+
{},
4705+
) catch |err| {
4706+
// We ignore this error because we don't want to fail this entire
4707+
// operation just because the apprt failed to activate the bell.
4708+
log.warn("apprt failed to activate the bell err={}", .{err});
4709+
};
4710+
}

src/apprt/action.zig

+4
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ pub const Action = union(Key) {
226226
/// for changes.
227227
config_change: ConfigChange,
228228

229+
/// The system has received a request to activate the bell.
230+
bell: void,
231+
229232
/// Sync with: ghostty_action_tag_e
230233
pub const Key = enum(c_int) {
231234
quit,
@@ -266,6 +269,7 @@ pub const Action = union(Key) {
266269
color_change,
267270
reload_config,
268271
config_change,
272+
bell,
269273
};
270274

271275
/// Sync with: ghostty_action_u

src/apprt/glfw.zig

+1
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ pub const App = struct {
238238
.pwd,
239239
.config_change,
240240
.toggle_maximize,
241+
.bell,
241242
=> log.info("unimplemented action={}", .{action}),
242243
}
243244
}

src/apprt/gtk/App.zig

+11
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ pub fn performAction(
548548
.toggle_split_zoom => self.toggleSplitZoom(target),
549549
.toggle_window_decorations => self.toggleWindowDecorations(target),
550550
.quit_timer => self.quitTimer(value),
551+
.bell => self.bell(target),
551552

552553
// Unimplemented
553554
.close_all_windows,
@@ -1007,6 +1008,16 @@ pub fn reloadConfig(
10071008
self.config = config;
10081009
}
10091010

1011+
fn bell(
1012+
_: *App,
1013+
target: apprt.Target,
1014+
) void {
1015+
switch (target) {
1016+
.app => {},
1017+
.surface => |surface| surface.rt_surface.bell(),
1018+
}
1019+
}
1020+
10101021
/// Call this anytime the configuration changes.
10111022
fn syncConfigChanges(self: *App) !void {
10121023
try self.updateConfigErrors();

src/apprt/gtk/Surface.zig

+76
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,82 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
12931293
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
12941294
}
12951295

1296+
pub fn bell(self: *Surface) void {
1297+
inline for (std.meta.fields(configpkg.Config.BellFeatures.Features)) |field| {
1298+
const feature = std.meta.stringToEnum(configpkg.Config.BellFeatures.Features, field.name) orelse unreachable;
1299+
const enabled = @field(self.app.config.@"bell-features", field.name);
1300+
if (enabled) {
1301+
switch (feature) {
1302+
.system => system: {
1303+
const native = c.gtk_widget_get_native(@ptrCast(@alignCast(self.overlay))) orelse break :system;
1304+
const surface = c.gtk_native_get_surface(native) orelse break :system;
1305+
c.gdk_surface_beep(surface);
1306+
},
1307+
.audio => audio: {
1308+
var arena = std.heap.ArenaAllocator.init(self.app.core_app.alloc);
1309+
defer arena.deinit();
1310+
const alloc = arena.allocator();
1311+
const filename = self.app.config.@"bell-audio" orelse break :audio;
1312+
const pathname = pathname: {
1313+
if (std.fs.path.isAbsolute(filename))
1314+
break :pathname alloc.dupeZ(u8, filename) catch |err| {
1315+
log.warn("unable to allocate space for bell audio pathname: {}", .{err});
1316+
break :audio;
1317+
}
1318+
else
1319+
break :pathname std.fs.path.joinZ(alloc, &.{
1320+
internal_os.xdg.config(alloc, .{ .subdir = "ghostty/media" }) catch |err| {
1321+
log.warn("unable to determine media config subdir: {}", .{err});
1322+
break :audio;
1323+
},
1324+
filename,
1325+
}) catch |err| {
1326+
log.warn("unable to allocate space for bell audio pathname: {}", .{err});
1327+
break :audio;
1328+
};
1329+
};
1330+
std.fs.accessAbsoluteZ(pathname, .{ .mode = .read_only }) catch {
1331+
log.warn("unable to find sound file: {s}", .{filename});
1332+
break :audio;
1333+
};
1334+
const stream = c.gtk_media_file_new_for_filename(pathname);
1335+
_ = c.g_signal_connect_data(
1336+
stream,
1337+
"notify::error",
1338+
c.G_CALLBACK(&gtkStreamError),
1339+
stream,
1340+
null,
1341+
c.G_CONNECT_DEFAULT,
1342+
);
1343+
_ = c.g_signal_connect_data(
1344+
stream,
1345+
"notify::ended",
1346+
c.G_CALLBACK(&gtkStreamEnded),
1347+
stream,
1348+
null,
1349+
c.G_CONNECT_DEFAULT,
1350+
);
1351+
c.gtk_media_stream_set_volume(stream, 1.0);
1352+
c.gtk_media_stream_play(stream);
1353+
},
1354+
// inline else => {
1355+
// log.warn("bell feature '{s}' is not supported", .{field.name});
1356+
// },
1357+
}
1358+
}
1359+
}
1360+
}
1361+
1362+
fn gtkStreamError(stream: ?*c.GObject) callconv(.C) void {
1363+
const err = c.gtk_media_stream_get_error(@ptrCast(stream));
1364+
if (err) |e|
1365+
log.err("error playing bell: {s} {d} {s}", .{ c.g_quark_to_string(e.*.domain), e.*.code, e.*.message });
1366+
}
1367+
1368+
fn gtkStreamEnded(stream: ?*c.GObject) callconv(.C) void {
1369+
c.g_object_unref(stream);
1370+
}
1371+
12961372
fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
12971373
log.debug("gl surface realized", .{});
12981374

src/apprt/surface.zig

+3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ pub const Message = union(enum) {
8181
/// The terminal has reported a change in the working directory.
8282
pwd_change: WriteReq,
8383

84+
/// Bell
85+
bell: void,
86+
8487
pub const ReportTitleStyle = enum {
8588
csi_21_t,
8689

src/config/Config.zig

+32
Original file line numberDiff line numberDiff line change
@@ -2247,6 +2247,30 @@ term: []const u8 = "xterm-ghostty",
22472247
/// This only works on macOS since only macOS has an auto-update feature.
22482248
@"auto-update-channel": ?build_config.ReleaseChannel = null,
22492249

2250+
/// Bell features to enable if bell support is available in your runtime. The
2251+
/// format of this is a list of features to enable separated by commas. If you
2252+
/// prefix a feature with `no-` then it is disabled. If you omit a feature, its
2253+
/// default value is used, so you must explicitly disable features you don't
2254+
/// want.
2255+
///
2256+
/// Available features:
2257+
///
2258+
/// * `system` - Use a system function to play an audible sound. This differs
2259+
/// from the `audio` feature in that the sound played is not customizable
2260+
/// from within Ghostty. Your system may allow for the sound to be
2261+
/// customized externally to Ghostty.
2262+
/// * `audio` - Play a custom sound. (GTK only).
2263+
///
2264+
/// Example: `audio`, `no-audio`, `system`, `no-system`:
2265+
///
2266+
/// By default, no bell features are enabled.
2267+
@"bell-features": BellFeatures = .{},
2268+
2269+
/// If `audio` is an enabled bell feature, this is a path to an audio file.
2270+
/// If the path is not absolute, it is considered relative to the `media`
2271+
/// subdirectory of the Ghostty configuration directory.
2272+
@"bell-audio": ?[:0]const u8 = null,
2273+
22502274
/// This is set by the CLI parser for deinit.
22512275
_arena: ?ArenaAllocator = null,
22522276

@@ -6881,3 +6905,11 @@ test "theme specifying light/dark sets theme usage in conditional state" {
68816905
try testing.expect(cfg._conditional_set.contains(.theme));
68826906
}
68836907
}
6908+
6909+
/// Bell features
6910+
pub const BellFeatures = packed struct {
6911+
system: bool = false,
6912+
audio: bool = false,
6913+
6914+
pub const Features = std.meta.FieldEnum(@This());
6915+
};

src/termio/stream_handler.zig

+2-3
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,8 @@ pub const StreamHandler = struct {
325325
try self.terminal.printRepeat(count);
326326
}
327327

328-
pub fn bell(self: StreamHandler) !void {
329-
_ = self;
330-
log.info("BELL", .{});
328+
pub fn bell(self: *StreamHandler) !void {
329+
self.surfaceMessageWriter(.{ .bell = {} });
331330
}
332331

333332
pub fn backspace(self: *StreamHandler) !void {

0 commit comments

Comments
 (0)