config: add launched-from to specify launch source

Related to #7433

This extracts our "launched from desktop" logic into a config option.
The default value is detection using the same logic as before, but now
this can be overridden by the user.

This also adds the systemd and dbus activation sources from #7433.

There are a number of reasons why we decided to do this:

  1. It automatically gets us caching since the configuration is only
     loaded once (per reload, a rare occurrence).

  2. It allows us to override the logic when testing. Previously, we
     had to do more complex environment faking to get the same
     behavior.

  3. It forces exhaustive switches in any desktop handling code, which
     will make it easier to ensure valid behaviors if we introduce new
     launch sources (as we are in #7433).

  4. It lowers code complexity since callsites don't need to have N
     `launchedFromX()` checks and can use a single value.
pull/7503/head
Mitchell Hashimoto 2025-06-02 08:34:03 -07:00
parent 1ff9162598
commit 5306e7cf56
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
7 changed files with 158 additions and 15 deletions

View File

@ -842,7 +842,10 @@ pub const Surface = struct {
// our translation settings for Ghostty. If we aren't from
// the desktop then we didn't set our LANGUAGE var so we
// don't need to remove it.
if (internal_os.launchedFromDesktop()) env.remove("LANGUAGE");
switch (self.app.config.@"launched-from".?) {
.desktop => env.remove("LANGUAGE"),
.dbus, .systemd, .cli => {},
}
}
return env;

View File

@ -273,7 +273,10 @@ 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(),
.desktop => switch (config.@"launched-from".?) {
.desktop, .systemd, .dbus => true,
.cli => false,
},
};
// Setup the flags for our application.

View File

