diff --git a/src/App.zig b/src/App.zig index f933b71268..4708d9c223 100644 --- a/src/App.zig +++ b/src/App.zig @@ -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 { diff --git a/src/Surface.zig b/src/Surface.zig index d597938818..982d741181 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -816,6 +816,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(), } } @@ -4158,6 +4160,14 @@ fn crashThreadState(self: *Surface) crash.sentry.ThreadState { }; } +/// 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"); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index a1843b9ac5..2a42757dcd 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -813,17 +813,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", >kActionQuit }, - .{ "open_config", >kActionOpenConfig }, - .{ "reload_config", >kActionReloadConfig }, + .{ "quit", >kActionQuit, null }, + .{ "open_config", >kActionOpenConfig, null }, + .{ "reload_config", >kActionReloadConfig, null }, + // https://docs.gtk.org/gio/type_func.Action.name_is_valid.html + .{ "present-surface", >kActionPresentSurface, 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, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index eb317e6402..f9f07d8bbe 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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 { @@ -1967,3 +1976,14 @@ 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| { + if (window.notebook.getTabPosition(tab)) |position| + window.notebook.gotoNthTab(position); + } + c.gtk_window_present(window.window); + } + self.grabFocus(); +} diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index b05da88ff6..d73dcea055 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -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,