mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
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:
@ -221,7 +221,7 @@ typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
||||
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *);
|
||||
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_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 struct {
|
||||
|
@ -60,7 +60,7 @@ extension Ghostty {
|
||||
read_clipboard_cb: { userdata in AppState.readClipboard(userdata) },
|
||||
write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) },
|
||||
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) }
|
||||
)
|
||||
|
||||
@ -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 }
|
||||
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) {
|
||||
|
@ -205,6 +205,9 @@ extension Ghostty {
|
||||
/// This will be set to true when the split requests that is become closed.
|
||||
@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 {
|
||||
let center = NotificationCenter.default
|
||||
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)
|
||||
SurfaceWrapper(surfaceView: leaf.surface)
|
||||
.onReceive(pub) { onNewSplit(notification: $0) }
|
||||
.onReceive(pubClose) { _ in requestClose = true }
|
||||
.onReceive(pubClose) { onClose(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) {
|
||||
|
@ -82,7 +82,11 @@ struct GhosttyApp: App {
|
||||
}
|
||||
|
||||
func close() {
|
||||
guard let surfaceView = focusedSurface else { return }
|
||||
guard let surfaceView = focusedSurface else {
|
||||
Self.closeWindow()
|
||||
return
|
||||
}
|
||||
|
||||
guard let surface = surfaceView.surface else { return }
|
||||
ghostty.requestClose(surface: surface)
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
|
||||
while (i < self.surfaces.items.len) {
|
||||
const surface = self.surfaces.items[i];
|
||||
if (surface.shouldClose()) {
|
||||
rt_app.closeSurface(surface);
|
||||
surface.close(false);
|
||||
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 {
|
||||
_ = rt_app;
|
||||
|
||||
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 {
|
||||
|
@ -96,6 +96,10 @@ config: DerivedConfig,
|
||||
/// like such as "control-v" will write a "v" even if they're intercepted.
|
||||
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.
|
||||
const Mouse = struct {
|
||||
/// 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 process, which should ultimately deinitialize this surface.
|
||||
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
|
||||
@ -553,6 +557,12 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
},
|
||||
|
||||
.close => self.close(),
|
||||
|
||||
// Close without confirmation.
|
||||
.child_exited => {
|
||||
self.child_exited = true;
|
||||
self.close();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ pub const App = struct {
|
||||
new_split: ?*const fn (SurfaceUD, input.SplitDirection) callconv(.C) void = null,
|
||||
|
||||
/// 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_split: ?*const fn (SurfaceUD, input.SplitFocusDirection) callconv(.C) void = null,
|
||||
@ -188,13 +188,13 @@ pub const Surface = struct {
|
||||
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 {
|
||||
log.info("runtime embedder does not support closing a surface", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
func(self.opts.userdata);
|
||||
func(self.opts.userdata, process_alive);
|
||||
}
|
||||
|
||||
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
|
||||
/// normal trigger process that a close surface input binding would.
|
||||
export fn ghostty_surface_request_close(ptr: *Surface) void {
|
||||
ptr.close();
|
||||
ptr.core_surface.close();
|
||||
}
|
||||
|
||||
/// Request that the surface split in the given direction.
|
||||
|
@ -409,8 +409,11 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
/// Close this surface.
|
||||
pub fn close(self: *const Surface) void {
|
||||
self.window.setShouldClose(true);
|
||||
pub fn close(self: *Surface, processActive: bool) void {
|
||||
_ = processActive;
|
||||
self.setShouldClose();
|
||||
self.deinit();
|
||||
self.app.app.alloc.destroy(self);
|
||||
}
|
||||
|
||||
/// Set the size limits of the window.
|
||||
|
@ -171,11 +171,6 @@ pub const App = struct {
|
||||
}
|
||||
|
||||
/// Close the given surface.
|
||||
pub fn closeSurface(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
surface.close();
|
||||
}
|
||||
|
||||
pub fn redrawSurface(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
surface.invalidate();
|
||||
@ -635,8 +630,44 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
/// Close this surface.
|
||||
pub fn close(self: *Surface) void {
|
||||
pub fn close(self: *Surface, processActive: bool) void {
|
||||
if (!processActive) {
|
||||
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(>kCloseConfirmation), self, null, G_CONNECT_DEFAULT);
|
||||
|
||||
c.gtk_widget_show(alert);
|
||||
}
|
||||
|
||||
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 {
|
||||
return @ptrCast(*Surface, @alignCast(@alignOf(Surface), ud));
|
||||
}
|
||||
|
@ -33,6 +33,10 @@ pub const Message = union(enum) {
|
||||
/// Close the surface. This will only close the current surface that
|
||||
/// receives this, not the full application.
|
||||
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.
|
||||
|
@ -401,7 +401,7 @@ fn processExit(
|
||||
|
||||
// Notify our surface we want to close
|
||||
_ = ev.surface_mailbox.push(.{
|
||||
.close = {},
|
||||
.child_exited = {},
|
||||
}, .{ .forever = {} });
|
||||
|
||||
return .disarm;
|
||||
|
Reference in New Issue
Block a user