Merge pull request #430 from mitchellh/confirm-quit

Confirm quit improvements, both macOS and GTK
This commit is contained in:
Mitchell Hashimoto
2023-09-11 16:04:52 -07:00
committed by GitHub
7 changed files with 72 additions and 27 deletions

View File

@ -316,6 +316,7 @@ bool ghostty_app_tick(ghostty_app_t);
void *ghostty_app_userdata(ghostty_app_t); void *ghostty_app_userdata(ghostty_app_t);
void ghostty_app_keyboard_changed(ghostty_app_t); void ghostty_app_keyboard_changed(ghostty_app_t);
void ghostty_app_reload_config(ghostty_app_t); void ghostty_app_reload_config(ghostty_app_t);
bool ghostty_app_needs_confirm_quit(ghostty_app_t);
ghostty_surface_config_s ghostty_surface_config_new(); ghostty_surface_config_s ghostty_surface_config_new();

View File

@ -102,6 +102,9 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
} }
} }
// If our app says we don't need to confirm, we can exit now.
if (!ghostty.needsConfirmQuit) { return .terminateNow }
// We have some visible window, and all our windows will watch the confirmQuit. // We have some visible window, and all our windows will watch the confirmQuit.
confirmQuit = true confirmQuit = true
return .terminateLater return .terminateLater

View File

@ -40,6 +40,12 @@ extension Ghostty {
} }
} }
/// True if we need to confirm before quitting.
var needsConfirmQuit: Bool {
guard let app = app else { return false }
return ghostty_app_needs_confirm_quit(app)
}
/// Cached clipboard string for `read_clipboard` callback. /// Cached clipboard string for `read_clipboard` callback.
private var cached_clipboard_string: String? = nil private var cached_clipboard_string: String? = nil

View File

@ -168,6 +168,16 @@ pub fn focusedSurface(self: *const App) ?*Surface {
return surface; return surface;
} }
/// Returns true if confirmation is needed to quit the app. It is up to
/// the apprt to call this.
pub fn needsConfirmQuit(self: *const App) bool {
for (self.surfaces.items) |v| {
if (v.core_surface.needsConfirmQuit()) return true;
}
return false;
}
/// Initialize once and return the font discovery mechanism. This remains /// Initialize once and return the font discovery mechanism. This remains
/// initialized throughout the lifetime of the application because some /// initialized throughout the lifetime of the application because some
/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. /// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit.

View File

@ -504,22 +504,24 @@ pub fn deinit(self: *Surface) void {
/// Close this surface. This will trigger the runtime to start the /// Close this surface. This will trigger the runtime to start the
/// close process, which should ultimately deinitialize this surface. /// close process, which should ultimately deinitialize this surface.
pub fn close(self: *Surface) void { pub fn close(self: *Surface) void {
const process_alive = process_alive: { self.rt_surface.close(self.needsConfirmQuit());
}
/// True if the surface requires confirmation to quit. This should be called
/// by apprt to determine if the surface should confirm before quitting.
pub fn needsConfirmQuit(self: *Surface) bool {
// If the child has exited then our process is certainly not alive. // If the child has exited then our process is certainly not alive.
// We check this first to avoid the locking overhead below. // We check this first to avoid the locking overhead below.
if (self.child_exited) break :process_alive false; if (self.child_exited) return false;
// If we are configured to not hold open surfaces explicitly, just // If we are configured to not hold open surfaces explicitly, just
// always say there is nothing alive. // always say there is nothing alive.
if (!self.config.confirm_close_surface) break :process_alive false; if (!self.config.confirm_close_surface) return false;
// We have to talk to the terminal. // We have to talk to the terminal.
self.renderer_state.mutex.lock(); self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock(); defer self.renderer_state.mutex.unlock();
break :process_alive !self.io.terminal.cursorIsAtPrompt(); return !self.io.terminal.cursorIsAtPrompt();
};
self.rt_surface.close(process_alive);
} }
/// Called from the app thread to handle mailbox messages to our specific /// Called from the app thread to handle mailbox messages to our specific

View File

@ -787,6 +787,11 @@ pub const CAPI = struct {
}; };
} }
/// Returns true if the app needs to confirm quitting.
export fn ghostty_app_needs_confirm_quit(v: *App) bool {
return v.core_app.needsConfirmQuit();
}
/// Returns initial surface options. /// Returns initial surface options.
export fn ghostty_surface_config_new() apprt.Surface.Options { export fn ghostty_surface_config_new() apprt.Surface.Options {
return .{}; return .{};

View File

@ -228,6 +228,12 @@ pub const App = struct {
} }
c.g_list_free(list); c.g_list_free(list);
// If the app says we don't need to confirm, then we can quit now.
if (!self.core_app.needsConfirmQuit()) {
self.quitNow();
return;
}
// If we have windows, then we want to confirm that we want to exit. // If we have windows, then we want to confirm that we want to exit.
const alert = c.gtk_message_dialog_new( const alert = c.gtk_message_dialog_new(
null, null,
@ -259,20 +265,8 @@ pub const App = struct {
c.gtk_widget_show(alert); c.gtk_widget_show(alert);
} }
fn gtkQuitConfirmation( fn quitNow(self: *App) void {
alert: *c.GtkMessageDialog, _ = self;
response: c.gint,
ud: ?*anyopaque,
) callconv(.C) void {
_ = ud;
// Close the alert window
c.gtk_window_destroy(@ptrCast(alert));
// If we didn't confirm then we're done
if (response != c.GTK_RESPONSE_YES) return;
// Force close all open windows
const list = c.gtk_window_list_toplevels(); const list = c.gtk_window_list_toplevels();
defer c.g_list_free(list); defer c.g_list_free(list);
c.g_list_foreach(list, struct { c.g_list_foreach(list, struct {
@ -285,6 +279,23 @@ pub const App = struct {
}.callback, null); }.callback, null);
} }
fn gtkQuitConfirmation(
alert: *c.GtkMessageDialog,
response: c.gint,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
// Close the alert window
c.gtk_window_destroy(@ptrCast(alert));
// If we didn't confirm then we're done
if (response != c.GTK_RESPONSE_YES) return;
// Force close all open windows
self.quitNow();
}
/// This is called by the "activate" signal. This is sent on program /// This is called by the "activate" signal. This is sent on program
/// startup and also when a secondary instance launches and requests /// startup and also when a secondary instance launches and requests
/// a new window. /// a new window.
@ -583,6 +594,13 @@ const Window = struct {
log.debug("window close request", .{}); log.debug("window close request", .{});
const self = userdataSelf(ud.?); const self = userdataSelf(ud.?);
// If none of our surfaces need confirmation, we can just exit.
for (self.app.core_app.surfaces.items) |surface| {
if (surface.window == self) {
if (surface.core_surface.needsConfirmQuit()) break;
}
} else return false;
// Setup our basic message // Setup our basic message
const alert = c.gtk_message_dialog_new( const alert = c.gtk_message_dialog_new(
self.window, self.window,