Skip to content

Commit c12b280

Browse files
authored
dbus and systemd activation - take 2 (ghostty-org#7679)
This replaces ghostty-org#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.
2 parents d92d1ca + b4e8194 commit c12b280

11 files changed

Lines changed: 222 additions & 82 deletions
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
[Desktop Entry]
2-
Name=Ghostty
2+
Version=1.0
3+
Name=@NAME@
34
Type=Application
45
Comment=A terminal emulator
5-
Exec=ghostty
6+
TryExec=@GHOSTTY@
7+
Exec=@GHOSTTY@ --launched-from=desktop
68
Icon=com.mitchellh.ghostty
79
Categories=System;TerminalEmulator;
810
Keywords=terminal;tty;pty;
911
StartupNotify=true
10-
StartupWMClass=com.mitchellh.ghostty
12+
StartupWMClass=@APPID@
1113
Terminal=false
1214
Actions=new-window;
1315
X-GNOME-UsesNotifications=true
@@ -16,7 +18,8 @@ X-TerminalArgTitle=--title=
1618
X-TerminalArgAppId=--class=
1719
X-TerminalArgDir=--working-directory=
1820
X-TerminalArgHold=--wait-after-command
21+
DBusActivatable=true
1922

2023
[Desktop Action new-window]
2124
Name=New Window
22-
Exec=ghostty
25+
Exec=@GHOSTTY@ --launched-from=desktop

dist/linux/com.mitchellh.ghostty.metainfo.xml renamed to dist/linux/com.mitchellh.ghostty.metainfo.xml.in

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<component type="desktop-application">
3-
<id>com.mitchellh.ghostty</id>
4-
<launchable type="desktop-id">com.mitchellh.ghostty.desktop</launchable>
5-
<name>Ghostty</name>
3+
<id>@APPID@id>
4+
<launchable type="desktop-id">@APPID@desktop</launchable>
5+
<name>@NAME@name>
66
<url type="homepage">https://ghostty.org</url>
77
<url type="help">https://ghostty.org/docs</url>
88
<url type="bugtracker">https://github.com/ghostty-org/ghostty/discussions</url>

dist/linux/dbus.service.flatpak.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[D-BUS Service]
2+
Name=@APPID@
3+
Exec=@GHOSTTY@ --launched-from=dbus

dist/linux/dbus.service.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[D-BUS Service]
2+
Name=@APPID@
3+
SystemdService=@APPID@.service
4+
Exec=@GHOSTTY@ --launched-from=dbus

dist/linux/systemd.service.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[Unit]
2+
Description=@NAME@
3+
4+
[Service]
5+
Type=dbus
6+
BusName=@APPID@
7+
ExecStart=@GHOSTTY@ --launched-from=systemd

flatpak/com.mitchellh.ghostty-debug.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ sdk-extensions:
66
- org.freedesktop.Sdk.Extension.ziglang
77
default-branch: tip
88
command: ghostty
9-
# Integrate the rename into zig build, maybe?
10-
rename-desktop-file: com.mitchellh.ghostty.desktop
11-
rename-appdata-file: com.mitchellh.ghostty.metainfo.xml
129
rename-icon: com.mitchellh.ghostty
13-
desktop-file-name-suffix: " (Debug)"
1410
finish-args:
1511
# 3D rendering
1612
- --device=dri

src/apprt/gtk/App.zig

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,11 +405,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
405405
// This just calls the `activate` signal but its part of the normal startup
406406
// routine so we just call it, but only if the config allows it (this allows
407407
// for launching Ghostty in the "background" without immediately opening
408-
// a window)
408+
// a window). An initial window will not be immediately created if we were
409+
// launched by D-Bus activation or systemd. D-Bus activation will send it's
410+
// own `activate` or `new-window` signal later.
409411
//
410412
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
411-
if (config.@"initial-window")
412-
gio_app.activate();
413+
if (config.@"initial-window") switch (config.@"launched-from".?) {
414+
.desktop, .cli => gio_app.activate(),
415+
.dbus, .systemd => {},
416+
};
413417

414418
// Internally, GTK ensures that only one instance of this provider exists in the provider list
415419
// for the display.
@@ -1683,6 +1687,17 @@ fn gtkActionShowGTKInspector(
16831687
};
16841688
}
16851689

