mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
D-Bus and SystemD activation
This updates Ghostty to use D-Bus and SystemD activation to start. In addition to making future Gnome/GTK integration easier, this also implements a "background daemon" mode (Fixing #2010). The configuration variable `quit-after-last-window-closed` is changed to an enum that takes `always`, `never`, and `after-timeout`. Always works like `true` used to and `never` works like `false` used to. If set to `after-timeout` Ghostty will quit a configurable delay after the last surface is closed. This is set by default to five minutes. The timeout only works on Linux for now. I attempted to update the macOS code to support the change to `quit-after-last-window-closed` but I have no way to test the changes so I didn't attempt to implement the background timeout.
This commit is contained in:
18
README.md
18
README.md
@ -580,6 +580,24 @@ on the search path for a lot of software (such as Gnome and KDE) and
|
||||
installing into a prefix with `-p` sets up a directory structure to ensure
|
||||
all features of Ghostty work.
|
||||
|
||||
### Linux D-Bus Debugging Tips
|
||||
|
||||
The GTK runtime uses D-Bus behind the scenes for sending signals and other features. Debug and release
|
||||
builds use different names and paths on the D-Bus bus so that they can run simultaneously.
|
||||
|
||||
| Type of Build | NAME | PATH |
|
||||
| ------------- | --------------------------- | ---------------------------- |
|
||||
| Release | com.mitchellh.ghostty | /com/mitchellh/ghostty |
|
||||
| Debug | com.mitchellh.ghostty-debug | /com/mitchellh/ghostty_debug |
|
||||
|
||||
- Introspect
|
||||
|
||||
gdbus introspect --session --dest ${NAME} --object-path ${PATH}
|
||||
|
||||
- Monitor
|
||||
|
||||
gdbus monitor --session --dest ${NAME}
|
||||
|
||||
### Mac `.app`
|
||||
|
||||
To build the official, fully featured macOS application, you must
|
||||
|
12
build.zig
12
build.zig
@ -147,6 +147,12 @@ pub fn build(b: *std.Build) !void {
|
||||
config.app_runtime == .none and
|
||||
(!emit_bench and !emit_test_exe and !emit_helpgen);
|
||||
|
||||
const linux_systemd_desktop = b.option(
|
||||
bool,
|
||||
"linux-systemd-desktop",
|
||||
"Install advanced desktop integration files that use systemd user units and D-Bus activation.",
|
||||
) orelse true;
|
||||
|
||||
// On NixOS, the built binary from `zig build` needs to patch the rpath
|
||||
// into the built binary for it to be portable across the NixOS system
|
||||
// it was built for. We default this to true if we can detect we're in
|
||||
@ -465,8 +471,12 @@ pub fn build(b: *std.Build) !void {
|
||||
// Desktop file so that we have an icon and other metadata
|
||||
if (config.flatpak) {
|
||||
b.installFile("dist/linux/app-flatpak.desktop", "share/applications/com.mitchellh.ghostty.desktop");
|
||||
} else if (linux_systemd_desktop) {
|
||||
b.installFile("dist/linux/release-systemd/app.desktop", "share/applications/com.mitchellh.ghostty.desktop");
|
||||
b.installFile("dist/linux/release-systemd/dbus.service", "share/dbus-1/services/com.mitchellh.ghostty.service");
|
||||
b.installFile("dist/linux/release-systemd/systemd.service", "lib/systemd/user/com.mitchellh.ghostty.service");
|
||||
} else {
|
||||
b.installFile("dist/linux/app.desktop", "share/applications/com.mitchellh.ghostty.desktop");
|
||||
b.installFile("dist/linux/release/app.desktop", "share/applications/com.mitchellh.ghostty.desktop");
|
||||
}
|
||||
|
||||
// Various icons that our application can use, including the icon
|
||||
|
20
dist/linux/debug-systemd/app.desktop
vendored
Normal file
20
dist/linux/debug-systemd/app.desktop
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Ghostty (Debug)
|
||||
Type=Application
|
||||
Comment=A terminal emulator
|
||||
TryExec=ghostty
|
||||
Exec=ghostty %F
|
||||
Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
StartupNotify=true
|
||||
StartupWMClass=com.mitchellh.ghostty-debug
|
||||
Terminal=false
|
||||
Actions=new_window;
|
||||
X-GNOME-UsesNotifications=true
|
||||
DBusActivatable=true
|
||||
|
||||
[Desktop Action new_window]
|
||||
Name=New Window
|
||||
Exec=ghostty
|
4
dist/linux/debug-systemd/dbus.service
vendored
Normal file
4
dist/linux/debug-systemd/dbus.service
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[D-BUS Service]
|
||||
Name=com.mitchellh.ghostty-debug
|
||||
SystemdService=com.mitchellh.ghostty-debug.service
|
||||
Exec=ghostty
|
7
dist/linux/debug-systemd/systemd.service
vendored
Normal file
7
dist/linux/debug-systemd/systemd.service
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
[Unit]
|
||||
Description=Ghostty
|
||||
|
||||
[Service]
|
||||
Type=dbus
|
||||
BusName=com.mitchellh.ghostty-debug
|
||||
ExecStart=ghostty
|
19
dist/linux/debug/app.desktop
vendored
Normal file
19
dist/linux/debug/app.desktop
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Ghostty (Debug)
|
||||
Type=Application
|
||||
Comment=A terminal emulator
|
||||
TryExec=ghostty
|
||||
Exec=ghostty %F
|
||||
Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
StartupNotify=true
|
||||
StartupWMClass=com.mitchellh.ghostty-debug
|
||||
Terminal=false
|
||||
Actions=new_window;
|
||||
X-GNOME-UsesNotifications=true
|
||||
|
||||
[Desktop Action new_window]
|
||||
Name=New Window
|
||||
Exec=ghostty
|
20
dist/linux/release-systemd/app.desktop
vendored
Normal file
20
dist/linux/release-systemd/app.desktop
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Ghostty
|
||||
Type=Application
|
||||
Comment=A terminal emulator
|
||||
TryExec=ghostty
|
||||
Exec=ghostty %F
|
||||
Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
StartupNotify=true
|
||||
StartupWMClass=com.mitchellh.ghostty
|
||||
Terminal=false
|
||||
Actions=new_window;
|
||||
X-GNOME-UsesNotifications=true
|
||||
DBusActivatable=true
|
||||
|
||||
[Desktop Action new_window]
|
||||
Name=New Window
|
||||
Exec=ghostty
|
4
dist/linux/release-systemd/dbus.service
vendored
Normal file
4
dist/linux/release-systemd/dbus.service
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[D-BUS Service]
|
||||
Name=com.mitchellh.ghostty
|
||||
SystemdService=com.mitchellh.ghostty.service
|
||||
Exec=ghostty
|
7
dist/linux/release-systemd/systemd.service
vendored
Normal file
7
dist/linux/release-systemd/systemd.service
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
[Unit]
|
||||
Description=Ghostty
|
||||
|
||||
[Service]
|
||||
Type=dbus
|
||||
BusName=com.mitchellh.ghostty
|
||||
ExecStart=ghostty
|
@ -1,16 +1,19 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Ghostty
|
||||
Type=Application
|
||||
Comment=A terminal emulator
|
||||
Exec=ghostty
|
||||
TryExec=ghostty
|
||||
Exec=ghostty %F
|
||||
Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
StartupNotify=true
|
||||
StartupWMClass=com.mitchellh.ghostty
|
||||
Terminal=false
|
||||
Actions=new-window;
|
||||
Actions=new_window;
|
||||
X-GNOME-UsesNotifications=true
|
||||
|
||||
[Desktop Action new-window]
|
||||
[Desktop Action new_window]
|
||||
Name=New Window
|
||||
Exec=ghostty
|
@ -146,7 +146,7 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return ghostty.config.shouldQuitAfterLastWindowClosed
|
||||
return ghostty.config.shouldQuitAfterLastWindowClosed != "never"
|
||||
}
|
||||
|
||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||
|
@ -151,12 +151,13 @@ extension Ghostty {
|
||||
/// details on what each means. We only add documentation if there is a strange conversion
|
||||
/// due to the embedded library and Swift.
|
||||
|
||||
var shouldQuitAfterLastWindowClosed: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = false;
|
||||
var shouldQuitAfterLastWindowClosed: String {
|
||||
guard let config = self.config else { return "never" }
|
||||
var v: UnsafePointer<Int8>? = nil;
|
||||
let key = "quit-after-last-window-closed"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "never" }
|
||||
guard let ptr = v else { return "never" };
|
||||
return String(cString: ptr)
|
||||
}
|
||||
|
||||
var windowColorspace: String {
|
||||
|
@ -167,6 +167,11 @@ in
|
||||
|
||||
mkdir -p "$out/nix-support"
|
||||
|
||||
sed -i -e "s@^Exec=.*ghostty@Exec=$out/bin/ghostty@" $out/share/applications/com.mitchellh.ghostty.desktop
|
||||
sed -i -e "s@^TryExec=.*ghostty@TryExec=$out/bin/ghostty@" $out/share/applications/com.mitchellh.ghostty.desktop
|
||||
sed -i -e "s@^Exec=.*ghostty@Exec=$out/bin/ghostty@" $out/share/dbus-1/services/com.mitchellh.ghostty.service
|
||||
sed -i -e "s@^ExecStart=.*ghostty@ExecStart=$out/bin/ghostty@" $out/lib/systemd/user/com.mitchellh.ghostty.service
|
||||
|
||||
mkdir -p "$terminfo/share"
|
||||
mv "$terminfo_src" "$terminfo/share/terminfo"
|
||||
ln -sf "$terminfo/share/terminfo" "$terminfo_src"
|
||||
@ -179,6 +184,7 @@ in
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
# explicitly add libX11 to the rpath because it's dynamically loaded
|
||||
patchelf --add-rpath "${lib.makeLibraryPath [libX11]}" "$out/bin/.ghostty-wrapped"
|
||||
'';
|
||||
|
||||
|
@ -309,7 +309,7 @@ pub const Message = union(enum) {
|
||||
redraw_inspector: *apprt.Surface,
|
||||
|
||||
const NewWindow = struct {
|
||||
/// The parent surface
|
||||
/// The parent surface.
|
||||
parent: ?*Surface = null,
|
||||
};
|
||||
};
|
||||
|
@ -32,6 +32,7 @@ const c = @import("c.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
const x11 = @import("x11.zig");
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -108,7 +109,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
const single_instance = switch (config.@"gtk-single-instance") {
|
||||
.true => true,
|
||||
.false => false,
|
||||
.desktop => internal_os.launchedFromDesktop(),
|
||||
.detect => internal_os.launchedFromDesktop() or internal_os.launchedByDBusActivation() or internal_os.launchedBySystemd(),
|
||||
};
|
||||
|
||||
// Setup the flags for our application.
|
||||
@ -178,6 +179,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
break :app @ptrCast(adw_app);
|
||||
};
|
||||
errdefer c.g_object_unref(app);
|
||||
|
||||
const gapp = @as(*c.GApplication, @ptrCast(app));
|
||||
|
||||
// force the resource path to a known value so that it doesn't depend on
|
||||
@ -253,10 +255,13 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
break :x11_xkb try x11.Xkb.init(display);
|
||||
};
|
||||
|
||||
// This just calls the "activate" signal but its part of the normal
|
||||
// startup routine so we just call it:
|
||||
// This just calls the `activate` signal but its part of the normal startup
|
||||
// routine so we just call it, but only if we were not launched by D-Bus
|
||||
// activation or systemd. D-Bus activation will send it's own `activate`
|
||||
// signal later.
|
||||
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||
c.g_application_activate(gapp);
|
||||
if (!internal_os.launchedByDBusActivation() and !internal_os.launchedBySystemd())
|
||||
c.g_application_activate(gapp);
|
||||
|
||||
// Register for dbus events
|
||||
if (c.g_application_get_dbus_connection(gapp)) |dbus_connection| {
|
||||
@ -277,6 +282,10 @@ 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 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,
|
||||
@ -293,6 +302,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
|
||||
// this so all global state can be cleaned up.
|
||||
pub fn terminate(self: *App) void {
|
||||
@ -463,7 +478,10 @@ pub fn run(self: *App) !void {
|
||||
self.transient_cgroup_base = path;
|
||||
} else log.debug("cgroup isoation disabled config={}", .{self.config.@"linux-cgroup"});
|
||||
|
||||
// Setup our menu items
|
||||
// The last instant that one or more surfaces were open
|
||||
var last_one = try std.time.Instant.now();
|
||||
|
||||
// If we're not remote, then we also setup our actions and menus.
|
||||
self.initActions();
|
||||
self.initMenu();
|
||||
self.initContextMenu();
|
||||
@ -478,9 +496,48 @@ pub fn run(self: *App) !void {
|
||||
while (self.running) {
|
||||
_ = 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);
|
||||
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) {
|
||||
switch (self.config.@"quit-after-last-window-closed") {
|
||||
.always => break :q true,
|
||||
.never => break :q false,
|
||||
.@"after-timeout" => {
|
||||
|
||||
// If the background timeout is not null, check to see
|
||||
// if the timeout has elapsed.
|
||||
if (self.config.@"background-timeout".duration) |duration| {
|
||||
const now = try std.time.Instant.now();
|
||||
|
||||
if (now.since(last_one) > duration)
|
||||
// The timeout has elapsed, quit.
|
||||
break :q true;
|
||||
|
||||
// Not enough time has elapsed, don't quit.
|
||||
break :q false;
|
||||
}
|
||||
|
||||
// `background-timeout` is null, don't quit.
|
||||
break :q false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
break :q false;
|
||||
};
|
||||
|
||||
if (q) self.quit();
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,8 +624,7 @@ fn quit(self: *App) void {
|
||||
}
|
||||
|
||||
/// This immediately destroys all windows, forcing the application to quit.
|
||||
fn quitNow(self: *App) void {
|
||||
_ = self;
|
||||
fn quitNow(_: *App) void {
|
||||
const list = c.gtk_window_list_toplevels();
|
||||
defer c.g_list_free(list);
|
||||
c.g_list_foreach(list, struct {
|
||||
@ -598,11 +654,10 @@ fn gtkQuitConfirmation(
|
||||
self.quitNow();
|
||||
}
|
||||
|
||||
/// This is called by the "activate" signal. This is sent on program
|
||||
/// startup and also when a secondary instance launches and requests
|
||||
/// a new window.
|
||||
fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = app;
|
||||
/// This is called by the `activate` signal. This is sent on program startup and
|
||||
/// also when a secondary instance launches and requests a new window.
|
||||
fn gtkActivate(_: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
||||
log.info("received activate signal", .{});
|
||||
|
||||
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
|
||||
|
||||
@ -726,6 +781,8 @@ fn gtkActionQuit(
|
||||
_: *c.GVariant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
log.info("gtk quit action received", .{});
|
||||
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||
self.core_app.setQuit() catch |err| {
|
||||
log.warn("error setting quit err={}", .{err});
|
||||
@ -733,6 +790,18 @@ fn gtkActionQuit(
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkActionNewWindow(
|
||||
_: *c.GSimpleAction,
|
||||
_: *c.GVariant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
log.info("received new window action", .{});
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||
_ = self.core_app.mailbox.push(.{
|
||||
.new_window = .{},
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
/// This is called to setup the action map that this application supports.
|
||||
/// This should be called only once on startup.
|
||||
fn initActions(self: *App) void {
|
||||
@ -740,6 +809,7 @@ fn initActions(self: *App) void {
|
||||
.{ "quit", >kActionQuit },
|
||||
.{ "open_config", >kActionOpenConfig },
|
||||
.{ "reload_config", >kActionReloadConfig },
|
||||
.{ "new_window", >kActionNewWindow },
|
||||
};
|
||||
|
||||
inline for (actions) |entry| {
|
||||
|
@ -545,6 +545,7 @@ fn realize(self: *Surface) !void {
|
||||
// Get our new surface config
|
||||
var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config);
|
||||
defer config.deinit();
|
||||
|
||||
if (!self.parent_surface) {
|
||||
// A hack, see the "parent_surface" field for more information.
|
||||
config.@"working-directory" = self.app.config.@"working-directory";
|
||||
|
124
src/apprt/gtk/dbus/background.zig
Normal file
124
src/apprt/gtk/dbus/background.zig
Normal file
@ -0,0 +1,124 @@
|
||||
const std = @import("std");
|
||||
const c = @import("../c.zig");
|
||||
const CoreApp = @import("../../../App.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_dbus);
|
||||
|
||||
const UserData = struct {
|
||||
core_app: *CoreApp,
|
||||
dbus_connection: *c.DBusConnection,
|
||||
};
|
||||
|
||||
pub fn requestBackgroundStart(core_app: *CoreApp, dbus_connection: *c.GDBusConnection) !void {
|
||||
const name = c.g_dbus_connection_get_unique_name(dbus_connection);
|
||||
log.info("dbus connection unique name: {s}", .{name});
|
||||
|
||||
const sender = try core_app.alloc.dupe(u8, std.mem.span(name)[1..]);
|
||||
defer core_app.alloc.free(sender);
|
||||
std.mem.replaceScalar(u8, sender, '.', '_');
|
||||
|
||||
var buf: [128]u8 = undefined;
|
||||
const handle = try std.fmt.bufPrintZ(&buf, "/org/freedesktop/portal/desktop/request/{s}/ghostty_background_{d}", .{ sender, std.crypto.random.int(u32) });
|
||||
log.info("handle: {s}", .{handle});
|
||||
|
||||
_ = c.g_dbus_connection_signal_subscribe(
|
||||
dbus_connection,
|
||||
null,
|
||||
"org.freedesktop.portal.Request",
|
||||
"Response",
|
||||
&buf,
|
||||
null,
|
||||
c.G_DBUS_SIGNAL_FLAGS_NONE,
|
||||
&requestBackgroundResult,
|
||||
core_app,
|
||||
null,
|
||||
);
|
||||
|
||||
const options = c.g_variant_builder_new(c.G_VARIANT_TYPE("a{sv}"));
|
||||
defer c.g_variant_builder_unref(options);
|
||||
|
||||
c.g_variant_builder_add(options, "{sv}", "handle_token", c.g_variant_new("s", "background"));
|
||||
c.g_variant_builder_add(options, "{sv}", "reason", c.g_variant_new("s", "because we're cool"));
|
||||
c.g_variant_builder_add(options, "{sv}", "autostart", c.g_variant_new("b", @as(u32, 0)));
|
||||
|
||||
const command = c.g_variant_builder_new(c.G_VARIANT_TYPE("as"));
|
||||
defer c.g_variant_builder_unref(command);
|
||||
c.g_variant_builder_add(command, "s", "ghostty");
|
||||
|
||||
c.g_variant_builder_add(options, "{sv}", "commandline", c.g_variant_builder_end(command));
|
||||
c.g_variant_builder_add(options, "{sv}", "dbus-activatable", c.g_variant_new("b", @as(u32, 0)));
|
||||
|
||||
const params = c.g_variant_new(
|
||||
"(s@a{sv})",
|
||||
"",
|
||||
c.g_variant_builder_end(options),
|
||||
);
|
||||
_ = c.g_variant_ref_sink(params);
|
||||
defer c.g_variant_unref(params);
|
||||
|
||||
c.g_dbus_connection_call(
|
||||
dbus_connection, // connection
|
||||
"org.freedesktop.portal.Desktop", // bus name
|
||||
"/org/freedesktop/portal/desktop", // object path
|
||||
"org.freedesktop.portal.Background", // interface name
|
||||
"RequestBackground", // method name
|
||||
params, // parameters
|
||||
null,
|
||||
// c.G_VARIANT_TYPE("(o)"), // reply type
|
||||
c.G_DBUS_CALL_FLAGS_NONE, // flags
|
||||
-1, // timeout_msec
|
||||
null, // cancellable
|
||||
requestBackgroundFinish,
|
||||
@ptrCast(dbus_connection),
|
||||
);
|
||||
}
|
||||
|
||||
fn requestBackgroundFinish(source_object: ?*c.GObject, res: ?*c.GAsyncResult, user_data: ?*anyopaque) callconv(.C) void {
|
||||
_ = source_object;
|
||||
const dbus_connection: *c.GDBusConnection = @ptrCast(@alignCast(user_data orelse unreachable));
|
||||
|
||||
var err: ?*c.GError = null;
|
||||
defer if (err) |e| c.g_error_free(e);
|
||||
|
||||
const value = c.g_dbus_connection_call_finish(
|
||||
dbus_connection,
|
||||
res,
|
||||
&err,
|
||||
) orelse {
|
||||
if (err) |e| log.err("unable to request background: {s}", .{e.message});
|
||||
return;
|
||||
};
|
||||
defer c.g_variant_unref(value);
|
||||
|
||||
log.err("return type: {s}", .{c.g_variant_get_type_string(value)});
|
||||
|
||||
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(o)")) != 1) {
|
||||
log.err("wrong return type: {s}", .{c.g_variant_get_type_string(value)});
|
||||
return;
|
||||
}
|
||||
|
||||
var path: ?[*c]u8 = null;
|
||||
c.g_variant_get(value, "(o)", &path);
|
||||
defer if (path) |p| c.g_free(p);
|
||||
|
||||
if (path) |p| {
|
||||
log.warn("background path: {s}", .{p});
|
||||
} else {
|
||||
log.warn("error with path", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn requestBackgroundResult(
|
||||
_: ?*c.GDBusConnection,
|
||||
_: [*c]const u8,
|
||||
_: [*c]const u8,
|
||||
_: [*c]const u8,
|
||||
_: [*c]const u8,
|
||||
parameters: ?*c.GVariant,
|
||||
user_data: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
_ = parameters;
|
||||
_ = user_data;
|
||||
|
||||
log.info("request background result", .{});
|
||||
}
|
@ -880,10 +880,48 @@ keybind: Keybinds = .{},
|
||||
/// true. If set to false, surfaces will close without any confirmation.
|
||||
@"confirm-close-surface": bool = true,
|
||||
|
||||
/// Whether or not to quit after the last window is closed. This defaults to
|
||||
/// false. Currently only supported on macOS. On Linux, the process always exits
|
||||
/// after the last window is closed.
|
||||
@"quit-after-last-window-closed": bool = false,
|
||||
/// Whether or not to quit after the last surface is closed.
|
||||
/// * `never` - Ghostty will continue running even if there are no open
|
||||
/// surfaces.
|
||||
/// * `always` - Ghostty will quit as soon as there are no open surfaces.
|
||||
/// * `after-timeout` - Ghostty will continue running after all surfaces are
|
||||
/// closed until the `background-timeout` expires.
|
||||
/// This defaults to `never` on macOS and `always` on other systems.
|
||||
@"quit-after-last-window-closed": QuitOptions = if (builtin.target.isDarwin())
|
||||
.never
|
||||
else switch (builtin.os.tag) {
|
||||
.linux => .always,
|
||||
else => .always,
|
||||
},
|
||||
|
||||
/// If `quit-after-last-window-closed` is set to `after-timeout`, this controls
|
||||
/// how long Ghostty will stay running after the last open surface has been
|
||||
/// closed. If the `background-timeout` is unset Ghostty will remain running
|
||||
/// indefinitely. The duration should be long enough to allow Ghostty to
|
||||
/// initialize and open it's first window. 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 the `background-timeout` is set to five minutes.
|
||||
///
|
||||
/// Only implemented on Linux.
|
||||
@"background-timeout": Duration = .{ .duration = 5 * std.time.ns_per_min },
|
||||
|
||||
/// Whether to enable shell integration auto-injection or not. Shell integration
|
||||
/// greatly enhances the terminal experience by enabling a number of features:
|
||||
@ -1109,19 +1147,20 @@ keybind: Keybinds = .{},
|
||||
/// must always be able to move themselves into an isolated cgroup.
|
||||
@"linux-cgroup-hard-fail": bool = false,
|
||||
|
||||
/// 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
|
||||
/// is already a running process.
|
||||
/// 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 is
|
||||
/// 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
|
||||
/// detects it was launched from the `.desktop` file such as an app launcher.
|
||||
/// If Ghostty is launched from the command line, it will default to `false`.
|
||||
/// The default value is `detect` which will default to `true` if Ghostty
|
||||
/// detects that it was launched from the `.desktop` file such as an app
|
||||
/// 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
|
||||
/// so you can test single instance without conflicting with release builds.
|
||||
@"gtk-single-instance": GtkSingleInstance = .desktop,
|
||||
@"gtk-single-instance": GtkSingleInstance = .detect,
|
||||
|
||||
/// When enabled, the full GTK titlebar is displayed instead of your window
|
||||
/// manager's simple titlebar. The behavior of this option will vary with your
|
||||
@ -3700,7 +3739,7 @@ pub const MacTitlebarStyle = enum {
|
||||
|
||||
/// See gtk-single-instance
|
||||
pub const GtkSingleInstance = enum {
|
||||
desktop,
|
||||
detect,
|
||||
false,
|
||||
true,
|
||||
};
|
||||
@ -3753,3 +3792,220 @@ pub const LinuxCgroup = enum {
|
||||
always,
|
||||
@"single-instance",
|
||||
};
|
||||
|
||||
/// See quit-after-last-window-closed
|
||||
pub const QuitOptions = enum {
|
||||
never,
|
||||
always,
|
||||
@"after-timeout",
|
||||
};
|
||||
|
||||
pub const Duration = struct {
|
||||
duration: ?u64 = null,
|
||||
|
||||
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 = null;
|
||||
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;
|
||||
}
|
||||
|
||||
pub fn formatEntry(self: @This(), formatter: anytype) !void {
|
||||
if (self.duration) |v| {
|
||||
var buf: [64]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
|
||||
var value = v;
|
||||
|
||||
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());
|
||||
} else try formatter.formatEntry(void, {});
|
||||
}
|
||||
};
|
||||
|
||||
test "parse duration" {
|
||||
var d: Duration = undefined;
|
||||
|
||||
try d.parseCLI(std.testing.allocator, "");
|
||||
try std.testing.expectEqual(@as(?u64, null), 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);
|
||||
}
|
||||
|
21
src/os/dbus.zig
Normal file
21
src/os/dbus.zig
Normal file
@ -0,0 +1,21 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Returns true if the program was launched by D-Bus activation.
|
||||
///
|
||||
/// On Linux GTK, this returns true if the program was launched using D-Bus
|
||||
/// activation. It will return false if Ghostty was launched any other way.
|
||||
///
|
||||
/// For other platforms and app runtimes, this returns false.
|
||||
pub fn launchedByDBusActivation() bool {
|
||||
return switch (builtin.os.tag) {
|
||||
|
||||
// On Linux, D-Bus activation sets `DBUS_STARTER_ADDRESS` and
|
||||
// `DBUS_STARTER_BUS_TYPE`. If these environment variables are present
|
||||
// (no matter the value) we were launched by D-Bus activation.
|
||||
.linux => std.posix.getenv("DBUS_STARTER_ADDRESS") != null and std.posix.getenv("DBUS_STARTER_BUS_TYPE") != null,
|
||||
|
||||
// No other system supports D-Bus so always return false.
|
||||
else => false,
|
||||
};
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
//! also OS-specific features and conventions.
|
||||
|
||||
pub usingnamespace @import("env.zig");
|
||||
pub usingnamespace @import("dbus.zig");
|
||||
pub usingnamespace @import("desktop.zig");
|
||||
pub usingnamespace @import("file.zig");
|
||||
pub usingnamespace @import("flatpak.zig");
|
||||
@ -13,6 +14,7 @@ pub usingnamespace @import("mouse.zig");
|
||||
pub usingnamespace @import("open.zig");
|
||||
pub usingnamespace @import("pipe.zig");
|
||||
pub usingnamespace @import("resourcesdir.zig");
|
||||
pub usingnamespace @import("systemd.zig");
|
||||
pub const CFReleaseThread = @import("cf_release_thread.zig");
|
||||
pub const TempDir = @import("TempDir.zig");
|
||||
pub const cgroup = @import("cgroup.zig");
|
||||
|
22
src/os/systemd.zig
Normal file
22
src/os/systemd.zig
Normal file
@ -0,0 +1,22 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Returns true if the program was launched as a systemd service.
|
||||
///
|
||||
/// On Linux, this returns true if the program was launched as a systemd
|
||||
/// service. It will return false if Ghostty was launched any other way.
|
||||
///
|
||||
/// For other platforms and app runtimes, this returns false.
|
||||
pub fn launchedBySystemd() bool {
|
||||
return switch (builtin.os.tag) {
|
||||
// On Linux, systemd sets the `INVOCATION_ID` (v232+) and the
|
||||
// `JOURNAL_STREAM` (v231+) enviroment variables. If these environment
|
||||
// variables are present (no matter the value) we were launched by
|
||||
// systemd. This can be fooled if Ghostty is launched from another
|
||||
// terminal that does not clean up these environment variables.
|
||||
.linux => std.posix.getenv("INVOCATION_ID") != null and std.posix.getenv("JOURNAL_STREAM") != null,
|
||||
|
||||
// No other system supports systemd so always return false.
|
||||
else => false,
|
||||
};
|
||||
}
|
@ -691,6 +691,17 @@ const Subprocess = struct {
|
||||
env.remove("GSK_RENDERER");
|
||||
}
|
||||
|
||||
// On Linux, remove some environment variables that are set when Ghostty
|
||||
// is launched from a `.desktop` file or by D-Bus activation.
|
||||
if (comptime builtin.os.tag == .linux) {
|
||||
env.remove("GIO_LAUNCHED_DESKTOP_FILE");
|
||||
env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
|
||||
env.remove("DBUS_STARTER_ADDRESS");
|
||||
env.remove("DBUS_STARTER_BUS_TYPE");
|
||||
env.remove("INVOCATION_ID");
|
||||
env.remove("JOURNAL_STREAM");
|
||||
}
|
||||
|
||||
// Setup our shell integration, if we can.
|
||||
const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: {
|
||||
const default_shell_command = cfg.command orelse switch (builtin.os.tag) {
|
||||
|
Reference in New Issue
Block a user