Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gtk: terminal bell #2231

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions media/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
These files are copied from the xdg-sound-theme, found at:

https://gitlab.freedesktop.org/xdg/xdg-sound-theme
Binary file added media/bell.oga
Binary file not shown.
Binary file added media/message.oga
Binary file not shown.
4 changes: 4 additions & 0 deletions nix/devShell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
pandoc,
hyperfine,
typos,
gst_all_1,
}: let
# See package.nix. Keep in sync.
rpathLibs =
Expand Down Expand Up @@ -153,6 +154,9 @@ in
libadwaita
gtk4
glib
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
];

# This should be set onto the rpath of the ghostty binary if you want
Expand Down
10 changes: 10 additions & 0 deletions nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
glib,
gtk4,
libadwaita,
gst_all_1,
wrapGAppsHook4,
gsettings-desktop-schemas,
git,
Expand Down Expand Up @@ -51,6 +52,7 @@
../conformance
../images
../include
../media
../pkg
../src
../vendor
Expand Down Expand Up @@ -144,6 +146,10 @@ in
libadwaita
gtk4
glib
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good

gsettings-desktop-schemas
];

Expand Down Expand Up @@ -177,6 +183,10 @@ in
mv "$out/share/ghostty/shell-integration" "$shell_integration/shell-integration"
ln -sf "$shell_integration/shell-integration" "$out/share/ghostty/shell-integration"
echo "$shell_integration" >> "$out/nix-support/propagated-user-env-packages"

echo "gst_all_1.gstreamer" >> "$out/nix-support/propagated-user-env-packages"
echo "gst_all_1.gst-plugins-base" >> "$out/nix-support/propagated-user-env-packages"
echo "gst_all_1.gst-plugins-good" >> "$out/nix-support/propagated-user-env-packages"
'';

postFixup = ''
Expand Down
8 changes: 8 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.present_surface => try self.presentSurface(),

.password_input => |v| try self.passwordInput(v),

.bell => try self.bell(),
}
}

Expand Down Expand Up @@ -4472,3 +4474,9 @@ fn presentSurface(self: *Surface) !void {
{},
);
}

fn bell(self: *Surface) !void {
if (@hasDecl(apprt.Surface, "bell")) {
try self.rt_surface.bell();
} else log.warn("runtime doesn't support bell", .{});
}
71 changes: 71 additions & 0 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,77 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
}

pub fn bell(self: *Surface) !void {
if (self.app.config.@"bell-features".audio) audio: {
const stream = switch (self.app.config.@"bell-audio") {
.bell => c.gtk_media_file_new_for_resource("/com/mitchellh/ghostty/media/bell.oga"),
.message => c.gtk_media_file_new_for_resource("/com/mitchellh/ghostty/media/message.oga"),
.custom => |filename| stream: {
var arena = std.heap.ArenaAllocator.init(self.app.core_app.alloc);
defer arena.deinit();
const alloc = arena.allocator();
const pathname = pathname: {
if (std.fs.path.isAbsolute(filename))
break :pathname try alloc.dupeZ(u8, filename)
else
break :pathname try std.fs.path.joinZ(alloc, &.{
try internal_os.xdg.config(
alloc,
.{ .subdir = "ghostty/media" },
),
filename,
});
};
std.fs.accessAbsoluteZ(pathname, .{ .mode = .read_only }) catch {
log.warn("unable to find sound file: {s}", .{filename});
break :audio;
};
break :stream c.gtk_media_file_new_for_filename(pathname);
},
};
_ = c.g_signal_connect_data(
stream,
"notify::error",
c.G_CALLBACK(&gtkStreamError),
stream,
null,
c.G_CONNECT_DEFAULT,
);
_ = c.g_signal_connect_data(
stream,
"notify::ended",
c.G_CALLBACK(&gtkStreamEnded),
stream,
null,
c.G_CONNECT_DEFAULT,
);
c.gtk_media_stream_set_volume(stream, 1.0);
c.gtk_media_stream_play(stream);
}
if (self.app.config.@"bell-features".visual) {
log.warn("visual bell is not supported", .{});
}
if (self.app.config.@"bell-features".notification) {
log.warn("notification bell is not supported", .{});
}
if (self.app.config.@"bell-features".title) {
log.warn("title bell is not supported", .{});
}
if (self.app.config.@"bell-features".command) {
log.warn("command bell is not supported", .{});
}
}

fn gtkStreamError(stream: ?*c.GObject) callconv(.C) void {
const err = c.gtk_media_stream_get_error(@ptrCast(stream));
if (err) |e|
log.err("error playing bell: {s} {d} {s}", .{ c.g_quark_to_string(e.*.domain), e.*.code, e.*.message });
}

fn gtkStreamEnded(stream: ?*c.GObject) callconv(.C) void {
c.g_object_unref(stream);
}

fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
log.debug("gl surface realized", .{});

Expand Down
21 changes: 21 additions & 0 deletions src/apprt/gtk/gresource.zig
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const icons = [_]struct {
};

