From 033d8c3099708504cff6746659424ecb29ece309 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 15:06:00 -0500 Subject: [PATCH] core/gtk: add apprt action to show native GUI warning when child exits Addresses #7649 for the core and GTK. macOS support will need to be added later. This adds an apprt action to show a native GUI warning of some kind when the child process of a terminal exits. Also adds a basic GTK implementation of this. In GTK it overlays an Adwaita banner at the bottom of the window (similar to the banner that shows up in at the top of windows in debug builds). --- include/ghostty.h | 8 ++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 2 ++ src/Surface.zig | 16 ++++++++++++++++ src/apprt/action.zig | 4 ++++ src/apprt/gtk/App.zig | 8 ++++++++ src/apprt/gtk/Surface.zig | 17 +++++++++++++++++ src/apprt/surface.zig | 2 +- 7 files changed, 56 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index bcd88251b..0c9b840e7 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -680,6 +680,12 @@ typedef struct { uintptr_t len; } ghostty_action_open_url_s; +// apprt.surface.Message.ChildExited +typedef struct { + uint32_t exit_code; + uint64_t timetime_ms; +} ghostty_surface_message_childexited_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_QUIT, @@ -731,6 +737,7 @@ typedef enum { GHOSTTY_ACTION_REDO, GHOSTTY_ACTION_CHECK_FOR_UPDATES, GHOSTTY_ACTION_OPEN_URL, + GHOSTTY_ACTION_SHOW_CHILD_EXITED } ghostty_action_tag_e; typedef union { @@ -759,6 +766,7 @@ typedef union { ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; ghostty_action_open_url_s open_url; + ghostty_surface_message_childexited_s child_exited; } ghostty_action_u; typedef struct { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 0fdea1760..f78585c9a 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -579,6 +579,8 @@ extension Ghostty { case GHOSTTY_ACTION_SIZE_LIMIT: fallthrough case GHOSTTY_ACTION_QUIT_TIMER: + fallthrough + case GHOSTTY_SHOW_CHILD_EXITED: Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)") return false default: diff --git a/src/Surface.zig b/src/Surface.zig index a4a8d46df..6e58ab5a5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1018,6 +1018,14 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { return; }; + _ = self.rt_app.performAction( + .{ .surface = self }, + .show_child_exited, + info, + ) catch |err| { + log.err("error trying to show native child exited GUI err={}", .{err}); + }; + return; } @@ -1044,6 +1052,14 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { t.screen.kitty_keyboard.set(.set, .{}); } + _ = self.rt_app.performAction( + .{ .surface = self }, + .show_child_exited, + info, + ) catch |err| { + log.err("error trying to show native child exited GUI err={}", .{err}); + }; + // Waiting after command we stop here. The terminal is updated, our // state is updated, and now its up to the user to decide what to do. if (self.config.wait_after_command) return; diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1c3c7c72c..201d27e31 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -272,6 +272,9 @@ pub const Action = union(Key) { /// apprt. open_url: OpenUrl, + /// Show a native GUI notification that the child process has exited. + show_child_exited: apprt.surface.Message.ChildExited, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -323,6 +326,7 @@ pub const Action = union(Key) { redo, check_for_updates, open_url, + show_child_exited, }; /// Sync with: ghostty_action_u diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index bdb2f0f24..a3a6ec411 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -521,6 +521,7 @@ pub fn performAction( .ring_bell => try self.ringBell(target), .toggle_command_palette => try self.toggleCommandPalette(target), .open_url => self.openUrl(value), + .show_child_exited => try self.showChildExited(target, value), // Unimplemented .close_all_windows, @@ -846,6 +847,13 @@ fn toggleCommandPalette(_: *App, target: apprt.Target) !void { } } +fn showChildExited(_: *App, target: apprt.Target, value: apprt.surface.Message.ChildExited) !void { + switch (target) { + .app => {}, + .surface => |surface| try surface.rt_surface.showChildExited(value), + } +} + fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { switch (mode) { .start => self.startQuitTimer(), diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index d16083d5a..7ea11bc17 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2503,3 +2503,20 @@ fn gtkStreamError(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopa fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void { media_file.unref(); } + +pub fn showChildExited(self: *Surface, _: apprt.surface.Message.ChildExited) (error{})!void { + if (!adw_version.supportsBanner()) return; + + const warning_box = gtk.Box.new(.vertical, 0); + + warning_box.as(gtk.Widget).setHalign(.fill); + warning_box.as(gtk.Widget).setValign(.end); + + const warning_text = i18n._("⚠️ Process exited. Press any key to close the terminal."); + const banner = adw.Banner.new(warning_text); + banner.setRevealed(1); + + warning_box.append(banner.as(gtk.Widget)); + + self.overlay.addOverlay(warning_box.as(gtk.Widget)); +} diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 9254b2fd5..1cd53b66a 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -98,7 +98,7 @@ pub const Message = union(enum) { // This enum is a placeholder for future title styles. }; - pub const ChildExited = struct { + pub const ChildExited = extern struct { exit_code: u32, runtime_ms: u64, };