mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
fix(gtk): add close confirmation for tabs (#4235)
On the discord this bit of feedback came up from [here](https://ncurses.scripts.mit.edu/?p=ncurses.git;a=blob;f=misc/terminfo.src;h=ccd4c32099cf740d331eeaa3955f48f177435878;hb=a28a11d84d969cfdc876e158deae7870e8948a24#l8323) ``` 8323 # - ghostty has tabs (imitating gnome-terminal); when closing a tab with a 8324 # running process (e.g., a hung vttest), ghostty does not prompt about the 8325 # process to be killed. ``` This PR adds confirmation to the places where tabs are closed directly Fixes: https://github.com/ghostty-org/ghostty/issues/4234
This commit is contained in:
@ -121,10 +121,63 @@ pub fn remove(self: *Tab) void {
|
|||||||
self.window.closeTab(self);
|
self.window.closeTab(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
/// Helper function to check if any surface in the split hierarchy needs close confirmation
|
||||||
|
fn needsConfirm(elem: Surface.Container.Elem) bool {
|
||||||
|
return switch (elem) {
|
||||||
|
.surface => |s| s.core_surface.needsConfirmQuit(),
|
||||||
|
.split => |s| needsConfirm(s.top_left) or needsConfirm(s.bottom_right),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the tab, asking for confirmation if any surface requests it.
|
||||||
|
pub fn closeWithConfirmation(tab: *Tab) void {
|
||||||
|
switch (tab.elem) {
|
||||||
|
.surface => |s| s.close(s.core_surface.needsConfirmQuit()),
|
||||||
|
.split => |s| {
|
||||||
|
if (needsConfirm(s.top_left) or needsConfirm(s.bottom_right)) {
|
||||||
|
const alert = c.gtk_message_dialog_new(
|
||||||
|
tab.window.window,
|
||||||
|
c.GTK_DIALOG_MODAL,
|
||||||
|
c.GTK_MESSAGE_QUESTION,
|
||||||
|
c.GTK_BUTTONS_YES_NO,
|
||||||
|
"Close this tab?",
|
||||||
|
);
|
||||||
|
c.gtk_message_dialog_format_secondary_text(
|
||||||
|
@ptrCast(alert),
|
||||||
|
"All terminal sessions in this tab will be terminated.",
|
||||||
|
);
|
||||||
|
|
||||||
|
// We want the "yes" to appear destructive.
|
||||||
|
const yes_widget = c.gtk_dialog_get_widget_for_response(
|
||||||
|
@ptrCast(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(alert),
|
||||||
|
c.GTK_RESPONSE_NO,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(>kTabCloseConfirmation), tab, null, c.G_CONNECT_DEFAULT);
|
||||||
|
c.gtk_widget_show(alert);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab.remove();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkTabCloseConfirmation(
|
||||||
|
alert: *c.GtkMessageDialog,
|
||||||
|
response: c.gint,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||||
const window = tab.window;
|
c.gtk_window_destroy(@ptrCast(alert));
|
||||||
window.closeTab(tab);
|
if (response != c.GTK_RESPONSE_YES) return;
|
||||||
|
tab.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||||
@ -135,17 +188,3 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
|||||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||||
tab.destroy(tab.window.app.core_app.alloc);
|
tab.destroy(tab.window.app.core_app.alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gtkTabClick(
|
|
||||||
gesture: *c.GtkGestureClick,
|
|
||||||
_: c.gint,
|
|
||||||
_: c.gdouble,
|
|
||||||
_: c.gdouble,
|
|
||||||
ud: ?*anyopaque,
|
|
||||||
) callconv(.C) void {
|
|
||||||
const self: *Tab = @ptrCast(@alignCast(ud));
|
|
||||||
const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
|
|
||||||
if (gtk_button == c.GDK_BUTTON_MIDDLE) {
|
|
||||||
self.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -17,6 +17,14 @@ pub const NotebookAdw = struct {
|
|||||||
/// the tab view
|
/// the tab view
|
||||||
tab_view: *AdwTabView,
|
tab_view: *AdwTabView,
|
||||||
|
|
||||||
|
/// Set to true so that the adw close-page handler knows we're forcing
|
||||||
|
/// and to allow a close to happen with no confirm. This is a bit of a hack
|
||||||
|
/// because we currently use GTK alerts to confirm tab close and they
|
||||||
|
/// don't carry with them the ADW state that we are confirming or not.
|
||||||
|
/// Long term we should move to ADW alerts so we can know if we are
|
||||||
|
/// confirming or not.
|
||||||
|
forcing_close: bool = false,
|
||||||
|
|
||||||
pub fn init(notebook: *Notebook) void {
|
pub fn init(notebook: *Notebook) void {
|
||||||
const window: *Window = @fieldParentPtr("notebook", notebook);
|
const window: *Window = @fieldParentPtr("notebook", notebook);
|
||||||
const app = window.app;
|
const app = window.app;
|
||||||
@ -38,6 +46,7 @@ pub const NotebookAdw = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(tab_view, "close-page", c.G_CALLBACK(&adwClosePage), window, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
|
||||||
}
|
}
|
||||||
@ -112,6 +121,12 @@ pub const NotebookAdw = struct {
|
|||||||
pub fn closeTab(self: *NotebookAdw, tab: *Tab) void {
|
pub fn closeTab(self: *NotebookAdw, tab: *Tab) void {
|
||||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||||
|
|
||||||
|
// closeTab always expects to close unconditionally so we mark this
|
||||||
|
// as true so that the close_page call below doesn't request
|
||||||
|
// confirmation.
|
||||||
|
self.forcing_close = true;
|
||||||
|
defer self.forcing_close = false;
|
||||||
|
|
||||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
|
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
|
||||||
c.adw_tab_view_close_page(self.tab_view, page);
|
c.adw_tab_view_close_page(self.tab_view, page);
|
||||||
|
|
||||||
@ -143,6 +158,28 @@ fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaqu
|
|||||||
window.focusCurrentTab();
|
window.focusCurrentTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn adwClosePage(
|
||||||
|
_: *AdwTabView,
|
||||||
|
page: *c.AdwTabPage,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) c.gboolean {
|
||||||
|
const child = c.adw_tab_page_get_child(page);
|
||||||
|
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(
|
||||||
|
@ptrCast(child),
|
||||||
|
Tab.GHOSTTY_TAB,
|
||||||
|
) orelse return 0));
|
||||||
|
|
||||||
|
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||||
|
const notebook = window.notebook.adw;
|
||||||
|
c.adw_tab_view_close_page_finish(
|
||||||
|
notebook.tab_view,
|
||||||
|
page,
|
||||||
|
@intFromBool(notebook.forcing_close),
|
||||||
|
);
|
||||||
|
if (!notebook.forcing_close) tab.closeWithConfirmation();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
fn adwTabViewCreateWindow(
|
fn adwTabViewCreateWindow(
|
||||||
_: *AdwTabView,
|
_: *AdwTabView,
|
||||||
ud: ?*anyopaque,
|
ud: ?*anyopaque,
|
||||||
|
@ -157,8 +157,8 @@ pub const NotebookGtk = struct {
|
|||||||
c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
|
c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
|
||||||
c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
|
c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
|
||||||
|
|
||||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||||
|
|
||||||
// Tab settings
|
// Tab settings
|
||||||
c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
|
c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
|
||||||
@ -283,3 +283,22 @@ fn gtkNotebookCreateWindow(
|
|||||||
|
|
||||||
return newWindow.notebook.gtk.notebook;
|
return newWindow.notebook.gtk.notebook;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||||
|
tab.closeWithConfirmation();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkTabClick(
|
||||||
|
gesture: *c.GtkGestureClick,
|
||||||
|
_: c.gint,
|
||||||
|
_: c.gdouble,
|
||||||
|
_: c.gdouble,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self: *Tab = @ptrCast(@alignCast(ud));
|
||||||
|
const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
|
||||||
|
if (gtk_button == c.GDK_BUTTON_MIDDLE) {
|
||||||
|
self.closeWithConfirmation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user