diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index 3a1cfc22a..bdd87711c 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -39,6 +39,7 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 2, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 2, .name = "resize-overlay" }, .{ .major = 1, .minor = 2, .name = "surface" }, + .{ .major = 1, .minor = 3, .name = "surface-child-exited" }, .{ .major = 1, .minor = 5, .name = "window" }, }; diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index d3e02e28d..2753391b0 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -485,6 +485,8 @@ pub const Application = extern struct { .set_title => Action.setTitle(target, value), + .show_child_exited => return Action.showChildExited(target, value), + .show_gtk_inspector => Action.showGtkInspector(), // Unimplemented but todo on gtk-ng branch @@ -514,7 +516,6 @@ pub const Application = extern struct { .ring_bell, .toggle_command_palette, .open_url, - .show_child_exited, .close_all_windows, .float_window, .toggle_visibility, @@ -1141,6 +1142,16 @@ const Action = struct { } } + pub fn showChildExited( + target: apprt.Target, + value: apprt.surface.Message.ChildExited, + ) bool { + return switch (target) { + .app => false, + .surface => |v| v.rt_surface.surface.childExited(value), + }; + } + pub fn showGtkInspector() void { gtk.Window.setInteractiveDebugging(@intFromBool(true)); } diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index afcc9ffd0..b1f9d8d50 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -21,6 +21,7 @@ const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Config = @import("config.zig").Config; const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay; +const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited; const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog; const log = std.log.scoped(.gtk_ghostty_surface); @@ -57,6 +58,26 @@ pub const Surface = extern struct { ); }; + pub const @"child-exited" = struct { + pub const name = "child-exited"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Child Exited", + .blurb = "True when the child process has exited.", + .default = false, + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "child_exited", + ), + }, + ); + }; + pub const focused = struct { pub const name = "focused"; const impl = gobject.ext.defineProperty( @@ -284,7 +305,11 @@ pub const Surface = extern struct { /// True when we have a precision scroll in progress precision_scroll: bool = false, + /// True when the child has exited. + child_exited: bool = false, + // Template binds + child_exited_overlay: *ChildExited, drop_target: *gtk.DropTarget, pub var offset: c_int = 0; @@ -634,6 +659,26 @@ pub const Surface = extern struct { ); } + pub fn childExited( + self: *Self, + data: apprt.surface.Message.ChildExited, + ) bool { + // If we have the noop child exited overlay then we don't do anything + // for child exited. The false return will force libghostty to show + // the normal text-based message. + if (comptime @hasDecl(ChildExited, "noop")) { + return false; + } + + const priv = self.private(); + priv.child_exited = true; + priv.child_exited_overlay.setData(&data); + self.as(gobject.Object).notifyByPspec( + properties.@"child-exited".impl.param_spec, + ); + return true; + } + pub fn cgroupPath(self: *Self) ?[]const u8 { return self.private().cgroup_path; } @@ -1015,6 +1060,14 @@ pub const Surface = extern struct { //--------------------------------------------------------------- // Signal Handlers + fn childExitedClose( + _: *ChildExited, + self: *Self, + ) callconv(.c) void { + // This closes the surface with no confirmation. + self.close(false); + } + fn dtDrop( _: *gtk.DropTarget, value: *gobject.Value, @@ -1762,6 +1815,7 @@ pub const Surface = extern struct { fn init(class: *Class) callconv(.C) void { gobject.ext.ensureType(ResizeOverlay); + gobject.ext.ensureType(ChildExited); gtk.Widget.Class.setTemplateFromResource( class.as(gtk.Widget.Class), comptime gresource.blueprint(.{ @@ -1775,6 +1829,7 @@ pub const Surface = extern struct { class.bindTemplateChildPrivate("gl_area", .{}); class.bindTemplateChildPrivate("url_left", .{}); class.bindTemplateChildPrivate("url_right", .{}); + class.bindTemplateChildPrivate("child_exited_overlay", .{}); class.bindTemplateChildPrivate("resize_overlay", .{}); class.bindTemplateChildPrivate("drop_target", .{}); class.bindTemplateChildPrivate("im_context", .{}); @@ -1802,6 +1857,7 @@ pub const Surface = extern struct { class.bindTemplateCallback("im_commit", &imCommit); class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter); class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave); + class.bindTemplateCallback("child_exited_close", &childExitedClose); class.bindTemplateCallback("notify_config", &propConfig); class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); @@ -1810,6 +1866,7 @@ pub const Surface = extern struct { // Properties gobject.ext.registerProperties(class, &.{ properties.config.impl, + properties.@"child-exited".impl, properties.focused.impl, properties.@"mouse-shape".impl, properties.@"mouse-hidden".impl, diff --git a/src/apprt/gtk-ng/class/surface_child_exited.zig b/src/apprt/gtk-ng/class/surface_child_exited.zig new file mode 100644 index 000000000..693425c09 --- /dev/null +++ b/src/apprt/gtk-ng/class/surface_child_exited.zig @@ -0,0 +1,272 @@ +const std = @import("std"); +const assert = std.debug.assert; +const adw = @import("adw"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const adw_version = @import("../adw_version.zig"); +const apprt = @import("../../../apprt.zig"); +const gresource = @import("../build/gresource.zig"); +const i18n = @import("../../../os/main.zig").i18n; +const Common = @import("../class.zig").Common; + +const log = std.log.scoped(.gtk_ghostty_surface_child_exited); + +pub const SurfaceChildExited = if (adw_version.supportsBanner()) + SurfaceChildExitedBanner +else + SurfaceChildExitedNoop; + +/// Child exited overlay based on adw.Banner introduced in +/// Adwaita 1.3. +const SurfaceChildExitedBanner = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = adw.Bin; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttySurfaceChildExited", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + pub const data = struct { + pub const name = "data"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*apprt.surface.Message.ChildExited, + .{ + .nick = "Data", + .blurb = "The child exit data.", + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "data", + ), + }, + ); + }; + }; + + pub const signals = struct { + /// Emitted when the banner would like to be closed. + pub const @"close-request" = struct { + pub const name = "close-request"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + }; + + const Private = struct { + /// The child exited data sent by the apprt. + data: ?*apprt.surface.Message.ChildExited = null, + + // Template bindings + banner: *adw.Banner, + + pub var offset: c_int = 0; + }; + + fn init(self: *Self, _: *Class) callconv(.C) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + } + + pub fn setData( + self: *Self, + data_: ?*const apprt.surface.Message.ChildExited, + ) void { + const priv = self.private(); + if (priv.data) |v| glib.ext.destroy(v); + const data = data_ orelse { + priv.data = null; + return; + }; + + const ptr = glib.ext.create(apprt.surface.Message.ChildExited); + ptr.* = data.*; + priv.data = ptr; + self.as(gobject.Object).notifyByPspec(properties.data.impl.param_spec); + } + + //--------------------------------------------------------------- + // Signal handlers + + fn propData( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + const banner = priv.banner; + const data = priv.data orelse { + // Not localized on purpose. + banner.as(adw.Banner).setTitle("This is a bug in Ghostty. Please report it."); + return; + }; + if (data.exit_code == 0) { + banner.as(adw.Banner).setTitle(i18n._("Command succeeded")); + self.as(gtk.Widget).addCssClass("normal"); + self.as(gtk.Widget).removeCssClass("abnormal"); + } else { + banner.as(adw.Banner).setTitle(i18n._("Command failed")); + self.as(gtk.Widget).removeCssClass("normal"); + self.as(gtk.Widget).addCssClass("abnormal"); + } + } + + fn closeButtonClicked( + _: *adw.Banner, + self: *Self, + ) callconv(.c) void { + signals.@"close-request".impl.emit( + self, + null, + .{}, + null, + ); + } + + //--------------------------------------------------------------- + // Virtual methods + + fn dispose(self: *Self) callconv(.C) void { + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + fn finalize(self: *Self) callconv(.C) void { + const priv = self.private(); + if (priv.data) |v| { + glib.ext.destroy(v); + priv.data = null; + } + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.C) void { + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + comptime gresource.blueprint(.{ + .major = 1, + .minor = 3, + .name = "surface-child-exited", + }), + ); + + // Template bindings + class.bindTemplateChildPrivate("banner", .{}); + class.bindTemplateCallback("clicked", &closeButtonClicked); + class.bindTemplateCallback("notify_data", &propData); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.data.impl, + }); + + // Signals + signals.@"close-request".impl.register(.{}); + + // Virtual methods + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); + } + + pub const as = C.Class.as; + pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; + }; +}; + +/// Empty widget that does nothing if we don't have a new enough +/// Adwaita version to support the child exited banner. +const SurfaceChildExitedNoop = extern struct { + /// Can be detected at comptime + pub const noop = true; + + const Self = @This(); + parent_instance: Parent, + pub const Parent = gtk.Widget; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttySurfaceChildExited", + .classInit = &Class.init, + .parent_class = &Class.parent, + }); + + pub const signals = struct { + pub const @"close-request" = struct { + pub const name = "close-request"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + }; + + pub fn setData( + self: *Self, + _: ?*const apprt.surface.Message.ChildExited, + ) void { + signals.@"close-request".impl.emit( + self, + null, + .{}, + null, + ); + } + + const C = Common(Self, null); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.C) void { + _ = class; + signals.@"close-request".impl.register(.{}); + } + + pub const as = C.Class.as; + pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; + }; +}; diff --git a/src/apprt/gtk-ng/css/style.css b/src/apprt/gtk-ng/css/style.css index e69a4e77d..3139155b7 100644 --- a/src/apprt/gtk-ng/css/style.css +++ b/src/apprt/gtk-ng/css/style.css @@ -69,3 +69,18 @@ label.url-overlay.right { .clipboard-confirmation-dialog .clipboard-contents.blurred { filter: blur(5px); } + +/* + * Child Exited Overlay + */ +.child-exited.normal revealer widget { + background-color: rgba(38, 162, 105, 0.5); + /* after GTK 4.16 is a requirement, switch to the following: + /* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */ +} + +.child-exited.abnormal revealer widget { + background-color: rgba(192, 28, 40, 0.5); + /* after GTK 4.16 is a requirement, switch to the following: + /* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */ +} diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index faa2daea5..593841b60 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -25,6 +25,12 @@ template $GhosttySurface: Adw.Bin { use-es: false; } + [overlay] + $GhosttySurfaceChildExited child_exited_overlay { + visible: bind template.child-exited; + close-request => $child_exited_close(); + } + [overlay] $GhosttyResizeOverlay resize_overlay { styles [ diff --git a/src/apprt/gtk-ng/ui/1.3/surface-child-exited.blp b/src/apprt/gtk-ng/ui/1.3/surface-child-exited.blp new file mode 100644 index 000000000..6745f0ca6 --- /dev/null +++ b/src/apprt/gtk-ng/ui/1.3/surface-child-exited.blp @@ -0,0 +1,22 @@ +using Gtk 4.0; +using Adw 1; + +template $GhosttySurfaceChildExited: Adw.Bin { + styles [ + "child-exited", + ] + + notify::data => $notify_data(); + + Adw.Bin { + Adw.Banner banner { + button-clicked => $clicked(); + revealed: true; + // Not localized on purpose because it should never be seen. + title: "This is a bug in Ghostty. Please report it."; + button-label: _("Close"); + halign: fill; + valign: end; + } + } +} diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 8c0ae5c91..f76e3d05a 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const apprt = @import("../apprt.zig"); +const build_config = @import("../build_config.zig"); const App = @import("../App.zig"); const Surface = @import("../Surface.zig"); const renderer = @import("../renderer.zig"); @@ -107,6 +108,18 @@ pub const Message = union(enum) { pub const ChildExited = extern struct { exit_code: u32, runtime_ms: u64, + + /// Make this a valid gobject if we're in a GTK environment. + pub const getGObjectType = switch (build_config.app_runtime) { + .gtk, + .@"gtk-ng", + => @import("gobject").ext.defineBoxed( + ChildExited, + .{ .name = "GhosttyApprtChildExited" }, + ), + + .none => void, + }; }; }; diff --git a/valgrind.supp b/valgrind.supp index 645b35b17..8ba1acb5d 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -82,6 +82,18 @@ fun:main } +{ + VMware Graphics Driver + Memcheck:Leak + match-leak-kinds: possible + ... + fun:vmw_fence_create + fun:vmw_ioctl_command + fun:vmw_swc_flush + fun:svga_context_flush + ... +} + { GSK Renderer GPU Stuff Memcheck:Leak