Skip to content

Commit

Permalink
GTK: Fix clicking on desktop notifications
Browse files Browse the repository at this point in the history
Currently, clicking on a desktop notification will bring Ghostty
to the foreground, but it won't necessarily bring the right window
to the top and it won't switch tabs or change the focus on splits.

With this patch, clicking on a desktop notification will raise the
correct window, change to the correct tab, and focus on the correct
split that send the original desktop notification.
  • Loading branch information
jcollie committed Aug 29, 2024
1 parent fcb8b04 commit 8ee2565
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 9 deletions.
9 changes: 9 additions & 0 deletions src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ pub fn focusedSurface(self: *const App) ?*Surface {
return surface;
}

/// Is this the last focused surface. This is only valid while on the main
/// thread before tick is called.
pub fn isFocused(self: *const App, surface: *const Surface) bool {
if (!self.hasSurface(surface)) return false;
const focused = self.focused_surface orelse return false;
if (!self.hasSurface(focused)) return false;
return surface == focused;
}

/// Returns true if confirmation is needed to quit the app. It is up to
/// the apprt to call this.
pub fn needsConfirmQuit(self: *const App) bool {
Expand Down
10 changes: 10 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.renderer_health => |health| self.updateRendererHealth(health),

.report_color_scheme => try self.reportColorScheme(),

.present_surface => try self.presentSurface(),
}
}

Expand Down Expand Up @@ -4083,6 +4085,14 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
try self.rt_surface.showDesktopNotification(title, body);
}

/// Tell the surface to present itself to the user. This may involve raising the
/// window and switching tabs.
fn presentSurface(self: *Surface) !void {
if (@hasDecl(apprt.Surface, "presentSurface")) {
self.rt_surface.presentSurface();
} else log.warn("runtime doesn't support presentSurface", .{});
}

pub const face_ttf = @embedFile("font/res/JetBrainsMono-Regular.ttf");
pub const face_bold_ttf = @embedFile("font/res/JetBrainsMono-Bold.ttf");
pub const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
Expand Down
43 changes: 39 additions & 4 deletions src/apprt/gtk/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -800,17 +800,52 @@ fn gtkActionQuit(
};
}

/// Action sent by the window manager asking us to present a specific surface to
/// the user. Usually because the user clicked on a desktop notification.
fn gtkActionPresentSurface(
_: *c.GSimpleAction,
parameter: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));

// Make sure that we've receiived a u64 from the system.
if (c.g_variant_is_of_type(parameter, c.G_VARIANT_TYPE("t")) == 0) {
return;
}

// Convert that u64 to pointer to a core surface.
const surface: *CoreSurface = @ptrFromInt(c.g_variant_get_uint64(parameter));

// Send a message through the core app mailbox rather than presenting the
// surface directly so that it can validate that the surface pointer is
// valid. We could get an invalid pointer if a desktop notification outlives
// a Ghostty instance and a new one starts up, or there are multiple Ghostty
// instances running.
_ = self.core_app.mailbox.push(
.{
.surface_message = .{
.surface = surface,
.message = .{ .present_surface = {} },
},
},
.{ .forever = {} },
);
}

/// This is called to setup the action map that this application supports.
/// This should be called only once on startup.
fn initActions(self: *App) void {
const actions = .{
.{ "quit", &gtkActionQuit },
.{ "open_config", &gtkActionOpenConfig },
.{ "reload_config", &gtkActionReloadConfig },
.{ "quit", &gtkActionQuit, null },
.{ "open_config", &gtkActionOpenConfig, null },
.{ "reload_config", &gtkActionReloadConfig, null },
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
.{ "present-surface", &gtkActionPresentSurface, c.G_VARIANT_TYPE("t") },
};

inline for (actions) |entry| {
const action = c.g_simple_action_new(entry[0], null);
const action = c.g_simple_action_new(entry[0], entry[2]);
defer c.g_object_unref(action);
_ = c.g_signal_connect_data(
action,
Expand Down
31 changes: 26 additions & 5 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1144,19 +1144,28 @@ pub fn showDesktopNotification(
else => title,
};

const notif = c.g_notification_new(t.ptr);
defer c.g_object_unref(notif);
c.g_notification_set_body(notif, body.ptr);
const notification = c.g_notification_new(t.ptr);
defer c.g_object_unref(notification);

c.g_notification_set_body(notification, body.ptr);

const icon = c.g_themed_icon_new("com.mitchellh.ghostty");
defer c.g_object_unref(icon);
c.g_notification_set_icon(notif, icon);

c.g_notification_set_icon(notification, icon);

const pointer = c.g_variant_new_uint64(@intFromPtr(&self.core_surface));
c.g_notification_set_default_action_and_target_value(
notification,
"app.present-surface",
pointer,
);

const g_app: *c.GApplication = @ptrCast(self.app.app);

// We set the notification ID to the body content. If the content is the
// same, this notification may replace a previous notification
c.g_application_send_notification(g_app, body.ptr, notif);
c.g_application_send_notification(g_app, body.ptr, notification);
}

fn showContextMenu(self: *Surface, x: f32, y: f32) void {
Expand Down Expand Up @@ -1967,3 +1976,15 @@ fn translateMods(state: c.GdkModifierType) input.Mods {
if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true;
return mods;
}

pub fn presentSurface(self: *Surface) void {
if (self.container.window()) |window| {
if (self.container.tab()) |tab| {
const page_num = c.gtk_notebook_page_num(window.notebook, @ptrCast(tab.box));
if (page_num >= 0) c.gtk_notebook_set_current_page(window.notebook, page_num);
}
c.gtk_window_present(window.window);
}
// self.app.core_app.focusSurface(&self.core_surface);
self.grabFocus();
}
4 changes: 4 additions & 0 deletions src/apprt/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ pub const Message = union(enum) {
/// Report the color scheme
report_color_scheme: void,

/// Tell the surface to present itself to the user. This may require raising
/// a window and switching tabs.
present_surface: void,

pub const ReportTitleStyle = enum {
csi_21_t,

Expand Down

0 comments on commit 8ee2565

Please sign in to comment.