Merge pull request #134 from mitchellh/gtk-confirm

gtk, macos: show confirmation dialog on surface close with active child process
This commit is contained in:
Mitchell Hashimoto
2023-03-26 10:59:09 -07:00
committed by GitHub
11 changed files with 124 additions and 23 deletions

View File

@ -221,7 +221,7 @@ typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *); typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *);
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *); typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *);
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e); typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e);
typedef void (*ghostty_runtime_close_surface_cb)(void *); typedef void (*ghostty_runtime_close_surface_cb)(void *, bool);
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e); typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e);
typedef struct { typedef struct {

View File

@ -60,7 +60,7 @@ extension Ghostty {
read_clipboard_cb: { userdata in AppState.readClipboard(userdata) }, read_clipboard_cb: { userdata in AppState.readClipboard(userdata) },
write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) }, write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) },
new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: direction) }, new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: direction) },
close_surface_cb: { userdata in AppState.closeSurface(userdata) }, close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) } focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }
) )
@ -132,9 +132,11 @@ extension Ghostty {
]) ])
} }
static func closeSurface(_ userdata: UnsafeMutableRawPointer?) { static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {
guard let surface = self.surfaceUserdata(from: userdata) else { return } guard let surface = self.surfaceUserdata(from: userdata) else { return }
NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface) NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [
"process_alive": processAlive,
])
} }
static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) { static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) {

View File

@ -205,6 +205,9 @@ extension Ghostty {
/// This will be set to true when the split requests that is become closed. /// This will be set to true when the split requests that is become closed.
@Binding var requestClose: Bool @Binding var requestClose: Bool
/// This controls whether we're actively confirming if we want to close or not.
@State private var confirmClose: Bool = false
var body: some View { var body: some View {
let center = NotificationCenter.default let center = NotificationCenter.default
let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface) let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface)
@ -212,8 +215,38 @@ extension Ghostty {
let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface) let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface)
SurfaceWrapper(surfaceView: leaf.surface) SurfaceWrapper(surfaceView: leaf.surface)
.onReceive(pub) { onNewSplit(notification: $0) } .onReceive(pub) { onNewSplit(notification: $0) }
.onReceive(pubClose) { _ in requestClose = true } .onReceive(pubClose) { onClose(notification: $0) }
.onReceive(pubFocus) { onMoveFocus(notification: $0) } .onReceive(pubFocus) { onMoveFocus(notification: $0) }
.confirmationDialog(
"Close Terminal?",
isPresented: $confirmClose) {
Button("Close the Terminal", role: .destructive) {
confirmClose = false
requestClose = true
}
.keyboardShortcut(.defaultAction)
} message: {
Text("The terminal still has a running process. If you close the terminal " +
"the process will be killed.")
}
}
private func onClose(notification: SwiftUI.Notification) {
var processAlive = false
if let valueAny = notification.userInfo?["process_alive"] {
if let value = valueAny as? Bool {
processAlive = value
}
}
// If the child process is not alive, then we exit immediately
guard processAlive else {
requestClose = true
return
}
// Child process is alive, so we want to show a confirmation.
confirmClose = true
} }
private func onNewSplit(notification: SwiftUI.Notification) { private func onNewSplit(notification: SwiftUI.Notification) {

View File

@ -82,7 +82,11 @@ struct GhosttyApp: App {
} }
func close() { func close() {
guard let surfaceView = focusedSurface else { return } guard let surfaceView = focusedSurface else {
Self.closeWindow()
return
}
guard let surface = surfaceView.surface else { return } guard let surface = surfaceView.surface else { return }
ghostty.requestClose(surface: surface) ghostty.requestClose(surface: surface)
} }

View File

@ -74,7 +74,7 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
while (i < self.surfaces.items.len) { while (i < self.surfaces.items.len) {
const surface = self.surfaces.items[i]; const surface = self.surfaces.items[i];
if (surface.shouldClose()) { if (surface.shouldClose()) {
rt_app.closeSurface(surface); surface.close(false);
continue; continue;
} }
@ -143,8 +143,10 @@ fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
} }
fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void { fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void {
_ = rt_app;
if (!self.hasSurface(surface)) return; if (!self.hasSurface(surface)) return;
rt_app.closeSurface(surface.rt_surface); surface.close();
} }
fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void { fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void {

View File

@ -96,6 +96,10 @@ config: DerivedConfig,
/// like such as "control-v" will write a "v" even if they're intercepted. /// like such as "control-v" will write a "v" even if they're intercepted.
ignore_char: bool = false, ignore_char: bool = false,
/// This is set to true if our IO thread notifies us our child exited.
/// This is used to determine if we need to confirm, hold open, etc.
child_exited: bool = false,
/// Mouse state for the surface. /// Mouse state for the surface.
const Mouse = struct { const Mouse = struct {
/// The last tracked mouse button state by button. /// The last tracked mouse button state by button.
@ -522,7 +526,7 @@ 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 {
self.rt_surface.close(); self.rt_surface.close(!self.child_exited);
} }
/// Called from the app thread to handle mailbox messages to our specific /// Called from the app thread to handle mailbox messages to our specific
@ -553,6 +557,12 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
}, },
.close => self.close(), .close => self.close(),
// Close without confirmation.
.child_exited => {
self.child_exited = true;
self.close();
},
} }
} }

View File

