diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 97466e9b5..67aeeaf7c 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index d1c8f2c59..d69102bda 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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. diff --git a/src/config/Config.zig b/src/config/Config.zig index 344c118d7..bf6d26f4b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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(); diff --git a/src/os/dbus.zig b/src/os/dbus.zig new file mode 100644 index 000000000..99824db71 --- /dev/null +++ b/src/os/dbus.zig @@ -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, + }; +} diff --git a/src/os/locale.zig b/src/os/locale.zig index 17e4d163c..b391d690f 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -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, diff --git a/src/os/main.zig b/src/os/main.zig index 36833f427..582ac75cd 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -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; diff --git a/src/os/systemd.zig b/src/os/systemd.zig new file mode 100644 index 000000000..9b67296d6 --- /dev/null +++ b/src/os/systemd.zig @@ -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//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, + }; +}