Skip to content

Commit c1698d6

Browse files
committed
gtk: implement audio bell
1 parent 6767493 commit c1698d6

File tree

11 files changed

+153
-3
lines changed

11 files changed

+153
-3
lines changed

include/ghostty.h

+1
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ typedef enum {
601601
GHOSTTY_ACTION_RELOAD_CONFIG,
602602
GHOSTTY_ACTION_CONFIG_CHANGE,
603603
GHOSTTY_ACTION_CLOSE_WINDOW,
604+
GHOSTTY_ACTION_BELL,
604605
} ghostty_action_tag_e;
605606

606607
typedef union {

nix/devShell.nix

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
gtk4,
3434
gtk4-layer-shell,
3535
gobject-introspection,
36+
gst_all_1,
3637
libadwaita,
3738
blueprint-compiler,
3839
gettext,
@@ -179,6 +180,9 @@ in
179180
wayland
180181
wayland-scanner
181182
wayland-protocols
183+
gst_all_1.gstreamer
184+
gst_all_1.gst-plugins-base
185+
gst_all_1.gst-plugins-good
182186
];
183187

184188
# This should be set onto the rpath of the ghostty binary if you want

nix/package.nix

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
gtk4,
1616
gtk4-layer-shell,
1717
gobject-introspection,
18+
gst_all_1,
1819
libadwaita,
1920
blueprint-compiler,
2021
libxml2,
@@ -113,6 +114,9 @@ in
113114
libadwaita
114115
gtk4
115116
glib
117+
gst_all_1.gstreamer
118+
gst_all_1.gst-plugins-base
119+
gst_all_1.gst-plugins-good
116120
gsettings-desktop-schemas
117121
]
118122
++ lib.optionals enableX11 [
@@ -165,6 +169,10 @@ in
165169
mv $out/share/vim/vimfiles "$vim"
166170
ln -sf "$vim" "$out/share/vim/vimfiles"
167171
echo "$vim" >> "$out/nix-support/propagated-user-env-packages"
172+
173+
echo "gst_all_1.gstreamer" >> "$out/nix-support/propagated-user-env-packages"
174+
echo "gst_all_1.gst-plugins-base" >> "$out/nix-support/propagated-user-env-packages"
175+
echo "gst_all_1.gst-plugins-good" >> "$out/nix-support/propagated-user-env-packages"
168176
'';
169177

170178
meta = {

src/Surface.zig

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

934934
.password_input => |v| try self.passwordInput(v),
935+
936+
.bell => self.bell(),
935937
}
936938
}
937939

@@ -4756,3 +4758,15 @@ fn presentSurface(self: *Surface) !void {
47564758
{},
47574759
);
47584760
}
4761+
4762+
fn bell(self: *Surface) void {
4763+
_ = self.rt_app.performAction(
4764+
.{ .surface = self },
4765+
.bell,
4766+
{},
4767+
) catch |err| {
4768+
// We ignore this error because we don't want to fail this entire
4769+
// operation just because the apprt failed to activate the bell.
4770+
log.warn("apprt failed to activate the bell err={}", .{err});
4771+
};
4772+
}

src/apprt/action.zig

