feat: add SSH integration wrapper for shell integration

- Implements opt-in SSH wrapper following sudo pattern
- Supports term_only, basic, and full integration levels
- Fixes xterm-ghostty TERM compatibility on remote systems
- Propagates shell integration environment variables
- Allows for automatic installation of terminfo if desired
- Addresses GitHub discussions #5892 and #4156
This commit is contained in:
Jason Rayne
2025-06-13 16:11:46 -07:00
parent fa47db5363
commit 142e07c502
9 changed files with 496 additions and 372 deletions

View File

@ -545,6 +545,7 @@ pub fn init(
.env_override = config.env, .env_override = config.env,
.shell_integration = config.@"shell-integration", .shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features", .shell_integration_features = config.@"shell-integration-features",
.ssh_integration = config.@"ssh-integration",
.working_directory = config.@"working-directory", .working_directory = config.@"working-directory",
.resources_dir = global_state.resources_dir.host(), .resources_dir = global_state.resources_dir.host(),
.term = config.term, .term = config.term,

View File

@ -33,6 +33,7 @@ pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
pub const RepeatablePath = Config.RepeatablePath; pub const RepeatablePath = Config.RepeatablePath;
pub const Path = Config.Path; pub const Path = Config.Path;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const SSHIntegration = Config.SSHIntegration;
pub const WindowPaddingColor = Config.WindowPaddingColor; pub const WindowPaddingColor = Config.WindowPaddingColor;
pub const BackgroundImagePosition = Config.BackgroundImagePosition; pub const BackgroundImagePosition = Config.BackgroundImagePosition;
pub const BackgroundImageFit = Config.BackgroundImageFit; pub const BackgroundImageFit = Config.BackgroundImageFit;

View File

@ -466,93 +466,6 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// Background image for the terminal.
///
/// This should be a path to a PNG or JPEG file, other image formats are
/// not yet supported.
///
/// The background image is currently per-terminal, not per-window. If
/// you are a heavy split user, the background image will be repeated across
/// splits. A future improvement to Ghostty will address this.
///
/// WARNING: Background images are currently duplicated in VRAM per-terminal.
/// For sufficiently large images, this could lead to a large increase in
/// memory usage (specifically VRAM usage). A future Ghostty improvement
/// will resolve this by sharing image textures across terminals.
@"background-image": ?Path = null,
/// Background image opacity.
///
/// This is relative to the value of `background-opacity`.
///
/// A value of `1.0` (the default) will result in the background image being
/// placed on top of the general background color, and then the combined result
/// will be adjusted to the opacity specified by `background-opacity`.
///
/// A value less than `1.0` will result in the background image being mixed
/// with the general background color before the combined result is adjusted
/// to the configured `background-opacity`.
///
/// A value greater than `1.0` will result in the background image having a
/// higher opacity than the general background color. For instance, if the
/// configured `background-opacity` is `0.5` and `background-image-opacity`
/// is set to `1.5`, then the final opacity of the background image will be
/// `0.5 * 1.5 = 0.75`.
@"background-image-opacity": f32 = 1.0,
/// Background image position.
///
/// Valid values are:
/// * `top-left`
/// * `top-center`
/// * `top-right`
/// * `center-left`
/// * `center`
/// * `center-right`
/// * `bottom-left`
/// * `bottom-center`
/// * `bottom-right`
///
/// The default value is `center`.
@"background-image-position": BackgroundImagePosition = .center,
/// Background image fit.
///
/// Valid values are:
///
/// * `contain`
///
/// Preserving the aspect ratio, scale the background image to the largest
/// size that can still be contained within the terminal, so that the whole
/// image is visible.
///
/// * `cover`
///
/// Preserving the aspect ratio, scale the background image to the smallest
/// size that can completely cover the terminal. This may result in one or
/// more edges of the image being clipped by the edge of the terminal.
///
/// * `stretch`
///
/// Stretch the background image to the full size of the terminal, without
/// preserving the aspect ratio.
///
/// * `none`
///
/// Don't scale the background image.
///
/// The default value is `contain`.
@"background-image-fit": BackgroundImageFit = .contain,
/// Whether to repeat the background image or not.
///
/// If this is set to true, the background image will be repeated if there
/// would otherwise be blank space around it because it doesn't completely
/// fill the terminal area.
///
/// The default value is `false`.
@"background-image-repeat": bool = false,
/// The foreground and background color for selection. If this is not set, then /// The foreground and background color for selection. If this is not set, then
/// the selection color is just the inverted window background and foreground /// the selection color is just the inverted window background and foreground
/// (note: not to be confused with the cell bg/fg). /// (note: not to be confused with the cell bg/fg).
@ -2062,27 +1975,14 @@ keybind: Keybinds = .{},
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` /// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title`
@"shell-integration-features": ShellIntegrationFeatures = .{}, @"shell-integration-features": ShellIntegrationFeatures = .{},
/// Custom entries into the command palette. /// SSH integration level. This controls what level of SSH integration
/// is performed when using the ssh wrapper provided by shell integration.
/// Requires shell integration to be enabled to function.
/// ///
/// Each entry requires the title, the corresponding action, and an optional /// See SSHIntegration for available options.
/// description. Each field should be prefixed with the field name, a colon
/// (`:`), and then the specified value. The syntax for actions is identical
/// to the one for keybind actions. Whitespace in between fields is ignored.
/// ///
/// ```ini /// The default value is `off`.
/// command-palette-entry = title:Reset Font Style, action:csi:0m @"ssh-integration": SSHIntegration = .off,
/// command-palette-entry = title:Crash on Main Thread,description:Causes a crash on the main (UI) thread.,action:crash:main
/// ```
///
/// By default, the command palette is preloaded with most actions that might
/// be useful in an interactive setting yet do not have easily accessible or
/// memorizable shortcuts. The default entries can be cleared by setting this
/// setting to an empty value:
///
/// ```ini
/// command-palette-entry =
/// ```
@"command-palette-entry": RepeatableCommand = .{},
/// Sets the reporting format for OSC sequences that request color information. /// Sets the reporting format for OSC sequences that request color information.
/// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and /// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and
@ -2893,9 +2793,6 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
// Add our default keybindings // Add our default keybindings
try result.keybind.init(alloc); try result.keybind.init(alloc);
// Add our default command palette entries
try result.@"command-palette-entry".init(alloc);
// Add our default link for URL detection // Add our default link for URL detection
try result.link.links.append(alloc, .{ try result.link.links.append(alloc, .{
.regex = url.regex, .regex = url.regex,
@ -3410,15 +3307,6 @@ fn expandPaths(self: *Config, base: []const u8) !void {
&self._diagnostics, &self._diagnostics,
); );
}, },
?RepeatablePath, ?Path => {
if (@field(self, field.name)) |*path| {
try path.expand(
arena_alloc,
base,
&self._diagnostics,
);
}
},
else => {}, else => {},
} }
} }
@ -5083,29 +4971,25 @@ pub const Keybinds = struct {
.{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } },
.{ .close_tab = {} }, .{ .close_tab = {} },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .shift = true } },
.{ .previous_tab = {} }, .{ .previous_tab = {} },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .shift = true } },
.{ .next_tab = {} }, .{ .next_tab = {} },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .page_up }, .mods = .{ .ctrl = true } }, .{ .key = .{ .physical = .page_up }, .mods = .{ .ctrl = true } },
.{ .previous_tab = {} }, .{ .previous_tab = {} },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .page_down }, .mods = .{ .ctrl = true } }, .{ .key = .{ .physical = .page_down }, .mods = .{ .ctrl = true } },
.{ .next_tab = {} }, .{ .next_tab = {} },
.{ .performable = true },
); );
try self.set.put( try self.set.put(
alloc, alloc,
@ -5117,67 +5001,57 @@ pub const Keybinds = struct {
.{ .key = .{ .unicode = 'e' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .{ .unicode = 'e' }, .mods = .{ .ctrl = true, .shift = true } },
.{ .new_split = .down }, .{ .new_split = .down },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } }, .{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } },
.{ .goto_split = .previous }, .{ .goto_split = .previous },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } }, .{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } },
.{ .goto_split = .next }, .{ .goto_split = .next },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true, .alt = true } }, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .up }, .{ .goto_split = .up },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true, .alt = true } }, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .down }, .{ .goto_split = .down },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .alt = true } }, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .left }, .{ .goto_split = .left },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .alt = true } }, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .right }, .{ .goto_split = .right },
.{ .performable = true },
); );
// Resizing splits // Resizing splits
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .up, 10 } }, .{ .resize_split = .{ .up, 10 } },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .down, 10 } }, .{ .resize_split = .{ .down, 10 } },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .left, 10 } }, .{ .resize_split = .{ .left, 10 } },
.{ .performable = true },
); );
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .right, 10 } }, .{ .resize_split = .{ .right, 10 } },
.{ .performable = true },
); );
// Viewport scrolling // Viewport scrolling
@ -5248,24 +5122,22 @@ pub const Keybinds = struct {
const end: u21 = '8'; const end: u21 = '8';
var i: u21 = start; var i: u21 = start;
while (i <= end) : (i += 1) { while (i <= end) : (i += 1) {
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .{
.key = .{ .unicode = i }, .key = .{ .unicode = i },
.mods = mods, .mods = mods,
}, },
.{ .goto_tab = (i - start) + 1 }, .{ .goto_tab = (i - start) + 1 },
.{ .performable = true },
); );
} }
try self.set.putFlags( try self.set.put(
alloc, alloc,
.{ .{
.key = .{ .unicode = '9' }, .key = .{ .unicode = '9' },
.mods = mods, .mods = mods,
}, },
.{ .last_tab = {} }, .{ .last_tab = {} },
.{ .performable = true },
); );
} }
@ -6235,147 +6107,32 @@ pub const ShellIntegrationFeatures = packed struct {
title: bool = true, title: bool = true,
}; };
pub const RepeatableCommand = struct { /// SSH integration levels for shell integration.
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty, /// Controls how much SSH integration is performed when connecting to remote hosts.
///
/// Allowable values are:
///
/// * `off` - No SSH integration, use standard ssh command
///
/// * `term_only` - Only fix TERM compatibility (xterm-ghostty -> xterm-256color)
///
/// * `basic` - TERM fix + environment variable propagation
///
/// * `full` - All features: TERM fix + env vars + terminfo installation + shell integration injection
///
/// The default value is `off`.
pub const SSHIntegration = enum {
off,
term_only,
basic,
full,
pub fn init(self: *RepeatableCommand, alloc: Allocator) !void { pub fn jsonStringify(
self.value = .empty; self: SSHIntegration,
try self.value.appendSlice(alloc, inputpkg.command.defaults); options: std.json.StringifyOptions,
} writer: anytype,
pub fn parseCLI(
self: *RepeatableCommand,
alloc: Allocator,
input_: ?[]const u8,
) !void { ) !void {
// Unset or empty input clears the list try std.json.stringify(@tagName(self), options, writer);
const input = input_ orelse "";
if (input.len == 0) {
self.value.clearRetainingCapacity();
return;
}
const cmd = try cli.args.parseAutoStruct(
inputpkg.Command,
alloc,
input,
);
try self.value.append(alloc, cmd);
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const RepeatableCommand, alloc: Allocator) Allocator.Error!RepeatableCommand {
const value = try self.value.clone(alloc);
for (value.items) |*item| {
item.* = try item.clone(alloc);
}
return .{ .value = value };
}
/// Compare if two of our value are equal. Required by Config.
pub fn equal(self: RepeatableCommand, other: RepeatableCommand) bool {
if (self.value.items.len != other.value.items.len) return false;
for (self.value.items, other.value.items) |a, b| {
if (!a.equal(b)) return false;
}
return true;
}
/// Used by Formatter
pub fn formatEntry(self: RepeatableCommand, formatter: anytype) !void {
if (self.value.items.len == 0) {
try formatter.formatEntry(void, {});
return;
}
var buf: [4096]u8 = undefined;
for (self.value.items) |item| {
const str = if (item.description.len > 0) std.fmt.bufPrint(
&buf,
"title:{s},description:{s},action:{}",
.{ item.title, item.description, item.action },
) else std.fmt.bufPrint(
&buf,
"title:{s},action:{}",
.{ item.title, item.action },
);
try formatter.formatEntry([]const u8, str catch return error.OutOfMemory);
}
}
test "RepeatableCommand parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Foo,action:ignore");
try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle");
try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5");
try testing.expectEqual(@as(usize, 3), list.value.items.len);
try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action);
try testing.expectEqualStrings("Foo", list.value.items[0].title);
try testing.expect(list.value.items[1].action == .text);
try testing.expectEqualStrings("ale bydle", list.value.items[1].action.text);
try testing.expectEqualStrings("Bar", list.value.items[1].title);
try testing.expectEqualStrings("bobr", list.value.items[1].description);
try testing.expectEqual(
inputpkg.Binding.Action{ .increase_font_size = 2.5 },
list.value.items[2].action,
);
try testing.expectEqualStrings("Quux", list.value.items[2].title);
try testing.expectEqualStrings("boo", list.value.items[2].description);
try list.parseCLI(alloc, "");
try testing.expectEqual(@as(usize, 0), list.value.items.len);
}
test "RepeatableCommand formatConfig empty" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var list: RepeatableCommand = .{};
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = \n", buf.items);
}
test "RepeatableCommand formatConfig single item" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Bobr, action:text:Bober");
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:Bober\n", buf.items);
}
test "RepeatableCommand formatConfig multiple items" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Bobr, action:text:kurwa");
try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle");
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:kurwa\na = title:Ja,description:pierdole,action:text:jakie bydle\n", buf.items);
} }
}; };
@ -6834,28 +6591,6 @@ pub const AlphaBlending = enum {
} }
}; };
/// See background-image-position
pub const BackgroundImagePosition = enum {
@"top-left",
@"top-center",
@"top-right",
@"center-left",
@"center-center",
@"center-right",
@"bottom-left",
@"bottom-center",
@"bottom-right",
center,
};
/// See background-image-fit
pub const BackgroundImageFit = enum {
contain,
cover,
stretch,
none,
};
/// See freetype-load-flag /// See freetype-load-flag
pub const FreetypeLoadFlags = packed struct { pub const FreetypeLoadFlags = packed struct {
// The defaults here at the time of writing this match the defaults // The defaults here at the time of writing this match the defaults

View File

@ -15,8 +15,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# We need to be in interactive mode to proceed. # We need to be in interactive mode and we need to have the Ghostty
if [[ "$-" != *i* ]] ; then builtin return; fi # resources dir set which also tells us we're running in Ghostty.
if [[ "$-" != *i* ]]; then builtin return; fi
if [ -z "$GHOSTTY_RESOURCES_DIR" ]; then builtin return; fi
# When automatic shell integration is active, we were started in POSIX # When automatic shell integration is active, we were started in POSIX
# mode and need to manually recreate the bash startup sequence. # mode and need to manually recreate the bash startup sequence.
@ -43,7 +45,10 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then
[ -r /etc/profile ] && builtin source "/etc/profile" [ -r /etc/profile ] && builtin source "/etc/profile"
for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
[ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; } [ -r "$__ghostty_rcfile" ] && {
builtin source "$__ghostty_rcfile"
break
}
done done
fi fi
else else
@ -55,7 +60,10 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
# Void Linux uses /etc/bash/bashrc # Void Linux uses /etc/bash/bashrc
# Nixos uses /etc/bashrc # Nixos uses /etc/bashrc
for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
[ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; } [ -r "$__ghostty_rcfile" ] && {
builtin source "$__ghostty_rcfile"
break
}
done done
if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
[ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE" [ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
@ -88,15 +96,105 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
fi fi
done done
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
builtin command sudo "$@"; builtin command sudo "$@"
else else
builtin command sudo TERMINFO="$TERMINFO" "$@"; builtin command sudo TERMINFO="$TERMINFO" "$@"
fi fi
} }
fi fi
if [[ -n "$GHOSTTY_SSH_INTEGRATION" && "$GHOSTTY_SSH_INTEGRATION" != "off" ]]; then
# Wrap `ssh` command to provide Ghostty SSH integration.
#
# This approach supports wrapping an `ssh` alias, but the alias definition
# must come _after_ this function is defined. Otherwise, the alias expansion
# will take precedence over this function, and it won't be wrapped.
function ssh {
case "$GHOSTTY_SSH_INTEGRATION" in
"term_only")
_ghostty_ssh_term_only "$@"
;;
"basic")
_ghostty_ssh_basic "$@"
;;
"full")
_ghostty_ssh_full "$@"
;;
*)
# Unknown level, fall back to basic
_ghostty_ssh_basic "$@"
;;
esac
}
# Level: term_only - Just fix TERM compatibility
_ghostty_ssh_term_only() {
if [[ "$TERM" == "xterm-ghostty" ]]; then
TERM=xterm-256color command ssh "$@"
else
command ssh "$@"
fi
}
# Level: basic - TERM fix + environment variable propagation
_ghostty_ssh_basic() {
local env_vars=()
# Fix TERM compatibility
if [[ "$TERM" == "xterm-ghostty" ]]; then
env_vars+=("TERM=xterm-256color")
fi
# Propagate Ghostty shell integration environment variables
[[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES")
[[ -n "$GHOSTTY_RESOURCES_DIR" ]] && env_vars+=("GHOSTTY_RESOURCES_DIR=$GHOSTTY_RESOURCES_DIR")
# Execute with environment variables if any were set
if [[ ${#env_vars[@]} -gt 0 ]]; then
env "${env_vars[@]}" ssh "$@"
else
builtin command ssh "$@"
fi
}
# Level: full - All features
_ghostty_ssh_full() {
# Full integration: Two-step terminfo installation
if command -v infocmp >/dev/null 2>&1; then
echo "Installing Ghostty terminfo on remote host..." >&2
# Step 1: Install terminfo using the same approach that works manually
# This requires authentication but is quick and reliable
if infocmp -x xterm-ghostty 2>/dev/null | command ssh "$@" 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'; then
echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2
# Step 2: Connect with xterm-ghostty since we know terminfo is now available
local env_vars=()
# Use xterm-ghostty since we just installed it
env_vars+=("TERM=xterm-ghostty")
# Propagate Ghostty shell integration environment variables
[[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR")
[[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO")
[[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE")
# Normal SSH connection with Ghostty terminfo available
env "${env_vars[@]}" ssh "$@"
return 0
else
echo "Terminfo installation failed. Using basic integration." >&2
fi
fi
# Fallback to basic integration
_ghostty_ssh_basic "$@"
}
fi
# Import bash-preexec, safe to do multiple times # Import bash-preexec, safe to do multiple times
builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh" builtin source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/bash-preexec.sh"
# This is set to 1 when we're executing a command so that we don't # This is set to 1 when we're executing a command so that we don't
# send prompt marks multiple times. # send prompt marks multiple times.
@ -104,68 +202,68 @@ _ghostty_executing=""
_ghostty_last_reported_cwd="" _ghostty_last_reported_cwd=""
function __ghostty_precmd() { function __ghostty_precmd() {
local ret="$?" local ret="$?"
if test "$_ghostty_executing" != "0"; then if test "$_ghostty_executing" != "0"; then
_GHOSTTY_SAVE_PS0="$PS0" _GHOSTTY_SAVE_PS0="$PS0"
_GHOSTTY_SAVE_PS1="$PS1" _GHOSTTY_SAVE_PS1="$PS1"
_GHOSTTY_SAVE_PS2="$PS2" _GHOSTTY_SAVE_PS2="$PS2"
# Marks # Marks
PS1=$PS1'\[\e]133;B\a\]' PS1=$PS1'\[\e]133;B\a\]'
PS2=$PS2'\[\e]133;B\a\]' PS2=$PS2'\[\e]133;B\a\]'
# bash doesn't redraw the leading lines in a multiline prompt so # bash doesn't redraw the leading lines in a multiline prompt so
# mark the last line as a secondary prompt (k=s) to prevent the # mark the last line as a secondary prompt (k=s) to prevent the
# preceding lines from being erased by ghostty after a resize. # preceding lines from being erased by ghostty after a resize.
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
PS1=$PS1'\[\e]133;A;k=s\a\]' PS1=$PS1'\[\e]133;A;k=s\a\]'
fi
# Cursor
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
PS1=$PS1'\[\e[5 q\]'
PS0=$PS0'\[\e[0 q\]'
fi
# Title (working directory)
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
PS1=$PS1'\[\e]2;\w\a\]'
fi
fi fi
if test "$_ghostty_executing" != ""; then # Cursor
# End of current command. Report its status. if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID" PS1=$PS1'\[\e[5 q\]'
PS0=$PS0'\[\e[0 q\]'
fi fi
# unfortunately bash provides no hooks to detect cwd changes # Title (working directory)
# in particular this means cwd reporting will not happen for a if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
# command like cd /test && cat. PS0 is evaluated before cd is run. PS1=$PS1'\[\e]2;\w\a\]'
if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then
_ghostty_last_reported_cwd="$PWD"
builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD"
fi fi
fi
# Fresh line and start of prompt. if test "$_ghostty_executing" != ""; then
builtin printf "\e]133;A;aid=%s\a" "$BASHPID" # End of current command. Report its status.
_ghostty_executing=0 builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID"
fi
# unfortunately bash provides no hooks to detect cwd changes
# in particular this means cwd reporting will not happen for a
# command like cd /test && cat. PS0 is evaluated before cd is run.
if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then
_ghostty_last_reported_cwd="$PWD"
builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD"
fi
# Fresh line and start of prompt.
builtin printf "\e]133;A;aid=%s\a" "$BASHPID"
_ghostty_executing=0
} }
function __ghostty_preexec() { function __ghostty_preexec() {
builtin local cmd="$1" builtin local cmd="$1"
PS0="$_GHOSTTY_SAVE_PS0" PS0="$_GHOSTTY_SAVE_PS0"
PS1="$_GHOSTTY_SAVE_PS1" PS1="$_GHOSTTY_SAVE_PS1"
PS2="$_GHOSTTY_SAVE_PS2" PS2="$_GHOSTTY_SAVE_PS2"
# Title (current command) # Title (current command)
if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}" builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]/}"
fi fi
# End of input, start of output. # End of input, start of output.
builtin printf "\e]133;C;\a" builtin printf "\e]133;C;\a"
_ghostty_executing=1 _ghostty_executing=1
} }
preexec_functions+=(__ghostty_preexec) preexec_functions+=(__ghostty_preexec)

View File

@ -98,6 +98,107 @@
(external sudo) $@args (external sudo) $@args
} }
fn ssh-with-ghostty-integration {|@args|
if (and (has-env GHOSTTY_SSH_INTEGRATION) (not-eq "" $E:GHOSTTY_SSH_INTEGRATION) (not-eq "off" $E:GHOSTTY_SSH_INTEGRATION)) {
if (eq "term_only" $E:GHOSTTY_SSH_INTEGRATION) {
ssh-term-only $@args
} elif (eq "basic" $E:GHOSTTY_SSH_INTEGRATION) {
ssh-basic $@args
} elif (eq "full" $E:GHOSTTY_SSH_INTEGRATION) {
ssh-full $@args
} else {
# Unknown level, fall back to basic
ssh-basic $@args
}
} else {
(external ssh) $@args
}
}
fn ssh-term-only {|@args|
# Level: term_only - Just fix TERM compatibility
if (eq "xterm-ghostty" $E:TERM) {
TERM=xterm-256color (external ssh) $@args
} else {
(external ssh) $@args
}
}
fn ssh-basic {|@args|
# Level: basic - TERM fix + environment variable propagation
var env-vars = []
# Fix TERM compatibility
if (eq "xterm-ghostty" $E:TERM) {
set env-vars = [$@env-vars TERM=xterm-256color]
}
# Propagate Ghostty shell integration environment variables
if (has-env GHOSTTY_SHELL_FEATURES) {
if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) {
set env-vars = [$@env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES]
}
}
if (has-env GHOSTTY_RESOURCES_DIR) {
if (not-eq "" $E:GHOSTTY_RESOURCES_DIR) {
set env-vars = [$@env-vars GHOSTTY_RESOURCES_DIR=$E:GHOSTTY_RESOURCES_DIR]
}
}
# Execute with environment variables if any were set
if (> (count $env-vars) 0) {
(external env) $@env-vars ssh $@args
} else {
(external ssh) $@args
}
}
fn ghostty-ssh-full {|@args|
# Full integration: Two-step terminfo installation
if (has-external infocmp) {
echo "Installing Ghostty terminfo on remote host..." >&2
# Step 1: Install terminfo using the same approach that works manually
# This requires authentication but is quick and reliable
try {
infocmp -x xterm-ghostty 2>/dev/null | command ssh $@args 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'
echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2
# Step 2: Connect with xterm-ghostty since we know terminfo is now available
var env-vars = []
# Use xterm-ghostty since we just installed it
set env-vars = [$@env-vars TERM=xterm-ghostty]
# Propagate Ghostty shell integration environment variables
if (has-env GHOSTTY_SHELL_INTEGRATION_NO_CURSOR) {
set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$E:GHOSTTY_SHELL_INTEGRATION_NO_CURSOR]
}
if (has-env GHOSTTY_SHELL_INTEGRATION_NO_SUDO) {
set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$E:GHOSTTY_SHELL_INTEGRATION_NO_SUDO]
}
if (has-env GHOSTTY_SHELL_INTEGRATION_NO_TITLE) {
set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$E:GHOSTTY_SHELL_INTEGRATION_NO_TITLE]
}
# Normal SSH connection with Ghostty terminfo available
env $@env-vars ssh $@args
return
} catch e {
echo "Terminfo installation failed. Using basic integration." >&2
}
}
# Fallback to basic integration
ghostty-ssh-basic $@args
}
# Register SSH integration if enabled
if (and (has-env GHOSTTY_SSH_INTEGRATION) (not-eq "" $E:GHOSTTY_SSH_INTEGRATION) (not-eq "off" $E:GHOSTTY_SSH_INTEGRATION) (has-external ssh)) {
edit:add-var ssh~ $ssh-with-ghostty-integration~
}
defer { defer {
mark-prompt-start mark-prompt-start
report-pwd report-pwd

View File

@ -63,14 +63,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
# When using sudo shell integration feature, ensure $TERMINFO is set # When using sudo shell integration feature, ensure $TERMINFO is set
# and `sudo` is not already a function or alias # and `sudo` is not already a function or alias
if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x")
# Wrap `sudo` command to ensure Ghostty terminfo is preserved # Wrap `sudo` command to ensure Ghostty terminfo is preserved
function sudo -d "Wrap sudo to preserve terminfo" function sudo -d "Wrap sudo to preserve terminfo"
set --function sudo_has_sudoedit_flags "no" set --function sudo_has_sudoedit_flags no
for arg in $argv for arg in $argv
# Check if argument is '-e' or '--edit' (sudoedit flags) # Check if argument is '-e' or '--edit' (sudoedit flags)
if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg" if string match -q -- -e "$arg"; or string match -q -- --edit "$arg"
set --function sudo_has_sudoedit_flags "yes" set --function sudo_has_sudoedit_flags yes
break break
end end
# Check if argument is neither an option nor a key-value pair # Check if argument is neither an option nor a key-value pair
@ -78,7 +78,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
break break
end end
end end
if test "$sudo_has_sudoedit_flags" = "yes" if test "$sudo_has_sudoedit_flags" = yes
command sudo $argv command sudo $argv
else else
command sudo TERMINFO="$TERMINFO" $argv command sudo TERMINFO="$TERMINFO" $argv
@ -86,6 +86,99 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
end end
end end
# SSH integration wrapper
if test -n "$GHOSTTY_SSH_INTEGRATION"; and test "$GHOSTTY_SSH_INTEGRATION" != off
function ssh -d "Wrap ssh to provide Ghostty SSH integration"
switch "$GHOSTTY_SSH_INTEGRATION"
case term_only
_ghostty_ssh_term_only $argv
case basic
_ghostty_ssh_basic $argv
case full
_ghostty_ssh_full $argv
case "*"
# Unknown level, fall back to basic
_ghostty_ssh_basic $argv
end
end
# Level: term_only - Just fix TERM compatibility
function _ghostty_ssh_term_only -d "SSH with TERM compatibility fix"
if test "$TERM" = xterm-ghostty
TERM=xterm-256color command ssh $argv
else
command ssh $argv
end
end
# Level: basic - TERM fix + environment variable propagation
function _ghostty_ssh_basic -d "SSH with TERM fix and environment propagation"
# Build environment variables to propagate
set --local env_vars
# Fix TERM compatibility
if test "$TERM" = xterm-ghostty
set env_vars $env_vars TERM=xterm-256color
end
# Propagate Ghostty shell integration environment variables
if test -n "$GHOSTTY_SHELL_FEATURES"
set env_vars $env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES"
end
if test -n "$GHOSTTY_RESOURCES_DIR"
set env_vars $env_vars GHOSTTY_RESOURCES_DIR="$GHOSTTY_RESOURCES_DIR"
end
# Execute with environment variables if any were set
if test (count $env_vars) -gt 0
env $env_vars ssh $argv
else
command ssh $argv
end
end
# Level: full - All features
function _ghostty_ssh_full
# Full integration: Two-step terminfo installation
if command -v infocmp >/dev/null 2>&1
echo "Installing Ghostty terminfo on remote host..." >&2
# Step 1: Install terminfo using the same approach that works manually
# This requires authentication but is quick and reliable
if infocmp -x xterm-ghostty 2>/dev/null | command ssh $argv 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'
echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2
# Step 2: Connect with xterm-ghostty since we know terminfo is now available
set -l env_vars
# Use xterm-ghostty since we just installed it
set -a env_vars TERM=xterm-ghostty
# Propagate Ghostty shell integration environment variables
if set -q GHOSTTY_SHELL_INTEGRATION_NO_CURSOR
set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR
end
if set -q GHOSTTY_SHELL_INTEGRATION_NO_SUDO
set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO
end
if set -q GHOSTTY_SHELL_INTEGRATION_NO_TITLE
set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE
end
# Normal SSH connection with Ghostty terminfo available
env $env_vars ssh $argv
return 0
else
echo "Terminfo installation failed. Using basic integration." >&2
end
end
# Fallback to basic integration
_ghostty_ssh_basic $argv
end
end
# Setup prompt marking # Setup prompt marking
function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_cancel --on-event fish_posterror function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_cancel --on-event fish_posterror
# If we never got the output end event, then we need to send it now. # If we never got the output end event, then we need to send it now.

View File

@ -243,6 +243,87 @@ _ghostty_deferred_init() {
fi fi
} }
fi fi
# SSH
if [[ -n "$GHOSTTY_SSH_INTEGRATION" && "$GHOSTTY_SSH_INTEGRATION" != "off" ]]; then
# Wrap `ssh` command to provide Ghostty SSH integration
ssh() {
case "$GHOSTTY_SSH_INTEGRATION" in
"term_only")
_ghostty_ssh_term_only "$@"
;;
"basic")
_ghostty_ssh_basic "$@"
;;
"full")
_ghostty_ssh_full "$@"
;;
*)
# Unknown level, fall back to basic
_ghostty_ssh_basic "$@"
;;
esac
}
# Level: term_only - Just fix TERM compatibility
_ghostty_ssh_term_only() {
if [[ "$TERM" == "xterm-ghostty" ]]; then
TERM=xterm-256color builtin command ssh "$@"
else
builtin command ssh "$@"
fi
}
# Level: basic - TERM fix + environment variable propagation
_ghostty_ssh_basic() {
# Fix TERM compatibility and propagate key environment variables
if [[ "$TERM" == "xterm-ghostty" ]]; then
TERM=xterm-256color \
GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES}" \
GHOSTTY_RESOURCES_DIR="${GHOSTTY_RESOURCES_DIR}" \
builtin command ssh "$@"
else
GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES}" \
GHOSTTY_RESOURCES_DIR="${GHOSTTY_RESOURCES_DIR}" \
builtin command ssh "$@"
fi
}
# Level: full - All features
_ghostty_ssh_full() {
# Full integration: Two-step terminfo installation
if command -v infocmp >/dev/null 2>&1; then
echo "Installing Ghostty terminfo on remote host..." >&2
# Step 1: Install terminfo using the same approach that works manually
# This requires authentication but is quick and reliable
if infocmp -x xterm-ghostty 2>/dev/null | command ssh "$@" 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'; then
echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2
# Step 2: Connect with xterm-ghostty since we know terminfo is now available
local env_vars=()
# Use xterm-ghostty since we just installed it
env_vars+=("TERM=xterm-ghostty")
# Propagate Ghostty shell integration environment variables
[[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR")
[[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO")
[[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE")
# Normal SSH connection with Ghostty terminfo available
env "${env_vars[@]}" ssh "$@"
return 0
else
echo "Terminfo installation failed. Using basic integration." >&2
fi
fi
# Fallback to basic integration
_ghostty_ssh_basic "$@"
}
fi
# Some zsh users manually run `source ~/.zshrc` in order to apply rc file # Some zsh users manually run `source ~/.zshrc` in order to apply rc file
# changes to the current shell. This is a terrible practice that breaks many # changes to the current shell. This is a terrible practice that breaks many

View File

@ -733,6 +733,7 @@ pub const Config = struct {
env_override: configpkg.RepeatableStringMap = .{}, env_override: configpkg.RepeatableStringMap = .{},
shell_integration: configpkg.Config.ShellIntegration = .detect, shell_integration: configpkg.Config.ShellIntegration = .detect,
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{}, shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
ssh_integration: configpkg.SSHIntegration,
working_directory: ?[]const u8 = null, working_directory: ?[]const u8 = null,
resources_dir: ?[]const u8, resources_dir: ?[]const u8,
term: []const u8, term: []const u8,
@ -937,6 +938,7 @@ const Subprocess = struct {
&env, &env,
force, force,
cfg.shell_integration_features, cfg.shell_integration_features,
cfg.ssh_integration,
) orelse { ) orelse {
log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); log.warn("shell could not be detected, no automatic shell integration will be injected", .{});
break :shell default_shell_command; break :shell default_shell_command;

View File

@ -45,6 +45,7 @@ pub fn setup(
env: *EnvMap, env: *EnvMap,
force_shell: ?Shell, force_shell: ?Shell,
features: config.ShellIntegrationFeatures, features: config.ShellIntegrationFeatures,
ssh_integration: config.SSHIntegration,
) !?ShellIntegration { ) !?ShellIntegration {
const exe = if (force_shell) |shell| switch (shell) { const exe = if (force_shell) |shell| switch (shell) {
.bash => "bash", .bash => "bash",
@ -70,8 +71,9 @@ pub fn setup(
exe, exe,
); );
// Setup our feature env vars // Setup our feature env vars and SSH integration
try setupFeatures(env, features); try setupFeatures(env, features);
try setupSSHIntegration(env, ssh_integration);
return result; return result;
} }
@ -159,6 +161,7 @@ test "force shell" {
&env, &env,
shell, shell,
.{}, .{},
.off,
); );
try testing.expectEqual(shell, result.?.shell); try testing.expectEqual(shell, result.?.shell);
} }
@ -224,6 +227,15 @@ test "setup features" {
} }
} }
pub fn setupSSHIntegration(
env: *EnvMap,
ssh_integration: config.SSHIntegration,
) !void {
if (ssh_integration != .off) {
try env.put("GHOSTTY_SSH_INTEGRATION", @tagName(ssh_integration));
}
}
/// Setup the bash automatic shell integration. This works by /// Setup the bash automatic shell integration. This works by
/// starting bash in POSIX mode and using the ENV environment /// starting bash in POSIX mode and using the ENV environment
/// variable to load our bash integration script. This prevents /// variable to load our bash integration script. This prevents