mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
apprt/gtk-ng: clean up our single instance, new window interactions
This removes `launched-from` entirely and moves our `gtk-single-instance` detection logic to assume true unless we detect CLI instead of assume false unless we detect desktop/dbus/systemd. The "assume true" scenario for single instance is desirable because detecting a CLI instance is much more reliable. Removing `launched-from` fixes an issue where we had a difficult-to-understand relationship between `launched-from`, `gtk-single-instance`, and `initial-window`. Now, only `gtk-single-instance` has some hueristic logic. And `initial-window` ALWAYS sends a GTK activation signal regardless of single instance or not. As a result, we need to be explicit in our systemd, dbus, desktop files about what we want Ghostty to do, but everything works as you'd mostly expect. Now, if you put plain old `ghostty` in your terminal, you get a new Ghostty instance. If you put it anywhere else, you get a GTK single instance activation call (either creates a first instance or opens a new window in the existing instance). Works for launchers and so on.
This commit is contained in:

committed by
Jeffrey C. Ollie

parent
d10e474860
commit
587f47a587
4
dist/linux/app.desktop.in
vendored
4
dist/linux/app.desktop.in
vendored
@@ -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
|
||||
|
2
dist/linux/dbus.service.flatpak.in
vendored
2
dist/linux/dbus.service.flatpak.in
vendored
@@ -1,3 +1,3 @@
|
||||
[D-BUS Service]
|
||||
Name=@APPID@
|
||||
Exec=@GHOSTTY@ --launched-from=dbus
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
|
||||
|
2
dist/linux/dbus.service.in
vendored
2
dist/linux/dbus.service.in
vendored
@@ -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
|
||||
|
2
dist/linux/systemd.service.in
vendored
2
dist/linux/systemd.service.in
vendored
@@ -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;
|
||||
|
@@ -416,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();
|
||||
@@ -426,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",
|
||||
|
@@ -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,16 +2979,23 @@ else
|
||||
///
|
||||
/// If `false`, each new ghostty process will launch a separate application.
|
||||
///
|
||||
/// If `detect`, Ghostty will act as if it was `true` if one of the following
|
||||
/// conditions is true:
|
||||
/// If `detect`, Ghostty will assume true (single instance) unless one of
|
||||
/// the following scenarios is found:
|
||||
///
|
||||
/// 1. If no CLI arguments have been set.
|
||||
/// 2. If `--launched-from` has been set to `desktop`, `dbus`, or `systemd`.
|
||||
/// 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.
|
||||
///
|
||||
/// Otherwise, Ghostty will act as if it was `false`.
|
||||
/// 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.
|
||||
///
|
||||
/// The pre-1.2 option `desktop` has been deprecated. If encountered it will be
|
||||
/// treated as `detect`.
|
||||
/// 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`.
|
||||
///
|
||||
@@ -3112,23 +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.
|
||||
///
|
||||
/// Specific details about the available values are documented on LaunchSource
|
||||
/// 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 = .default,
|
||||
|
||||
/// Configures the low-level API to use for async IO, eventing, etc.
|
||||
///
|
||||
/// Most users should leave this set to `auto`. This will automatically detect
|
||||
@@ -3493,24 +3487,8 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
||||
switch (builtin.os.tag) {
|
||||
.windows => {},
|
||||
|
||||
// Fast-path if we are Linux and have no args.
|
||||
.linux, .freebsd => {
|
||||
if (std.os.argv.len <= 1) {
|
||||
if (self.@"gtk-single-instance" == .detect) {
|
||||
const arena_alloc = self._arena.?.allocator();
|
||||
// Add an artificial replay step so that replaying the
|
||||
// inputs doesn't undo this change.
|
||||
try self._replay_steps.append(
|
||||
arena_alloc,
|
||||
.{
|
||||
.arg = "--gtk-single-instance=true",
|
||||
},
|
||||
);
|
||||
self.@"gtk-single-instance" = .true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
// 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
|
||||
// not use std.os.argv.
|
||||
@@ -3606,34 +3584,6 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
||||
// directory.
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
try self.expandPaths(try std.fs.cwd().realpath(".", &buf));
|
||||
|
||||
if (self.@"gtk-single-instance" == .detect) {
|
||||
const arena_alloc = self._arena.?.allocator();
|
||||
switch (self.@"launched-from") {
|
||||
.cli => {
|
||||
// Add an artificial replay step so that replaying the
|
||||
// inputs doesn't undo this change.
|
||||
try self._replay_steps.append(
|
||||
arena_alloc,
|
||||
.{
|
||||
.arg = "--gtk-single-instance=false",
|
||||
},
|
||||
);
|
||||
self.@"gtk-single-instance" = .false;
|
||||
},
|
||||
.desktop, .systemd, .dbus => {
|
||||
// Add an artificial replay step so that replaying the
|
||||
// inputs doesn't undo this change.
|
||||
try self._replay_steps.append(
|
||||
arena_alloc,
|
||||
.{
|
||||
.arg = "--gtk-single-instance=true",
|
||||
},
|
||||
);
|
||||
self.@"gtk-single-instance" = .true;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load and parse the config files that were added in the "config-file" key.
|
||||
@@ -3967,6 +3917,10 @@ pub fn finalize(self: *Config) !void {
|
||||
|
||||
const alloc = self._arena.?.allocator();
|
||||
|
||||
// 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
|
||||
// --font-family=foo, then we try to get the stylized versions of
|
||||
@@ -3991,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
|
||||
@@ -4016,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});
|
||||
@@ -4074,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;
|
||||
@@ -4201,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,
|
||||
@@ -4538,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) {
|
||||
@@ -7175,18 +7188,6 @@ pub const GtkSingleInstance = enum {
|
||||
detect,
|
||||
|
||||
pub const default: GtkSingleInstance = .detect;
|
||||
|
||||
pub fn parseCLI(input_: ?[]const u8) error{ ValueRequired, InvalidValue }!GtkSingleInstance {
|
||||
const input = std.mem.trim(
|
||||
u8,
|
||||
input_ orelse return error.ValueRequired,
|
||||
cli.args.whitespace,
|
||||
);
|
||||
|
||||
if (std.mem.eql(u8, input, "desktop")) return .detect;
|
||||
|
||||
return std.meta.stringToEnum(GtkSingleInstance, input) orelse error.InvalidValue;
|
||||
}
|
||||
};
|
||||
|
||||
/// See gtk-tabs-location
|
||||
@@ -8078,30 +8079,6 @@ pub const Duration = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const LaunchSource = enum {
|
||||
/// Ghostty was launched via the CLI. This is the default on non-macOS
|
||||
/// platforms.
|
||||
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.
|
||||
///
|
||||
/// This is the default on macOS.
|
||||
desktop,
|
||||
|
||||
/// Ghostty was started via dbus activation.
|
||||
dbus,
|
||||
|
||||
/// Ghostty was started via systemd unit.
|
||||
systemd,
|
||||
|
||||
pub const default: LaunchSource = switch (builtin.os.tag) {
|
||||
.macos => .desktop,
|
||||
else => .cli,
|
||||
};
|
||||
};
|
||||
|
||||
pub const WindowPadding = struct {
|
||||
const Self = @This();
|
||||
|
||||
@@ -8766,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;
|
||||
|
Reference in New Issue
Block a user