diff --git a/src/config/Config.zig b/src/config/Config.zig index 66b8c6057..1b1fd57ea 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2807,7 +2807,10 @@ keybind: Keybinds = .{}, /// /// * `detect` - Detect the shell based on the filename. /// -/// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell injection scheme. +/// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell +/// injection scheme if the command is this shell. Has no effect if the +/// command is not a shell, or is a different shell from the one specified +/// here. /// /// The default value is `detect`. @"shell-integration": ShellIntegration = .detect, diff --git a/src/config/command.zig b/src/config/command.zig index 7cd70acb3..87f3da6aa 100644 --- a/src/config/command.zig +++ b/src/config/command.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const builtin = @import("builtin"); + const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const formatterpkg = @import("formatter.zig"); @@ -197,6 +199,87 @@ pub const Command = union(enum) { } } + /// Shell types we support + pub const Shell = enum { + bash, + elvish, + fish, + nushell, + zsh, + }; + + /// Detect if this command is a known shell. + pub fn detectShell(self: Self) ?Shell { + var buf: [std.fs.max_path_bytes]u8 = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buf); + + const arg0 = switch (self) { + .direct => |v| v[0], + .shell => |v| arg: { + var it = std.process.ArgIteratorGeneral(.{}).init(fba.allocator(), v) catch return null; + break :arg it.next() orelse return null; + }, + }; + + const exe = std.fs.path.basename(arg0); + + if (std.mem.eql(u8, "bash", exe)) { + // Apple distributes their own patched version of Bash 3.2 + // on macOS that disables the ENV-based POSIX startup path. + // This means we're unable to perform our automatic shell + // integration sequence in this specific environment. + // + // If we're running "/bin/bash" on Darwin, we can assume + // we're using Apple's Bash because /bin is non-writable + // on modern macOS due to System Integrity Protection. + if (comptime builtin.target.os.tag.isDarwin()) { + if (std.mem.eql(u8, "/bin/bash", arg0)) { + return null; + } + } + return .bash; + } + + if (std.mem.eql(u8, "elvish", exe)) return .elvish; + if (std.mem.eql(u8, "fish", exe)) return .fish; + if (std.mem.eql(u8, "nu", exe)) return .nushell; + if (std.mem.eql(u8, "zsh", exe)) return .zsh; + + return null; + } + + test detectShell { + const testing = std.testing; + + try testing.expect(detectShell(.{ .shell = "sh" }) == null); + try testing.expectEqual(.bash, detectShell(.{ .shell = "bash" })); + try testing.expectEqual(.elvish, detectShell(.{ .shell = "elvish" })); + try testing.expectEqual(.fish, detectShell(.{ .shell = "fish" })); + try testing.expectEqual(.nushell, detectShell(.{ .shell = "nu" })); + try testing.expectEqual(.zsh, detectShell(.{ .shell = "zsh" })); + + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expect(detectShell(.{ .shell = "/bin/bash" }) == null); + } + + try testing.expectEqual(.bash, detectShell(.{ .shell = "bash -c 'command'" })); + try testing.expectEqual(.bash, detectShell(.{ .shell = "\"/a b/bash\"" })); + + try testing.expect(detectShell(.{ .direct = &.{"sh"} }) == null); + try testing.expectEqual(.bash, detectShell(.{ .direct = &.{"bash"} })); + try testing.expectEqual(.elvish, detectShell(.{ .direct = &.{"elvish"} })); + try testing.expectEqual(.fish, detectShell(.{ .direct = &.{"fish"} })); + try testing.expectEqual(.nushell, detectShell(.{ .direct = &.{"nu"} })); + try testing.expectEqual(.zsh, detectShell(.{ .direct = &.{"zsh"} })); + + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expect(detectShell(.{&.{ .direct = "/bin/bash" }}) == null); + } + + try testing.expectEqual(.bash, detectShell(.{ .direct = &.{ "bash", "-c", "command" } })); + try testing.expectEqual(.bash, detectShell(.{ .direct = &.{"/a b/bash"} })); + } + test "Command: parseCLI errors" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 2dd09ee29..2d74db6f9 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -10,13 +10,7 @@ const internal_os = @import("../os/main.zig"); const log = std.log.scoped(.shell_integration); /// Shell types we support -pub const Shell = enum { - bash, - elvish, - fish, - nushell, - zsh, -}; +pub const Shell = config.Command.Shell; /// The result of setting up a shell integration. pub const ShellIntegration = struct { @@ -46,10 +40,21 @@ pub fn setup( env: *EnvMap, force_shell: ?Shell, ) !?ShellIntegration { + const actual_shell = command.detectShell(); const shell: Shell = force_shell orelse - try detectShell(alloc_arena, command) orelse + actual_shell orelse return null; + // Don't do any shell integration if we're not actually running the shell. + // This prevents problems when the command is overridden by `+new-window` + // or some other mechanism and `shell-integration` is forced to a specific + // shell rather than being detected. + // + // This means that `shell-integration=` has no effect if we detect + // that the command is not a shell, or is not the shell specified by + // `shell-integration`. + if (shell != actual_shell) return null; + const new_command: config.Command = switch (shell) { .bash => try setupBash( alloc_arena, @@ -107,7 +112,7 @@ test "force shell" { &env, shell, ); - try testing.expectEqual(shell, result.?.shell); + try testing.expect(result == null); } } @@ -133,57 +138,6 @@ test "shell integration failure" { try testing.expectEqual(0, env.count()); } -fn detectShell(alloc: Allocator, command: config.Command) !?Shell { - var arg_iter = try command.argIterator(alloc); - defer arg_iter.deinit(); - - const arg0 = arg_iter.next() orelse return null; - const exe = std.fs.path.basename(arg0); - - if (std.mem.eql(u8, "bash", exe)) { - // Apple distributes their own patched version of Bash 3.2 - // on macOS that disables the ENV-based POSIX startup path. - // This means we're unable to perform our automatic shell - // integration sequence in this specific environment. - // - // If we're running "/bin/bash" on Darwin, we can assume - // we're using Apple's Bash because /bin is non-writable - // on modern macOS due to System Integrity Protection. - if (comptime builtin.target.os.tag.isDarwin()) { - if (std.mem.eql(u8, "/bin/bash", arg0)) { - return null; - } - } - return .bash; - } - - if (std.mem.eql(u8, "elvish", exe)) return .elvish; - if (std.mem.eql(u8, "fish", exe)) return .fish; - if (std.mem.eql(u8, "nu", exe)) return .nushell; - if (std.mem.eql(u8, "zsh", exe)) return .zsh; - - return null; -} - -test detectShell { - const testing = std.testing; - const alloc = testing.allocator; - - try testing.expect(try detectShell(alloc, .{ .shell = "sh" }) == null); - try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash" })); - try testing.expectEqual(.elvish, try detectShell(alloc, .{ .shell = "elvish" })); - try testing.expectEqual(.fish, try detectShell(alloc, .{ .shell = "fish" })); - try testing.expectEqual(.nushell, try detectShell(alloc, .{ .shell = "nu" })); - try testing.expectEqual(.zsh, try detectShell(alloc, .{ .shell = "zsh" })); - - if (comptime builtin.target.os.tag.isDarwin()) { - try testing.expect(try detectShell(alloc, .{ .shell = "/bin/bash" }) == null); - } - - try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash -c 'command'" })); - try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "\"/a b/bash\"" })); -} - /// Set up the shell integration features environment variable. pub fn setupFeatures( env: *EnvMap,