@ -56,7 +56,7 @@ pub const App = struct {
new_split: ?*const fn (SurfaceUD, input.SplitDirection) callconv(.C) void = null, new_split: ?*const fn (SurfaceUD, input.SplitDirection) callconv(.C) void = null,
/// Close the current surface given by this function. /// Close the current surface given by this function.
close_surface: ?*const fn (SurfaceUD) callconv(.C) void = null, close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null,
/// Focus the previous/next split (if any). /// Focus the previous/next split (if any).
focus_split: ?*const fn (SurfaceUD, input.SplitFocusDirection) callconv(.C) void = null, focus_split: ?*const fn (SurfaceUD, input.SplitFocusDirection) callconv(.C) void = null,
@ -188,13 +188,13 @@ pub const Surface = struct {
func(self.opts.userdata, direction); func(self.opts.userdata, direction);
} }
pub fn close(self: *const Surface) void { pub fn close(self: *const Surface, process_alive: bool) void {
const func = self.app.opts.close_surface orelse { const func = self.app.opts.close_surface orelse {
log.info("runtime embedder does not support closing a surface", .{}); log.info("runtime embedder does not support closing a surface", .{});
return; return;
}; };
func(self.opts.userdata); func(self.opts.userdata, process_alive);
} }
pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void { pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void {
@ -509,7 +509,7 @@ pub const CAPI = struct {
/// Request that the surface become closed. This will go through the /// Request that the surface become closed. This will go through the
/// normal trigger process that a close surface input binding would. /// normal trigger process that a close surface input binding would.
export fn ghostty_surface_request_close(ptr: *Surface) void { export fn ghostty_surface_request_close(ptr: *Surface) void {
ptr.close(); ptr.core_surface.close();
} }
/// Request that the surface split in the given direction. /// Request that the surface split in the given direction.

View File

@ -409,8 +409,11 @@ pub const Surface = struct {
} }
/// Close this surface. /// Close this surface.
pub fn close(self: *const Surface) void { pub fn close(self: *Surface, processActive: bool) void {
self.window.setShouldClose(true); _ = processActive;
self.setShouldClose();
self.deinit();
self.app.app.alloc.destroy(self);
} }
/// Set the size limits of the window. /// Set the size limits of the window.

View File

@ -171,11 +171,6 @@ pub const App = struct {
} }
/// Close the given surface. /// Close the given surface.
pub fn closeSurface(self: *App, surface: *Surface) void {
_ = self;
surface.close();
}
pub fn redrawSurface(self: *App, surface: *Surface) void { pub fn redrawSurface(self: *App, surface: *Surface) void {
_ = self; _ = self;
surface.invalidate(); surface.invalidate();
@ -635,8 +630,44 @@ pub const Surface = struct {
} }
/// Close this surface. /// Close this surface.
pub fn close(self: *Surface) void { pub fn close(self: *Surface, processActive: bool) void {
if (!processActive) {
self.window.closeSurface(self); self.window.closeSurface(self);
return;
}
// Setup our basic message
const alert = c.gtk_message_dialog_new(
self.window.window,
c.GTK_DIALOG_MODAL,
c.GTK_MESSAGE_QUESTION,
c.GTK_BUTTONS_YES_NO,
"Close this terminal?",
);
c.gtk_message_dialog_format_secondary_text(
@ptrCast(*c.GtkMessageDialog, alert),
"There is still a running process in the terminal. " ++
"Closing the terminal will kill this process. " ++
"Are you sure you want to close the terminal?" ++
"Click 'No' to cancel and return to your terminal.",
);
// We want the "yes" to appear destructive.
const yes_widget = c.gtk_dialog_get_widget_for_response(
@ptrCast(*c.GtkDialog, alert),
c.GTK_RESPONSE_YES,
);
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
// We want the "no" to be the default action
c.gtk_dialog_set_default_response(
@ptrCast(*c.GtkDialog, alert),
c.GTK_RESPONSE_NO,
);
_ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(&gtkCloseConfirmation), self, null, G_CONNECT_DEFAULT);
c.gtk_widget_show(alert);
} }
pub fn newTab(self: *Surface) !void { pub fn newTab(self: *Surface) !void {
@ -1010,6 +1041,18 @@ pub const Surface = struct {
}; };
} }
fn gtkCloseConfirmation(
alert: *c.GtkMessageDialog,
response: c.gint,
ud: ?*anyopaque,
) callconv(.C) void {
c.gtk_window_destroy(@ptrCast(*c.GtkWindow, alert));
if (response == c.GTK_RESPONSE_YES) {
const self = userdataSelf(ud.?);
self.window.closeSurface(self);
}
}
fn userdataSelf(ud: *anyopaque) *Surface { fn userdataSelf(ud: *anyopaque) *Surface {
return @ptrCast(*Surface, @alignCast(@alignOf(Surface), ud)); return @ptrCast(*Surface, @alignCast(@alignOf(Surface), ud));
} }

View File

@ -33,6 +33,10 @@ pub const Message = union(enum) {
/// Close the surface. This will only close the current surface that /// Close the surface. This will only close the current surface that
/// receives this, not the full application. /// receives this, not the full application.
close: void, close: void,
/// The child process running in the surface has exited. This may trigger
/// a surface close, it may not.
child_exited: void,
}; };
/// A surface mailbox. /// A surface mailbox.

View File

@ -401,7 +401,7 @@ fn processExit(
// Notify our surface we want to close // Notify our surface we want to close
_ = ev.surface_mailbox.push(.{ _ = ev.surface_mailbox.push(.{
.close = {}, .child_exited = {},
}, .{ .forever = {} }); }, .{ .forever = {} });
return .disarm; return .disarm;