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).
This commit is contained in:
Jeffrey C. Ollie
2025-07-06 15:06:00 -05:00
parent 391290aa4a
commit 033d8c3099
7 changed files with 56 additions and 1 deletions

View File

@ -680,6 +680,12 @@ typedef struct {
uintptr_t len; uintptr_t len;
} ghostty_action_open_url_s; } 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 // apprt.Action.Key
typedef enum { typedef enum {
GHOSTTY_ACTION_QUIT, GHOSTTY_ACTION_QUIT,
@ -731,6 +737,7 @@ typedef enum {
GHOSTTY_ACTION_REDO, GHOSTTY_ACTION_REDO,
GHOSTTY_ACTION_CHECK_FOR_UPDATES, GHOSTTY_ACTION_CHECK_FOR_UPDATES,
GHOSTTY_ACTION_OPEN_URL, GHOSTTY_ACTION_OPEN_URL,
GHOSTTY_ACTION_SHOW_CHILD_EXITED
} ghostty_action_tag_e; } ghostty_action_tag_e;
typedef union { typedef union {
@ -759,6 +766,7 @@ typedef union {
ghostty_action_reload_config_s reload_config; ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change; ghostty_action_config_change_s config_change;
ghostty_action_open_url_s open_url; ghostty_action_open_url_s open_url;
ghostty_surface_message_childexited_s child_exited;
} ghostty_action_u; } ghostty_action_u;
typedef struct { typedef struct {

View File

@ -579,6 +579,8 @@ extension Ghostty {
case GHOSTTY_ACTION_SIZE_LIMIT: case GHOSTTY_ACTION_SIZE_LIMIT:
fallthrough fallthrough
case GHOSTTY_ACTION_QUIT_TIMER: case GHOSTTY_ACTION_QUIT_TIMER:
fallthrough
case GHOSTTY_SHOW_CHILD_EXITED:
Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)") Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)")
return false return false
default: default:

View File

@ -1018,6 +1018,14 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void {
return; 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; return;
} }
@ -1044,6 +1052,14 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void {
t.screen.kitty_keyboard.set(.set, .{}); 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 // 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. // state is updated, and now its up to the user to decide what to do.
if (self.config.wait_after_command) return; if (self.config.wait_after_command) return;

View File

@ -272,6 +272,9 @@ pub const Action = union(Key) {
/// apprt. /// apprt.
open_url: OpenUrl, 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 /// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) { pub const Key = enum(c_int) {
quit, quit,
@ -323,6 +326,7 @@ pub const Action = union(Key) {
redo, redo,
check_for_updates, check_for_updates,
open_url, open_url,
show_child_exited,
}; };
/// Sync with: ghostty_action_u /// Sync with: ghostty_action_u

View File

@ -521,6 +521,7 @@ pub fn performAction(
.ring_bell => try self.ringBell(target), .ring_bell => try self.ringBell(target),
.toggle_command_palette => try self.toggleCommandPalette(target), .toggle_command_palette => try self.toggleCommandPalette(target),
.open_url => self.openUrl(value), .open_url => self.openUrl(value),
.show_child_exited => try self.showChildExited(target, value),
// Unimplemented // Unimplemented
.close_all_windows, .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 { fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
switch (mode) { switch (mode) {
.start => self.startQuitTimer(), .start => self.startQuitTimer(),

View File

@ -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 { fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void {
media_file.unref(); 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));
}

View File

@ -98,7 +98,7 @@ pub const Message = union(enum) {
// This enum is a placeholder for future title styles. // This enum is a placeholder for future title styles.
}; };
pub const ChildExited = struct { pub const ChildExited = extern struct {
exit_code: u32, exit_code: u32,
runtime_ms: u64, runtime_ms: u64,
}; };