mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +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,
|
.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,
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user