From ec0f90d1b6215eeee41a5bfcf7cd672eecc75478 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 1 Aug 2024 14:49:02 -0500 Subject: [PATCH] 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. --- src/App.zig | 4 ++ src/apprt/gtk/App.zig | 94 ++++++++++++++++++++++++------------------- src/main_ghostty.zig | 5 +++ 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/src/App.zig b/src/App.zig index 314d0b25b..69145ee58 100644 --- a/src/App.zig +++ b/src/App.zig @@ -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 diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b6ab12472..80ea89ca9 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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; } } diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 62b424f62..6354214d0 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -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(); }