mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
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:
@ -545,6 +545,7 @@ pub fn init(
|
||||
.env_override = config.env,
|
||||
.shell_integration = config.@"shell-integration",
|
||||
.shell_integration_features = config.@"shell-integration-features",
|
||||
.ssh_integration = config.@"ssh-integration",
|
||||
.working_directory = config.@"working-directory",
|
||||
.resources_dir = global_state.resources_dir.host(),
|
||||
.term = config.term,
|
||||
|
@ -33,6 +33,7 @@ pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
|
||||
pub const RepeatablePath = Config.RepeatablePath;
|
||||
pub const Path = Config.Path;
|
||||
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||
pub const SSHIntegration = Config.SSHIntegration;
|
||||
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
||||
pub const BackgroundImagePosition = Config.BackgroundImagePosition;
|
||||
pub const BackgroundImageFit = Config.BackgroundImageFit;
|
||||
|
@ -466,93 +466,6 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
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 selection color is just the inverted window background and foreground
|
||||
/// (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`
|
||||
@"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
|
||||
/// 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.
|
||||
/// See SSHIntegration for available options.
|
||||
///
|
||||
/// ```ini
|
||||
/// command-palette-entry = title:Reset Font Style, action:csi:0m
|
||||
/// 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 = .{},
|
||||
/// The default value is `off`.
|
||||
@"ssh-integration": SSHIntegration = .off,
|
||||
|
||||
/// Sets the reporting format for OSC sequences that request color information.
|
||||
/// 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
|
||||
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
|
||||
try result.link.links.append(alloc, .{
|
||||
.regex = url.regex,
|
||||
@ -3410,15 +3307,6 @@ fn expandPaths(self: *Config, base: []const u8) !void {
|
||||
&self._diagnostics,
|
||||
);
|
||||
},
|
||||
?RepeatablePath, ?Path => {
|
||||
if (@field(self, field.name)) |*path| {
|
||||
try path.expand(
|
||||
arena_alloc,
|
||||
base,
|
||||
&self._diagnostics,
|
||||
);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
@ -5083,29 +4971,25 @@ pub const Keybinds = struct {
|
||||
.{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } },
|
||||
.{ .close_tab = {} },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .shift = true } },
|
||||
.{ .previous_tab = {} },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .shift = true } },
|
||||
.{ .next_tab = {} },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .page_up }, .mods = .{ .ctrl = true } },
|
||||
.{ .previous_tab = {} },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .page_down }, .mods = .{ .ctrl = true } },
|
||||
.{ .next_tab = {} },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.put(
|
||||
alloc,
|
||||
@ -5117,67 +5001,57 @@ pub const Keybinds = struct {
|
||||
.{ .key = .{ .unicode = 'e' }, .mods = .{ .ctrl = true, .shift = true } },
|
||||
.{ .new_split = .down },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } },
|
||||
.{ .goto_split = .previous },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } },
|
||||
.{ .goto_split = .next },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true, .alt = true } },
|
||||
.{ .goto_split = .up },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true, .alt = true } },
|
||||
.{ .goto_split = .down },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .alt = true } },
|
||||
.{ .goto_split = .left },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .alt = true } },
|
||||
.{ .goto_split = .right },
|
||||
.{ .performable = true },
|
||||
);
|
||||
|
||||
// Resizing splits
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||
.{ .resize_split = .{ .up, 10 } },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||
.{ .resize_split = .{ .down, 10 } },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||
.{ .resize_split = .{ .left, 10 } },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||
.{ .resize_split = .{ .right, 10 } },
|
||||
.{ .performable = true },
|
||||
);
|
||||
|
||||
// Viewport scrolling
|
||||
@ -5248,24 +5122,22 @@ pub const Keybinds = struct {
|
||||
const end: u21 = '8';
|
||||
var i: u21 = start;
|
||||
while (i <= end) : (i += 1) {
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{
|
||||
.key = .{ .unicode = i },
|
||||
.mods = mods,
|
||||
},
|
||||
.{ .goto_tab = (i - start) + 1 },
|
||||
.{ .performable = true },
|
||||
);
|
||||
}
|
||||
try self.set.putFlags(
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{
|
||||
.key = .{ .unicode = '9' },
|
||||
.mods = mods,
|
||||
},
|
||||
.{ .last_tab = {} },
|
||||
.{ .performable = true },
|
||||
);
|
||||
}
|
||||
|
||||
@ -6235,147 +6107,32 @@ pub const ShellIntegrationFeatures = packed struct {
|
||||
title: bool = true,
|
||||
};
|
||||
|
||||
pub const RepeatableCommand = struct {
|
||||
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty,
|
||||
/// SSH integration levels for shell integration.
|
||||
/// 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 {
|
||||
self.value = .empty;
|
||||
try self.value.appendSlice(alloc, inputpkg.command.defaults);
|
||||
}
|
||||
|
||||
pub fn parseCLI(
|
||||
self: *RepeatableCommand,
|
||||
alloc: Allocator,
|
||||
input_: ?[]const u8,
|
||||
pub fn jsonStringify(
|
||||
self: SSHIntegration,
|
||||
options: std.json.StringifyOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
// Unset or empty input clears the list
|
||||
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);
|
||||
try std.json.stringify(@tagName(self), options, writer);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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
|
||||
pub const FreetypeLoadFlags = packed struct {
|
||||
// The defaults here at the time of writing this match the defaults
|
||||
|
@ -15,8 +15,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# We need to be in interactive mode to proceed.
|
||||
if [[ "$-" != *i* ]] ; then builtin return; fi
|
||||
# We need to be in interactive mode and we need to have the Ghostty
|
||||
# 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
|
||||
# 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
|
||||
[ -r /etc/profile ] && builtin source "/etc/profile"
|
||||
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
|
||||
fi
|
||||
else
|
||||
@ -55,7 +60,10 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
# Void Linux uses /etc/bash/bashrc
|
||||
# Nixos uses /etc/bashrc
|
||||
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
|
||||
if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
|
||||
[ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
|
||||
@ -88,15 +96,105 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
|
||||
fi
|
||||
done
|
||||
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
|
||||
builtin command sudo "$@";
|
||||
builtin command sudo "$@"
|
||||
else
|
||||
builtin command sudo TERMINFO="$TERMINFO" "$@";
|
||||
builtin command sudo TERMINFO="$TERMINFO" "$@"
|
||||
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
|
||||
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
|
||||
# send prompt marks multiple times.
|
||||
@ -104,68 +202,68 @@ _ghostty_executing=""
|
||||
_ghostty_last_reported_cwd=""
|
||||
|
||||
function __ghostty_precmd() {
|
||||
local ret="$?"
|
||||
if test "$_ghostty_executing" != "0"; then
|
||||
_GHOSTTY_SAVE_PS0="$PS0"
|
||||
_GHOSTTY_SAVE_PS1="$PS1"
|
||||
_GHOSTTY_SAVE_PS2="$PS2"
|
||||
local ret="$?"
|
||||
if test "$_ghostty_executing" != "0"; then
|
||||
_GHOSTTY_SAVE_PS0="$PS0"
|
||||
_GHOSTTY_SAVE_PS1="$PS1"
|
||||
_GHOSTTY_SAVE_PS2="$PS2"
|
||||
|
||||
# Marks
|
||||
PS1=$PS1'\[\e]133;B\a\]'
|
||||
PS2=$PS2'\[\e]133;B\a\]'
|
||||
# Marks
|
||||
PS1=$PS1'\[\e]133;B\a\]'
|
||||
PS2=$PS2'\[\e]133;B\a\]'
|
||||
|
||||
# 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
|
||||
# preceding lines from being erased by ghostty after a resize.
|
||||
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
|
||||
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
|
||||
# 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
|
||||
# preceding lines from being erased by ghostty after a resize.
|
||||
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
|
||||
PS1=$PS1'\[\e]133;A;k=s\a\]'
|
||||
fi
|
||||
|
||||
if test "$_ghostty_executing" != ""; then
|
||||
# End of current command. Report its status.
|
||||
builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID"
|
||||
# Cursor
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
PS1=$PS1'\[\e[5 q\]'
|
||||
PS0=$PS0'\[\e[0 q\]'
|
||||
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"
|
||||
# Title (working directory)
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
PS1=$PS1'\[\e]2;\w\a\]'
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fresh line and start of prompt.
|
||||
builtin printf "\e]133;A;aid=%s\a" "$BASHPID"
|
||||
_ghostty_executing=0
|
||||
if test "$_ghostty_executing" != ""; then
|
||||
# End of current command. Report its status.
|
||||
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() {
|
||||
builtin local cmd="$1"
|
||||
builtin local cmd="$1"
|
||||
|
||||
PS0="$_GHOSTTY_SAVE_PS0"
|
||||
PS1="$_GHOSTTY_SAVE_PS1"
|
||||
PS2="$_GHOSTTY_SAVE_PS2"
|
||||
PS0="$_GHOSTTY_SAVE_PS0"
|
||||
PS1="$_GHOSTTY_SAVE_PS1"
|
||||
PS2="$_GHOSTTY_SAVE_PS2"
|
||||
|
||||
# Title (current command)
|
||||
if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}"
|
||||
fi
|
||||
# Title (current command)
|
||||
if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]/}"
|
||||
fi
|
||||
|
||||
# End of input, start of output.
|
||||
builtin printf "\e]133;C;\a"
|
||||
_ghostty_executing=1
|
||||
# End of input, start of output.
|
||||
builtin printf "\e]133;C;\a"
|
||||
_ghostty_executing=1
|
||||
}
|
||||
|
||||
preexec_functions+=(__ghostty_preexec)
|
||||
|
@ -98,6 +98,107 @@
|
||||
(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 {
|
||||
mark-prompt-start
|
||||
report-pwd
|
||||
|
@ -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
|
||||
# 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
|
||||
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
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg"
|
||||
set --function sudo_has_sudoedit_flags "yes"
|
||||
if string match -q -- -e "$arg"; or string match -q -- --edit "$arg"
|
||||
set --function sudo_has_sudoedit_flags yes
|
||||
break
|
||||
end
|
||||
# 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
|
||||
end
|
||||
end
|
||||
if test "$sudo_has_sudoedit_flags" = "yes"
|
||||
if test "$sudo_has_sudoedit_flags" = yes
|
||||
command sudo $argv
|
||||
else
|
||||
command sudo TERMINFO="$TERMINFO" $argv
|
||||
@ -86,6 +86,99 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
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
|
||||
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.
|
||||
|
@ -243,6 +243,87 @@ _ghostty_deferred_init() {
|
||||
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
|
||||
# changes to the current shell. This is a terrible practice that breaks many
|
||||
|
@ -733,6 +733,7 @@ pub const Config = struct {
|
||||
env_override: configpkg.RepeatableStringMap = .{},
|
||||
shell_integration: configpkg.Config.ShellIntegration = .detect,
|
||||
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
|
||||
ssh_integration: configpkg.SSHIntegration,
|
||||
working_directory: ?[]const u8 = null,
|
||||
resources_dir: ?[]const u8,
|
||||
term: []const u8,
|
||||
@ -937,6 +938,7 @@ const Subprocess = struct {
|
||||
&env,
|
||||
force,
|
||||
cfg.shell_integration_features,
|
||||
cfg.ssh_integration,
|
||||
) orelse {
|
||||
log.warn("shell could not be detected, no automatic shell integration will be injected", .{});
|
||||
break :shell default_shell_command;
|
||||
|
@ -45,6 +45,7 @@ pub fn setup(
|
||||
env: *EnvMap,
|
||||
force_shell: ?Shell,
|
||||
features: config.ShellIntegrationFeatures,
|
||||
ssh_integration: config.SSHIntegration,
|
||||
) !?ShellIntegration {
|
||||
const exe = if (force_shell) |shell| switch (shell) {
|
||||
.bash => "bash",
|
||||
@ -70,8 +71,9 @@ pub fn setup(
|
||||
exe,
|
||||
);
|
||||
|
||||
// Setup our feature env vars
|
||||
// Setup our feature env vars and SSH integration
|
||||
try setupFeatures(env, features);
|
||||
try setupSSHIntegration(env, ssh_integration);
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -159,6 +161,7 @@ test "force shell" {
|
||||
&env,
|
||||
shell,
|
||||
.{},
|
||||
.off,
|
||||
);
|
||||
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
|
||||
/// starting bash in POSIX mode and using the ENV environment
|
||||
/// variable to load our bash integration script. This prevents
|
||||
|
Reference in New Issue
Block a user