From 81403f59ce98009d16dc6719635b543a88cc2ad5 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 24 Jun 2025 22:56:17 -0500 Subject: [PATCH] dbus and systemd activation - take 2 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. --- PACKAGING.md | 3 +- dist/linux/app.desktop | 11 +++-- dist/linux/dbus.service | 4 ++ dist/linux/systemd.service | 7 +++ nix/package.nix | 1 + src/apprt/gtk/App.zig | 23 +++++++-- src/apprt/gtk/Surface.zig | 9 ++++ src/build/Config.zig | 9 +++- src/build/GhosttyResources.zig | 86 ++++++++++++++++++++++++++++++++-- src/config/Config.zig | 7 ++- 10 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 dist/linux/dbus.service create mode 100644 dist/linux/systemd.service diff --git a/PACKAGING.md b/PACKAGING.md index d85f55de7..e85a9e987 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -83,7 +83,8 @@ zig build \ --prefix /usr \ --system /tmp/offline-cache/p \ -Doptimize=ReleaseFast \ - -Dcpu=baseline + -Dcpu=baseline \ + -Dsystem-package=true ``` The build options are covered in the next section, but this will build diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop index 6e464ea87..7dc0062f7 100644 --- a/dist/linux/app.desktop +++ b/dist/linux/app.desktop @@ -1,13 +1,15 @@ [Desktop Entry] -Name=Ghostty +Version=1.0 +Name=Ghostty@@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=com.mitchellh.ghostty@@DEBUG@@ 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 diff --git a/dist/linux/dbus.service b/dist/linux/dbus.service new file mode 100644 index 000000000..f9c5ce7a4 --- /dev/null +++ b/dist/linux/dbus.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=com.mitchellh.ghostty@@DEBUG@@ +SystemdService=com.mitchellh.ghostty@@DEBUG@@.service +Exec=@@GHOSTTY@@ --launched-from=dbus diff --git a/dist/linux/systemd.service b/dist/linux/systemd.service new file mode 100644 index 000000000..2d1acdaea --- /dev/null +++ b/dist/linux/systemd.service @@ -0,0 +1,7 @@ +[Unit] +Description=Ghostty@@NAME@@ + +[Service] +Type=dbus +BusName=com.mitchellh.ghostty@@DEBUG@@ +ExecStart=@@GHOSTTY@@ --launched-from=systemd diff --git a/nix/package.nix b/nix/package.nix index 08dfd710b..f24b401cc 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -99,6 +99,7 @@ in "-Dgtk-x11=${lib.boolToString enableX11}" "-Dgtk-wayland=${lib.boolToString enableWayland}" "-Dstrip=${lib.boolToString strip}" + "-Dsystem-package=true" ]; outputs = [ diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 7c9c15191..93e069376 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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(); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index cf8d651dd..5c886e663 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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")) |_| { diff --git a/src/build/Config.zig b/src/build/Config.zig index 8974e1f0c..ede42c738 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -45,6 +45,7 @@ version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, pie: bool = false, strip: bool = false, patch_rpath: ?[]const u8 = null, +system_package: bool = false, /// Artifacts flatpak: bool = false, @@ -87,7 +88,11 @@ 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.option( + bool, + "system-package", + "Controls whether we build a system package", + ) orelse b.graph.system_package_mode; // This specifies our target wasm runtime. For now only one semi-usable // one exists so this is hardcoded. @@ -261,6 +266,8 @@ pub fn init(b: *std.Build) !Config { .ReleaseFast, .ReleaseSmall => true, }; + config.system_package = system_package; + //--------------------------------------------------------------- // Artifacts to Emit diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 640491fd6..f3b169de1 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -224,10 +224,57 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html // 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 steps.append( + formatService( + b, + cfg, + b.path("dist/linux/app.desktop"), + b.fmt( + "share/applications/com.mitchellh.ghostty{s}.desktop", + .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + ), + ), + ); + // Service for DBus activation. + try steps.append( + formatService( + b, + cfg, + b.path("dist/linux/dbus.service"), + b.fmt( + "share/dbus-1/services/com.mitchellh.ghostty{s}.service", + .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + ), + ), + ); + // systemd user service + try steps.append( + formatService( + b, + cfg, + b.path("dist/linux/systemd.service"), + b.fmt( + "{s}/systemd/user/com.mitchellh.ghostty{s}.service", + .{ + if (cfg.system_package) "lib" else "share", + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + ), + ), + ); // AppStream metainfo so that application has rich metadata within app stores try steps.append(&b.addInstallFile( @@ -303,3 +350,34 @@ pub fn install(self: *const GhosttyResources) void { const b = self.steps[0].owner; for (self.steps) |step| b.getInstallStep().dependOn(step); } + +pub fn formatService(b: *std.Build, cfg: *const Config, src: std.Build.LazyPath, dest: []const u8) *std.Build.Step { + var cmd = b.addSystemCommand(&.{"sed"}); + cmd.setStdIn(.{ .lazy_path = src }); + const output = cmd.captureStdOut(); + + cmd.addArg(b.fmt( + "-e s!@@NAME@@!{s}!g", + .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => " Debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + )); + cmd.addArg(b.fmt( + "-e s!@@DEBUG@@!{s}!g", + .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + )); + cmd.addArg(b.fmt( + "-e s!@@GHOSTTY@@!{s}/bin/ghostty!g", + .{b.install_prefix}, + )); + + return &b.addInstallFile(output, dest).step; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index be59ae94f..64b00eb1c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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). ///