From 5f759a19d116b1bf764aa94af9cb291c2572fc64 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 29 Aug 2024 11:34:59 -0500 Subject: [PATCH 1/3] GTK: Fix clicking on desktop notifications 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. --- src/App.zig | 9 ++++++++ src/Surface.zig | 10 +++++++++ src/apprt/gtk/App.zig | 43 +++++++++++++++++++++++++++++++++++---- src/apprt/gtk/Surface.zig | 30 ++++++++++++++++++++++----- src/apprt/surface.zig | 4 ++++ 5 files changed, 87 insertions(+), 9 deletions(-) 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 ec500d4ac6..bb9490f7ab 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -825,17 +825,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, From 9e73d865fb995f8bdaec1b17f1eb9be7cb8f2fc9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Sep 2024 20:45:03 -0700 Subject: [PATCH 2/3] apprt/gtk: small comments --- src/App.zig | 9 --------- src/apprt/gtk/App.zig | 8 +++++++- src/apprt/gtk/Surface.zig | 2 -- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/App.zig b/src/App.zig index 4708d9c223..f933b71268 100644 --- a/src/App.zig +++ b/src/App.zig @@ -178,15 +178,6 @@ 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/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index bb9490f7ab..244fee7f0a 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -861,11 +861,17 @@ fn gtkActionPresentSurface( /// 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 { + // The set of actions. Each action has (in order): + // [0] The action name + // [1] The callback function + // [2] The GVariantType of the parameter + // + // For action names: + // https://docs.gtk.org/gio/type_func.Action.name_is_valid.html const actions = .{ .{ "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") }, }; diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index f9f07d8bbe..7d337fbe09 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1146,12 +1146,10 @@ pub fn showDesktopNotification( 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(notification, icon); const pointer = c.g_variant_new_uint64(@intFromPtr(&self.core_surface)); From 941adcdac8e801778d9438c54eaf9d2ad9961ce8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Sep 2024 20:49:20 -0700 Subject: [PATCH 3/3] apprt/gtk: rename the other underscore actions to match naming rules --- src/apprt/gtk/App.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 244fee7f0a..67b28ff932 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -382,8 +382,8 @@ fn updateConfigErrors(self: *App) !void { fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("app.quit", .{ .quit = {} }); - try self.syncActionAccelerator("app.open_config", .{ .open_config = {} }); - try self.syncActionAccelerator("app.reload_config", .{ .reload_config = {} }); + try self.syncActionAccelerator("app.open-config", .{ .open_config = {} }); + try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} }); try self.syncActionAccelerator("win.toggle_inspector", .{ .inspector = .toggle }); try self.syncActionAccelerator("win.close", .{ .close_surface = {} }); try self.syncActionAccelerator("win.new_window", .{ .new_window = {} }); @@ -870,8 +870,8 @@ fn initActions(self: *App) void { // https://docs.gtk.org/gio/type_func.Action.name_is_valid.html const actions = .{ .{ "quit", >kActionQuit, null }, - .{ "open_config", >kActionOpenConfig, null }, - .{ "reload_config", >kActionReloadConfig, null }, + .{ "open-config", >kActionOpenConfig, null }, + .{ "reload-config", >kActionReloadConfig, null }, .{ "present-surface", >kActionPresentSurface, c.G_VARIANT_TYPE("t") }, }; @@ -912,8 +912,8 @@ fn initMenu(self: *App) void { defer c.g_object_unref(section); c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector"); - c.g_menu_append(section, "Open Configuration", "app.open_config"); - c.g_menu_append(section, "Reload Configuration", "app.reload_config"); + c.g_menu_append(section, "Open Configuration", "app.open-config"); + c.g_menu_append(section, "Reload Configuration", "app.reload-config"); c.g_menu_append(section, "About Ghostty", "win.about"); }