mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
Compare commits
6 Commits
8d11c08db3
...
gtk-ng-bel
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1f104cc09c | ||
![]() |
2444fcdce5 | ||
![]() |
f9896c8ef7 | ||
![]() |
156278e6c1 | ||
![]() |
02e3aac1e5 | ||
![]() |
c1b187d120 |
@@ -53,6 +53,23 @@ pub fn Common(
|
||||
}
|
||||
}).private else {};
|
||||
|
||||
/// A helper that creates a property that reads and writes a
|
||||
/// private field with only shallow copies. This is good for primitives
|
||||
/// such as bools, numbers, etc.
|
||||
pub fn privateShallowFieldAccessor(
|
||||
comptime name: []const u8,
|
||||
) gobject.ext.Accessor(
|
||||
Self,
|
||||
@FieldType(Private.?, name),
|
||||
) {
|
||||
return gobject.ext.privateFieldAccessor(
|
||||
Self,
|
||||
Private.?,
|
||||
&Private.?.offset,
|
||||
name,
|
||||
);
|
||||
}
|
||||
|
||||
/// A helper that can be used to create a property that reads and
|
||||
/// writes a private boxed gobject field type.
|
||||
///
|
||||
|
@@ -2043,7 +2043,7 @@ const Action = struct {
|
||||
pub fn ringBell(target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.ringBell(),
|
||||
.surface => |v| v.rt_surface.surface.setBellRinging(true),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -418,6 +418,20 @@ pub const SplitTree = extern struct {
|
||||
if (entry.view.getFocused()) return entry.handle;
|
||||
}
|
||||
|
||||
// If none are currently focused, the most previously focused
|
||||
// surface (if it exists) is our active surface. This lets things
|
||||
// like apprt actions and bell ringing continue to work in the
|
||||
// background.
|
||||
if (self.private().last_focused.get()) |v| {
|
||||
defer v.unref();
|
||||
|
||||
// We need to find the handle of the last focused surface.
|
||||
it = tree.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (entry.view == v) return entry.handle;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@@ -47,6 +47,19 @@ pub const Surface = extern struct {
|
||||
pub const Tree = datastruct.SplitTree(Self);
|
||||
|
||||
pub const properties = struct {
|
||||
pub const @"bell-ringing" = struct {
|
||||
pub const name = "bell-ringing";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = C.privateShallowFieldAccessor("bell_ringing"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const config = struct {
|
||||
pub const name = "config";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
@@ -257,21 +270,6 @@ pub const Surface = extern struct {
|
||||
);
|
||||
};
|
||||
|
||||
/// The bell is rung.
|
||||
///
|
||||
/// The surface view handles the audio bell feature but none of the
|
||||
/// others so it is up to the embedding widget to react to this.
|
||||
pub const bell = struct {
|
||||
pub const name = "bell";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted whenever the clipboard has been written.
|
||||
pub const @"clipboard-write" = struct {
|
||||
pub const name = "clipboard-write";
|
||||
@@ -456,6 +454,11 @@ pub const Surface = extern struct {
|
||||
// Progress bar
|
||||
progress_bar_timer: ?c_uint = null,
|
||||
|
||||
// True while the bell is ringing. This will be set to false (after
|
||||
// true) under various scenarios, but can also manually be set to
|
||||
// false by a parent widget.
|
||||
bell_ringing: bool = false,
|
||||
|
||||
// Template binds
|
||||
child_exited_overlay: *ChildExited,
|
||||
context_menu: *gtk.PopoverMenu,
|
||||
@@ -520,18 +523,6 @@ pub const Surface = extern struct {
|
||||
priv.gl_area.queueRender();
|
||||
}
|
||||
|
||||
/// Ring the bell.
|
||||
pub fn ringBell(self: *Self) void {
|
||||
// TODO: Audio feature
|
||||
|
||||
signals.bell.impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Self) void {
|
||||
signals.@"toggle-fullscreen".impl.emit(
|
||||
self,
|
||||
@@ -691,7 +682,7 @@ pub const Surface = extern struct {
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) bool {
|
||||
log.warn("keyEvent action={}", .{action});
|
||||
//log.warn("keyEvent action={}", .{action});
|
||||
const event = ec_key.as(gtk.EventController).getCurrentEvent() orelse return false;
|
||||
const key_event = gobject.ext.cast(gdk.KeyEvent, event) orelse return false;
|
||||
const priv = self.private();
|
||||
@@ -881,6 +872,10 @@ pub const Surface = extern struct {
|
||||
surface.preeditCallback(null) catch {};
|
||||
}
|
||||
|
||||
// Bell stops ringing when any key is pressed that is used by
|
||||
// the core in any way.
|
||||
self.setBellRinging(false);
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
@@ -1383,6 +1378,17 @@ pub const Surface = extern struct {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"mouse-hover-url".impl.param_spec);
|
||||
}
|
||||
|
||||
pub fn getBellRinging(self: *Self) bool {
|
||||
return self.private().bell_ringing;
|
||||
}
|
||||
|
||||
pub fn setBellRinging(self: *Self, ringing: bool) void {
|
||||
const priv = self.private();
|
||||
if (priv.bell_ringing == ringing) return;
|
||||
priv.bell_ringing = ringing;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec);
|
||||
}
|
||||
|
||||
fn propConfig(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
@@ -1515,6 +1521,64 @@ pub const Surface = extern struct {
|
||||
priv.gl_area.as(gtk.Widget).setCursorFromName(name.ptr);
|
||||
}
|
||||
|
||||
fn propBellRinging(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
if (!priv.bell_ringing) return;
|
||||
|
||||
// Activate actions if they exist
|
||||
_ = self.as(gtk.Widget).activateAction("tab.ring-bell", null);
|
||||
_ = self.as(gtk.Widget).activateAction("win.ring-bell", null);
|
||||
|
||||
// Do our sound
|
||||
const config = if (priv.config) |c| c.get() else return;
|
||||
if (config.@"bell-features".audio) audio: {
|
||||
const config_path = config.@"bell-audio-path" orelse break :audio;
|
||||
const path, const required = switch (config_path) {
|
||||
.optional => |path| .{ path, false },
|
||||
.required => |path| .{ path, true },
|
||||
};
|
||||
|
||||
const volume = std.math.clamp(
|
||||
config.@"bell-audio-volume",
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
|
||||
assert(std.fs.path.isAbsolute(path));
|
||||
const media_file = gtk.MediaFile.newForFilename(path);
|
||||
|
||||
// If the audio file is marked as required, we'll emit an error if
|
||||
// there was a problem playing it. Otherwise there will be silence.
|
||||
if (required) {
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
media_file,
|
||||
?*anyopaque,
|
||||
mediaFileError,
|
||||
null,
|
||||
.{ .detail = "error" },
|
||||
);
|
||||
}
|
||||
|
||||
// Watch for the "ended" signal so that we can clean up after
|
||||
// ourselves.
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
media_file,
|
||||
?*anyopaque,
|
||||
mediaFileEnded,
|
||||
null,
|
||||
.{ .detail = "ended" },
|
||||
);
|
||||
|
||||
const media_stream = media_file.as(gtk.MediaStream);
|
||||
media_stream.setVolume(volume);
|
||||
media_stream.play();
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Signal Handlers
|
||||
|
||||
@@ -1668,6 +1732,9 @@ pub const Surface = extern struct {
|
||||
priv.im_context.as(gtk.IMContext).focusIn();
|
||||
_ = glib.idleAddOnce(idleFocus, self.ref());
|
||||
self.as(gobject.Object).notifyByPspec(properties.focused.impl.param_spec);
|
||||
|
||||
// Bell stops ringing as soon as we gain focus
|
||||
self.setBellRinging(false);
|
||||
}
|
||||
|
||||
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
||||
@@ -1704,6 +1771,9 @@ pub const Surface = extern struct {
|
||||
) callconv(.c) void {
|
||||
const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
|
||||
|
||||
// Bell stops ringing if any mouse button is pressed.
|
||||
self.setBellRinging(false);
|
||||
|
||||
// If we don't have focus, grab it.
|
||||
const priv = self.private();
|
||||
const gl_area_widget = priv.gl_area.as(gtk.Widget);
|
||||
@@ -2314,6 +2384,35 @@ pub const Surface = extern struct {
|
||||
right.setVisible(0);
|
||||
}
|
||||
|
||||
fn mediaFileError(
|
||||
media_file: *gtk.MediaFile,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const path = path: {
|
||||
const file = media_file.getFile() orelse break :path null;
|
||||
break :path file.getPath();
|
||||
};
|
||||
defer if (path) |p| glib.free(p);
|
||||
|
||||
const media_stream = media_file.as(gtk.MediaStream);
|
||||
const err = media_stream.getError() orelse return;
|
||||
log.warn("error playing bell from {s}: {s} {d} {s}", .{
|
||||
path orelse "<<unknown>>",
|
||||
glib.quarkToString(err.f_domain),
|
||||
err.f_code,
|
||||
err.f_message orelse "",
|
||||
});
|
||||
}
|
||||
|
||||
fn mediaFileEnded(
|
||||
media_file: *gtk.MediaFile,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
media_file.unref();
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
@@ -2378,9 +2477,11 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
|
||||
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
|
||||
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
|
||||
class.bindTemplateCallback("notify_bell_ringing", &propBellRinging);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.@"bell-ringing".impl,
|
||||
properties.config.impl,
|
||||
properties.@"child-exited".impl,
|
||||
properties.@"default-size".impl,
|
||||
@@ -2397,7 +2498,6 @@ pub const Surface = extern struct {
|
||||
|
||||
// Signals
|
||||
signals.@"close-request".impl.register(.{});
|
||||
signals.bell.impl.register(.{});
|
||||
signals.@"clipboard-read".impl.register(.{});
|
||||
signals.@"clipboard-write".impl.register(.{});
|
||||
signals.init.impl.register(.{});
|
||||
|
@@ -11,6 +11,7 @@ const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
@@ -175,6 +176,9 @@ pub const Tab = extern struct {
|
||||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
|
||||
// Init our actions
|
||||
self.initActions();
|
||||
|
||||
// If our configuration is null then we get the configuration
|
||||
// from the application.
|
||||
const priv = self.private();
|
||||
@@ -194,6 +198,46 @@ pub const Tab = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Setup our action map.
|
||||
fn initActions(self: *Self) void {
|
||||
// The set of actions. Each action has (in order):
|
||||
// [0] The action name
|
||||
// [1] The callback function
|
||||
// [2] The glib.VariantType of the parameter
|
||||
//
|
||||
// For action names:
|
||||
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
|
||||
const actions = .{
|
||||
.{ "ring-bell", actionRingBell, null },
|
||||
};
|
||||
|
||||
// We need to collect our actions into a group since we're just
|
||||
// a plain widget that doesn't implement ActionGroup directly.
|
||||
const group = gio.SimpleActionGroup.new();
|
||||
errdefer group.unref();
|
||||
const map = group.as(gio.ActionMap);
|
||||
inline for (actions) |entry| {
|
||||
const action = gio.SimpleAction.new(
|
||||
entry[0],
|
||||
entry[2],
|
||||
);
|
||||
defer action.unref();
|
||||
_ = gio.SimpleAction.signals.activate.connect(
|
||||
action,
|
||||
*Self,
|
||||
entry[1],
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
map.addAction(action.as(gio.Action));
|
||||
}
|
||||
|
||||
self.as(gtk.Widget).insertActionGroup(
|
||||
"tab",
|
||||
group.as(gio.ActionGroup),
|
||||
);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Properties
|
||||
|
||||
@@ -223,6 +267,15 @@ pub const Tab = extern struct {
|
||||
return core_surface.needsConfirmQuit();
|
||||
}
|
||||
|
||||
/// Get the tab page holding this tab, if any.
|
||||
fn getTabPage(self: *Self) ?*adw.TabPage {
|
||||
const tab_view = ext.getAncestor(
|
||||
adw.TabView,
|
||||
self.as(gtk.Widget),
|
||||
) orelse return null;
|
||||
return tab_view.getPage(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual methods
|
||||
|
||||
@@ -291,33 +344,66 @@ pub const Tab = extern struct {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
|
||||
}
|
||||
|
||||
fn actionRingBell(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Future note: I actually don't like this logic living here at all.
|
||||
// I think a better approach will be for the ring bell action to
|
||||
// specify its sending surface and then do all this in the window.
|
||||
|
||||
// If the page is selected already we don't mark it as needing
|
||||
// attention. We only want to mark unfocused pages. This will then
|
||||
// clear when the page is selected.
|
||||
const page = self.getTabPage() orelse return;
|
||||
if (page.getSelected() != 0) return;
|
||||
page.setNeedsAttention(@intFromBool(true));
|
||||
}
|
||||
|
||||
fn closureComputedTitle(
|
||||
_: *Self,
|
||||
config_: ?*Config,
|
||||
plain_: ?[*:0]const u8,
|
||||
zoomed_: c_int,
|
||||
bell_ringing_: c_int,
|
||||
_: *gobject.ParamSpec,
|
||||
) callconv(.c) ?[*:0]const u8 {
|
||||
const zoomed = zoomed_ != 0;
|
||||
const bell_ringing = bell_ringing_ != 0;
|
||||
|
||||
const plain = plain: {
|
||||
const default = "Ghostty";
|
||||
const plain = plain_ orelse break :plain default;
|
||||
break :plain std.mem.span(plain);
|
||||
};
|
||||
|
||||
// If we're zoomed, prefix with the magnifying glass emoji.
|
||||
if (zoomed) zoomed: {
|
||||
// This results in an extra allocation (that we free), but I
|
||||
// prefer using the Zig APIs so much more than the libc ones.
|
||||
const alloc = Application.default().allocator();
|
||||
const slice = std.fmt.allocPrint(
|
||||
alloc,
|
||||
"🔍 {s}",
|
||||
.{plain},
|
||||
) catch break :zoomed;
|
||||
defer alloc.free(slice);
|
||||
return glib.ext.dupeZ(u8, slice);
|
||||
// We don't need a config in every case, but if we don't have a config
|
||||
// let's just assume something went terribly wrong and use our
|
||||
// default title. Its easier then guarding on the config existing
|
||||
// in every case for something so unlikely.
|
||||
const config = if (config_) |v| v.get() else {
|
||||
log.warn("config unavailable for computed title, likely bug", .{});
|
||||
return glib.ext.dupeZ(u8, plain);
|
||||
};
|
||||
|
||||
// Use an allocator to build up our string as we write it.
|
||||
var buf: std.ArrayList(u8) = .init(Application.default().allocator());
|
||||
defer buf.deinit();
|
||||
const writer = buf.writer();
|
||||
|
||||
// If our bell is ringing, then we prefix the bell icon to the title.
|
||||
if (bell_ringing and config.@"bell-features".title) {
|
||||
writer.writeAll("🔔 ") catch {};
|
||||
}
|
||||
|
||||
return glib.ext.dupeZ(u8, plain);
|
||||
// If we're zoomed, prefix with the magnifying glass emoji.
|
||||
if (zoomed) {
|
||||
writer.writeAll("🔍 ") catch {};
|
||||
}
|
||||
|
||||
writer.writeAll(plain) catch return glib.ext.dupeZ(u8, plain);
|
||||
return glib.ext.dupeZ(u8, buf.items);
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
|
@@ -336,6 +336,7 @@ pub const Window = extern struct {
|
||||
.{ "close-tab", actionCloseTab, null },
|
||||
.{ "new-tab", actionNewTab, null },
|
||||
.{ "new-window", actionNewWindow, null },
|
||||
.{ "ring-bell", actionRingBell, null },
|
||||
.{ "split-right", actionSplitRight, null },
|
||||
.{ "split-left", actionSplitLeft, null },
|
||||
.{ "split-up", actionSplitUp, null },
|
||||
@@ -1317,6 +1318,10 @@ pub const Window = extern struct {
|
||||
// Setup our binding group. This ensures things like the title
|
||||
// are synced from the active tab.
|
||||
priv.tab_bindings.setSource(child.as(gobject.Object));
|
||||
|
||||
// If the tab was previously marked as needing attention
|
||||
// (e.g. due to a bell character), we now unmark that
|
||||
page.setNeedsAttention(@intFromBool(false));
|
||||
}
|
||||
|
||||
fn tabViewPageAttached(
|
||||
@@ -1729,6 +1734,30 @@ pub const Window = extern struct {
|
||||
self.performBindingAction(.clear_screen);
|
||||
}
|
||||
|
||||
fn actionRingBell(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
const config = if (priv.config) |v| v.get() else return;
|
||||
|
||||
if (config.@"bell-features".system) system: {
|
||||
const native = self.as(gtk.Native).getSurface() orelse {
|
||||
log.warn("unable to get native surface from window", .{});
|
||||
break :system;
|
||||
};
|
||||
native.beep();
|
||||
}
|
||||
|
||||
if (config.@"bell-features".attention) {
|
||||
// Request user attention
|
||||
self.winproto().setUrgent(true) catch |err| {
|
||||
log.warn("failed to request user attention={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the command palette.
|
||||
///
|
||||
/// TODO: accept the surface that toggled the command palette as a parameter
|
||||
|
@@ -6,6 +6,7 @@ template $GhosttySurface: Adw.Bin {
|
||||
"surface",
|
||||
]
|
||||
|
||||
notify::bell-ringing => $notify_bell_ringing();
|
||||
notify::config => $notify_config();
|
||||
notify::mouse-hover-url => $notify_mouse_hover_url();
|
||||
notify::mouse-hidden => $notify_mouse_hidden();
|
||||
|
@@ -8,7 +8,7 @@ template $GhosttyTab: Box {
|
||||
orientation: vertical;
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
title: bind $computed_title(split_tree.active-surface as <$GhosttySurface>.title, split_tree.is-zoomed) as <string>;
|
||||
title: bind $computed_title(template.config, split_tree.active-surface as <$GhosttySurface>.title, split_tree.is-zoomed, split_tree.active-surface as <$GhosttySurface>.bell-ringing) as <string>;
|
||||
tooltip: bind split_tree.active-surface as <$GhosttySurface>.pwd;
|
||||
|
||||
$GhosttySplitTree split_tree {
|
||||
|
Reference in New Issue
Block a user