Skip to content

Commit b24024f

Browse files
committed
gtk: implement audio bell
1 parent 2d0940f commit b24024f

File tree

11 files changed

+159
-3
lines changed

11 files changed

+159
-3
lines changed

Diff for: include/ghostty.h

+1
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@ typedef enum {
599599
GHOSTTY_ACTION_COLOR_CHANGE,
600600
GHOSTTY_ACTION_RELOAD_CONFIG,
601601
GHOSTTY_ACTION_CONFIG_CHANGE,
602+
GHOSTTY_ACTION_BELL,
602603
} ghostty_action_tag_e;
603604

604605
typedef union {

Diff for: 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
blueprint-compiler,
3637
adwaita-icon-theme,
@@ -170,6 +171,9 @@ in
170171
wayland
171172
wayland-scanner
172173
wayland-protocols
174+
gst_all_1.gstreamer
175+
gst_all_1.gst-plugins-base
176+
gst_all_1.gst-plugins-good
173177
];
174178

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

Diff for: 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
blueprint-compiler,
1920
wrapGAppsHook4,
@@ -107,6 +108,9 @@ in
107108
libadwaita
108109
gtk4
109110
glib
111+
gst_all_1.gstreamer
112+
gst_all_1.gst-plugins-base
113+
gst_all_1.gst-plugins-good
110114
gsettings-desktop-schemas
111115
]
112116
++ lib.optionals enableX11 [
@@ -158,6 +162,10 @@ in
158162
mv $out/share/vim/vimfiles "$vim"
159163
ln -sf "$vim" "$out/share/vim/vimfiles"
160164
echo "$vim" >> "$out/nix-support/propagated-user-env-packages"
165+
166+
echo "gst_all_1.gstreamer" >> "$out/nix-support/propagated-user-env-packages"
167+
echo "gst_all_1.gst-plugins-base" >> "$out/nix-support/propagated-user-env-packages"
168+
echo "gst_all_1.gst-plugins-good" >> "$out/nix-support/propagated-user-env-packages"
161169
'';
162170

163171
meta = {

Diff for: src/Surface.zig

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

955955
.password_input => |v| try self.passwordInput(v),
956+
957+
.bell => self.bell(),
956958
}
957959
}
958960

@@ -4711,3 +4713,15 @@ fn presentSurface(self: *Surface) !void {
47114713
{},
47124714
);
47134715
}
4716+
4717+
fn bell(self: *Surface) void {
4718+
_ = self.rt_app.performAction(
4719+
.{ .surface = self },
4720+
.bell,
4721+
{},
4722+
) catch |err| {
4723+
// We ignore this error because we don't want to fail this entire
4724+
// operation just because the apprt failed to activate the bell.
4725+
log.warn("apprt failed to activate the bell err={}", .{err});
4726+
};
4727+
}

Diff for: src/apprt/action.zig

+4
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ pub const Action = union(Key) {
230230
/// for changes.
231231
config_change: ConfigChange,
232232

233+
/// The system has received a request to activate the bell.
234+
bell: void,
235+
233236
/// Sync with: ghostty_action_tag_e
234237
pub const Key = enum(c_int) {
235238
quit,
@@ -271,6 +274,7 @@ pub const Action = union(Key) {
271274
color_change,
272275
reload_config,
273276
config_change,
277+
bell,
274278
};
275279

276280
/// Sync with: ghostty_action_u

Diff for: src/apprt/glfw.zig

+1
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ pub const App = struct {
240240
.config_change,
241241
.toggle_maximize,
242242
.prompt_title,
243+
.bell,
243244
=> {
244245
log.info("unimplemented action={}", .{action});
245246
return false;

Diff for: src/apprt/gtk/App.zig

+11
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ pub fn performAction(
482482
.toggle_split_zoom => self.toggleSplitZoom(target),
483483
.toggle_window_decorations => self.toggleWindowDecorations(target),
484484
.quit_timer => self.quitTimer(value),
485+
.bell => self.bell(target),
485486

486487
// Unimplemented
487488
.close_all_windows,
@@ -951,6 +952,16 @@ pub fn reloadConfig(
951952
self.config = config;
952953
}
953954

955+
fn bell(
956+
_: *App,
957+
target: apprt.Target,
958+
) void {
959+
switch (target) {
960+
.app => {},
961+
.surface => |surface| surface.rt_surface.bell(),
962+
}
963+
}
964+
954965
/// Call this anytime the configuration changes.
955966
fn syncConfigChanges(self: *App) !void {
956967
try self.updateConfigErrors();

Diff for: 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

Diff for: 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

Diff for: src/config/Config.zig

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

2275+
/// Bell features to enable if bell support is available in your runtime. The
2276+
/// format of this is a list of features to enable separated by commas. If you
2277+
/// prefix a feature with `no-` then it is disabled. If you omit a feature, its
2278+
/// default value is used, so you must explicitly disable features you don't
2279+
/// want.
2280+
///
2281+
/// Available features:
2282+
///
2283+
/// * `system` - Use a system function to play an audible sound. This differs
2284+
/// from the `audio` feature in that the sound played is not customizable
2285+
/// from within Ghostty. Your system may allow for the sound to be
2286+
/// customized externally to Ghostty.
2287+
/// * `audio` - Play a custom sound. (GTK only).
2288+
///
2289+
/// Example: `audio`, `no-audio`, `system`, `no-system`:
2290+
///
2291+
/// By default, no bell features are enabled.
2292+
@"bell-features": BellFeatures = .{},
2293+
2294+
/// If `audio` is an enabled bell feature, this is a path to an audio file.
2295+
/// If the path is not absolute, it is considered relative to the `media`
2296+
/// subdirectory of the Ghostty configuration directory.
2297+
@"bell-audio": ?[:0]const u8 = null,
2298+
22752299
/// This is set by the CLI parser for deinit.
22762300
_arena: ?ArenaAllocator = null,
22772301

@@ -6913,3 +6937,11 @@ test "theme specifying light/dark sets theme usage in conditional state" {
69136937
try testing.expect(cfg._conditional_set.contains(.theme));
69146938
}
69156939
}
6940+
6941+
/// Bell features
6942+
pub const BellFeatures = packed struct {
6943+
system: bool = false,
6944+
audio: bool = false,
6945+
6946+
pub const Features = std.meta.FieldEnum(@This());
6947+
};

Diff for: 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)