+4
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ pub const Action = union(Key) {
244244
/// Closes the currently focused window.
245245
close_window,
246246

247+
/// The system has received a request to activate the bell.
248+
bell,
249+
247250
/// Sync with: ghostty_action_tag_e
248251
pub const Key = enum(c_int) {
249252
quit,
@@ -287,6 +290,7 @@ pub const Action = union(Key) {
287290
reload_config,
288291
config_change,
289292
close_window,
293+
bell,
290294
};
291295

292296
/// Sync with: ghostty_action_u

src/apprt/glfw.zig

+1
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ pub const App = struct {
242242
.toggle_maximize,
243243
.prompt_title,
244244
.reset_window_size,
245+
.bell,
245246
=> {
246247
log.info("unimplemented action={}", .{action});
247248
return false;

src/apprt/gtk/App.zig

+11
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ pub fn performAction(
510510
.prompt_title => try self.promptTitle(target),
511511
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
512512
.secure_input => self.setSecureInput(target, value),
513+
.bell => self.bell(target),
513514

514515
// Unimplemented
515516
.close_all_windows,
@@ -1014,6 +1015,16 @@ pub fn reloadConfig(
10141015
self.config = config;
10151016
}
10161017

1018+
fn bell(
1019+
_: *App,
1020+
target: apprt.Target,
1021+
) void {
1022+
switch (target) {
1023+
.app => {},
1024+
.surface => |surface| surface.rt_surface.bell(),
1025+
}
1026+
}
1027+
10171028
/// Call this anytime the configuration changes.
10181029
fn syncConfigChanges(self: *App) !void {
10191030
try self.updateConfigErrors();

src/apprt/gtk/Surface.zig

+69
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const adw = @import("adw");
99
const gtk = @import("gtk");
1010
const gio = @import("gio");
1111
const gobject = @import("gobject");
12+
const glib = @import("glib");
1213

1314
const Allocator = std.mem.Allocator;
1415
const build_config = @import("../../build_config.zig");
@@ -1239,6 +1240,74 @@ pub fn showDesktopNotification(
12391240
c.g_application_send_notification(g_app, body.ptr, notification);
12401241
}
12411242

1243+
/// Handle bell features.
1244+
pub fn bell(self: *Surface) void {
1245+
if (self.app.config.@"bell-features".system) system: {
1246+
// Handle "system" beep by calling the GTK "beep" API.
1247+
1248+
// FIXME: when surface is converted to zig-gobject
1249+
const native = gtk.Widget.getNative(@ptrCast(@alignCast(self.overlay))) orelse break :system;
1250+
const surface = native.getSurface() orelse break :system;
1251+
surface.beep();
1252+
}
1253+
if (self.app.config.@"bell-features".audio) audio: {
1254+
// Play a user-specified audio file.
1255+
1256+
const pathname, const optional = switch (self.app.config.@"bell-audio" orelse break :audio) {
1257+
.optional => |path| .{ path, true },
1258+
.required => |path| .{ path, false },
1259+
};
1260+
1261+
std.debug.assert(std.fs.path.isAbsolute(pathname));
1262+
const media_file = gtk.MediaFile.newForFilename(pathname);
1263+
1264+
if (!optional) {
1265+
_ = gobject.Object.signals.notify.connect(
1266+
media_file,
1267+
?*anyopaque,
1268+
gtkStreamError,
1269+
null,
1270+
.{ .detail = "error" },
1271+
);
1272+
}
1273+
_ = gobject.Object.signals.notify.connect(
1274+
media_file,
1275+
?*anyopaque,
1276+
gtkStreamEnded,
1277+
null,
1278+
.{ .detail = "ended" },
1279+
);
1280+
1281+
const media_stream = media_file.as(gtk.MediaStream);
1282+
media_stream.setVolume(1.0);
1283+
media_stream.play();
1284+
}
1285+
}
1286+
1287+
/// Handle a stream that is in an error state.
1288+
fn gtkStreamError(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.C) void {
1289+
const path = path: {
1290+
const file = media_file.getFile() orelse break :path null;
1291+
break :path file.getPath();
1292+
};
1293+
defer if (path) |p| glib.free(p);
1294+
1295+
const media_stream = media_file.as(gtk.MediaStream);
1296+
const err = media_stream.getError() orelse return;
1297+
1298+
log.warn("error playing bell from {s}: {s} {d} {s}", .{
1299+
path orelse "<<unknown>>",
1300+
glib.quarkToString(err.f_domain),
1301+
err.f_code,
1302+
err.f_message orelse "",
1303+
});
1304+
}
1305+
1306+
/// Stream is finished, release the memory.
1307+
fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.C) void {
1308+
media_file.unref();
1309+
}
1310+
12421311
fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
12431312
log.debug("gl surface realized", .{});
12441313

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

+36
Original file line numberDiff line numberDiff line change
@@ -2329,6 +2329,34 @@ term: []const u8 = "xterm-ghostty",
23292329
/// This only works on macOS since only macOS has an auto-update feature.
23302330
@"auto-update-channel": ?build_config.ReleaseChannel = null,
23312331

2332+
/// Bell features to enable if bell support is available in your runtime. The
2333+
/// format of this is a list of features to enable separated by commas. If you
2334+
/// prefix a feature with `no-` then it is disabled. If you omit a feature, its
2335+
/// default value is used, so you must explicitly disable features you don't
2336+
/// want.
2337+
///
2338+
/// Available features:
2339+
///
2340+
/// * `system` - Use a system function to play an audible sound. This differs
2341+
/// from the `audio` feature in that the sound played is not customizable
2342+
/// from within Ghostty. Your system may allow for the sound to be
2343+
/// customized externally to Ghostty (GTK only).
2344+
/// * `audio` - Play a custom sound. (GTK only).
2345+
///
2346+
/// Example: `audio`, `no-audio`, `system`, `no-system`:
2347+
///
2348+
/// By default, no bell features are enabled.
2349+
@"bell-features": BellFeatures = .{},
2350+
2351+
/// If `audio` is an enabled bell feature, this is a path to an audio file. If
2352+
/// the path is not absolute, it is considered relative to the directory of the
2353+
/// configuration file that it is referenced from, or from the current working
2354+
/// directory if this is used as a CLI flag. The path may be prefixed with `~/`
2355+
/// to reference the user's home directory.
2356+
///
2357+
/// GTK only.
2358+
@"bell-audio": ?Path = null,
2359+
23322360
/// This is set by the CLI parser for deinit.
23332361
_arena: ?ArenaAllocator = null,
23342362

@@ -6786,3 +6814,11 @@ test "theme specifying light/dark sets theme usage in conditional state" {
67866814
try testing.expect(cfg._conditional_set.contains(.theme));
67876815
}
67886816
}
6817+
6818+
/// Bell features
6819+
pub const BellFeatures = packed struct {
6820+
system: bool = false,
6821+
audio: bool = false,
6822+
6823+
pub const Features = std.meta.FieldEnum(@This());
6824+
};

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)