gtk: Fix +new-window -e <command> inheriting forced shell integration from the running GTK application. Fixes #12378. (#13741)

## Summary 
Fix `+new-window -e <command>` inheriting forced shell integration from
the running GTK application.

Arguments after `-e` are now marked as an explicit command that requires
shell detection. That marker is propagated with the existing command
override to the new surface for both the current `+new-window` and
`+new-tab` GTK paths. When the override is applied, forced shell
integration becomes `detect`; an explicit `none` remains disabled.

This is scoped to the GTK `-e` path. It does not change the generic
forced shell-integration behavior, so configured integration continues
to support shell executables with non-standard names.

Fixes #12378.

## Root cause

The GTK IPC path replaced `config.command` for the new surface but
retained the running application's `shell-integration` value. `termio`
therefore treated an arbitrary explicit command such as Vim as the
forced shell and appended shell-specific arguments.

## AI usage

I used Hermes Agent (with ChatGPT 5.6) to help inspect the codebase,
trace the GTK command path, and draft parts of the implementation and
test. I manually reviewed and edited the changes, validated the behavior
and test results, and understand the affected code paths.
This commit is contained in:
Jeffrey C. Ollie
2026-08-12 20:03:26 -05:00
committed by GitHub
7 changed files with 118 additions and 11 deletions

View File

