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 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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
self.window.closeSurface(self);
|
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 {
|
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));
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user