From 7206d9dbe3973cbb870b71121835aa46f625a251 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 26 Sep 2024 17:31:09 -0500 Subject: [PATCH] gtk: terminal bell Use GTK's media streaming APIs to implement the terminal bell. By default it's disabled since it's likely that only a small subset of people will want this feature. Currently only audio support is implemented but there's scaffolding for several other methods of alerting the user. NOTE: GTK needs to have access to GStreamer and the "base" and "good" plugins or it crashes when you try to play a sound. I've added the necessary packages to the NixOS developer shell and package but I haven't found a method yet to detect the situation and avoid the crash. --- media/README.md | 3 + media/bell.oga | Bin 0 -> 8495 bytes media/message.oga | Bin 0 -> 10429 bytes nix/devShell.nix | 4 + nix/package.nix | 10 ++ src/Surface.zig | 8 ++ src/apprt/gtk/Surface.zig | 50 +++++++++ src/apprt/gtk/gresource.zig | 21 ++++ src/apprt/surface.zig | 3 + src/config/Config.zig | 193 ++++++++++++++++++++++++++++++++++ src/termio/stream_handler.zig | 5 +- 11 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 media/README.md create mode 100644 media/bell.oga create mode 100644 media/message.oga diff --git a/media/README.md b/media/README.md new file mode 100644 index 000000000..3d1be07bf --- /dev/null +++ b/media/README.md @@ -0,0 +1,3 @@ +These files are copied from the xdg-sound-theme, found at: + +https://gitlab.freedesktop.org/xdg/xdg-sound-theme diff --git a/media/bell.oga b/media/bell.oga new file mode 100644 index 0000000000000000000000000000000000000000..144d2b3670e8e294ee7cfe343355bf90123cb687 GIT binary patch literal 8495 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 literal 0 HcmV?d00001 diff --git a/nix/devShell.nix b/nix/devShell.nix index 7f0e206b7..3d186297f 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -51,6 +51,7 @@ pandoc, hyperfine, typos, + gst_all_1, }: let # See package.nix. Keep in sync. rpathLibs = @@ -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 diff --git a/nix/package.nix b/nix/package.nix index 889eb978f..e4477035a 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -17,6 +17,7 @@ glib, gtk4, libadwaita, + gst_all_1, wrapGAppsHook4, gsettings-desktop-schemas, git, @@ -51,6 +52,7 @@ ../conformance ../images ../include + ../media ../pkg ../src ../vendor @@ -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 ]; @@ -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 = '' diff --git a/src/Surface.zig b/src/Surface.zig index fbb589638..e497d005e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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(), } } @@ -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", .{}); +} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 8172b7490..5ec41afe4 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1153,6 +1153,56 @@ 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) { + 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| c.gtk_media_file_new_for_filename(filename), + }; + _ = c.g_signal_connect_data( + stream, + "notify::error", + c.G_CALLBACK(>kStreamError), + stream, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + stream, + "notify::ended", + c.G_CALLBACK(>kStreamEnded), + 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", .{}); diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index db987cbea..13e21a93b 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -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 { @@ -97,6 +101,23 @@ fn writeGResourceXML(writer: anytype) !void { } try writer.writeAll( \\ + \\ + ); + try writer.writeAll( + \\ + \\ + ); + for (media) |pathname| { + try writer.print( + " {s}\n", + .{ std.fs.path.basename(pathname), pathname }, + ); + } + try writer.writeAll( + \\ + \\ + ); + try writer.writeAll( \\ \\ ); diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 58faa9633..7aa4004e8 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -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, diff --git a/src/config/Config.zig b/src/config/Config.zig index a3ee8ccf0..5323bd7a9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1721,6 +1721,50 @@ 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. (Currently Linux-only if libcanberra +/// support has been compiled in.) +/// +/// * `visual` - Flashes a visiual indication in the surface that triggered +/// the bell. (Currently not implemented.) +/// +/// * `notification` - Displays a desktop notification. +/// +/// * `title` - Will add a visual indicator to the window/tab title. +/// +/// * `command` - Will run a command (e.g. for haptic feedback or flashing a +/// physical light). +/// +/// 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:` - 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, @@ -4940,3 +4984,152 @@ 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 parseCLI(self: *BellAudio, alloc: std.mem.Allocator, input: ?[]const u8) !void { + const value = input orelse return error.ValueRequired; + const key_str = value[0 .. std.mem.indexOfScalar(u8, value, ':') orelse value.len]; + if (std.meta.stringToEnum(std.meta.Tag(BellAudio), std.mem.trim(u8, key_str, &std.ascii.whitespace))) |key| switch (key) { + .bell => { + self.* = .{ .bell = {} }; + }, + .message => { + self.* = .{ .message = {} }; + }, + .custom => { + if (key_str.len == value.len) return error.ValueRequired; + const rest = std.mem.trim(u8, value[key_str.len + 1 ..], &std.ascii.whitespace); + if (rest.len == 0) return error.ValueRequired; + if (std.fs.path.isAbsolute(rest)) + self.* = .{ + .custom = try alloc.dupeZ(u8, rest), + } + else + self.* = .{ + .custom = try std.fs.path.joinZ(alloc, &.{ + try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/media" }), + rest, + }), + }; + }, + } else { + return error.ValueRequired; + } + } + + 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 "parseCLI" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + { + var b: BellAudio = undefined; + try b.parseCLI(alloc, "bell"); + try std.testing.expect(b == .bell); + } + { + var b: BellAudio = undefined; + try b.parseCLI(alloc, "message"); + try std.testing.expect(b == .message); + } + { + var b: BellAudio = undefined; + try b.parseCLI(alloc, "message:"); + try std.testing.expect(b == .message); + } + { + var b: BellAudio = undefined; + try b.parseCLI(alloc, " message : "); + try std.testing.expect(b == .message); + } + { + var b: BellAudio = undefined; + try b.parseCLI(alloc, "custom:/tmp/bell.oga"); + try std.testing.expect(b == .custom); + try std.testing.expectEqualStrings("/tmp/bell.oga", b.custom); + } + { + var b: BellAudio = undefined; + try b.parseCLI(alloc, " custom : /tmp/bell.oga "); + try std.testing.expect(b == .custom); + try std.testing.expectEqualStrings("/tmp/bell.oga", b.custom); + } + { + var b: BellAudio = undefined; + try std.testing.expectError(error.ValueRequired, b.parseCLI(alloc, " custom : ")); + } + { + var b: BellAudio = undefined; + try std.testing.expectError(error.ValueRequired, b.parseCLI(alloc, " custom ")); + } + { + var b: BellAudio = undefined; + try std.testing.expectError(error.ValueRequired, b.parseCLI(alloc, " ")); + } + { + var b: BellAudio = undefined; + try std.testing.expectError(error.ValueRequired, b.parseCLI(alloc, "")); + } + { + var b: BellAudio = undefined; + try std.testing.expectError(error.ValueRequired, b.parseCLI(alloc, null)); + } + } + + 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); + } +}; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 37d176de3..ac3a4b4b7 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -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 {