gtk-ng: deprecate detection of launch source (#8511)
Detecting the launch source frequently failed because various launchers fail to sanitize the environment variables that Ghostty used to detect the launch source. For example, if your desktop environment was launched by `systemd`, but your desktop environment did not sanitize the `INVOCATION_ID` or the `JOURNAL_STREAM` environment variables, Ghostty would assume that it had been launched by `systemd` and behave as such. This led to complaints about Ghostty not creating new windows when users expected that it would. To remedy this, Ghostty no longer does any detection of the launch source. If your launch source is something other than the CLI, it must be explicitly speciflied on the CLI. All of Ghostty's default desktop and service files do this. Users or packagers that create custom desktop or service files will need to take this into account. On GTK, the `desktop` setting for `gtk-single-instance` is replaced with `detect`. `detect` behaves as `gtk-single-instance=true` if one of the following conditions is true: 1. If no CLI arguments have been set. 2. If `--launched-from` has been set to `desktop`, `dbus`, or `systemd`. Otherwise `detect` behaves as `gtk-single-instance=false`.pull/8534/head
commit
0492cd16fa
|
|
@ -4,7 +4,7 @@ Name=@NAME@
|
|||
Type=Application
|
||||
Comment=A terminal emulator
|
||||
TryExec=@GHOSTTY@
|
||||
Exec=@GHOSTTY@ --launched-from=desktop
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true
|
||||
Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
|
|
@ -23,4 +23,4 @@ X-KDE-Shortcuts=Ctrl+Alt+T
|
|||
|
||||
[Desktop Action new-window]
|
||||
Name=New Window
|
||||
Exec=@GHOSTTY@ --launched-from=desktop
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
[D-BUS Service]
|
||||
Name=@APPID@
|
||||
Exec=@GHOSTTY@ --launched-from=dbus
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[D-BUS Service]
|
||||
Name=@APPID@
|
||||
SystemdService=app-@APPID@.service
|
||||
Exec=@GHOSTTY@ --launched-from=dbus
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Requires=dbus.socket
|
|||
Type=notify-reload
|
||||
ReloadSignal=SIGUSR2
|
||||
BusName=@APPID@
|
||||
ExecStart=@GHOSTTY@ --launched-from=systemd
|
||||
ExecStart=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
|
|
|
|||
|
|
@ -909,10 +909,7 @@ 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.
|
||||
switch (self.app.config.@"launched-from".?) {
|
||||
.desktop => env.remove("LANGUAGE"),
|
||||
.dbus, .systemd, .cli => {},
|
||||
}
|
||||
if (internal_os.launchedFromDesktop()) env.remove("LANGUAGE");
|
||||
}
|
||||
|
||||
return env;
|
||||
|
|
|
|||
|
|
@ -223,10 +223,8 @@ pub const Application = extern struct {
|
|||
const single_instance = switch (config.@"gtk-single-instance") {
|
||||
.true => true,
|
||||
.false => false,
|
||||
.desktop => switch (config.@"launched-from".?) {
|
||||
.desktop, .systemd, .dbus => true,
|
||||
.cli => false,
|
||||
},
|
||||
// This should have been resolved to true/false during config loading.
|
||||
.detect => unreachable,
|
||||
};
|
||||
|
||||
// Setup the flags for our application.
|
||||
|
|
@ -418,9 +416,7 @@ pub const Application = extern struct {
|
|||
// 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). 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.
|
||||
// a window).
|
||||
//
|
||||
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||
const priv = self.private();
|
||||
|
|
@ -428,15 +424,11 @@ pub const Application = extern struct {
|
|||
// We need to scope any config access because once we run our
|
||||
// event loop, this can change out from underneath us.
|
||||
const config = priv.config.get();
|
||||
if (config.@"initial-window") switch (config.@"launched-from".?) {
|
||||
.desktop, .cli => self.as(gio.Application).activate(),
|
||||
.dbus, .systemd => {},
|
||||
};
|
||||
if (config.@"initial-window") self.as(gio.Application).activate();
|
||||
}
|
||||
|
||||
// If we are NOT the primary instance, then we never want to run.
|
||||
// This means that another instance of the GTK app is running and
|
||||
// our "activate" call above will open a window.
|
||||
// This means that another instance of the GTK app is running.
|
||||
if (self.as(gio.Application).getIsRemote() != 0) {
|
||||
log.debug(
|
||||
"application is remote, exiting run loop after activation",
|
||||
|
|
|
|||
|
|
@ -260,6 +260,7 @@ fn formatInvalidValue(
|
|||
}
|
||||
|
||||
fn formatValues(comptime T: type, key: []const u8, writer: anytype) std.mem.Allocator.Error!void {
|
||||
@setEvalBranchQuota(2000);
|
||||
const typeinfo = @typeInfo(T);
|
||||
inline for (typeinfo.@"struct".fields) |f| {
|
||||
if (std.mem.eql(u8, key, f.name)) {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ pub const compatibility = std.StaticStringMap(
|
|||
// Ghostty 1.2 merged `bold-is-bright` into the new `bold-color`
|
||||
// by setting the value to "bright".
|
||||
.{ "bold-is-bright", compatBoldIsBright },
|
||||
|
||||
// Ghostty 1.2 removed the "desktop" option and renamed it to "detect".
|
||||
// The semantics also changed slightly but this is the correct mapping.
|
||||
.{ "gtk-single-instance", compatGtkSingleInstance },
|
||||
});
|
||||
|
||||
/// The font families to use.
|
||||
|
|
@ -2975,14 +2979,29 @@ else
|
|||
///
|
||||
/// If `false`, each new ghostty process will launch a separate application.
|
||||
///
|
||||
/// The default value is `desktop` which will default to `true` if Ghostty
|
||||
/// detects that it was launched from the `.desktop` file such as an app
|
||||
/// launcher (like Gnome Shell) or by D-Bus activation. If Ghostty is launched
|
||||
/// from the command line, it will default to `false`.
|
||||
/// If `detect`, Ghostty will assume true (single instance) unless one of
|
||||
/// the following scenarios is found:
|
||||
///
|
||||
/// 1. TERM_PROGRAM environment variable is a non-empty value. In this
|
||||
/// case, we assume Ghostty is being launched from a graphical terminal
|
||||
/// session and you want a dedicated instance.
|
||||
///
|
||||
/// 2. Any CLI arguments exist. In this case, we assume you are passing
|
||||
/// custom Ghostty configuration. Single instance mode inherits the
|
||||
/// configuration from when it was launched, so we must disable single
|
||||
/// instance to load the new configuration.
|
||||
///
|
||||
/// If either of these scenarios is producing a false positive, you can
|
||||
/// set this configuration explicitly to the behavior you want.
|
||||
///
|
||||
/// The pre-1.2 option `desktop` has been deprecated. Please replace
|
||||
/// this with `detect`.
|
||||
///
|
||||
/// The default value is `detect`.
|
||||
///
|
||||
/// Note that debug builds of Ghostty have a separate single-instance ID
|
||||
/// so you can test single instance without conflicting with release builds.
|
||||
@"gtk-single-instance": GtkSingleInstance = .desktop,
|
||||
@"gtk-single-instance": GtkSingleInstance = .default,
|
||||
|
||||
/// When enabled, the full GTK titlebar is displayed instead of your window
|
||||
/// manager's simple titlebar. The behavior of this option will vary with your
|
||||
|
|
@ -3104,25 +3123,6 @@ 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.
|
||||
///
|
||||
/// Available since: 1.2.0
|
||||
@"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
|
||||
|
|
@ -3487,7 +3487,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
|||
switch (builtin.os.tag) {
|
||||
.windows => {},
|
||||
|
||||
// Fast-path if we are Linux and have no args.
|
||||
// Fast-path if we are Linux/BSD and have no args.
|
||||
.linux, .freebsd => if (std.os.argv.len <= 1) return,
|
||||
|
||||
// Everything else we have to at least try because it may
|
||||
|
|
@ -3917,10 +3917,9 @@ 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();
|
||||
}
|
||||
// Used for a variety of defaults. See the function docs as well the
|
||||
// specific variable use sites for more details.
|
||||
const probable_cli = probableCliEnvironment();
|
||||
|
||||
// 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
|
||||
|
|
@ -3946,12 +3945,14 @@ pub fn finalize(self: *Config) !void {
|
|||
}
|
||||
|
||||
// The default for the working directory depends on the system.
|
||||
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 elsewhere.
|
||||
.desktop => "home",
|
||||
.cli, .dbus, .systemd => "inherit",
|
||||
};
|
||||
const wd = self.@"working-directory" orelse if (probable_cli)
|
||||
// From the CLI, we want to inherit where we were launched from.
|
||||
"inherit"
|
||||
else
|
||||
// Otherwise we typically just want the home directory because
|
||||
// our pwd is probably a runtime state dir or root or something
|
||||
// (launchers and desktop environments typically do this).
|
||||
"home";
|
||||
|
||||
// If we are missing either a command or home directory, we need
|
||||
// to look up defaults which is kind of expensive. We only do this
|
||||
|
|
@ -3971,12 +3972,9 @@ pub fn finalize(self: *Config) !void {
|
|||
if (internal_os.isFlatpak()) break :shell_env;
|
||||
|
||||
// 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.
|
||||
switch (self.@"launched-from".?) {
|
||||
.desktop, .dbus, .systemd => break :shell_env,
|
||||
.cli => {},
|
||||
}
|
||||
// will represent our SHELL at login time. We only want to
|
||||
// read from SHELL if we're in a probable CLI environment.
|
||||
if (!probable_cli) break :shell_env;
|
||||
|
||||
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| {
|
||||
log.info("default shell source=env value={s}", .{value});
|
||||
|
|
@ -4029,6 +4027,23 @@ pub fn finalize(self: *Config) !void {
|
|||
}
|
||||
}
|
||||
|
||||
// Apprt-specific defaults
|
||||
switch (build_config.app_runtime) {
|
||||
.none => {},
|
||||
.gtk => {
|
||||
switch (self.@"gtk-single-instance") {
|
||||
.true, .false => {},
|
||||
|
||||
// For detection, we assume single instance unless we're
|
||||
// in a CLI environment, then we disable single instance.
|
||||
.detect => self.@"gtk-single-instance" = if (probable_cli)
|
||||
.false
|
||||
else
|
||||
.true,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// If we have the special value "inherit" then set it to null which
|
||||
// does the same. In the future we should change to a tagged union.
|
||||
if (std.mem.eql(u8, wd, "inherit")) self.@"working-directory" = null;
|
||||
|
|
@ -4156,6 +4171,23 @@ fn compatGtkTabsLocation(
|
|||
return false;
|
||||
}
|
||||
|
||||
fn compatGtkSingleInstance(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value: ?[]const u8,
|
||||
) bool {
|
||||
_ = alloc;
|
||||
assert(std.mem.eql(u8, key, "gtk-single-instance"));
|
||||
|
||||
if (std.mem.eql(u8, value orelse "", "desktop")) {
|
||||
self.@"gtk-single-instance" = .detect;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn compatCursorInvertFgBg(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
|
|
@ -4493,6 +4525,32 @@ fn equalField(comptime T: type, old: T, new: T) bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// This runs a heuristic to determine if we are likely running
|
||||
/// Ghostty in a CLI environment. We need this to change some behaviors.
|
||||
/// We should keep the set of behaviors that depend on this as small
|
||||
/// as possible because magic sucks, but each place is well documented.
|
||||
fn probableCliEnvironment() bool {
|
||||
switch (builtin.os.tag) {
|
||||
// Windows has its own problems, just ignore it for now since
|
||||
// its not a real supported target and GTK via WSL2 assuming
|
||||
// single instance is probably fine.
|
||||
.windows => return false,
|
||||
else => {},
|
||||
}
|
||||
|
||||
// If we have TERM_PROGRAM set to a non-empty value, we assume
|
||||
// a graphical terminal environment.
|
||||
if (std.posix.getenv("TERM_PROGRAM")) |v| {
|
||||
if (v.len > 0) return true;
|
||||
}
|
||||
|
||||
// CLI arguments makes things probable
|
||||
if (std.os.argv.len > 1) return true;
|
||||
|
||||
// Unlikely CLI environment
|
||||
return false;
|
||||
}
|
||||
|
||||
/// This is used to "replay" the configuration. See loadTheme for details.
|
||||
const Replay = struct {
|
||||
const Step = union(enum) {
|
||||
|
|
@ -7125,9 +7183,11 @@ pub const MacShortcuts = enum {
|
|||
|
||||
/// See gtk-single-instance
|
||||
pub const GtkSingleInstance = enum {
|
||||
desktop,
|
||||
false,
|
||||
true,
|
||||
detect,
|
||||
|
||||
pub const default: GtkSingleInstance = .detect;
|
||||
};
|
||||
|
||||
/// See gtk-tabs-location
|
||||
|
|
@ -8019,34 +8079,6 @@ 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();
|
||||
|
||||
|
|
@ -8711,6 +8743,27 @@ test "theme specifying light/dark sets theme usage in conditional state" {
|
|||
}
|
||||
}
|
||||
|
||||
test "compatibility: gtk-single-instance desktop" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var cfg = try Config.default(alloc);
|
||||
defer cfg.deinit();
|
||||
var it: TestIterator = .{ .data = &.{
|
||||
"--gtk-single-instance=desktop",
|
||||
} };
|
||||
try cfg.loadIter(alloc, &it);
|
||||
|
||||
// We need to test this BEFORE finalize, because finalize will
|
||||
// convert our detect to a real value.
|
||||
try testing.expectEqual(
|
||||
GtkSingleInstance.detect,
|
||||
cfg.@"gtk-single-instance",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "compatibility: removed cursor-invert-fg-bg" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
|
|||
Loading…
Reference in New Issue