dbus and systemd activation - take 2 (#7679)

This replaces #7433. The improvements are:

1) Install the systemd user service in the proper directory depending on
if it's a 'user' install or a 'system' install. This is controlled
either by using the `--system` build flag (as most packages will) or by
the `-Dsystem-package` flag.

2) Add the absolute path to the `ghostty` binary in the application
file, the DBus service, and the systemd user service. This is done so
that they do not depend on `ghostty` being in the `PATH` of whatever is
launching Ghostty. That `PATH` is not necessarily the same as the `PATH`
in a user shell (especially for DBus activation and systemd user
services).

3) Adjust the DBus bus name that is expected by the system depending on
the optimization level that Ghostty is compiled with.
This commit is contained in:
Mitchell Hashimoto
2025-06-26 13:31:29 -07:00
committed by GitHub
11 changed files with 222 additions and 82 deletions

View File

@@ -1,13 +1,15 @@
[Desktop Entry]
Name=Ghostty
Version=1.0
Name=@NAME@
Type=Application
Comment=A terminal emulator
Exec=ghostty
TryExec=@GHOSTTY@
Exec=@GHOSTTY@ --launched-from=desktop
Icon=com.mitchellh.ghostty
Categories=System;TerminalEmulator;
Keywords=terminal;tty;pty;
StartupNotify=true
StartupWMClass=com.mitchellh.ghostty
StartupWMClass=@APPID@
Terminal=false
Actions=new-window;
X-GNOME-UsesNotifications=true
@@ -16,7 +18,8 @@ X-TerminalArgTitle=--title=
X-TerminalArgAppId=--class=
X-TerminalArgDir=--working-directory=
X-TerminalArgHold=--wait-after-command
DBusActivatable=true
[Desktop Action new-window]
Name=New Window
Exec=ghostty
Exec=@GHOSTTY@ --launched-from=desktop

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.mitchellh.ghostty</id>
<launchable type="desktop-id">com.mitchellh.ghostty.desktop</launchable>
<name>Ghostty</name>
<id>@APPID@id>
<launchable type="desktop-id">@APPID@desktop</launchable>
<name>@NAME@name>
<url type="homepage">https://ghostty.org</url>
<url type="help">https://ghostty.org/docs</url>
<url type="bugtracker">https://github.com/ghostty-org/ghostty/discussions</url>

3
dist/linux/dbus.service.flatpak.in vendored Normal file
View File

@@ -0,0 +1,3 @@
[D-BUS Service]
Name=@APPID@
Exec=@GHOSTTY@ --launched-from=dbus

4
dist/linux/dbus.service.in vendored Normal file
View File

@@ -0,0 +1,4 @@
[D-BUS Service]
Name=@APPID@
SystemdService=@APPID@.service
Exec=@GHOSTTY@ --launched-from=dbus

7
dist/linux/systemd.service.in vendored Normal file
View File

@@ -0,0 +1,7 @@
[Unit]
Description=@NAME@
[Service]
Type=dbus
BusName=@APPID@
ExecStart=@GHOSTTY@ --launched-from=systemd

View File

@@ -6,11 +6,7 @@ sdk-extensions:
- org.freedesktop.Sdk.Extension.ziglang
default-branch: tip
command: ghostty
# Integrate the rename into zig build, maybe?
rename-desktop-file: com.mitchellh.ghostty.desktop
rename-appdata-file: com.mitchellh.ghostty.metainfo.xml
rename-icon: com.mitchellh.ghostty
desktop-file-name-suffix: " (Debug)"
finish-args:
# 3D rendering
- --device=dri

View File

