-
Notifications
You must be signed in to change notification settings - Fork 898
Add SSH Integration Configuration Option #7608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
142e07c
8f93d8f
34af3ff
2babdb4
842ced9
c706434
b07b3e4
2e9a0e9
050cb3b
4206ab1
8fafd5a
af28763
80475e1
fb8f6c7
3319b2b
2ddcf2f
995fb09
b6bb9ab
4cebee5
69f9976
f206e76
3068397
ddd3da4
e73313e
81641e5
c8d5e60
bbb02a8
8a2fa64
6789b7f
0565ed3
f617c9b
0ccb7cf
21d95c4
931efcd
59229d7
e5e2a56
1873add
eed2006
b537246
076f742
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
const std = @import("std"); | ||
const Allocator = std.mem.Allocator; | ||
const args = @import("args.zig"); | ||
const Action = @import("action.zig").Action; | ||
const ssh_cache = @import("ssh_cache.zig"); | ||
|
||
pub const Options = struct { | ||
pub fn deinit(self: Options) void { | ||
_ = self; | ||
} | ||
|
||
/// Enables `-h` and `--help` to work. | ||
pub fn help(self: Options) !void { | ||
_ = self; | ||
return Action.help_error; | ||
} | ||
}; | ||
|
||
/// Clear the Ghostty SSH terminfo cache. | ||
/// | ||
/// This command removes the cache of hosts where Ghostty's terminfo has been installed | ||
/// via the ssh-terminfo shell integration feature. After clearing, terminfo will be | ||
/// reinstalled on the next SSH connection to previously cached hosts. | ||
/// | ||
/// Use this if you need to force reinstallation of terminfo or clean up old entries. | ||
pub fn run(alloc: Allocator) !u8 { | ||
var opts: Options = .{}; | ||
defer opts.deinit(); | ||
|
||
{ | ||
var iter = try args.argsIterator(alloc); | ||
defer iter.deinit(); | ||
try args.parse(Options, alloc, &opts, &iter); | ||
} | ||
|
||
const stdout = std.io.getStdOut().writer(); | ||
try ssh_cache.clearCache(alloc, stdout); | ||
|
||
return 0; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
const std = @import("std"); | ||
const Allocator = std.mem.Allocator; | ||
const args = @import("args.zig"); | ||
const Action = @import("action.zig").Action; | ||
const ssh_cache = @import("ssh_cache.zig"); | ||
|
||
pub const Options = struct { | ||
pub fn deinit(self: Options) void { | ||
_ = self; | ||
} | ||
|
||
/// Enables `-h` and `--help` to work. | ||
pub fn help(self: Options) !void { | ||
_ = self; | ||
return Action.help_error; | ||
} | ||
}; | ||
|
||
/// List hosts with Ghostty SSH terminfo installed via the ssh-terminfo shell integration feature. | ||
/// | ||
/// This command shows all remote hosts where Ghostty's terminfo has been successfully | ||
/// installed through the SSH integration. The cache is automatically maintained when | ||
/// connecting to remote hosts with `shell-integration-features = ssh-terminfo` enabled. | ||
/// | ||
/// Use `+clear-ssh-cache` to remove cached entries if you need to force terminfo | ||
/// reinstallation or clean up stale host entries. | ||
pub fn run(alloc: Allocator) !u8 { | ||
var opts: Options = .{}; | ||
defer opts.deinit(); | ||
|
||
{ | ||
var iter = try args.argsIterator(alloc); | ||
defer iter.deinit(); | ||
try args.parse(Options, alloc, &opts, &iter); | ||
} | ||
|
||
const stdout = std.io.getStdOut().writer(); | ||
try ssh_cache.listCachedHosts(alloc, stdout); | ||
|
||
return 0; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
const std = @import("std"); | ||
const Allocator = std.mem.Allocator; | ||
const Child = std.process.Child; | ||
|
||
/// Get the path to the shared cache script | ||
fn getCacheScriptPath(alloc: Allocator) ![]u8 { | ||
// Use GHOSTTY_RESOURCES_DIR if available, otherwise assume relative path | ||
const resources_dir = std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR") catch { | ||
// Fallback: assume we're running from build directory | ||
return try alloc.dupe(u8, "src"); | ||
}; | ||
defer alloc.free(resources_dir); | ||
|
||
return try std.fs.path.join(alloc, &[_][]const u8{ resources_dir, "shell-integration", "shared", "ghostty-ssh-cache" }); | ||
} | ||
|
||
/// Generic function to run cache script commands | ||
fn runCacheCommand(alloc: Allocator, writer: anytype, command: []const u8) !void { | ||
const script_path = try getCacheScriptPath(alloc); | ||
defer alloc.free(script_path); | ||
|
||
var child = Child.init(&[_][]const u8{ script_path, command }, alloc); | ||
child.stdout_behavior = .Pipe; | ||
child.stderr_behavior = .Pipe; | ||
|
||
try child.spawn(); | ||
|
||
const stdout = try child.stdout.?.readToEndAlloc(alloc, std.math.maxInt(usize)); | ||
defer alloc.free(stdout); | ||
|
||
const stderr = try child.stderr.?.readToEndAlloc(alloc, std.math.maxInt(usize)); | ||
defer alloc.free(stderr); | ||
|
||
_ = try child.wait(); | ||
|
||
// Output the results regardless of exit code | ||
try writer.writeAll(stdout); | ||
if (stderr.len > 0) { | ||
try writer.writeAll(stderr); | ||
} | ||
} | ||
|
||
/// List cached hosts by calling the external script | ||
pub fn listCachedHosts(alloc: Allocator, writer: anytype) !void { | ||
try runCacheCommand(alloc, writer, "list"); | ||
} | ||
|
||
/// Clear cache by calling the external script | ||
pub fn clearCache(alloc: Allocator, writer: anytype) !void { | ||
try runCacheCommand(alloc, writer, "clear"); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2051,6 +2051,8 @@ keybind: Keybinds = .{}, | |
/// its default value is used, so you must explicitly disable features you don't | ||
/// want. You can also use `true` or `false` to turn all features on or off. | ||
/// | ||
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` | ||
/// | ||
/// Available features: | ||
/// | ||
/// * `cursor` - Set the cursor to a blinking bar at the prompt. | ||
|
@@ -2059,7 +2061,23 @@ keybind: Keybinds = .{}, | |
/// | ||
/// * `title` - Set the window title via shell integration. | ||
/// | ||
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` | ||
/// * `ssh-env` - Enable SSH environment variable compatibility. Automatically | ||
/// converts TERM from `xterm-ghostty` to `xterm-256color` when connecting to | ||
/// remote hosts and propagates COLORTERM, TERM_PROGRAM, and TERM_PROGRAM_VERSION. | ||
/// Whether or not these variables will be accepted by the remote host(s) will | ||
/// depend on whether or not the variables are allowed in their sshd_config. | ||
/// | ||
/// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts. | ||
/// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when | ||
/// connecting to hosts that lack it. Requires `infocmp` to be available locally | ||
/// and `tic` to be available on remote hosts. Provides `+list-ssh-cache` and | ||
/// `+clear-ssh-cache` CLI actions for managing the installation cache (caching | ||
Comment on lines
+2073
to
+2074
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current implementation, these commands are always available ( |
||
/// is otherwise automatic and requires no user intervention). | ||
/// | ||
/// SSH features work independently and can be combined for optimal experience: | ||
/// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its | ||
/// terminfo on remote hosts and use `xterm-ghostty` as TERM, falling back to | ||
/// `xterm-256color` with environment variables if terminfo installation fails. | ||
@"shell-integration-features": ShellIntegrationFeatures = .{}, | ||
|
||
/// Custom entries into the command palette. | ||
|
@@ -6233,6 +6251,8 @@ pub const ShellIntegrationFeatures = packed struct { | |
cursor: bool = true, | ||
sudo: bool = false, | ||
title: bool = true, | ||
@"ssh-env": bool = false, | ||
@"ssh-terminfo": bool = false, | ||
}; | ||
|
||
pub const RepeatableCommand = struct { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -95,6 +95,78 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then | |||||
} | ||||||
fi | ||||||
|
||||||
# SSH Integration | ||||||
if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then | ||||||
|
||||||
if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then | ||||||
readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reintroduces a minor dependency on |
||||||
fi | ||||||
|
||||||
# SSH wrapper | ||||||
ssh() { | ||||||
local env=() opts=() ctrl=() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not a big thing either way, but it might be helpful to prefix these particular local variables with
Suggested change
|
||||||
|
||||||
# Set up env vars first so terminfo installation inherits them | ||||||
if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then | ||||||
local vars=( | ||||||
COLORTERM=truecolor | ||||||
TERM_PROGRAM=ghostty | ||||||
${TERM_PROGRAM_VERSION:+TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION} | ||||||
) | ||||||
for v in "${vars[@]}"; do | ||||||
builtin export "${v?}" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This Could also take the same |
||||||
opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My (limited) understanding is that |
||||||
done | ||||||
fi | ||||||
|
||||||
# Install terminfo if needed, reuse control connection for main session | ||||||
if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then | ||||||
# Get target (only when needed for terminfo) | ||||||
builtin local target | ||||||
target=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This command pipeline doesn't appear to work for me (on macOS):
I think it's attempting to form the string There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this is partly why we should avoid writing things in Bash. The behavior differences between GNU/BSD awk/grep/sed/etc. are just too great. In this case specifically GNU paste says:
And I presume BSD paste requires specifying EDIT: Yup:
|
||||||
|
||||||
if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then | ||||||
env+=(TERM=xterm-ghostty) | ||||||
elif builtin command -v infocmp >/dev/null 2>&1; then | ||||||
builtin local tinfo | ||||||
tinfo=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 | ||||||
if [[ -n "$tinfo" ]]; then | ||||||
builtin echo "Setting up Ghostty terminfo on remote host..." >&2 | ||||||
builtin local cpath | ||||||
cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't recall the exact best practice for this (will need to do some research), but I feel like we should be using something like |
||||||
case $(builtin echo "$tinfo" | builtin command ssh "${opts[@]}" -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s "$@" ' | ||||||
infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit | ||||||
command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } | ||||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL | ||||||
Comment on lines
+138
to
+140
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that the |
||||||
') in | ||||||
OK) | ||||||
builtin echo "Terminfo setup complete." >&2 | ||||||
[[ -n "$target" ]] && "$_CACHE" add "$target" | ||||||
env+=(TERM=xterm-ghostty) | ||||||
ctrl+=(-o "ControlPath=$cpath") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the only place we add to |
||||||
;; | ||||||
*) builtin echo "Warning: Failed to install terminfo." >&2 ;; | ||||||
esac | ||||||
fi | ||||||
else | ||||||
builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 | ||||||
fi | ||||||
fi | ||||||
|
||||||
# Fallback TERM only if terminfo didn't set it | ||||||
if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then | ||||||
[[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure |
||||||
fi | ||||||
|
||||||
# Execute | ||||||
if [[ ${#env[@]} -gt 0 ]]; then | ||||||
env "${env[@]}" ssh "${opts[@]}" "${ctrl[@]}" "$@" | ||||||
else | ||||||
builtin command ssh "${opts[@]}" "${ctrl[@]}" "$@" | ||||||
fi | ||||||
} | ||||||
fi | ||||||
|
||||||
# Import bash-preexec, safe to do multiple times | ||||||
builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh" | ||||||
|
||||||
|
Uh oh!
There was an error while loading. Please reload this page.