mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
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:
@ -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);
|
||||||
|
@ -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:
|
||||||
|
25
src/App.zig
25
src/App.zig
@ -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, .{}),
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user