Skip to content

Commit 32b8873

Browse files
committed
gtk: implement audio bell
1 parent 256281c commit 32b8873

File tree

11 files changed

+159
-3
lines changed

11 files changed

+159
-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 {

nix/devShell.nix

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
glslang,
3232
gtk4,
3333
gobject-introspection,
34+
gst_all_1,
3435
libadwaita,
3536
adwaita-icon-theme,
3637
hicolor-icon-theme,
@@ -166,6 +167,9 @@ in
166167
wayland
167168
wayland-scanner
168169
wayland-protocols
170+
gst_all_1.gstreamer
171+
gst_all_1.gst-plugins-base
172+
gst_all_1.gst-plugins-good
169173
];
170174

171175
# 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
@@ -14,6 +14,7 @@
1414
glib,
1515
gtk4,
1616
gobject-introspection,
17+
gst_all_1,
1718
libadwaita,
1819
wrapGAppsHook4,
1920
gsettings-desktop-schemas,
@@ -105,6 +106,9 @@ in
105106
libadwaita
106107
gtk4
107108
glib
109+
gst_all_1.gstreamer
110+
gst_all_1.gst-plugins-base
111+
gst_all_1.gst-plugins-good
108112
gsettings-desktop-schemas
109113
]
110114
++ lib.optionals enableX11 [
@@ -156,6 +160,10 @@ in
156160
mv $out/share/vim/vimfiles "$vim"
157161
ln -sf "$vim" "$out/share/vim/vimfiles"
158162
echo "$vim" >> "$out/nix-support/propagated-user-env-packages"
163+
164+
echo "gst_all_1.gstreamer" >> "$out/nix-support/propagated-user-env-packages"
165+
echo "gst_all_1.gst-plugins-base" >> "$out/nix-support/propagated-user-env-packages"
166+
echo "gst_all_1.gst-plugins-good" >> "$out/nix-support/propagated-user-env-packages"
159167
'';
160168

161169
meta = {

src/Surface.zig

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

953953
.password_input => |v| try self.passwordInput(v),
954+
955+
.bell => self.bell(),
954956
}
955957
}
956958