@ -2428,6 +2428,23 @@ term: []const u8 = "xterm-ghostty",
/// running. Defaults to an empty string if not set.
@"enquiry-response": []const u8 = "",
/// The mechanism used to launch Ghostty. This should generally not be
/// set by users, see the warning below.
///
/// WARNING: This is a low-level configuration that is not intended to be
/// modified by users. All the values will be automatically detected as they
/// are needed by Ghostty. This is only here in case our detection logic is
/// incorrect for your environment or for developers who want to test
/// Ghostty's behavior in different, forced environments.
///
/// This is set using the standard `no-[value]`, `[value]` syntax separated
/// by commas. Example: "no-desktop,systemd". Specific details about the
/// available values are documented on LaunchProperties in the code. Since
/// this isn't intended to be modified by users, the documentation is
/// lighter than the other configurations and users are expected to
/// refer to the code for details.
@"launched-from": ?LaunchSource = null,
/// Configures the low-level API to use for async IO, eventing, etc.
///
/// Most users should leave this set to `auto`. This will automatically detect
@ -3111,6 +3128,11 @@ pub fn finalize(self: *Config) !void {
const alloc = self._arena.?.allocator();
// Ensure our launch source is properly set.
if (self.@"launched-from" == null) {
self.@"launched-from" = .detect();
}
// If we have a font-family set and don't set the others, default
// the others to the font family. This way, if someone does
// --font-family=foo, then we try to get the stylized versions of
@ -3135,14 +3157,11 @@ pub fn finalize(self: *Config) !void {
}
// The default for the working directory depends on the system.
const wd = self.@"working-directory" orelse wd: {
const wd = self.@"working-directory" orelse switch (self.@"launched-from".?) {
// If we have no working directory set, our default depends on
// whether we were launched from the desktop or CLI.
if (internal_os.launchedFromDesktop()) {
break :wd "home";
}
break :wd "inherit";
// whether we were launched from the desktop or elsewhere.
.desktop => "home",
.cli, .dbus, .systemd => "inherit",
};
// If we are missing either a command or home directory, we need
@ -3165,7 +3184,10 @@ pub fn finalize(self: *Config) !void {
// If we were launched from the desktop, our SHELL env var
// will represent our SHELL at login time. We want to use the
// latest shell from /etc/passwd or directory services.
if (internal_os.launchedFromDesktop()) break :shell_env;
switch (self.@"launched-from".?) {
.desktop, .dbus, .systemd => break :shell_env,
.cli => {},
}
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| {
log.info("default shell source=env value={s}", .{value});
@ -6595,6 +6617,34 @@ pub const Duration = struct {
}
};
pub const LaunchSource = enum {
/// Ghostty was launched via the CLI. This is the default if
/// no other source is detected.
cli,
/// Ghostty was launched in a desktop environment (not via the CLI).
/// This is used to determine some behaviors such as how to read
/// settings, whether single instance defaults to true, etc.
desktop,
/// Ghostty was started via dbus activation.
dbus,
/// Ghostty was started via systemd activation.
systemd,
pub fn detect() LaunchSource {
return if (internal_os.launchedFromDesktop())
.desktop
else if (internal_os.launchedByDbusActivation())
.dbus
else if (internal_os.launchedBySystemd())
.systemd
else
.cli;
}
};
pub const WindowPadding = struct {
const Self = @This();

21
src/os/dbus.zig Normal file
View 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,
};
}

View File

@ -108,11 +108,8 @@ fn setLangFromCocoa() void {
}
// Get our preferred languages and set that to the LANGUAGE
// env var in case our language differs from our locale. We only
// do this when the app is launched from the desktop because then
// we're in an app bundle and we are expected to read from our
// Bundle's preferred languages.
if (internal_os.launchedFromDesktop()) language: {
// env var in case our language differs from our locale.
language: {
var buf: [1024]u8 = undefined;
const pref_ = preferredLanguageFromCocoa(
&buf,

View File

@ -2,6 +2,7 @@
//! system. These aren't restricted to syscalls or low-level operations, but
//! also OS-specific features and conventions.
const dbus = @import("dbus.zig");
const desktop = @import("desktop.zig");
const env = @import("env.zig");
const file = @import("file.zig");
@ -12,6 +13,7 @@ const mouse = @import("mouse.zig");
const openpkg = @import("open.zig");
const pipepkg = @import("pipe.zig");
const resourcesdir = @import("resourcesdir.zig");
const systemd = @import("systemd.zig");
// Namespaces
pub const args = @import("args.zig");
@ -35,6 +37,8 @@ pub const getenv = env.getenv;
pub const setenv = env.setenv;
pub const unsetenv = env.unsetenv;
pub const launchedFromDesktop = desktop.launchedFromDesktop;
pub const launchedByDbusActivation = dbus.launchedByDbusActivation;
pub const launchedBySystemd = systemd.launchedBySystemd;
pub const desktopEnvironment = desktop.desktopEnvironment;
pub const rlimit = file.rlimit;
pub const fixMaxFiles = file.fixMaxFiles;

65
src/os/systemd.zig Normal file
View File

@ -0,0 +1,65 @@
const std = @import("std");
const builtin = @import("builtin");
const log = std.log.scoped(.systemd);
/// 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) {
.linux => linux: {
// On Linux, systemd sets the `INVOCATION_ID` (v232+) and the
// `JOURNAL_STREAM` (v231+) environment variables. If these
// environment variables are not present we were not launched by
// systemd.
if (std.posix.getenv("INVOCATION_ID") == null) break :linux false;
if (std.posix.getenv("JOURNAL_STREAM") == null) break :linux false;
// If `INVOCATION_ID` and `JOURNAL_STREAM` are present, check to make sure
// that our parent process is actually `systemd`, not some other terminal
// emulator that doesn't clean up those environment variables.
const ppid = std.os.linux.getppid();
if (ppid == 1) break :linux true;
// If the parent PID is not 1 we need to check to see if we were launched by
// a user systemd daemon. Do that by checking the `/proc/<ppid>/comm`
// to see if it ends with `systemd`.
var comm_path_buf: [std.fs.max_path_bytes]u8 = undefined;
const comm_path = std.fmt.bufPrint(&comm_path_buf, "/proc/{d}/comm", .{ppid}) catch {
log.err("unable to format comm path for pid {d}", .{ppid});
break :linux false;
};
const comm_file = std.fs.openFileAbsolute(comm_path, .{ .mode = .read_only }) catch {
log.err("unable to open '{s}' for reading", .{comm_path});
break :linux false;
};
defer comm_file.close();
// The maximum length of the command name is defined by
// `TASK_COMM_LEN` in the Linux kernel. This is usually 16
// bytes at the time of writing (Jun 2025) so its set to that.
// Also, since we only care to compare to "systemd", anything
// longer can be assumed to not be systemd.
const TASK_COMM_LEN = 16;
var comm_data_buf: [TASK_COMM_LEN]u8 = undefined;
const comm_size = comm_file.readAll(&comm_data_buf) catch {
log.err("problems reading from '{s}'", .{comm_path});
break :linux false;
};
const comm_data = comm_data_buf[0..comm_size];
break :linux std.mem.eql(
u8,
std.mem.trimRight(u8, comm_data, "\n"),
"systemd",
);
},
// No other system supports systemd so always return false.
else => false,
};
}