Skip to content

Commit 7d8f74e

Browse files
committed
gtk: implement audio bell
1 parent c5508e7 commit 7d8f74e

16 files changed

+249
-4
lines changed

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

Diff for: media/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
These files are copied from the xdg-sound-theme, found at:
2+
3+
https://gitlab.freedesktop.org/xdg/xdg-sound-theme

Diff for: media/bell.oga

8.3 KB
Binary file not shown.

Diff for: media/message.oga

10.2 KB
Binary file not shown.

Diff for: nix/devShell.nix

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
wayland,
5555
wayland-scanner,
5656
wayland-protocols,
57+
gst_all_1,
5758
}: let
5859
# See package.nix. Keep in sync.
5960
rpathLibs =
@@ -160,6 +161,9 @@ in
160161
wayland
161162
wayland-scanner
162163
wayland-protocols
164+
gst_all_1.gstreamer
165+
gst_all_1.gst-plugins-base
166+
gst_all_1.gst-plugins-good
163167
];
164168

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

Diff for: nix/package.nix

+10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
glib,
1414
gtk4,
1515
libadwaita,
16+
gst_all_1,
1617
wrapGAppsHook4,
1718
gsettings-desktop-schemas,
1819
git,
@@ -55,6 +56,7 @@
5556
../dist/linux
5657
../images
5758
../include
59+
../media
5860
../pkg
5961
../src
6062
../vendor
@@ -148,6 +150,10 @@ in
148150
libadwaita
149151
gtk4
150152
glib
153+
gst_all_1.gstreamer
154+
gst_all_1.gst-plugins-base
155+
gst_all_1.gst-plugins-good
156+
151157
gsettings-desktop-schemas
152158
]
153159
++ lib.optionals enableX11 [
@@ -199,6 +205,10 @@ in
199205
mv $out/share/vim/vimfiles "$vim"
200206
ln -sf "$vim" "$out/share/vim/vimfiles"
201207
echo "$vim" >> "$out/nix-support/propagated-user-env-packages"
208+
209+
echo "gst_all_1.gstreamer" >> "$out/nix-support/propagated-user-env-packages"
210+
echo "gst_all_1.gst-plugins-base" >> "$out/nix-support/propagated-user-env-packages"
211+
echo "gst_all_1.gst-plugins-good" >> "$out/nix-support/propagated-user-env-packages"
202212
'';
203213

204214
meta = {

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

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

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

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

+11
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ pub fn performAction(
540540
.toggle_split_zoom => self.toggleSplitZoom(target),
541541
.toggle_window_decorations => self.toggleWindowDecorations(target),
542542
.quit_timer => self.quitTimer(value),
543+
.bell => self.bell(target),
543544

544545
// Unimplemented
545546
.close_all_windows,
@@ -999,6 +1000,16 @@ pub fn reloadConfig(
9991000
self.config = config;
10001001
}
10011002

1003+
fn bell(
1004+
_: *App,
1005+
target: apprt.Target,
1006+
) void {
1007+
switch (target) {
1008+
.app => {},
1009+
.surface => |surface| surface.rt_surface.bell(),
1010+
}
1011+
}
1012+
10021013
/// Call this anytime the configuration changes.
10031014
fn syncConfigChanges(self: *App) !void {
10041015
try self.updateConfigErrors();

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

+80
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,86 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
12791279
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
12801280
}
12811281

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

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

+26-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ const icons = [_]struct {
5555

5656
pub const gresource_xml = comptimeGenerateGResourceXML();
5757

58+
const media = [_][]const u8{
59+
"media/bell.oga",
60+
"media/message.oga",
61+
};
62+
5863
fn comptimeGenerateGResourceXML() []const u8 {
5964
comptime {
6065
@setEvalBranchQuota(13000);
@@ -101,18 +106,38 @@ fn writeGResourceXML(writer: anytype) !void {
101106
}
102107
try writer.writeAll(
103108
\\ </gresource>
109+
\\
110+
);
111+
try writer.writeAll(
112+
\\ <gresource prefix="/com/mitchellh/ghostty/media">
113+
\\
114+
);
115+
for (media) |pathname| {
116+
try writer.print(
117+
" <file alias=\"{s}\">{s}</file>\n",
118+
.{ std.fs.path.basename(pathname), pathname },
119+
);
120+
}
121+
try writer.writeAll(
122+
\\ </gresource>
123+
\\
124+
);
125+
try writer.writeAll(
104126
\\</gresources>
105127
\\
106128
);
107129
}
108130

109131
pub const dependencies = deps: {
110-
var deps: [css_files.len + icons.len][]const u8 = undefined;
132+
var deps: [css_files.len + icons.len + media.len][]const u8 = undefined;
111133
for (css_files, 0..) |css_file, i| {
112134
deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
113135
}
114136
for (icons, css_files.len..) |icon, i| {
115137
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
116138
}
139+
for (media, css_files.len + icons.len..) |pathname, i| {
140+
deps[i] = std.fmt.comptimePrint("{s}", .{pathname});
141+
}
117142
break :deps deps;
118143
};

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.zig

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub const RepeatableString = Config.RepeatableString;
3030
pub const RepeatablePath = Config.RepeatablePath;
3131
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
3232
pub const WindowPaddingColor = Config.WindowPaddingColor;
33+
pub const BellFeatures = Config.BellFeatures;
3334

3435
// Alternate APIs
3536
pub const CAPI = @import("config/CAPI.zig");

0 commit comments

Comments
 (0)