mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
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:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user