mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
gtk: allow running in the background
This patch fixes #2010 by implementing `quit-after-last-window-closed` for the GTK apprt. It also adds the ability for the GTK apprt to exit after a delay once all surfaces have been closed and adds the ability to start Ghostty without opening an initial window.
This commit is contained in:
@ -253,10 +253,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
break :x11_xkb try x11.Xkb.init(display);
|
break :x11_xkb try x11.Xkb.init(display);
|
||||||
};
|
};
|
||||||
|
|
||||||
// This just calls the "activate" signal but its part of the normal
|
// This just calls the `activate` signal but its part of the normal startup
|
||||||
// startup routine so we just call it:
|
// routine so we just call it, but only if the config allows it (this allows
|
||||||
|
// for launching Ghostty in the "background" without immediately opening
|
||||||
|
// a window)
|
||||||
|
//
|
||||||
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||||
c.g_application_activate(gapp);
|
if (config.@"initial-window")
|
||||||
|
c.g_application_activate(gapp);
|
||||||
|
|
||||||
// Register for dbus events
|
// Register for dbus events
|
||||||
if (c.g_application_get_dbus_connection(gapp)) |dbus_connection| {
|
if (c.g_application_get_dbus_connection(gapp)) |dbus_connection| {
|
||||||
@ -277,6 +281,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
const css_provider = c.gtk_css_provider_new();
|
const css_provider = c.gtk_css_provider_new();
|
||||||
try loadRuntimeCss(&config, css_provider);
|
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 .{
|
return .{
|
||||||
.core_app = core_app,
|
.core_app = core_app,
|
||||||
.app = app,
|
.app = app,
|
||||||
@ -293,6 +301,12 @@ 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
|
// Terminate the application. The application will not be restarted after
|
||||||
// this so all global state can be cleaned up.
|
// this so all global state can be cleaned up.
|
||||||
pub fn terminate(self: *App) void {
|
pub fn terminate(self: *App) void {
|
||||||
@ -463,6 +477,9 @@ pub fn run(self: *App) !void {
|
|||||||
self.transient_cgroup_base = path;
|
self.transient_cgroup_base = path;
|
||||||
} else log.debug("cgroup isoation disabled config={}", .{self.config.@"linux-cgroup"});
|
} 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
|
// Setup our menu items
|
||||||
self.initActions();
|
self.initActions();
|
||||||
self.initMenu();
|
self.initMenu();
|
||||||
@ -478,9 +495,48 @@ pub fn run(self: *App) !void {
|
|||||||
while (self.running) {
|
while (self.running) {
|
||||||
_ = c.g_main_context_iteration(self.ctx, 1);
|
_ = c.g_main_context_iteration(self.ctx, 1);
|
||||||
|
|
||||||
// Tick the terminal app
|
// Tick the terminal app and see if we should quit.
|
||||||
const should_quit = try self.core_app.tick(self);
|
const should_quit = try self.core_app.tick(self);
|
||||||
if (should_quit or self.core_app.surfaces.items.len == 0) self.quit();
|
|
||||||
|
// 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: {
|
||||||
|
// 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 != 0) {
|
||||||
|
const now = try std.time.Instant.now();
|
||||||
|
|
||||||
|
if (now.since(last_one) > self.config.@"quit-after-last-window-closed-delay".duration) {
|
||||||
|
log.info("timeout elapsed", .{});
|
||||||
|
// 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 zero, don't quit.
|
||||||
|
break :q false;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :q false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// there's at least one surface open, don't quit.
|
||||||
|
break :q false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (q) self.quit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,9 +654,8 @@ fn gtkQuitConfirmation(
|
|||||||
self.quitNow();
|
self.quitNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is called by the "activate" signal. This is sent on program
|
/// This is called by the `activate` signal. This is sent on program startup and
|
||||||
/// startup and also when a secondary instance launches and requests
|
/// also when a secondary instance launches and requests a new window.
|
||||||
/// a new window.
|
|
||||||
fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
||||||
_ = app;
|
_ = app;
|
||||||
|
|
||||||
|
@ -880,11 +880,46 @@ keybind: Keybinds = .{},
|
|||||||
/// true. If set to false, surfaces will close without any confirmation.
|
/// true. If set to false, surfaces will close without any confirmation.
|
||||||
@"confirm-close-surface": bool = true,
|
@"confirm-close-surface": bool = true,
|
||||||
|
|
||||||
/// Whether or not to quit after the last window is closed. This defaults to
|
/// Whether or not to quit after the last surface is closed. This
|
||||||
/// false. Currently only supported on macOS. On Linux, the process always exits
|
/// defaults to `false`. On Linux, if this is `true`, Ghostty can delay
|
||||||
/// after the last window is closed.
|
/// quitting fully for a configurable amount. See the documentation of
|
||||||
|
/// `quit-after-last-window-closed-delay` for more information.
|
||||||
@"quit-after-last-window-closed": bool = false,
|
@"quit-after-last-window-closed": bool = false,
|
||||||
|
|
||||||
|
/// If `quit-after-last-window-closed` is `true`, this controls how long
|
||||||
|
/// Ghostty will stay running after the last open surface has been closed. If
|
||||||
|
/// `quit-after-last-window-closed-delay` is set to 0 ns Ghostty will remain
|
||||||
|
/// running indefinitely. The duration should be at least long enough to allow
|
||||||
|
/// Ghostty to initialize and open it's first window, but this is not enforced
|
||||||
|
/// nor will a warning be issued. The duration is specified as a series of
|
||||||
|
/// numbers followed by time units. Whitespace is allowed between numbers and
|
||||||
|
/// units. The allowed time units are as follows:
|
||||||
|
///
|
||||||
|
/// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments
|
||||||
|
/// are made for leap years or leap seconds.
|
||||||
|
/// * `d` - one SI day, or 86400 seconds.
|
||||||
|
/// * `h` - one hour, or 3600 seconds.
|
||||||
|
/// * `m` - one minute, or 60 seconds.
|
||||||
|
/// * `s` - one second.
|
||||||
|
/// * `ms` - one millisecond, or 0.001 second.
|
||||||
|
/// * `us` or `µs` - one microsecond, or 0.000001 second.
|
||||||
|
/// * `ns` - one nanosecond, or 0.000000001 second.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// * `1h30m`
|
||||||
|
/// * `45s`
|
||||||
|
///
|
||||||
|
/// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`.
|
||||||
|
///
|
||||||
|
/// By default `quit-after-last-window-closed-delay` is set to `0 ns`.
|
||||||
|
///
|
||||||
|
/// Only implemented on Linux.
|
||||||
|
@"quit-after-last-window-closed-delay": Duration = .{ .duration = 0 },
|
||||||
|
|
||||||
|
/// This controls whether an initial window is created when Ghostty is run. Only
|
||||||
|
/// implemented on Linux.
|
||||||
|
@"initial-window": bool = true,
|
||||||
|
|
||||||
/// Whether to enable shell integration auto-injection or not. Shell integration
|
/// Whether to enable shell integration auto-injection or not. Shell integration
|
||||||
/// greatly enhances the terminal experience by enabling a number of features:
|
/// greatly enhances the terminal experience by enabling a number of features:
|
||||||
///
|
///
|
||||||
@ -1109,15 +1144,16 @@ keybind: Keybinds = .{},
|
|||||||
/// must always be able to move themselves into an isolated cgroup.
|
/// must always be able to move themselves into an isolated cgroup.
|
||||||
@"linux-cgroup-hard-fail": bool = false,
|
@"linux-cgroup-hard-fail": bool = false,
|
||||||
|
|
||||||
/// If true, the Ghostty GTK application will run in single-instance mode:
|
/// If `true`, the Ghostty GTK application will run in single-instance mode:
|
||||||
/// each new `ghostty` process launched will result in a new window if there
|
/// each new `ghostty` process launched will result in a new window if there is
|
||||||
/// is already a running process.
|
/// already a running process.
|
||||||
///
|
///
|
||||||
/// If false, each new ghostty process will launch a separate application.
|
/// If `false`, each new ghostty process will launch a separate application.
|
||||||
///
|
///
|
||||||
/// The default value is `desktop` which will default to `true` if Ghostty
|
/// The default value is `detect` which will default to `true` if Ghostty
|
||||||
/// detects it was launched from the `.desktop` file such as an app launcher.
|
/// detects that it was launched from the `.desktop` file such as an app
|
||||||
/// If Ghostty is launched from the command line, it will default to `false`.
|
/// launcher (like Gnome Shell) or by D-Bus activation. If Ghostty is launched
|
||||||
|
/// from the command line, it will default to `false`.
|
||||||
///
|
///
|
||||||
/// Note that debug builds of Ghostty have a separate single-instance ID
|
/// Note that debug builds of Ghostty have a separate single-instance ID
|
||||||
/// so you can test single instance without conflicting with release builds.
|
/// so you can test single instance without conflicting with release builds.
|
||||||
@ -3753,3 +3789,211 @@ pub const LinuxCgroup = enum {
|
|||||||
always,
|
always,
|
||||||
@"single-instance",
|
@"single-instance",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Duration = struct {
|
||||||
|
duration: u64 = 0,
|
||||||
|
|
||||||
|
pub fn clone(self: *const @This(), _: Allocator) !@This() {
|
||||||
|
return .{ .duration = self.duration };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn equal(self: @This(), other: @This()) bool {
|
||||||
|
return self.duration == other.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parseCLI(self: *@This(), _: Allocator, input: ?[]const u8) !void {
|
||||||
|
var remaining = input orelse {
|
||||||
|
self.duration = 0;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const units = [_]struct {
|
||||||
|
name: []const u8,
|
||||||
|
factor: u64,
|
||||||
|
}{
|
||||||
|
.{ .name = "y", .factor = 365 * std.time.ns_per_day },
|
||||||
|
.{ .name = "w", .factor = std.time.ns_per_week },
|
||||||
|
.{ .name = "d", .factor = std.time.ns_per_day },
|
||||||
|
.{ .name = "h", .factor = std.time.ns_per_hour },
|
||||||
|
.{ .name = "m", .factor = std.time.ns_per_min },
|
||||||
|
.{ .name = "s", .factor = std.time.ns_per_s },
|
||||||
|
.{ .name = "ms", .factor = std.time.ns_per_ms },
|
||||||
|
.{ .name = "us", .factor = std.time.ns_per_us },
|
||||||
|
.{ .name = "µs", .factor = std.time.ns_per_us },
|
||||||
|
.{ .name = "ns", .factor = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var value: ?u64 = null;
|
||||||
|
|
||||||
|
while (remaining.len > 0) {
|
||||||
|
// Skip over whitespace before the number
|
||||||
|
while (remaining.len > 0 and std.ascii.isWhitespace(remaining[0])) {
|
||||||
|
remaining = remaining[1..];
|
||||||
|
}
|
||||||
|
// There was whitespace at the end, that's OK
|
||||||
|
if (remaining.len == 0) break;
|
||||||
|
|
||||||
|
// Find the longest number
|
||||||
|
const number = number: {
|
||||||
|
var prev_number: ?u64 = null;
|
||||||
|
var prev_remaining: ?[]const u8 = null;
|
||||||
|
for (1..remaining.len + 1) |index| {
|
||||||
|
prev_number = std.fmt.parseUnsigned(u64, remaining[0..index], 10) catch {
|
||||||
|
if (prev_remaining) |prev| remaining = prev;
|
||||||
|
break :number prev_number;
|
||||||
|
};
|
||||||
|
prev_remaining = remaining[index..];
|
||||||
|
}
|
||||||
|
if (prev_remaining) |prev| remaining = prev;
|
||||||
|
break :number prev_number;
|
||||||
|
} orelse return error.InvalidValue;
|
||||||
|
|
||||||
|
// Skip over any whitespace between the number and the unit
|
||||||
|
while (remaining.len > 0 and std.ascii.isWhitespace(remaining[0])) {
|
||||||
|
remaining = remaining[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
// A number without a unit is invalid
|
||||||
|
if (remaining.len == 0) return error.InvalidValue;
|
||||||
|
|
||||||
|
// Find the longest matching unit. Needs to be the longest matching
|
||||||
|
// to distinguish 'm' from 'ms'.
|
||||||
|
const factor = factor: {
|
||||||
|
var prev_factor: ?u64 = null;
|
||||||
|
var prev_index: ?usize = null;
|
||||||
|
for (1..remaining.len + 1) |index| {
|
||||||
|
const next_factor = next: {
|
||||||
|
for (units) |unit| {
|
||||||
|
if (std.mem.eql(u8, unit.name, remaining[0..index])) {
|
||||||
|
break :next unit.factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :next null;
|
||||||
|
};
|
||||||
|
if (next_factor) |next| {
|
||||||
|
prev_factor = next;
|
||||||
|
prev_index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prev_index) |index| {
|
||||||
|
remaining = remaining[index..];
|
||||||
|
}
|
||||||
|
break :factor prev_factor;
|
||||||
|
} orelse return error.InvalidValue;
|
||||||
|
|
||||||
|
if (value) |v|
|
||||||
|
value = v + number * factor
|
||||||
|
else
|
||||||
|
value = number * factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.duration = value orelse 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn formatEntry(self: @This(), formatter: anytype) !void {
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
var fbs = std.io.fixedBufferStream(&buf);
|
||||||
|
const writer = fbs.writer();
|
||||||
|
|
||||||
|
var value = self.duration;
|
||||||
|
|
||||||
|
const units = [_]struct {
|
||||||
|
name: []const u8,
|
||||||
|
factor: u64,
|
||||||
|
}{
|
||||||
|
.{ .name = "y", .factor = 365 * std.time.ns_per_day },
|
||||||
|
.{ .name = "w", .factor = std.time.ns_per_week },
|
||||||
|
.{ .name = "d", .factor = std.time.ns_per_day },
|
||||||
|
.{ .name = "h", .factor = std.time.ns_per_hour },
|
||||||
|
.{ .name = "m", .factor = std.time.ns_per_min },
|
||||||
|
.{ .name = "s", .factor = std.time.ns_per_s },
|
||||||
|
.{ .name = "ms", .factor = std.time.ns_per_ms },
|
||||||
|
.{ .name = "µs", .factor = std.time.ns_per_us },
|
||||||
|
.{ .name = "ns", .factor = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
for (units) |unit| {
|
||||||
|
if (value > unit.factor) {
|
||||||
|
if (i > 0) writer.writeAll(" ") catch unreachable;
|
||||||
|
const remainder = value % unit.factor;
|
||||||
|
const quotient = (value - remainder) / unit.factor;
|
||||||
|
writer.print("{d}{s}", .{ quotient, unit.name }) catch unreachable;
|
||||||
|
value = remainder;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try formatter.formatEntry([]const u8, fbs.getWritten());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "parse duration" {
|
||||||
|
var d: Duration = undefined;
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "");
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "0ns");
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1ns");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "100ns");
|
||||||
|
try std.testing.expectEqual(@as(u64, 100), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1µs");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1000), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1µs1ns");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1001), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1µs 1ns");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1001), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, " 1µs1ns");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1001), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1µs1ns ");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1001), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1y");
|
||||||
|
try std.testing.expectEqual(@as(u64, 365 * std.time.ns_per_day), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1d");
|
||||||
|
try std.testing.expectEqual(@as(u64, std.time.ns_per_day), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1h");
|
||||||
|
try std.testing.expectEqual(@as(u64, std.time.ns_per_hour), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1m");
|
||||||
|
try std.testing.expectEqual(@as(u64, std.time.ns_per_min), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1s");
|
||||||
|
try std.testing.expectEqual(@as(u64, std.time.ns_per_s), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "1ms");
|
||||||
|
try std.testing.expectEqual(@as(u64, std.time.ns_per_ms), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "30s");
|
||||||
|
try std.testing.expectEqual(@as(u64, 30 * std.time.ns_per_s), d.duration);
|
||||||
|
|
||||||
|
try d.parseCLI(std.testing.allocator, "584y 49w 23h 34m 33s 709ms 551µs 615ns");
|
||||||
|
try std.testing.expectEqual(std.math.maxInt(u64), d.duration);
|
||||||
|
|
||||||
|
try std.testing.expectError(error.InvalidValue, d.parseCLI(std.testing.allocator, "1"));
|
||||||
|
try std.testing.expectError(error.InvalidValue, d.parseCLI(std.testing.allocator, "s"));
|
||||||
|
try std.testing.expectError(error.InvalidValue, d.parseCLI(std.testing.allocator, "1x"));
|
||||||
|
try std.testing.expectError(error.InvalidValue, d.parseCLI(std.testing.allocator, "1 "));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "format duration" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
var p: Duration = .{ .duration = std.math.maxInt(u64) };
|
||||||
|
try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||||
|
try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.items);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user