From 60b859dbf258c2d5c5d8f888bb86ff75cf5f6691 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Jul 2025 09:01:55 -0700 Subject: [PATCH 1/3] apprt/gtk-ng: open config, open url --- src/apprt/gtk-ng/class/application.zig | 50 +++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 45581a7d0..1cabcf008 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -515,14 +515,18 @@ pub const Application = extern struct { }, ), + .open_config => return Action.openConfig(self), + + .open_url => Action.openUrl(self, value), + .pwd => Action.pwd(target, value), + .progress_report => return Action.progressReport(target, value), + .quit => self.quit(), .quit_timer => try Action.quitTimer(self, value), - .progress_report => return Action.progressReport(target, value), - .reload_config => try Action.reloadConfig(self, target, value), .render => Action.render(target), @@ -544,7 +548,6 @@ pub const Application = extern struct { .resize_split, .equalize_splits, .goto_split, - .open_config, .inspector, .desktop_notification, .present_terminal, @@ -555,7 +558,6 @@ pub const Application = extern struct { .prompt_title, .toggle_quick_terminal, .toggle_command_palette, - .open_url, => { log.warn("unimplemented action={}", .{action}); return false; @@ -825,6 +827,7 @@ pub const Application = extern struct { const actions = .{ .{ "new-window", actionNewWindow, null }, .{ "new-window-command", actionNewWindow, as_variant_type }, + .{ "open-config", actionOpenConfig, null }, .{ "quit", actionQuit, null }, .{ "reload-config", actionReloadConfig, null }, }; @@ -1145,6 +1148,14 @@ pub const Application = extern struct { }, .{ .forever = {} }); } + pub fn actionOpenConfig( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + _ = self.core().mailbox.push(.open_config, .forever); + } + //---------------------------------------------------------------- // Boilerplate/Noise @@ -1375,6 +1386,37 @@ const Action = struct { gtk.Window.present(win.as(gtk.Window)); } + pub fn openConfig(self: *Application) bool { + // Get the config file path + const alloc = self.allocator(); + const path = configpkg.edit.openPath(alloc) catch |err| { + log.warn("error getting config file path: {}", .{err}); + return false; + }; + defer alloc.free(path); + + // Open it using openURL. "path" isn't actually a URL but + // at the time of writing that works just fine for GTK. + openUrl(self, .{ .kind = .text, .url = path }); + return true; + } + + pub fn openUrl( + self: *Application, + value: apprt.action.OpenUrl, + ) void { + // TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html + + // Fallback to the minimal cross-platform way of opening a URL. + // This is always a safe fallback and enables for example Windows + // to open URLs (GTK on Windows via WSL is a thing). + internal_os.open( + self.allocator(), + value.kind, + value.url, + ) catch |err| log.warn("unable to open url: {}", .{err}); + } + pub fn pwd( target: apprt.Target, value: apprt.action.Pwd, From e7ea084cc316276015eb465791690047ee0d77f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Jul 2025 09:19:15 -0700 Subject: [PATCH 2/3] apprt/gtk-ng: present surface --- src/apprt/gtk-ng/class/application.zig | 60 +++++++++++++++++++++++++- src/apprt/gtk-ng/class/surface.zig | 25 +++++++++++ src/apprt/gtk-ng/class/window.zig | 51 ++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 1cabcf008..3027e06e9 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -521,6 +521,8 @@ pub const Application = extern struct { .pwd => Action.pwd(target, value), + .present_terminal => return Action.presentTerminal(target), + .progress_report => return Action.progressReport(target, value), .quit => self.quit(), @@ -550,7 +552,6 @@ pub const Application = extern struct { .goto_split, .inspector, .desktop_notification, - .present_terminal, .initial_size, .size_limit, .toggle_split_zoom, @@ -828,6 +829,7 @@ pub const Application = extern struct { .{ "new-window", actionNewWindow, null }, .{ "new-window-command", actionNewWindow, as_variant_type }, .{ "open-config", actionOpenConfig, null }, + .{ "present-surface", actionPresentSurface, t_variant_type }, .{ "quit", actionQuit, null }, .{ "reload-config", actionReloadConfig, null }, }; @@ -1156,6 +1158,50 @@ pub const Application = extern struct { _ = self.core().mailbox.push(.open_config, .forever); } + fn actionPresentSurface( + _: *gio.SimpleAction, + parameter_: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + const parameter = parameter_ orelse return; + + const t = glib.ext.VariantType.newFor(u64); + defer glib.VariantType.free(t); + + // Make sure that we've receiived a u64 from the system. + if (glib.Variant.isOfType(parameter, t) == 0) { + return; + } + + // Convert that u64 to pointer to a core surface. A value of zero + // means that there was no target surface for the notification so + // we don't focus any surface. + // + // This is admittedly SUPER SUS and we should instead do what we + // do on macOS which is generate a UUID per surface and then pass + // that around. But, we do validate the pointer below so at worst + // this may result in focusing the wrong surface if the pointer was + // reused for a surface. + const ptr_int = parameter.getUint64(); + if (ptr_int == 0) return; + const surface: *CoreSurface = @ptrFromInt(ptr_int); + + // 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().mailbox.push( + .{ + .surface_message = .{ + .surface = surface, + .message = .present_surface, + }, + }, + .forever, + ); + } + //---------------------------------------------------------------- // Boilerplate/Noise @@ -1445,6 +1491,18 @@ const Action = struct { } } + pub fn presentTerminal( + target: apprt.Target, + ) bool { + return switch (target) { + .app => false, + .surface => |v| surface: { + v.rt_surface.surface.present(); + break :surface true; + }, + }; + } + pub fn progressReport( target: apprt.Target, value: terminal.osc.Command.ProgressReport, diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 2c787be48..90a1d9e40 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -285,6 +285,19 @@ pub const Surface = extern struct { ); }; + /// Emitted when the focus wants to be brought to the top and + /// focused. + pub const @"present-request" = struct { + pub const name = "present-request"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + /// Emitted when this surface requests its container to toggle its /// fullscreen state. pub const @"toggle-fullscreen" = struct { @@ -578,6 +591,17 @@ pub const Surface = extern struct { return @intFromBool(glib.SOURCE_REMOVE); } + /// Request that this terminal come to the front and become focused. + /// It is up to the embedding widget to react to this. + pub fn present(self: *Self) void { + signals.@"present-request".impl.emit( + self, + null, + .{}, + null, + ); + } + /// Key press event (press or release). /// /// At a high level, we want to construct an `input.KeyEvent` and @@ -2173,6 +2197,7 @@ pub const Surface = extern struct { signals.bell.impl.register(.{}); signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-write".impl.register(.{}); + signals.@"present-request".impl.register(.{}); signals.@"toggle-fullscreen".impl.register(.{}); signals.@"toggle-maximize".impl.register(.{}); diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 5a61c725b..426b57797 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -955,6 +955,13 @@ pub const Window = extern struct { self, .{}, ); + _ = Surface.signals.@"present-request".connect( + surface, + *Self, + surfacePresentRequest, + self, + .{}, + ); _ = Surface.signals.@"clipboard-write".connect( surface, *Self, @@ -1093,6 +1100,50 @@ pub const Window = extern struct { } } + fn surfacePresentRequest( + surface: *Surface, + self: *Self, + ) callconv(.c) void { + // Verify that this surface is actually in this window. + { + const surface_window = ext.getAncestor( + Self, + surface.as(gtk.Widget), + ) orelse { + log.warn( + "present request called for non-existent surface", + .{}, + ); + return; + }; + if (surface_window != self) { + log.warn( + "present request called for surface in different window", + .{}, + ); + return; + } + } + + // Get the tab for this surface. + const tab = ext.getAncestor( + Tab, + surface.as(gtk.Widget), + ) orelse { + log.warn("present request surface not found", .{}); + return; + }; + + // Get the page that contains this tab + const priv = self.private(); + const tab_view = priv.tab_view; + const page = tab_view.getPage(tab.as(gtk.Widget)); + tab_view.setSelectedPage(page); + + // Grab focus + surface.grabFocus(); + } + fn surfaceToggleFullscreen( surface: *Surface, self: *Self, From 6c952d4168c1703562cb2122c50ba6e8f5653352 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Jul 2025 09:24:06 -0700 Subject: [PATCH 3/3] apprt/gtk-ng: desktop notification --- src/apprt/gtk-ng/class/application.zig | 59 +++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 3027e06e9..8cbe9fd18 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -497,6 +497,8 @@ pub const Application = extern struct { value.config, ), + .desktop_notification => Action.desktopNotification(self, target, value), + .goto_tab => return Action.gotoTab(target, value), .mouse_over_link => Action.mouseOverLink(target, value), @@ -546,19 +548,20 @@ pub const Application = extern struct { .toggle_tab_overview => return Action.toggleTabOverview(target), // Unimplemented but todo on gtk-ng branch + .initial_size, + .size_limit, + .prompt_title, + .toggle_command_palette, + .inspector, + // TODO: splits .new_split, .resize_split, .equalize_splits, .goto_split, - .inspector, - .desktop_notification, - .initial_size, - .size_limit, .toggle_split_zoom, - .toggle_window_decorations, - .prompt_title, + // TODO: winproto .toggle_quick_terminal, - .toggle_command_palette, + .toggle_window_decorations, => { log.warn("unimplemented action={}", .{action}); return false; @@ -1275,6 +1278,48 @@ const Action = struct { } } + pub fn desktopNotification( + self: *Application, + target: apprt.Target, + n: apprt.action.DesktopNotification, + ) void { + // TODO: We should move the surface target to a function call + // on Surface and emit a signal that embedders can connect to. This + // will let us handle notifications differently depending on where + // a surface is presented. At the time of writing this, we always + // want to show the notification AND the logic below was directly + // ported from "legacy" GTK so this is fine, but I want to leave this + // note so we can do it one day. + + // Set a default title if we don't already have one + const t = switch (n.title.len) { + 0 => "Ghostty", + else => n.title, + }; + + const notification = gio.Notification.new(t); + defer notification.unref(); + notification.setBody(n.body); + + const icon = gio.ThemedIcon.new("com.mitchellh.ghostty"); + defer icon.unref(); + notification.setIcon(icon.as(gio.Icon)); + + const pointer = glib.Variant.newUint64(switch (target) { + .app => 0, + .surface => |v| @intFromPtr(v), + }); + notification.setDefaultActionAndTargetValue( + "app.present-surface", + pointer, + ); + + // We set the notification ID to the body content. If the content is the + // same, this notification may replace a previous notification + const gio_app = self.as(gio.Application); + gio_app.sendNotification(n.body, notification); + } + pub fn gotoTab( target: apprt.Target, tab: apprt.action.GotoTab,