@@ -4702,3 +4704,15 @@ fn presentSurface(self: *Surface) !void {
47024704
{},
47034705
);
47044706
}
4707+
4708+
fn bell(self: *Surface) void {
4709+
_ = self.rt_app.performAction(
4710+
.{ .surface = self },
4711+
.bell,
4712+
{},
4713+
) catch |err| {
4714+
// We ignore this error because we don't want to fail this entire
4715+
// operation just because the apprt failed to activate the bell.
4716+
log.warn("apprt failed to activate the bell err={}", .{err});
4717+
};
4718+
}

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
@@ -239,6 +239,7 @@ pub const App = struct {
239239
.pwd,
240240
.config_change,
241241
.toggle_maximize,
242+
.bell,
242243
=> {
243244
log.info("unimplemented action={}", .{action});
244245
return false;

src/apprt/gtk/App.zig

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

553554
// Unimplemented
554555
.close_all_windows,
@@ -1019,6 +1020,16 @@ pub fn reloadConfig(
10191020
self.config = config;
10201021
}
10211022

1023+
fn bell(
1024+
_: *App,
1025+
target: apprt.Target,
1026+
) void {
1027+
switch (target) {
1028+
.app => {},
1029+
.surface => |surface| surface.rt_surface.bell(),
1030+
}
1031+
}
1032+
10221033
/// Call this anytime the configuration changes.
10231034
fn syncConfigChanges(self: *App) !void {
10241035
try self.updateConfigErrors();

src/apprt/gtk/Surface.zig

+79
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const inspector = @import("inspector.zig");
2626
const gtk_key = @import("key.zig");
2727
const c = @import("c.zig").c;
2828

29+
const gtk = @import("gtk");
30+
const gobject = @import("gobject");
31+
const glib = @import("glib");
32+
2933
const log = std.log.scoped(.gtk_surface);
3034

3135
/// This is detected by the OpenGL renderer to move to a single-threaded
@@ -1295,6 +1299,81 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
12951299
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
12961300
}
12971301

1302+
pub fn bell(self: *Surface) void {
1303+
inline for (std.meta.fields(configpkg.Config.BellFeatures.Features)) |field| {
1304+
const feature = std.meta.stringToEnum(configpkg.Config.BellFeatures.Features, field.name) orelse unreachable;
1305+
const enabled = @field(self.app.config.@"bell-features", field.name);
1306+
if (enabled) {
1307+
switch (feature) {
1308+
.system => system: {
1309+
const native = gtk.Widget.getNative(@ptrCast(@alignCast(self.overlay))) orelse break :system;
1310+
const surface = native.getSurface() orelse break :system;
1311+
surface.beep();
1312+
},
1313+
.audio => audio: {
1314+
var arena = std.heap.ArenaAllocator.init(self.app.core_app.alloc);
1315+
defer arena.deinit();
1316+
const alloc = arena.allocator();
1317+
const filename = self.app.config.@"bell-audio" orelse break :audio;
1318+
const pathname = pathname: {
1319+
if (std.fs.path.isAbsolute(filename))
1320+
break :pathname alloc.dupeZ(u8, filename) catch |err| {
1321+
log.warn("unable to allocate space for bell audio pathname: {}", .{err});
1322+
break :audio;
1323+
}
1324+
else
1325+
break :pathname std.fs.path.joinZ(alloc, &.{
1326+
internal_os.xdg.config(alloc, .{ .subdir = "ghostty/media" }) catch |err| {
1327+
log.warn("unable to determine media config subdir: {}", .{err});
1328+
break :audio;
1329+
},
1330+
filename,
1331+
}) catch |err| {
1332+
log.warn("unable to allocate space for bell audio pathname: {}", .{err});
1333+
break :audio;
1334+
};
1335+
};
1336+
std.fs.accessAbsoluteZ(pathname, .{ .mode = .read_only }) catch {
1337+
log.warn("unable to find sound file: {s}", .{filename});
1338+
break :audio;
1339+
};
1340+
1341+
const file = gtk.MediaFile.newForFilename(pathname);
1342+
const stream = file.as(gtk.MediaStream);
1343+
1344+
_ = gobject.Object.signals.notify.connect(
1345+
stream,
1346+
?*anyopaque,
1347+
gtkStreamError,
1348+
null,
1349+
.{ .detail = "error" },
1350+
);
1351+
_ = gobject.Object.signals.notify.connect(
1352+
stream,
1353+
?*anyopaque,
1354+
gtkStreamEnded,
1355+
null,
1356+
.{ .detail = "ended" },
1357+
);
1358+
1359+
stream.setVolume(1.0);
1360+
stream.play();
1361+
},
1362+
}
1363+
}
1364+
}
1365+
}
1366+
1367+
fn gtkStreamError(stream: *gtk.MediaStream, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.C) void {
1368+
const err = stream.getError();
1369+
if (err) |e|
1370+
log.err("error playing bell: {s} {d} {s}", .{ glib.quarkToString(e.f_domain), e.f_code, e.f_message orelse "" });
1371+
}
1372+
1373+
fn gtkStreamEnded(stream: *gtk.MediaStream, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.C) void {
1374+
stream.unref();
1375+
}
1376+
12981377
fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
12991378
log.debug("gl surface realized", .{});
13001379

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
@@ -2250,6 +2250,30 @@ term: []const u8 = "xterm-ghostty",
22502250
/// This only works on macOS since only macOS has an auto-update feature.
22512251
@"auto-update-channel": ?build_config.ReleaseChannel = null,
22522252

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

@@ -6892,3 +6916,11 @@ test "theme specifying light/dark sets theme usage in conditional state" {
68926916
try testing.expect(cfg._conditional_set.contains(.theme));
68936917
}
68946918
}
6919+
6920+
/// Bell features
6921+
pub const BellFeatures = packed struct {
6922+
system: bool = false,
6923+
audio: bool = false,
6924+
6925+
pub const Features = std.meta.FieldEnum(@This());
6926+
};

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)