mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
apprt/gtk-ng: implement quit timer, close app confirmation (#8006)
This PR tidies up our quit logic to match the GTK implementation, respecting quit delays and also showing a confirmation dialog if required. There shouldn't be anything surprising here, its mostly a copy/paste of the old logic with very small tweaks to fit the new style.
This commit is contained in:
@ -30,6 +30,7 @@ pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 };
|
||||
///
|
||||
/// These will be asserted to exist at runtime.
|
||||
pub const blueprints: []const Blueprint = &.{
|
||||
.{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "surface" },
|
||||
.{ .major = 1, .minor = 5, .name = "window" },
|
||||
|
@ -28,6 +28,7 @@ const Common = @import("../class.zig").Common;
|
||||
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Window = @import("window.zig").Window;
|
||||
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_application);
|
||||
@ -110,6 +111,15 @@ pub const Application = extern struct {
|
||||
/// only be set by the main loop thread.
|
||||
running: bool = false,
|
||||
|
||||
/// The timer used to quit the application after the last window is
|
||||
/// closed. Even if there is no quit delay set, this is the state
|
||||
/// used to determine to close the app.
|
||||
quit_timer: union(enum) {
|
||||
off,
|
||||
active: c_uint,
|
||||
expired,
|
||||
} = .off,
|
||||
|
||||
/// If non-null, we're currently showing a config errors dialog.
|
||||
/// This is a WeakRef because the dialog can close on its own
|
||||
/// outside of our own lifecycle and that's okay.
|
||||
@ -309,6 +319,9 @@ pub const Application = extern struct {
|
||||
|
||||
// The final cleanup that is always required at the end of running.
|
||||
defer {
|
||||
// Ensure our timer source is removed
|
||||
self.stopQuitTimer();
|
||||
|
||||
// Sync any remaining settings
|
||||
gio.Settings.sync();
|
||||
|
||||
@ -378,19 +391,64 @@ pub const Application = extern struct {
|
||||
if (!config.@"quit-after-last-window-closed") break :q false;
|
||||
|
||||
// If the quit timer has expired, quit.
|
||||
// if (self.quit_timer == .expired) break :q true;
|
||||
if (priv.quit_timer == .expired) break :q true;
|
||||
|
||||
// There's no quit timer running, or it hasn't expired, don't quit.
|
||||
break :q false;
|
||||
};
|
||||
|
||||
if (must_quit) {
|
||||
//self.quit();
|
||||
priv.running = false;
|
||||
}
|
||||
if (must_quit) self.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Quit the application. This will start the process to stop the
|
||||
/// run loop. It will not `posix.exit`.
|
||||
pub fn quit(self: *Self) void {
|
||||
const priv = self.private();
|
||||
|
||||
// If our run loop has already exited then we are done.
|
||||
if (!priv.running) return;
|
||||
|
||||
// If our core app doesn't need to confirm quit then we
|
||||
// can exit immediately.
|
||||
if (!priv.core_app.needsConfirmQuit()) {
|
||||
self.quitNow();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show a confirmation dialog
|
||||
const dialog: *CloseConfirmationDialog = .new(.app);
|
||||
|
||||
// Connect to the reload signal so we know to reload our config.
|
||||
_ = CloseConfirmationDialog.signals.@"close-request".connect(
|
||||
dialog,
|
||||
*Application,
|
||||
handleCloseConfirmation,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Show it
|
||||
dialog.present();
|
||||
}
|
||||
|
||||
fn quitNow(self: *Self) void {
|
||||
// Get all our windows and destroy them, forcing them to
|
||||
// free their memory.
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
list.foreach(struct {
|
||||
fn callback(data: ?*anyopaque, _: ?*anyopaque) callconv(.c) void {
|
||||
const ptr = data orelse return;
|
||||
const window: *gtk.Window = @ptrCast(@alignCast(ptr));
|
||||
window.destroy();
|
||||
}
|
||||
}.callback, null);
|
||||
|
||||
// Trigger our runloop exit.
|
||||
self.private().running = false;
|
||||
}
|
||||
|
||||
/// apprt API to perform an action.
|
||||
pub fn performAction(
|
||||
self: *Self,
|
||||
@ -418,6 +476,8 @@ pub const Application = extern struct {
|
||||
|
||||
.pwd => Action.pwd(target, value),
|
||||
|
||||
.quit => self.quit(),
|
||||
|
||||
.quit_timer => try Action.quitTimer(self, value),
|
||||
|
||||
.render => Action.render(self, target),
|
||||
@ -425,7 +485,6 @@ pub const Application = extern struct {
|
||||
.set_title => Action.setTitle(target, value),
|
||||
|
||||
// Unimplemented but todo on gtk-ng branch
|
||||
.quit,
|
||||
.close_window,
|
||||
.toggle_maximize,
|
||||
.toggle_fullscreen,
|
||||
@ -525,6 +584,51 @@ pub const Application = extern struct {
|
||||
return &self.private().winproto;
|
||||
}
|
||||
|
||||
/// This will get called when there are no more open surfaces.
|
||||
fn startQuitTimer(self: *Self) void {
|
||||
const priv = self.private();
|
||||
const config = priv.config.get();
|
||||
|
||||
// Cancel any previous timer.
|
||||
self.stopQuitTimer();
|
||||
|
||||
// This is a no-op unless we are configured to quit after last window is closed.
|
||||
if (!config.@"quit-after-last-window-closed") return;
|
||||
|
||||
// If a delay is configured, set a timeout function to quit after the delay.
|
||||
if (config.@"quit-after-last-window-closed-delay") |v| {
|
||||
priv.quit_timer = .{
|
||||
.active = glib.timeoutAdd(
|
||||
v.asMilliseconds(),
|
||||
handleQuitTimerExpired,
|
||||
self,
|
||||
),
|
||||
};
|
||||
} else {
|
||||
// If no delay is configured, treat it as expired.
|
||||
priv.quit_timer = .expired;
|
||||
}
|
||||
}
|
||||
|
||||
/// This will get called when a new surface gets opened.
|
||||
fn stopQuitTimer(self: *Self) void {
|
||||
const priv = self.private();
|
||||
switch (priv.quit_timer) {
|
||||
.off => {},
|
||||
.expired => priv.quit_timer = .off,
|
||||
.active => |source| {
|
||||
if (glib.Source.remove(source) == 0) {
|
||||
log.warn(
|
||||
"unable to remove quit timer source={d}",
|
||||
.{source},
|
||||
);
|
||||
}
|
||||
|
||||
priv.quit_timer = .off;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Libghostty Callbacks
|
||||
|
||||
@ -744,6 +848,20 @@ pub const Application = extern struct {
|
||||
//---------------------------------------------------------------
|
||||
// Signal Handlers
|
||||
|
||||
fn handleCloseConfirmation(
|
||||
_: *CloseConfirmationDialog,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.quitNow();
|
||||
}
|
||||
|
||||
fn handleQuitTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *Self = @ptrCast(@alignCast(ud));
|
||||
const priv = self.private();
|
||||
priv.quit_timer = .expired;
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn handleStyleManagerDark(
|
||||
style: *adw.StyleManager,
|
||||
_: *gobject.ParamSpec,
|
||||
@ -967,14 +1085,9 @@ const Action = struct {
|
||||
self: *Application,
|
||||
mode: apprt.action.QuitTimer,
|
||||
) !void {
|
||||
// TODO: An actual quit timer implementation. For now, we immediately
|
||||
// quit on no windows regardless of the config.
|
||||
switch (mode) {
|
||||
.start => {
|
||||
self.private().running = false;
|
||||
},
|
||||
|
||||
.stop => {},
|
||||
.start => self.startQuitTimer(),
|
||||
.stop => self.stopQuitTimer(),
|
||||
}
|
||||
}
|
||||
|
||||
|
200
src/apprt/gtk-ng/class/close_confirmation_dialog.zig
Normal file
200
src/apprt/gtk-ng/class/close_confirmation_dialog.zig
Normal file
@ -0,0 +1,200 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Dialog = @import("dialog.zig").Dialog;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_config_errors_dialog);
|
||||
|
||||
pub const CloseConfirmationDialog = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = Dialog;
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhosttyCloseConfirmationDialog",
|
||||
.instanceInit = &init,
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
pub const properties = struct {
|
||||
pub const target = struct {
|
||||
pub const name = "target";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
Target,
|
||||
.{
|
||||
.nick = "Target",
|
||||
.blurb = "The target for this close confirmation.",
|
||||
.default = .app,
|
||||
.accessor = gobject.ext.privateFieldAccessor(
|
||||
Self,
|
||||
Private,
|
||||
&Private.offset,
|
||||
"target",
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
pub const @"close-request" = struct {
|
||||
pub const name = "close-request";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
target: Target,
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
pub fn new(target: Target) *Self {
|
||||
return gobject.ext.newInstance(Self, .{
|
||||
.target = target,
|
||||
});
|
||||
}
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.C) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
|
||||
// Setup our title/body text.
|
||||
const priv = self.private();
|
||||
self.as(Dialog.Parent).setHeading(priv.target.title());
|
||||
self.as(Dialog.Parent).setBody(priv.target.body());
|
||||
}
|
||||
|
||||
pub fn present(self: *Self) void {
|
||||
const priv = self.private();
|
||||
self.as(Dialog).present(priv.target.dialogParent());
|
||||
}
|
||||
|
||||
pub fn close(self: *Self) void {
|
||||
self.as(Dialog).close();
|
||||
}
|
||||
|
||||
fn response(
|
||||
self: *Self,
|
||||
response_id: [*:0]const u8,
|
||||
) callconv(.C) void {
|
||||
if (std.mem.orderZ(u8, response_id, "close") != .eq) return;
|
||||
signals.@"close-request".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
fn dispose(self: *Self) callconv(.C) void {
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
);
|
||||
|
||||
gobject.Object.virtual_methods.dispose.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
pub const unref = C.unref;
|
||||
const private = C.private;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.C) void {
|
||||
gobject.ext.ensureType(Dialog);
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
.major = 1,
|
||||
.minor = 2,
|
||||
.name = "close-confirmation-dialog",
|
||||
}),
|
||||
);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.target.impl,
|
||||
});
|
||||
|
||||
// Signals
|
||||
signals.@"close-request".impl.register(.{});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
Dialog.virtual_methods.response.implement(class, &response);
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
};
|
||||
};
|
||||
|
||||
/// The target of a close dialog.
|
||||
///
|
||||
/// This is here so that we can consolidate all logic related to
|
||||
/// prompting the user and closing windows/tabs/surfaces/etc.
|
||||
/// together into one struct that is the sole source of truth.
|
||||
pub const Target = enum(c_int) {
|
||||
app,
|
||||
|
||||
pub fn title(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("Quit Ghostty?"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn body(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("All terminal sessions will be terminated."),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dialogParent(self: Target) ?*gtk.Widget {
|
||||
return switch (self) {
|
||||
.app => {
|
||||
// Find the currently focused window.
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
const focused = list.findCustom(null, findActiveWindow);
|
||||
return @ptrCast(@alignCast(focused.f_data));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub const getGObjectType = gobject.ext.defineEnum(
|
||||
Target,
|
||||
.{ .name = "GhosttyCloseConfirmationDialogTarget" },
|
||||
);
|
||||
};
|
||||
|
||||
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
|
||||
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
|
||||
|
||||
// Confusingly, `isActive` returns 1 when active,
|
||||
// but we want to return 0 to indicate equality.
|
||||
// Abusing integers to be enums and booleans is a terrible idea, C.
|
||||
return if (window.isActive() != 0) 0 else -1;
|
||||
}
|
10
src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp
Normal file
10
src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp
Normal file
@ -0,0 +1,10 @@
|
||||
using Gtk 4.0;
|
||||
// This is unused but if we remove it we get a blueprint-compiler error.
|
||||
using Adw 1;
|
||||
|
||||
template $GhosttyCloseConfirmationDialog: $GhosttyDialog {
|
||||
responses [
|
||||
cancel: _("Cancel"),
|
||||
close: _("Close") destructive,
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user