1690+
fn gtkActionNewWindow(
1691+
_: *gio.SimpleAction,
1692+
_: ?*glib.Variant,
1693+
self: *App,
1694+
) callconv(.c) void {
1695+
log.info("received new window action", .{});
1696+
_ = self.core_app.mailbox.push(.{
1697+
.new_window = .{},
1698+
}, .{ .forever = {} });
1699+
}
1700+
16861701
/// This is called to setup the action map that this application supports.
16871702
/// This should be called only once on startup.
16881703
fn initActions(self: *App) void {
@@ -1702,7 +1717,9 @@ fn initActions(self: *App) void {
17021717
.{ "reload-config", gtkActionReloadConfig, null },
17031718
.{ "present-surface", gtkActionPresentSurface, t },
17041719
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
1720+
.{ "new-window", gtkActionNewWindow, null },
17051721
};
1722+
17061723
inline for (actions) |entry| {
17071724
const action = gio.SimpleAction.new(entry[0], entry[2]);
17081725
defer action.unref();

src/apprt/gtk/Surface.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2325,6 +2325,15 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
23252325
env.remove("GDK_DISABLE");
23262326
env.remove("GSK_RENDERER");
23272327

2328+
// Remove some environment variables that are set when Ghostty is launched
2329+
// from a `.desktop` file, by D-Bus activation, or systemd.
2330+
env.remove("GIO_LAUNCHED_DESKTOP_FILE");
2331+
env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
2332+
env.remove("DBUS_STARTER_ADDRESS");
2333+
env.remove("DBUS_STARTER_BUS_TYPE");
2334+
env.remove("INVOCATION_ID");
2335+
env.remove("JOURNAL_STREAM");
2336+
23282337
// Unset environment varies set by snaps if we're running in a snap.
23292338
// This allows Ghostty to further launch additional snaps.
23302339
if (env.get("SNAP")) |_| {

src/build/Config.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub fn init(b: *std.Build) !Config {
8787
// This is set to true when we're building a system package. For now
8888
// this is trivially detected using the "system_package_mode" bool
8989
// but we may want to make this more sophisticated in the future.
90-
const system_package: bool = b.graph.system_package_mode;
90+
const system_package = b.graph.system_package_mode;
9191

9292
// This specifies our target wasm runtime. For now only one semi-usable
9393
// one exists so this is hardcoded.

src/build/GhosttyResources.zig

Lines changed: 162 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const GhosttyResources = @This();
22

33
const std = @import("std");
44
const builtin = @import("builtin");
5+
const assert = std.debug.assert;
56
const buildpkg = @import("main.zig");
67
const Config = @import("Config.zig");
78
const config_vim = @import("../config/vim.zig");
@@ -220,83 +221,178 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
220221
}
221222

222223
// App (Linux)
223-
if (cfg.target.result.os.tag == .linux) {
224-
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
224+
if (cfg.target.result.os.tag == .linux) try addLinuxAppResources(
225+
b,
226+
cfg,
227+
&steps,
228+
);
229+
230+
return .{ .steps = steps.items };
231+
}
232+
233+
/// Add the resource files needed to make Ghostty a proper
234+
/// Linux desktop application (for various desktop environments).
235+
fn addLinuxAppResources(
236+
b: *std.Build,
237+
cfg: *const Config,
238+
steps: *std.ArrayList(*std.Build.Step),
239+
) !void {
240+
assert(cfg.target.result.os.tag == .linux);
241+
242+
// Background:
243+
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
244+
245+
const name = b.fmt("Ghostty{s}", .{
246+
switch (cfg.optimize) {
247+
.Debug, .ReleaseSafe => " (Debug)",
248+
.ReleaseFast, .ReleaseSmall => "",
249+
},
250+
});
251+
252+
const app_id = b.fmt("com.mitchellh.ghostty{s}", .{
253+
switch (cfg.optimize) {
254+
.Debug, .ReleaseSafe => "-debug",
255+
.ReleaseFast, .ReleaseSmall => "",
256+
},
257+
});
258+
259+
const exe_abs_path = b.fmt(
260+
"{s}/bin/ghostty",
261+
.{b.install_prefix},
262+
);
263+
264+
// The templates that we will process. The templates are in
265+
// cmake format and will be processed and saved to the
266+
// second element of the tuple.
267+
const Template = struct { std.Build.LazyPath, []const u8 };
268+
const templates: []const Template = templates: {
269+
var ts: std.ArrayList(Template) = .init(b.allocator);
225270

226271
// Desktop file so that we have an icon and other metadata
227-
try steps.append(&b.addInstallFile(
228-
b.path("dist/linux/app.desktop"),
229-
"share/applications/com.mitchellh.ghostty.desktop",
230-
).step);
272+
try ts.append(.{
273+
b.path("dist/linux/app.desktop.in"),
274+
b.fmt("share/applications/{s}.desktop", .{app_id}),
275+
});
231276

232-
// AppStream metainfo so that application has rich metadata within app stores
233-
try steps.append(&b.addInstallFile(
234-
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"),
235-
"share/metainfo/com.mitchellh.ghostty.metainfo.xml",
236-
).step);
277+
// Service for DBus activation.
278+
try ts.append(.{
279+
if (cfg.flatpak)
280+
b.path("dist/linux/dbus.service.flatpak.in")
281+
else
282+
b.path("dist/linux/dbus.service.in"),
283+
b.fmt("share/dbus-1/services/{s}.service", .{app_id}),
284+
});
237285

238-
// Right click menu action for Plasma desktop
239-
try steps.append(&b.addInstallFile(
240-
b.path("dist/linux/ghostty_dolphin.desktop"),
241-
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
242-
).step);
286+
// systemd user service. This is kind of nasty but systemd
287+
// looks for user services in different paths depending on
288+
// if we are installed as a system package or not (lib vs.
289+
// share) so we have to handle that here. We might be able
290+
// to get away with always installing to both because it
291+
// only ever searches in one... but I don't want to do that hack
292+
// until we have to.
293+
if (!cfg.flatpak) try ts.append(.{
294+
b.path("dist/linux/systemd.service.in"),
295+
b.fmt(
296+
"{s}/systemd/user/{s}.service",
297+
.{
298+
if (b.graph.system_package_mode) "lib" else "share",
299+
app_id,
300+
},
301+
),
302+
});
243303

244-
// Right click menu action for Nautilus. Note that this _must_ be named
245-
// `ghostty.py`. Using the full app id causes problems (see #5468).
246-
try steps.append(&b.addInstallFile(
247-
b.path("dist/linux/ghostty_nautilus.py"),
248-
"share/nautilus-python/extensions/ghostty.py",
249-
).step);
304+
// AppStream metainfo so that application has rich metadata
305+
// within app stores
306+
try ts.append(.{
307+
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"),
308+
b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}),
309+
});
250310

251-
// Various icons that our application can use, including the icon
252-
// that will be used for the desktop.
253-
try steps.append(&b.addInstallFile(
254-
b.path("images/icons/icon_16.png"),
255-
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
256-
).step);
257-
try steps.append(&b.addInstallFile(
258-
b.path("images/icons/icon_32.png"),
259-
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
260-
).step);
261-
try steps.append(&b.addInstallFile(
262-
b.path("images/icons/icon_128.png"),
263-
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
264-
).step);
265-
try steps.append(&b.addInstallFile(
266-
b.path("images/icons/icon_256.png"),
267-
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
268-
).step);
269-
try steps.append(&b.addInstallFile(
270-
b.path("images/icons/icon_512.png"),
271-
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
272-
).step);
273-
// Flatpaks only support icons up to 512x512.
274-
if (!cfg.flatpak) {
275-
try steps.append(&b.addInstallFile(
276-
b.path("images/icons/icon_1024.png"),
277-
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
278-
).step);
279-
}
311+
break :templates ts.items;
312+
};
313+
314+
// Process all our templates
315+
for (templates) |template| {
316+
const tpl = b.addConfigHeader(.{
317+
.style = .{ .cmake = template[0] },
318+
}, .{
319+
.NAME = name,
320+
.APPID = app_id,
321+
.GHOSTTY = exe_abs_path,
322+
});
280323

324+
// Template output has a single header line we want to remove.
325+
// We use `tail` to do it since its part of the POSIX standard.
326+
const tail = b.addSystemCommand(&.{ "tail", "-n", "+2" });
327+
tail.setStdIn(.{ .lazy_path = tpl.getOutput() });
328+
329+
const copy = b.addInstallFile(
330+
tail.captureStdOut(),
331+
template[1],
332+
);
333+
334+
try steps.append(&copy.step);
335+
}
336+
337+
// Right click menu action for Plasma desktop
338+
try steps.append(&b.addInstallFile(
339+
b.path("dist/linux/ghostty_dolphin.desktop"),
340+
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
341+
).step);
342+
343+
// Right click menu action for Nautilus. Note that this _must_ be named
344+
// `ghostty.py`. Using the full app id causes problems (see #5468).
345+
try steps.append(&b.addInstallFile(
346+
b.path("dist/linux/ghostty_nautilus.py"),
347+
"share/nautilus-python/extensions/ghostty.py",
348+
).step);
349+
350+
// Various icons that our application can use, including the icon
351+
// that will be used for the desktop.
352+
try steps.append(&b.addInstallFile(
353+
b.path("images/icons/icon_16.png"),
354+
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
355+
).step);
356+
try steps.append(&b.addInstallFile(
357+
b.path("images/icons/icon_32.png"),
358+
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
359+
).step);
360+
try steps.append(&b.addInstallFile(
361+
b.path("images/icons/icon_128.png"),
362+
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
363+
).step);
364+
try steps.append(&b.addInstallFile(
365+
b.path("images/icons/icon_256.png"),
366+
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
367+
).step);
368+
try steps.append(&b.addInstallFile(
369+
b.path("images/icons/icon_512.png"),
370+
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
371+
).step);
372+
// Flatpaks only support icons up to 512x512.
373+
if (!cfg.flatpak) {
281374
try steps.append(&b.addInstallFile(
282-
b.path("images/icons/icon_16@2x.png"),
283-
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
284-
).step);
285-
try steps.append(&b.addInstallFile(
286-
b.path("images/icons/icon_32@2x.png"),
287-
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
288-
).step);
289-
try steps.append(&b.addInstallFile(
290-
b.path("images/icons/icon_128@2x.png"),
291-
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
292-
).step);
293-
try steps.append(&b.addInstallFile(
294-
b.path("images/icons/icon_256@2x.png"),
295-
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
375+
b.path("images/icons/icon_1024.png"),
376+
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
296377
).step);
297378
}
298379

299-
return .{ .steps = steps.items };
380+
try steps.append(&b.addInstallFile(
381+
b.path("images/icons/icon_16@2x.png"),
382+
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
383+
).step);
384+
try steps.append(&b.addInstallFile(
385+
b.path("images/icons/icon_32@2x.png"),
386+
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
387+
).step);
388+
try steps.append(&b.addInstallFile(
389+
b.path("images/icons/icon_128@2x.png"),
390+
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
391+
).step);
392+
try steps.append(&b.addInstallFile(
393+
b.path("images/icons/icon_256@2x.png"),
394+
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
395+
).step);
300396
}
301397

302398
pub fn install(self: *const GhosttyResources) void {

0 commit comments

Comments
 (0)