@@ -405,11 +405,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// This just calls the `activate` signal but its part of the normal startup
// 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)
// a window). An initial window will not be immediately created if we were
// launched by D-Bus activation or systemd. D-Bus activation will send it's
// own `activate` or `new-window` signal later.
//
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
if (config.@"initial-window")
gio_app.activate();
if (config.@"initial-window") switch (config.@"launched-from".?) {
.desktop, .cli => gio_app.activate(),
.dbus, .systemd => {},
};
// Internally, GTK ensures that only one instance of this provider exists in the provider list
// for the display.
@@ -1683,6 +1687,17 @@ fn gtkActionShowGTKInspector(
};
}
fn gtkActionNewWindow(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
log.info("received new window action", .{});
_ = 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 {
@@ -1702,7 +1717,9 @@ fn initActions(self: *App) void {
.{ "reload-config", gtkActionReloadConfig, null },
.{ "present-surface", gtkActionPresentSurface, t },
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
.{ "new-window", gtkActionNewWindow, null },
};
inline for (actions) |entry| {
const action = gio.SimpleAction.new(entry[0], entry[2]);
defer action.unref();

View File

@@ -2325,6 +2325,15 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
env.remove("GDK_DISABLE");
env.remove("GSK_RENDERER");
// Remove some environment variables that are set when Ghostty is launched
// from a `.desktop` file, by D-Bus activation, or systemd.
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");
// Unset environment varies set by snaps if we're running in a snap.
// This allows Ghostty to further launch additional snaps.
if (env.get("SNAP")) |_| {

View File

@@ -87,7 +87,7 @@ pub fn init(b: *std.Build) !Config {
// This is set to true when we're building a system package. For now
// this is trivially detected using the "system_package_mode" bool
// but we may want to make this more sophisticated in the future.
const system_package: bool = b.graph.system_package_mode;
const system_package = b.graph.system_package_mode;
// This specifies our target wasm runtime. For now only one semi-usable
// one exists so this is hardcoded.

View File

@@ -2,6 +2,7 @@ const GhosttyResources = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const buildpkg = @import("main.zig");
const Config = @import("Config.zig");
const config_vim = @import("../config/vim.zig");
@@ -220,83 +221,178 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
}
// App (Linux)
if (cfg.target.result.os.tag == .linux) {
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
if (cfg.target.result.os.tag == .linux) try addLinuxAppResources(
b,
cfg,
&steps,
);
return .{ .steps = steps.items };
}
/// Add the resource files needed to make Ghostty a proper
/// Linux desktop application (for various desktop environments).
fn addLinuxAppResources(
b: *std.Build,
cfg: *const Config,
steps: *std.ArrayList(*std.Build.Step),
) !void {
assert(cfg.target.result.os.tag == .linux);
// Background:
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
const name = b.fmt("Ghostty{s}", .{
switch (cfg.optimize) {
.Debug, .ReleaseSafe => " (Debug)",
.ReleaseFast, .ReleaseSmall => "",
},
});
const app_id = b.fmt("com.mitchellh.ghostty{s}", .{
switch (cfg.optimize) {
.Debug, .ReleaseSafe => "-debug",
.ReleaseFast, .ReleaseSmall => "",
},
});
const exe_abs_path = b.fmt(
"{s}/bin/ghostty",
.{b.install_prefix},
);
// The templates that we will process. The templates are in
// cmake format and will be processed and saved to the
// second element of the tuple.
const Template = struct { std.Build.LazyPath, []const u8 };
const templates: []const Template = templates: {
var ts: std.ArrayList(Template) = .init(b.allocator);
// Desktop file so that we have an icon and other metadata
try steps.append(&b.addInstallFile(
b.path("dist/linux/app.desktop"),
"share/applications/com.mitchellh.ghostty.desktop",
).step);
try ts.append(.{
b.path("dist/linux/app.desktop.in"),
b.fmt("share/applications/{s}.desktop", .{app_id}),
});
// AppStream metainfo so that application has rich metadata within app stores
try steps.append(&b.addInstallFile(
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"),
"share/metainfo/com.mitchellh.ghostty.metainfo.xml",
).step);
// Service for DBus activation.
try ts.append(.{
if (cfg.flatpak)
b.path("dist/linux/dbus.service.flatpak.in")
else
b.path("dist/linux/dbus.service.in"),
b.fmt("share/dbus-1/services/{s}.service", .{app_id}),
});
// Right click menu action for Plasma desktop
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_dolphin.desktop"),
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
).step);
// systemd user service. This is kind of nasty but systemd
// looks for user services in different paths depending on
// if we are installed as a system package or not (lib vs.
// share) so we have to handle that here. We might be able
// to get away with always installing to both because it
// only ever searches in one... but I don't want to do that hack
// until we have to.
if (!cfg.flatpak) try ts.append(.{
b.path("dist/linux/systemd.service.in"),
b.fmt(
"{s}/systemd/user/{s}.service",
.{
if (b.graph.system_package_mode) "lib" else "share",
app_id,
},
),
});
// Right click menu action for Nautilus. Note that this _must_ be named
// `ghostty.py`. Using the full app id causes problems (see #5468).
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_nautilus.py"),
"share/nautilus-python/extensions/ghostty.py",
).step);
// AppStream metainfo so that application has rich metadata
// within app stores
try ts.append(.{
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"),
b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}),
});
// Various icons that our application can use, including the icon
// that will be used for the desktop.
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16.png"),
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32.png"),
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128.png"),
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256.png"),
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_512.png"),
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
).step);
// Flatpaks only support icons up to 512x512.
if (!cfg.flatpak) {
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_1024.png"),
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
).step);
}
break :templates ts.items;
};
// Process all our templates
for (templates) |template| {
const tpl = b.addConfigHeader(.{
.style = .{ .cmake = template[0] },
}, .{
.NAME = name,
.APPID = app_id,
.GHOSTTY = exe_abs_path,
});
// Template output has a single header line we want to remove.
// We use `tail` to do it since its part of the POSIX standard.
const tail = b.addSystemCommand(&.{ "tail", "-n", "+2" });
tail.setStdIn(.{ .lazy_path = tpl.getOutput() });
const copy = b.addInstallFile(
tail.captureStdOut(),
template[1],
);
try steps.append(&copy.step);
}
// Right click menu action for Plasma desktop
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_dolphin.desktop"),
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
).step);
// Right click menu action for Nautilus. Note that this _must_ be named
// `ghostty.py`. Using the full app id causes problems (see #5468).
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_nautilus.py"),
"share/nautilus-python/extensions/ghostty.py",
).step);
// Various icons that our application can use, including the icon
// that will be used for the desktop.
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16.png"),
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32.png"),
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128.png"),
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256.png"),
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_512.png"),
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
).step);
// Flatpaks only support icons up to 512x512.
if (!cfg.flatpak) {
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16@2x.png"),
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32@2x.png"),
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128@2x.png"),
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256@2x.png"),
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
b.path("images/icons/icon_1024.png"),
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
).step);
}
return .{ .steps = steps.items };
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16@2x.png"),
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32@2x.png"),
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128@2x.png"),
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256@2x.png"),
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
).step);
}
pub fn install(self: *const GhosttyResources) void {

View File

@@ -1029,12 +1029,17 @@ title: ?[:0]const u8 = null,
/// The setting that will change the application class value.
///
/// This controls the class field of the `WM_CLASS` X11 property (when running
/// under X11), and the Wayland application ID (when running under Wayland).
/// under X11), the Wayland application ID (when running under Wayland), and the
/// bus name that Ghostty uses to connect to DBus.
///
/// Note that changing this value between invocations will create new, separate
/// instances, of Ghostty when running with `gtk-single-instance=true`. See that
/// option for more details.
///
/// Changing this value may break launching Ghostty from `.desktop` files, via
/// DBus activation, or systemd user services as the system is expecting Ghostty
/// to connect to DBus using the default `class` when it is launched.
///
/// The class name must follow the requirements defined [in the GTK
/// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html).
///