@@ -12,6 +12,7 @@ const gtk = @import("gtk");
const build_config = @import("../../../build_config.zig");
const build_info = @import("../build/info.zig");
const cli = @import("../../../cli.zig");
const global = @import("../../../global.zig");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
@@ -1855,6 +1856,7 @@ pub const Application = extern struct {
null,
if (overrides) |o| .{
.command = o.command,
.shell_integration = o.shell_integration,
.working_directory = o.working_directory,
.title = o.title,
} else .none,
@@ -1935,6 +1937,7 @@ pub const Application = extern struct {
},
.{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
@@ -1947,6 +1950,7 @@ pub const Application = extern struct {
null,
.{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
@@ -1958,6 +1962,7 @@ pub const Application = extern struct {
fn parseOverrides(arena_alloc: Allocator, arguments_it: *glib.VariantIter) (Allocator.Error || error{ValueRequired})!struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
} {
@@ -1966,6 +1971,9 @@ pub const Application = extern struct {
var working_directory: ?[:0]const u8 = null;
var title: ?[:0]const u8 = null;
var command: ?configpkg.Command = null;
var parsed_shell_integration: struct {
@"shell-integration": ?configpkg.Config.ShellIntegration = null,
} = .{};
const s_variant_type = glib.VariantType.new("s");
defer s_variant_type.free();
@@ -2011,6 +2019,19 @@ pub const Application = extern struct {
command = cmd;
continue;
}
if (std.mem.cutPrefix(u8, str, "--shell-integration=")) |v| {
cli.args.parseIntoField(
@TypeOf(parsed_shell_integration),
arena_alloc,
&parsed_shell_integration,
"shell-integration",
std.mem.trim(u8, v, &std.ascii.whitespace),
) catch |err| {
log.warn("unable to parse shell integration {s}: {t}", .{ v, err });
continue;
};
continue;
}
if (std.mem.cutPrefix(u8, str, "--working-directory=")) |v| {
working_directory = arena_alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| {
log.warn("unable to duplicate working directory: {t}", .{err});
@@ -2035,6 +2056,7 @@ pub const Application = extern struct {
return .{
.command = command,
.shell_integration = parsed_shell_integration.@"shell-integration",
.working_directory = working_directory,
.title = title,
};
@@ -2636,6 +2658,7 @@ const Action = struct {
target: apprt.Target,
overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -2662,6 +2685,7 @@ const Action = struct {
};
window.newTab(core, .{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
});
@@ -2675,6 +2699,7 @@ const Action = struct {
parent: ?*CoreSurface,
overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -2697,6 +2722,7 @@ const Action = struct {
parent,
.{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
@@ -2709,6 +2735,7 @@ const Action = struct {
parent: ?*CoreSurface,
overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -2729,6 +2756,7 @@ const Action = struct {
// Create a new tab with window context (first tab in new window)
win.newTabForWindow(parent, .{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
});

View File

@@ -214,6 +214,7 @@ pub const SplitTree = extern struct {
parent_: ?*Surface,
overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -225,6 +226,7 @@ pub const SplitTree = extern struct {
// Create our new surface.
const surface: *Surface = .new(.{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
});

View File

@@ -736,6 +736,7 @@ pub const Surface = extern struct {
overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
pub const none: @This() = .{};
@@ -746,6 +747,7 @@ pub const Surface = extern struct {
pub fn new(overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -758,6 +760,7 @@ pub const Surface = extern struct {
const priv: *Private = self.private();
priv.overrides = .{
.command = if (overrides.command) |c| c.clone(alloc) catch null else null,
.shell_integration = overrides.shell_integration,
.working_directory = if (overrides.working_directory) |wd| alloc.dupeZ(u8, wd) catch null else null,
};
return self;
@@ -3527,9 +3530,11 @@ pub const Surface = extern struct {
);
defer config.deinit();
if (priv.overrides.command) |c| {
config.command = try c.clone(config._arena.?.allocator());
}
try applyCommandOverrides(
&config,
priv.overrides.command,
priv.overrides.shell_integration,
);
if (priv.overrides.working_directory) |wd| {
const config_alloc = config.arenaAlloc();
var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, wd) };
@@ -4393,3 +4398,57 @@ test "computeFraction" {
try std.testing.expectEqual(0.0, computeFraction(0));
try std.testing.expectEqual(0.5, computeFraction(50));
}
/// Apply command and shell integration overrides received from the CLI.
/// Explicit commands should only receive shell integration when their
/// executable can be detected as a supported shell. An explicit shell
/// integration override is also valid without a command.
fn applyCommandOverrides(
config: *configpkg.Config,
command: ?configpkg.Command,
shell_integration: ?configpkg.Config.ShellIntegration,
) Allocator.Error!void {
if (command) |value| {
config.command = try value.clone(config.arenaAlloc());
if (shell_integration) |integration| {
config.@"shell-integration" = integration;
} else if (config.@"shell-integration" != .none) {
config.@"shell-integration" = .detect;
}
} else if (shell_integration) |value| {
config.@"shell-integration" = value;
}
}
test "command and shell integration overrides" {
const testing = std.testing;
var config = try configpkg.Config.default(testing.allocator);
defer config.deinit();
config.@"shell-integration" = .nushell;
try applyCommandOverrides(&config, .{ .shell = "vim" }, null);
try testing.expectEqual(.detect, config.@"shell-integration");
config.@"shell-integration" = .none;
try applyCommandOverrides(&config, .{ .shell = "vim" }, null);
try testing.expectEqual(.none, config.@"shell-integration");
try applyCommandOverrides(&config, .{ .shell = "nu" }, .nushell);
try testing.expectEqual(.nushell, config.@"shell-integration");
try applyCommandOverrides(&config, .{ .shell = "vim" }, .none);
try testing.expectEqual(.none, config.@"shell-integration");
config.@"shell-integration" = .nushell;
try applyCommandOverrides(&config, null, null);
try testing.expectEqual(.nushell, config.@"shell-integration");
config.@"shell-integration" = .none;
try applyCommandOverrides(&config, null, .nushell);
try testing.expectEqual(.nushell, config.@"shell-integration");
try applyCommandOverrides(&config, null, .none);
try testing.expectEqual(.none, config.@"shell-integration");
}

View File

@@ -189,6 +189,7 @@ pub const Tab = extern struct {
pub fn new(config: ?*Config, overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -212,6 +213,7 @@ pub const Tab = extern struct {
// Create our initial surface in the split tree.
priv.split_tree.newSplit(.right, null, .{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
}) catch |err| switch (err) {

View File

@@ -410,6 +410,7 @@ pub const Window = extern struct {
/// The new tab will be selected.
pub fn newTab(self: *Self, parent_: ?*CoreSurface, overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -417,6 +418,7 @@ pub const Window = extern struct {
}) void {
_ = self.newTabPage(parent_, .tab, .{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
});
@@ -427,6 +429,7 @@ pub const Window = extern struct {
parent_: ?*CoreSurface,
overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -438,6 +441,7 @@ pub const Window = extern struct {
.window,
.{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
@@ -450,6 +454,7 @@ pub const Window = extern struct {
context: apprt.surface.NewSurfaceContext,
overrides: struct {
command: ?configpkg.Command = null,
shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -464,6 +469,7 @@ pub const Window = extern struct {
priv.config,
.{
.command = overrides.command,
.shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
},

View File

@@ -120,10 +120,11 @@ pub const Options = struct {
/// and `--surface-id` flag) will be sent to the remote Ghostty instance and
/// will be parsed as command line flags. These flags will override certain
/// settings when creating the first surface in the new tab. Currently, only
/// `--working-directory`, `--command`, and `--title` are supported. `-e` will
/// also work as an alias for `--command`, except that if `-e` is found on the
/// command line all following arguments will become part of the command and no
/// more arguments will be parsed for configuration settings.
/// `--working-directory`, `--command`, `--shell-integration=<mode>`, and
/// `--title` are supported. `-e` will also work as an alias for `--command`, except that if
/// `-e` is found on the command line all following arguments will become part
/// of the command and no more arguments will be parsed for configuration
/// settings.
///
/// If `--working-directory` is found on the command line and is a relative
/// path (i.e. doesn't start with `/`) it will be resolved to an absolute
@@ -164,6 +165,10 @@ pub const Options = struct {
///
/// * `--command`: The command to be executed in the first surface of the new tab.
///
/// * `--shell-integration=<mode>`: Whether to enable shell integration and
/// which shell to use. See the main configuration documentation for
/// supported values.
///
/// * `--working-directory=<directory>`: The working directory to pass to Ghostty.
///
/// * `--title`: A title that will override the title of the first surface in

View File

@@ -119,10 +119,11 @@ pub const Options = struct {
/// `--class` flag) will be sent to the remote Ghostty instance and will be
/// parsed as command line flags. These flags will override certain settings
/// when creating the first surface in the new window. Currently, only
/// `--working-directory`, `--command`, and `--title` are supported. `-e` will
/// also work as an alias for `--command`, except that if `-e` is found on the
/// command line all following arguments will become part of the command and no
/// more arguments will be parsed for configuration settings.
/// `--working-directory`, `--command`, `--shell-integration=<mode>`, and
/// `--title` are supported. `-e` will also work as an alias for `--command`, except that if
/// `-e` is found on the command line all following arguments will become part
/// of the command and no more arguments will be parsed for configuration
/// settings.
///
/// If `--working-directory` is found on the command line and is a relative
/// path (i.e. doesn't start with `/`) it will be resolved to an absolute path
@@ -158,6 +159,10 @@ pub const Options = struct {
///
/// * `--command`: The command to be executed in the first surface of the new window.
///
/// * `--shell-integration=<mode>`: Whether to enable shell integration and
/// which shell to use. See the main configuration documentation for
/// supported values.
///
/// * `--working-directory=<directory>`: The working directory to pass to Ghostty.
///
/// * `--title`: A title that will override the title of the first surface in