Improve quit timers.

Instead of "polling" to see if a quit timer has expired, start a single
timer that expires after the confiugred delay when no more surfaces are
open. That timer can be cancelled if necessary.
This commit is contained in:
Jeffrey C. Ollie
2024-08-01 14:49:02 -05:00
parent 3d6ca14dc6
commit ec0f90d1b6
3 changed files with 62 additions and 41 deletions

View File

@ -134,6 +134,8 @@ pub fn updateConfig(self: *App, config: *const Config) !void {
/// The surface must be from the pool.
pub fn addSurface(self: *App, rt_surface: *apprt.Surface) !void {
try self.surfaces.append(self.alloc, rt_surface);
if (@hasDecl(apprt.App, "cancelQuitTimer")) rt_surface.app.cancelQuitTimer();
}
/// Delete the surface from the known surface list. This will NOT call the
@ -158,6 +160,8 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
i += 1;
}
if (@hasDecl(apprt.App, "startQuitTimer") and self.surfaces.items.len == 0) rt_surface.app.startQuitTimer();
}
/// The last focused surface. This is only valid while on the main thread

View File

@ -76,6 +76,12 @@ transient_cgroup_base: ?[]const u8 = null,
/// CSS Provider for any styles based on ghostty configuration values
css_provider: *c.GtkCssProvider,
/// GLib source for tracking quit timer.
quit_timer_source: ?c.guint = null,
/// If there is a quit timer, has it expired?
quit_timer_expired: bool = false,
pub fn init(core_app: *CoreApp, opts: Options) !App {
_ = opts;
@ -281,10 +287,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
const css_provider = c.gtk_css_provider_new();
try loadRuntimeCss(&config, css_provider);
// Run a small no-op function every 500 milliseconds so that we don't get
// stuck in g_main_context_iteration forever if there are no open surfaces.
_ = c.g_timeout_add(500, gtkTimeout, null);
return .{
.core_app = core_app,
.app = app,
@ -301,12 +303,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
};
}
// This timeout function is run periodically so that we don't get stuck in
// g_main_context_iteration forever if there are no open surfaces.
pub fn gtkTimeout(_: ?*anyopaque) callconv(.C) c.gboolean {
return 1;
}
// Terminate the application. The application will not be restarted after
// this so all global state can be cleaned up.
pub fn terminate(self: *App) void {
@ -477,9 +473,6 @@ pub fn run(self: *App) !void {
self.transient_cgroup_base = path;
} else log.debug("cgroup isoation disabled config={}", .{self.config.@"linux-cgroup"});
// The last instant that one or more surfaces were open
var last_one = try std.time.Instant.now();
// Setup our menu items
self.initActions();
self.initMenu();
@ -498,44 +491,63 @@ pub fn run(self: *App) !void {
// Tick the terminal app and see if we should quit.
const should_quit = try self.core_app.tick(self);
// If there are one or more surfaces open, update the timer.
if (self.core_app.surfaces.items.len > 0) last_one = try std.time.Instant.now();
const q = q: {
const must_quit = q: {
// If we've been told by GTK that we should quit, do so regardless
// of any other setting.
if (should_quit) break :q true;
// If there are no surfaces check to see if we should stay in the
// background or not.
if (self.core_app.surfaces.items.len == 0) {
if (self.config.@"quit-after-last-window-closed") {
// If the background timeout is not zero, check to see if
// the timeout has elapsed.
if (self.config.@"quit-after-last-window-closed-delay") |duration| {
const now = try std.time.Instant.now();
// If we are configured to always stay running, don't quit.
if (!self.config.@"quit-after-last-window-closed") break :q false;
if (now.since(last_one) > duration.duration) {
// The timeout has elapsed, quit.
break :q true;
}
// Not enough time has elapsed, don't quit.
break :q false;
}
// `quit-after-last-window-closed-delay` is not set, don't quit.
break :q false;
}
break :q false;
if (self.quit_timer_source) |_| {
// if the quit timer has expired, quit.
if (self.quit_timer_expired) break :q true;
}
// there's at least one surface open, don't quit.
// There's no quit timer running, or it hasn't expired, don't quit.
break :q false;
};
if (q) self.quit();
if (must_quit) self.quit();
}
}
// This timeout function is started when no surfaces are open. It can be
// cancelled if a new surface is opened before the timer expires.
pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean {
const self: *App = @ptrCast(@alignCast(ud));
self.quit_timer_expired = true;
return c.FALSE;
}
/// This will get called when there are no more open surfaces.
pub fn startQuitTimer(self: *App) void {
// Cancel any previous timeout.
if (self.quit_timer_source) |source| {
if (c.g_source_remove(source) == c.FALSE)
log.warn("unable to remove quit timer {d}", .{source});
self.quit_timer_source = null;
}
// This is a no-op unless we are configured to quit after last window is closed.
if (!self.config.@"quit-after-last-window-closed") return;
// If a delay is configured, set a timeout function to quit after the delay.
if (self.config.@"quit-after-last-window-closed-delay") |duration| {
const t: c.guint = if (duration.duration > (std.math.maxInt(c.guint) * std.time.ns_per_ms))
std.math.maxInt(c.guint)
else
@intCast(duration.duration / std.time.ns_per_ms);
self.quit_timer_source = c.g_timeout_add(t, gtkQuitTimerExpired, self);
}
}
/// This will get called when a new surface gets opened.
pub fn cancelQuitTimer(self: *App) void {
if (self.quit_timer_source) |source| {
if (c.g_source_remove(source) == c.FALSE)
log.warn("unable to remove quit timer {d}", .{source});
self.quit_timer_source = null;
}
}

View File

@ -107,6 +107,11 @@ pub fn main() !MainReturn {
var app_runtime = try apprt.App.init(app, .{});
defer app_runtime.terminate();
// Since - by definition - there are no surfaces when first started, the
// quit timer may need to be started. The start timer will get cancelled if/
// when the first surface is created.
if (@hasDecl(apprt.App, "startQuitTimer")) app_runtime.startQuitTimer();
// Run the GUI event loop
try app_runtime.run();
}