Skip to content

Commit 8f5f432

Browse files
authored
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.
2 parents 1baf892 + 6b30736 commit 8f5f432

File tree

7 files changed

+49
-54
lines changed

7 files changed

+49
-54
lines changed

include/ghostty.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ typedef struct {
559559

560560
// apprt.Action.Key
561561
typedef enum {
562+
GHOSTTY_ACTION_QUIT,
562563
GHOSTTY_ACTION_NEW_WINDOW,
563564
GHOSTTY_ACTION_NEW_TAB,
564565
GHOSTTY_ACTION_NEW_SPLIT,
@@ -681,7 +682,7 @@ void ghostty_config_open();
681682
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
682683
ghostty_config_t);
683684
void ghostty_app_free(ghostty_app_t);
684-
bool ghostty_app_tick(ghostty_app_t);
685+
void ghostty_app_tick(ghostty_app_t);
685686
void* ghostty_app_userdata(ghostty_app_t);
686687
void ghostty_app_set_focus(ghostty_app_t, bool);
687688
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);

macos/Sources/Ghostty/Ghostty.App.swift

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,23 +117,7 @@ extension Ghostty {
117117

118118
func appTick() {
119119
guard let app = self.app else { return }
120-
121-
// Tick our app, which lets us know if we want to quit
122-
let exit = ghostty_app_tick(app)
123-
if (!exit) { return }
124-
125-
// On iOS, applications do not terminate programmatically like they do
126-
// on macOS. On iOS, applications are only terminated when a user physically
127-
// closes the application (i.e. going to the home screen). If we request
128-
// exit on iOS we ignore it.
129-
#if os(iOS)
130-
logger.info("quit request received, ignoring on iOS")
131-
#endif
132-
133-
#if os(macOS)
134-
// We want to quit, start that process
135-
NSApplication.shared.terminate(nil)
136-
#endif
120+
ghostty_app_tick(app)
137121
}
138122

139123
func openConfig() {
@@ -454,6 +438,9 @@ extension Ghostty {
454438

455439
// Action dispatch
456440
switch (action.tag) {
441+
case GHOSTTY_ACTION_QUIT:
442+
quit(app)
443+
457444
case GHOSTTY_ACTION_NEW_WINDOW:
458445
newWindow(app, target: target)
459446

@@ -559,6 +546,21 @@ extension Ghostty {
559546
}
560547
}
561548

549+
private static func quit(_ app: ghostty_app_t) {
550+
// On iOS, applications do not terminate programmatically like they do
551+
// on macOS. On iOS, applications are only terminated when a user physically
552+
// closes the application (i.e. going to the home screen). If we request
553+
// exit on iOS we ignore it.
554+
#if os(iOS)
555+
logger.info("quit request received, ignoring on iOS")
556+
#endif
557+
558+
#if os(macOS)
559+
// We want to quit, start that process
560+
NSApplication.shared.terminate(nil)
561+
#endif
562+
}
563+
562564
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
563565
switch (target.tag) {
564566
case GHOSTTY_TARGET_APP:

src/App.zig

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ focused_surface: ?*Surface = null,
5454
/// this is a blocking queue so if it is full you will get errors (or block).
5555
mailbox: Mailbox.Queue,
5656

57-
/// Set to true once we're quitting. This never goes false again.
58-
quit: bool,
59-
6057
/// The set of font GroupCache instances shared by surfaces with the
6158
/// same font configuration.
6259
font_grid_set: font.SharedGridSet,
@@ -98,7 +95,6 @@ pub fn create(
9895
.alloc = alloc,
9996
.surfaces = .{},
10097
.mailbox = .{},
101-
.quit = false,
10298
.font_grid_set = font_grid_set,
10399
.config_conditional_state = .{},
104100
};
@@ -125,9 +121,7 @@ pub fn destroy(self: *App) void {
125121
/// Tick ticks the app loop. This will drain our mailbox and process those
126122
/// events. This should be called by the application runtime on every loop
127123
/// tick.
128-
///
129-
/// This returns whether the app should quit or not.
130-
pub fn tick(self: *App, rt_app: *apprt.App) !bool {
124+
pub fn tick(self: *App, rt_app: *apprt.App) !void {
131125
// If any surfaces are closing, destroy them
132126
var i: usize = 0;
133127
while (i < self.surfaces.items.len) {
@@ -142,13 +136,6 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
142136

143137
// Drain our mailbox
144138
try self.drainMailbox(rt_app);
145-
146-
// No matter what, we reset the quit flag after a tick. If the apprt
147-
// doesn't want to quit, then we can't force it to.
148-
defer self.quit = false;
149-
150-
// We quit if our quit flag is on
151-
return self.quit;
152139
}
153140

154141
/// Update the configuration associated with the app. This can only be
@@ -272,7 +259,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
272259
// can try to quit as quickly as possible.
273260
.quit => {
274261
log.info("quit message received, short circuiting mailbox drain", .{});
275-
self.setQuit();
262+
try self.performAction(rt_app, .quit);
276263
return;
277264
},
278265
}
@@ -314,12 +301,6 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
314301
);
315302
}
316303

317-
/// Start quitting
318-
pub fn setQuit(self: *App) void {
319-
if (self.quit) return;
320-
self.quit = true;
321-
}
322-
323304
/// Handle an app-level focus event. This should be called whenever
324305
/// the focus state of the entire app containing Ghostty changes.
325306
/// This is separate from surface focus events. See the `focused`
@@ -437,7 +418,7 @@ pub fn performAction(
437418
switch (action) {
438419
.unbind => unreachable,
439420
.ignore => {},
440-
.quit => self.setQuit(),
421+
.quit => try rt_app.performAction(.app, .quit, {}),
441422
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
442423
.open_config => try rt_app.performAction(.app, .open_config, {}),
443424
.reload_config => try rt_app.performAction(.app, .reload_config, .{}),

src/apprt/action.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ pub const Action = union(Key) {
7070
// entry. If the value type is void then only the key needs to be
7171
// added. Ensure the order matches exactly with the Zig code.
7272

73+
/// Quit the application.
74+
quit,
75+
7376
/// Open a new window. The target determines whether properties such
7477
/// as font size should be inherited.
7578
new_window,
@@ -219,6 +222,7 @@ pub const Action = union(Key) {
219222

220223
/// Sync with: ghostty_action_tag_e
221224
pub const Key = enum(c_int) {
225+
quit,
222226
new_window,
223227
new_tab,
224228
new_split,

src/apprt/embedded.zig

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,10 +1332,9 @@ pub const CAPI = struct {
13321332

13331333
/// Tick the event loop. This should be called whenever the "wakeup"
13341334
/// callback is invoked for the runtime.
1335-
export fn ghostty_app_tick(v: *App) bool {
1336-
return v.core_app.tick(v) catch |err| err: {
1335+
export fn ghostty_app_tick(v: *App) void {
1336+
v.core_app.tick(v) catch |err| {
13371337
log.err("error app tick err={}", .{err});
1338-
break :err false;
13391338
};
13401339
}
13411340

src/apprt/glfw.zig

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ pub const App = struct {
3535
app: *CoreApp,
3636
config: Config,
3737

38+
/// Flips to true to quit on the next event loop tick. This
39+
/// never goes false and forces the event loop to exit.
40+
quit: bool = false,
41+
3842
/// Mac-specific state.
3943
darwin: if (Darwin.enabled) Darwin else void,
4044

@@ -124,8 +128,10 @@ pub const App = struct {
124128
glfw.waitEvents();
125129

126130
// Tick the terminal app
127-
const should_quit = try self.app.tick(self);
128-
if (should_quit or self.app.surfaces.items.len == 0) {
131+
try self.app.tick(self);
132+
133+
// If the tick caused us to quit, then we're done.
134+
if (self.quit or self.app.surfaces.items.len == 0) {
129135
for (self.app.surfaces.items) |surface| {
130136
surface.close(false);
131137
}
@@ -149,6 +155,8 @@ pub const App = struct {
149155
value: apprt.Action.Value(action),
150156
) !void {
151157
switch (action) {
158+
.quit => self.quit = true,
159+
152160
.new_window => _ = try self.newSurface(switch (target) {
153161
.app => null,
154162
.surface => |v| v,

src/apprt/gtk/App.zig

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ pub fn performAction(
460460
value: apprt.Action.Value(action),
461461
) !void {
462462
switch (action) {
463+
.quit => self.quit(),
463464
.new_window => _ = try self.newWindow(switch (target) {
464465
.app => null,
465466
.surface => |v| v,
@@ -1075,9 +1076,7 @@ fn loadCustomCss(self: *App) !void {
10751076
defer file.close();
10761077

10771078
log.info("loading gtk-custom-css path={s}", .{path});
1078-
const contents = try file.reader().readAllAlloc(
1079-
self.core_app.alloc,
1080-
5 * 1024 * 1024 // 5MB
1079+
const contents = try file.reader().readAllAlloc(self.core_app.alloc, 5 * 1024 * 1024 // 5MB
10811080
);
10821081
defer self.core_app.alloc.free(contents);
10831082

@@ -1174,14 +1173,10 @@ pub fn run(self: *App) !void {
11741173
_ = c.g_main_context_iteration(self.ctx, 1);
11751174

11761175
// Tick the terminal app and see if we should quit.
1177-
const should_quit = try self.core_app.tick(self);
1176+
try self.core_app.tick(self);
11781177

11791178
// Check if we must quit based on the current state.
11801179
const must_quit = q: {
1181-
// If we've been told by GTK that we should quit, do so regardless
1182-
// of any other setting.
1183-
if (should_quit) break :q true;
1184-
11851180
// If we are configured to always stay running, don't quit.
11861181
if (!self.config.@"quit-after-last-window-closed") break :q false;
11871182

@@ -1285,6 +1280,9 @@ fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
12851280
}
12861281

12871282
fn quit(self: *App) void {
1283+
// If we're already not running, do nothing.
1284+
if (!self.running) return;
1285+
12881286
// If we have no toplevel windows, then we're done.
12891287
const list = c.gtk_window_list_toplevels();
12901288
if (list == null) {
@@ -1625,7 +1623,9 @@ fn gtkActionQuit(
16251623
ud: ?*anyopaque,
16261624
) callconv(.C) void {
16271625
const self: *App = @ptrCast(@alignCast(ud orelse return));
1628-
self.core_app.setQuit();
1626+
self.core_app.performAction(self, .quit) catch |err| {
1627+
log.err("error quitting err={}", .{err});
1628+
};
16291629
}
16301630

16311631
/// Action sent by the window manager asking us to present a specific surface to

0 commit comments

Comments
 (0)