ghostty/src/os/desktop.zig
Mitchell Hashimoto 984d123fe4 macos: support configuration via CLI arguments
This makes it so `zig build run` can take arguments such as
`--config-default-files=false` or any other configuration. Previously,
it only accepted commands such as `+version`.

Incidentally, this also makes it so that the app in general can now take
configuration arguments via the CLI if it is launched as a new instance
via `open`. For example:

    open -n Ghostty.app --args --config-default-files=false

This previously didn't work. This is kind of cool.

To make this work, the libghostty C API was modified so that
initialization requires the CLI args, and there is a new C API to try to
execute an action if it was set.
2025-07-05 21:31:23 -07:00

161 lines
6.4 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const posix = std.posix;
const c = @cImport({
@cInclude("unistd.h");
});
/// Returns true if the program was launched from a desktop environment.
///
/// On macOS, this returns true if the program was launched from Finder.
///
/// On Linux GTK, this returns true if the program was launched using the
/// desktop file. This also includes when `gtk-launch` is used because I
/// can't find a way to distinguish the two scenarios.
///
/// For other platforms and app runtimes, this returns false.
pub fn launchedFromDesktop() bool {
return switch (builtin.os.tag) {
// macOS apps launched from finder or `open` always have the init
// process as their parent.
.macos => macos: {
// This special case is so that if we launch the app via the
// app bundle (i.e. via open) then we still treat it as if it
// was launched from the desktop.
if (build_config.artifact == .lib) lib: {
const env = "GHOSTTY_MAC_LAUNCH_SOURCE";
const source = posix.getenv(env) orelse break :lib;
// Source can be "app", "cli", or "zig_run". We assume
// its the desktop only if its "app". We may want to do
// "zig_run" but at the moment there's no reason.
if (std.mem.eql(u8, source, "app")) break :macos true;
}
break :macos c.getppid() == 1;
},
// On Linux and BSD, GTK sets GIO_LAUNCHED_DESKTOP_FILE and
// GIO_LAUNCHED_DESKTOP_FILE_PID. We only check the latter to see if
// we match the PID and assume that if we do, we were launched from
// the desktop file. Pid comparing catches the scenario where
// another terminal was launched from a desktop file and then launches
// Ghostty and Ghostty inherits the env.
.linux, .freebsd => ul: {
const gio_pid_str = posix.getenv("GIO_LAUNCHED_DESKTOP_FILE_PID") orelse
break :ul false;
const pid = c.getpid();
const gio_pid = std.fmt.parseInt(
@TypeOf(pid),
gio_pid_str,
10,
) catch break :ul false;
break :ul gio_pid == pid;
},
// TODO: This should have some logic to detect this. Perhaps std.builtin.subsystem
.windows => false,
// iPhone/iPad is always launched from the "desktop"
.ios => true,
else => @compileError("unsupported platform"),
};
}
/// The list of desktop environments that we detect. New Linux desktop
/// environments should only be added to this list if there's a specific reason
/// to differentiate between `gnome` and `other`.
pub const DesktopEnvironment = enum {
gnome,
macos,
other,
windows,
};
/// Detect what desktop environment we are running under. This is mainly used
/// on Linux and BSD to enable or disable certain features but there may be more uses in
/// the future.
pub fn desktopEnvironment() DesktopEnvironment {
return switch (comptime builtin.os.tag) {
.macos => .macos,
.windows => .windows,
.linux, .freebsd => de: {
if (@inComptime()) @compileError("Checking for the desktop environment on Linux/BSD must be done at runtime.");
// Use $XDG_SESSION_DESKTOP to determine what DE we are using on Linux
// https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html#desktop=
if (posix.getenv("XDG_SESSION_DESKTOP")) |sd| {
if (std.ascii.eqlIgnoreCase("gnome", sd)) break :de .gnome;
if (std.ascii.eqlIgnoreCase("gnome-xorg", sd)) break :de .gnome;
}
// If $XDG_SESSION_DESKTOP is not set, or doesn't match any known
// DE, check $XDG_CURRENT_DESKTOP. $XDG_CURRENT_DESKTOP is a
// colon-separated list of up to three desktop names, although we
// only look at the first.
// https://specifications.freedesktop.org/desktop-entry-spec/latest/recognized-keys.html
if (posix.getenv("XDG_CURRENT_DESKTOP")) |cd| {
var cd_it = std.mem.splitScalar(u8, cd, ':');
const cd_first = cd_it.first();
if (std.ascii.eqlIgnoreCase(cd_first, "gnome")) break :de .gnome;
}
break :de .other;
},
else => .other,
};
}
test "desktop environment" {
const testing = std.testing;
switch (builtin.os.tag) {
.macos => try testing.expectEqual(.macos, desktopEnvironment()),
.windows => try testing.expectEqual(.windows, desktopEnvironment()),
.linux, .freebsd => {
const getenv = std.posix.getenv;
const setenv = @import("env.zig").setenv;
const unsetenv = @import("env.zig").unsetenv;
const xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP");
defer if (xdg_current_desktop) |v| {
_ = setenv("XDG_CURRENT_DESKTOP", v);
} else {
_ = unsetenv("XDG_CURRENT_DESKTOP");
};
_ = unsetenv("XDG_CURRENT_DESKTOP");
const xdg_session_desktop = getenv("XDG_SESSION_DESKTOP");
defer if (xdg_session_desktop) |v| {
_ = setenv("XDG_SESSION_DESKTOP", v);
} else {
_ = unsetenv("XDG_SESSION_DESKTOP");
};
_ = unsetenv("XDG_SESSION_DESKTOP");
_ = setenv("XDG_SESSION_DESKTOP", "gnome");
try testing.expectEqual(.gnome, desktopEnvironment());
_ = setenv("XDG_SESSION_DESKTOP", "gnome-xorg");
try testing.expectEqual(.gnome, desktopEnvironment());
_ = setenv("XDG_SESSION_DESKTOP", "foobar");
try testing.expectEqual(.other, desktopEnvironment());
_ = unsetenv("XDG_SESSION_DESKTOP");
try testing.expectEqual(.other, desktopEnvironment());
_ = setenv("XDG_CURRENT_DESKTOP", "GNOME");
try testing.expectEqual(.gnome, desktopEnvironment());
_ = setenv("XDG_CURRENT_DESKTOP", "FOOBAR");
try testing.expectEqual(.other, desktopEnvironment());
_ = unsetenv("XDG_CURRENT_DESKTOP");
try testing.expectEqual(.other, desktopEnvironment());
},
else => try testing.expectEqual(.other, DesktopEnvironment()),
}
}