Move app quit to apprt action (#4577)

This changes quit signaling from a boolean return from core app `tick()`
to an apprt action. This simplifies the API and conceptually makes more
sense to me now.

This wasn't done just for that; this change was also needed so that
macOS can quit cleanly while fixing #4540 since we may no longer trigger
menu items. I wanted to split this out into a separate commit/PR because
it adds complexity making the diff harder to read.
This commit is contained in:
Mitchell Hashimoto
2025-01-04 07:37:54 -08:00
committed by GitHub
7 changed files with 49 additions and 54 deletions

View File

@ -559,6 +559,7 @@ typedef struct {
// apprt.Action.Key // apprt.Action.Key
typedef enum { typedef enum {
GHOSTTY_ACTION_QUIT,
GHOSTTY_ACTION_NEW_WINDOW, GHOSTTY_ACTION_NEW_WINDOW,
GHOSTTY_ACTION_NEW_TAB, GHOSTTY_ACTION_NEW_TAB,
GHOSTTY_ACTION_NEW_SPLIT, GHOSTTY_ACTION_NEW_SPLIT,
@ -681,7 +682,7 @@ void ghostty_config_open();
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
ghostty_config_t); ghostty_config_t);
void ghostty_app_free(ghostty_app_t); void ghostty_app_free(ghostty_app_t);
bool ghostty_app_tick(ghostty_app_t); void ghostty_app_tick(ghostty_app_t);
void* ghostty_app_userdata(ghostty_app_t); void* ghostty_app_userdata(ghostty_app_t);
void ghostty_app_set_focus(ghostty_app_t, bool); void ghostty_app_set_focus(ghostty_app_t, bool);
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);

View File

@ -117,23 +117,7 @@ extension Ghostty {
func appTick() { func appTick() {
guard let app = self.app else { return } guard let app = self.app else { return }
ghostty_app_tick(app)
// Tick our app, which lets us know if we want to quit
let exit = ghostty_app_tick(app)
if (!exit) { return }
// On iOS, applications do not terminate programmatically like they do
// on macOS. On iOS, applications are only terminated when a user physically
// closes the application (i.e. going to the home screen). If we request
// exit on iOS we ignore it.
#if os(iOS)
logger.info("quit request received, ignoring on iOS")
#endif
#if os(macOS)
// We want to quit, start that process
NSApplication.shared.terminate(nil)
#endif
} }
func openConfig() { func openConfig() {
@ -454,6 +438,9 @@ extension Ghostty {
// Action dispatch // Action dispatch
switch (action.tag) { switch (action.tag) {
case GHOSTTY_ACTION_QUIT:
quit(app)
case GHOSTTY_ACTION_NEW_WINDOW: case GHOSTTY_ACTION_NEW_WINDOW:
newWindow(app, target: target) newWindow(app, target: target)
@ -559,6 +546,21 @@ extension Ghostty {
} }
} }
private static func quit(_ app: ghostty_app_t) {
// On iOS, applications do not terminate programmatically like they do
// on macOS. On iOS, applications are only terminated when a user physically
// closes the application (i.e. going to the home screen). If we request
// exit on iOS we ignore it.
#if os(iOS)
logger.info("quit request received, ignoring on iOS")
#endif
#if os(macOS)
// We want to quit, start that process
NSApplication.shared.terminate(nil)
#endif
}
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) { private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
switch (target.tag) { switch (target.tag) {
case GHOSTTY_TARGET_APP: case GHOSTTY_TARGET_APP:

View File

@ -54,9 +54,6 @@ focused_surface: ?*Surface = null,
/// this is a blocking queue so if it is full you will get errors (or block). /// this is a blocking queue so if it is full you will get errors (or block).
mailbox: Mailbox.Queue, mailbox: Mailbox.Queue,
/// Set to true once we're quitting. This never goes false again.
quit: bool,
/// The set of font GroupCache instances shared by surfaces with the /// The set of font GroupCache instances shared by surfaces with the
/// same font configuration. /// same font configuration.
font_grid_set: font.SharedGridSet, font_grid_set: font.SharedGridSet,
@ -98,7 +95,6 @@ pub fn create(
.alloc = alloc, .alloc = alloc,
.surfaces = .{}, .surfaces = .{},
.mailbox = .{}, .mailbox = .{},
.quit = false,
.font_grid_set = font_grid_set, .font_grid_set = font_grid_set,
.config_conditional_state = .{}, .config_conditional_state = .{},
}; };
@ -125,9 +121,7 @@ pub fn destroy(self: *App) void {
/// Tick ticks the app loop. This will drain our mailbox and process those /// Tick ticks the app loop. This will drain our mailbox and process those
/// events. This should be called by the application runtime on every loop /// events. This should be called by the application runtime on every loop
/// tick. /// tick.
/// pub fn tick(self: *App, rt_app: *apprt.App) !void {
/// This returns whether the app should quit or not.
pub fn tick(self: *App, rt_app: *apprt.App) !bool {
// If any surfaces are closing, destroy them // If any surfaces are closing, destroy them
var i: usize = 0; var i: usize = 0;
while (i < self.surfaces.items.len) { while (i < self.surfaces.items.len) {
@ -142,13 +136,6 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
// Drain our mailbox // Drain our mailbox
try self.drainMailbox(rt_app); try self.drainMailbox(rt_app);
// No matter what, we reset the quit flag after a tick. If the apprt
// doesn't want to quit, then we can't force it to.
defer self.quit = false;
// We quit if our quit flag is on
return self.quit;
} }
/// Update the configuration associated with the app. This can only be /// Update the configuration associated with the app. This can only be
@ -272,7 +259,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
// can try to quit as quickly as possible. // can try to quit as quickly as possible.
.quit => { .quit => {
log.info("quit message received, short circuiting mailbox drain", .{}); log.info("quit message received, short circuiting mailbox drain", .{});
self.setQuit(); try self.performAction(rt_app, .quit);
return; return;
}, },
} }
@ -314,12 +301,6 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
); );
} }
/// Start quitting
pub fn setQuit(self: *App) void {
if (self.quit) return;
self.quit = true;
}
/// Handle an app-level focus event. This should be called whenever /// Handle an app-level focus event. This should be called whenever
/// the focus state of the entire app containing Ghostty changes. /// the focus state of the entire app containing Ghostty changes.
/// This is separate from surface focus events. See the `focused` /// This is separate from surface focus events. See the `focused`
@ -437,7 +418,7 @@ pub fn performAction(
switch (action) { switch (action) {
.unbind => unreachable, .unbind => unreachable,
.ignore => {}, .ignore => {},
.quit => self.setQuit(), .quit => try rt_app.performAction(.app, .quit, {}),
.new_window => try self.newWindow(rt_app, .{ .parent = null }), .new_window => try self.newWindow(rt_app, .{ .parent = null }),
.open_config => try rt_app.performAction(.app, .open_config, {}), .open_config => try rt_app.performAction(.app, .open_config, {}),
.reload_config => try rt_app.performAction(.app, .reload_config, .{}), .reload_config => try rt_app.performAction(.app, .reload_config, .{}),

View File

@ -70,6 +70,9 @@ pub const Action = union(Key) {
// entry. If the value type is void then only the key needs to be // entry. If the value type is void then only the key needs to be
// added. Ensure the order matches exactly with the Zig code. // added. Ensure the order matches exactly with the Zig code.
/// Quit the application.
quit,
/// Open a new window. The target determines whether properties such /// Open a new window. The target determines whether properties such
/// as font size should be inherited. /// as font size should be inherited.
new_window, new_window,
@ -219,6 +222,7 @@ pub const Action = union(Key) {
/// 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,
new_window, new_window,
new_tab, new_tab,
new_split, new_split,

View File

@ -1332,10 +1332,9 @@ pub const CAPI = struct {
/// Tick the event loop. This should be called whenever the "wakeup" /// Tick the event loop. This should be called whenever the "wakeup"
/// callback is invoked for the runtime. /// callback is invoked for the runtime.
export fn ghostty_app_tick(v: *App) bool { export fn ghostty_app_tick(v: *App) void {
return v.core_app.tick(v) catch |err| err: { v.core_app.tick(v) catch |err| {
log.err("error app tick err={}", .{err}); log.err("error app tick err={}", .{err});
break :err false;
}; };
} }

View File

@ -35,6 +35,10 @@ pub const App = struct {
app: *CoreApp, app: *CoreApp,
config: Config, config: Config,
/// Flips to true to quit on the next event loop tick. This
/// never goes false and forces the event loop to exit.
quit: bool = false,
/// Mac-specific state. /// Mac-specific state.
darwin: if (Darwin.enabled) Darwin else void, darwin: if (Darwin.enabled) Darwin else void,
@ -124,8 +128,10 @@ pub const App = struct {
glfw.waitEvents(); glfw.waitEvents();
// Tick the terminal app // Tick the terminal app
const should_quit = try self.app.tick(self); try self.app.tick(self);
if (should_quit or self.app.surfaces.items.len == 0) {
// If the tick caused us to quit, then we're done.
if (self.quit or self.app.surfaces.items.len == 0) {
for (self.app.surfaces.items) |surface| { for (self.app.surfaces.items) |surface| {
surface.close(false); surface.close(false);
} }
@ -149,6 +155,8 @@ pub const App = struct {
value: apprt.Action.Value(action), value: apprt.Action.Value(action),
) !void { ) !void {
switch (action) { switch (action) {
.quit => self.quit = true,
.new_window => _ = try self.newSurface(switch (target) { .new_window => _ = try self.newSurface(switch (target) {
.app => null, .app => null,
.surface => |v| v, .surface => |v| v,

View File

@ -460,6 +460,7 @@ pub fn performAction(
value: apprt.Action.Value(action), value: apprt.Action.Value(action),
) !void { ) !void {
switch (action) { switch (action) {
.quit => self.quit(),
.new_window => _ = try self.newWindow(switch (target) { .new_window => _ = try self.newWindow(switch (target) {
.app => null, .app => null,
.surface => |v| v, .surface => |v| v,
@ -1075,9 +1076,7 @@ fn loadCustomCss(self: *App) !void {
defer file.close(); defer file.close();
log.info("loading gtk-custom-css path={s}", .{path}); log.info("loading gtk-custom-css path={s}", .{path});
const contents = try file.reader().readAllAlloc( const contents = try file.reader().readAllAlloc(self.core_app.alloc, 5 * 1024 * 1024 // 5MB
self.core_app.alloc,
5 * 1024 * 1024 // 5MB
); );
defer self.core_app.alloc.free(contents); defer self.core_app.alloc.free(contents);
@ -1174,14 +1173,10 @@ pub fn run(self: *App) !void {
_ = c.g_main_context_iteration(self.ctx, 1); _ = c.g_main_context_iteration(self.ctx, 1);
// Tick the terminal app and see if we should quit. // Tick the terminal app and see if we should quit.
const should_quit = try self.core_app.tick(self); try self.core_app.tick(self);
// Check if we must quit based on the current state. // Check if we must quit based on the current state.
const must_quit = q: { const must_quit = q: {
// If we've been told by GTK that we should quit, do so regardless
// of any other setting.
if (should_quit) break :q true;
// If we are configured to always stay running, don't quit. // If we are configured to always stay running, don't quit.
if (!self.config.@"quit-after-last-window-closed") break :q false; if (!self.config.@"quit-after-last-window-closed") break :q false;
@ -1285,6 +1280,9 @@ fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
} }
fn quit(self: *App) void { fn quit(self: *App) void {
// If we're already not running, do nothing.
if (!self.running) return;
// If we have no toplevel windows, then we're done. // If we have no toplevel windows, then we're done.
const list = c.gtk_window_list_toplevels(); const list = c.gtk_window_list_toplevels();
if (list == null) { if (list == null) {
@ -1625,7 +1623,9 @@ fn gtkActionQuit(
ud: ?*anyopaque, ud: ?*anyopaque,
) callconv(.C) void { ) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return)); const self: *App = @ptrCast(@alignCast(ud orelse return));
self.core_app.setQuit(); self.core_app.performAction(self, .quit) catch |err| {
log.err("error quitting err={}", .{err});
};
} }
/// Action sent by the window manager asking us to present a specific surface to /// Action sent by the window manager asking us to present a specific surface to