diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 0b630b6e6..5b9d3bf2e 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -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, }); diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 7180ec5f1..ca7437126 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -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, }); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index cc7726617..b5e8b0b2c 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -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"); +} diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index 8d97c4608..204be58f1 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -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) { diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 287f4f44c..e2fe6403c 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -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, }, diff --git a/src/cli/new_tab.zig b/src/cli/new_tab.zig index a2dc2cd02..31176c9bb 100644 --- a/src/cli/new_tab.zig +++ b/src/cli/new_tab.zig @@ -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=`, 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=`: Whether to enable shell integration and +/// which shell to use. See the main configuration documentation for +/// supported values. +/// /// * `--working-directory=`: The working directory to pass to Ghostty. /// /// * `--title`: A title that will override the title of the first surface in diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index bda8bd6cc..29115a2e1 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -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=`, 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=`: Whether to enable shell integration and +/// which shell to use. See the main configuration documentation for +/// supported values. +/// /// * `--working-directory=`: The working directory to pass to Ghostty. /// /// * `--title`: A title that will override the title of the first surface in