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.
|
/// These will be asserted to exist at runtime.
|
||||||
pub const blueprints: []const Blueprint = &.{
|
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 = "config-errors-dialog" },
|
||||||
.{ .major = 1, .minor = 2, .name = "surface" },
|
.{ .major = 1, .minor = 2, .name = "surface" },
|
||||||
.{ .major = 1, .minor = 5, .name = "window" },
|
.{ .major = 1, .minor = 5, .name = "window" },
|
||||||
|
@ -28,6 +28,7 @@ const Common = @import("../class.zig").Common;
|
|||||||
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
||||||
const Config = @import("config.zig").Config;
|
const Config = @import("config.zig").Config;
|
||||||
const Window = @import("window.zig").Window;
|
const Window = @import("window.zig").Window;
|
||||||
|
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||||
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
|
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_ghostty_application);
|
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.
|
/// only be set by the main loop thread.
|
||||||
running: bool = false,
|
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.
|
/// If non-null, we're currently showing a config errors dialog.
|
||||||
/// This is a WeakRef because the dialog can close on its own
|
/// This is a WeakRef because the dialog can close on its own
|
||||||
/// outside of our own lifecycle and that's okay.
|
/// 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.
|
// The final cleanup that is always required at the end of running.
|
||||||
defer {
|
defer {
|
||||||
|
// Ensure our timer source is removed
|
||||||
|
self.stopQuitTimer();
|
||||||
|
|
||||||
// Sync any remaining settings
|
// Sync any remaining settings
|
||||||
gio.Settings.sync();
|
gio.Settings.sync();
|
||||||
|
|
||||||
@ -378,19 +391,64 @@ pub const Application = extern struct {
|
|||||||
if (!config.@"quit-after-last-window-closed") break :q false;
|
if (!config.@"quit-after-last-window-closed") break :q false;
|
||||||
|
|
||||||
// If the quit timer has expired, quit.
|
// 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.
|
// There's no quit timer running, or it hasn't expired, don't quit.
|
||||||
break :q false;
|
break :q false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (must_quit) {
|
if (must_quit) self.quit();
|
||||||
//self.quit();
|
|
||||||
priv.running = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// apprt API to perform an action.
|
||||||
pub fn performAction(
|
pub fn performAction(
|
||||||
self: *Self,
|
self: *Self,
|
||||||
@ -418,6 +476,8 @@ pub const Application = extern struct {
|
|||||||
|
|
||||||
.pwd => Action.pwd(target, value),
|
.pwd => Action.pwd(target, value),
|
||||||
|
|
||||||
|
.quit => self.quit(),
|
||||||
|
|
||||||
.quit_timer => try Action.quitTimer(self, value),
|
.quit_timer => try Action.quitTimer(self, value),
|
||||||
|
|
||||||
.render => Action.render(self, target),
|
.render => Action.render(self, target),
|
||||||
@ -425,7 +485,6 @@ pub const Application = extern struct {
|
|||||||
.set_title => Action.setTitle(target, value),
|
.set_title => Action.setTitle(target, value),
|
||||||
|
|
||||||
// Unimplemented but todo on gtk-ng branch
|
// Unimplemented but todo on gtk-ng branch
|
||||||
.quit,
|
|
||||||
.close_window,
|
.close_window,
|
||||||
.toggle_maximize,
|
.toggle_maximize,
|
||||||
.toggle_fullscreen,
|
.toggle_fullscreen,
|
||||||
@ -525,6 +584,51 @@ pub const Application = extern struct {
|
|||||||
return &self.private().winproto;
|
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
|
// Libghostty Callbacks
|
||||||
|
|
||||||
@ -744,6 +848,20 @@ pub const Application = extern struct {
|
|||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// Signal Handlers
|
// 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(
|
fn handleStyleManagerDark(
|
||||||
style: *adw.StyleManager,
|
style: *adw.StyleManager,
|
||||||
_: *gobject.ParamSpec,
|
_: *gobject.ParamSpec,
|
||||||
@ -967,14 +1085,9 @@ const Action = struct {
|
|||||||
self: *Application,
|
self: *Application,
|
||||||
mode: apprt.action.QuitTimer,
|
mode: apprt.action.QuitTimer,
|
||||||
) !void {
|
) !void {
|
||||||
// TODO: An actual quit timer implementation. For now, we immediately
|
|
||||||
// quit on no windows regardless of the config.
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
.start => {
|
.start => self.startQuitTimer(),
|
||||||
self.private().running = false;
|
.stop => self.stopQuitTimer(),
|
||||||
},
|
|
||||||
|
|
||||||
.stop => {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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