pub const gresource_xml = comptimeGenerateGResourceXML();
const media = [_][]const u8{
"media/bell.oga",
"media/message.oga",
};

fn comptimeGenerateGResourceXML() []const u8 {
comptime {
Expand Down Expand Up @@ -97,6 +101,23 @@ fn writeGResourceXML(writer: anytype) !void {
}
try writer.writeAll(
\\ </gresource>
\\
);
try writer.writeAll(
\\ <gresource prefix="/com/mitchellh/ghostty/media">
\\
);
for (media) |pathname| {
try writer.print(
" <file alias=\"{s}\">{s}</file>\n",
.{ std.fs.path.basename(pathname), pathname },
);
}
try writer.writeAll(
\\ </gresource>
\\
);
try writer.writeAll(
\\</gresources>
\\
);
Expand Down
3 changes: 3 additions & 0 deletions src/apprt/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ pub const Message = union(enum) {
/// The terminal has reported a change in the working directory.
pwd_change: WriteReq,

/// Bell
bell: void,

pub const ReportTitleStyle = enum {
csi_21_t,

Expand Down
104 changes: 104 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,51 @@ term: []const u8 = "xterm-ghostty",
/// Changing this value at runtime works after a small delay.
@"auto-update": AutoUpdate = .check,

/// Bell features to enable if bell support is available in your runtime. The
/// format of this is a list of features to enable separated by commas. If you
/// prefix a feature with `no-` then it is disabled. If you omit a feature, its
/// default value is used, so you must explicitly disable features you don't
/// want.
///
/// Available features:
///
/// * `audio` - Play an audible sound. (GTK only).
///
/// * `visual` - Flashes a visual indication in the surface that triggered
/// the bell. (Currently not implemented.)
///
/// * `notification` - Displays a desktop notification. (Currently not
/// implemented.)
///
/// * `title` - Will add a visual indicator to the window/tab title.
/// (Currently not implemented.)
///
/// * `command` - Will run a command (e.g. for haptic feedback or flashing a
/// physical light). (Currently not implemented.)
///
/// Example: `audio`, `no-audio`, `visual`, `no-visual`, `notification`, `no-notification`
///
/// By default, no bell features are enabled.
@"bell-features": BellFeatures = .{},

/// If `audio` is an enabled bell feature, this determines whether to use an
/// internal audio file or whether to use a custom file on disk.
///
/// * `bell` - A simple bell sound.
///
/// * `message` - Another bell sound.
///
/// * `custom:<filename>` - The filename of an audio file to play as the bell.
/// If the filename is not an absolute pathname the directory `~/.config/
/// ghostty/media` will be searched for the file.
///
/// The default value is `bell`
@"bell-audio": BellAudio = .{ .bell = {} },

/// If `command` is an enabled bell feature, the command to be run. By default,
/// this value is unset and no command will run.
@"bell-command": ?[:0]const u8 = null,

/// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null,

Expand Down Expand Up @@ -4940,3 +4985,62 @@ test "test entryFormatter" {
try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.items);
}

/// Bell features
pub const BellFeatures = packed struct {
audio: bool = false,
visual: bool = false,
notification: bool = false,
title: bool = false,
command: bool = false,
};

pub const BellAudio = union(enum) {
bell: void,
message: void,
custom: [:0]const u8,

pub fn formatEntry(self: BellAudio, formatter: anytype) !void {
switch (self) {
.bell, .message => try formatter.formatEntry([]const u8, @tagName(self)),
.custom => |filename| {
var buf: [std.fs.max_path_bytes + 7]u8 = undefined;
try formatter.formatEntry(
[]const u8,
std.fmt.bufPrint(
&buf,
"custom:{s}",
.{filename},
) catch return error.OutOfMemory,
);
},
}
}

test "test formatEntry 1" {
var buf = std.ArrayList(u8).init(std.testing.allocator);
defer buf.deinit();

var b: BellAudio = .{ .bell = {} };
try b.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualStrings("a = bell\n", buf.items);
}

test "test formatEntry 2" {
var buf = std.ArrayList(u8).init(std.testing.allocator);
defer buf.deinit();

var b: BellAudio = .{ .message = {} };
try b.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualStrings("a = message\n", buf.items);
}

test "test formatEntry 3" {
var buf = std.ArrayList(u8).init(std.testing.allocator);
defer buf.deinit();

var b: BellAudio = .{ .custom = "custom.oga" };
try b.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualStrings("a = custom:custom.oga\n", buf.items);
}
};
5 changes: 2 additions & 3 deletions src/termio/stream_handler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,8 @@ pub const StreamHandler = struct {
try self.terminal.printRepeat(count);
}

pub fn bell(self: StreamHandler) !void {
_ = self;
log.info("BELL", .{});
pub fn bell(self: *StreamHandler) !void {
self.surfaceMessageWriter(.{ .bell = {} });
}

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