diff --git a/dist/linux/app.desktop.in b/dist/linux/app.desktop.in index 32ba00cfd..e05c47b6e 100644 --- a/dist/linux/app.desktop.in +++ b/dist/linux/app.desktop.in @@ -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 diff --git a/dist/linux/dbus.service.flatpak.in b/dist/linux/dbus.service.flatpak.in index 213cda78f..873f8dcf1 100644 --- a/dist/linux/dbus.service.flatpak.in +++ b/dist/linux/dbus.service.flatpak.in @@ -1,3 +1,3 @@ [D-BUS Service] Name=@APPID@ -Exec=@GHOSTTY@ --launched-from=dbus +Exec=@GHOSTTY@ --gtk-single-instance=true --initial-window=false diff --git a/dist/linux/dbus.service.in b/dist/linux/dbus.service.in index df31a1abd..8758a34a2 100644 --- a/dist/linux/dbus.service.in +++ b/dist/linux/dbus.service.in @@ -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 diff --git a/dist/linux/systemd.service.in b/dist/linux/systemd.service.in index 76ccdd3f4..17589f00f 100644 --- a/dist/linux/systemd.service.in +++ b/dist/linux/systemd.service.in @@ -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 diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 5f43e1659..08d8291ef 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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; diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 22fe3f618..5f87613cd 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -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", diff --git a/src/cli/args.zig b/src/cli/args.zig index 0ff3dc047..4db0a29a2 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -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)) { diff --git a/src/config/Config.zig b/src/config/Config.zig index f9d8fcf7e..221a7cf93 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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;