diff --git a/src/cli/action.zig b/src/cli/action.zig index 009afb4c9..1d1c3bfa0 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -9,6 +9,8 @@ const list_keybinds = @import("list_keybinds.zig"); const list_themes = @import("list_themes.zig"); const list_colors = @import("list_colors.zig"); const list_actions = @import("list_actions.zig"); +const list_ssh_cache = @import("list_ssh_cache.zig"); +const clear_ssh_cache = @import("clear_ssh_cache.zig"); const edit_config = @import("edit_config.zig"); const show_config = @import("show_config.zig"); const validate_config = @import("validate_config.zig"); @@ -41,6 +43,12 @@ pub const Action = enum { /// List keybind actions @"list-actions", + /// List hosts with Ghostty SSH terminfo installed + @"list-ssh-cache", + + /// Clear Ghostty SSH terminfo cache + @"clear-ssh-cache", + /// Edit the config file in the configured terminal editor. @"edit-config", @@ -155,6 +163,8 @@ pub const Action = enum { .@"list-themes" => try list_themes.run(alloc), .@"list-colors" => try list_colors.run(alloc), .@"list-actions" => try list_actions.run(alloc), + .@"list-ssh-cache" => @import("list_ssh_cache.zig").run(alloc), + .@"clear-ssh-cache" => @import("clear_ssh_cache.zig").run(alloc), .@"edit-config" => try edit_config.run(alloc), .@"show-config" => try show_config.run(alloc), .@"validate-config" => try validate_config.run(alloc), @@ -192,6 +202,8 @@ pub const Action = enum { .@"list-themes" => list_themes.Options, .@"list-colors" => list_colors.Options, .@"list-actions" => list_actions.Options, + .@"list-ssh-cache" => list_ssh_cache.Options, + .@"clear-ssh-cache" => clear_ssh_cache.Options, .@"edit-config" => edit_config.Options, .@"show-config" => show_config.Options, .@"validate-config" => validate_config.Options, diff --git a/src/cli/clear_ssh_cache.zig b/src/cli/clear_ssh_cache.zig new file mode 100644 index 000000000..062af4221 --- /dev/null +++ b/src/cli/clear_ssh_cache.zig @@ -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; +} diff --git a/src/cli/list_ssh_cache.zig b/src/cli/list_ssh_cache.zig new file mode 100644 index 000000000..a799d95bd --- /dev/null +++ b/src/cli/list_ssh_cache.zig @@ -0,0 +1,38 @@ +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. +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; +} diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig new file mode 100644 index 000000000..c3484735a --- /dev/null +++ b/src/cli/ssh_cache.zig @@ -0,0 +1,71 @@ +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/shell-integration/shared/ghostty-ssh-cache"); + }; + defer alloc.free(resources_dir); + + return try std.fs.path.join(alloc, &[_][]const u8{ resources_dir, "shell-integration", "shared", "ghostty-ssh-cache" }); +} + +/// List cached hosts by calling the external script +pub fn listCachedHosts(alloc: Allocator, writer: anytype) !void { + const script_path = try getCacheScriptPath(alloc); + defer alloc.free(script_path); + + var child = Child.init(&[_][]const u8{ script_path, "list" }, 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); + } + + // Script handles its own success/error messaging, so we don't need to check exit code +} + +/// Clear cache by calling the external script +pub fn clearCache(alloc: Allocator, writer: anytype) !void { + const script_path = try getCacheScriptPath(alloc); + defer alloc.free(script_path); + + var child = Child.init(&[_][]const u8{ script_path, "clear" }, 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); + } + + // Script handles its own success/error messaging, so we don't need to check exit code +} diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 9ba11c845..f07df125e 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -100,19 +100,11 @@ 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" - # If 'ssh-terminfo' flag is enabled, wrap ghostty to provide cache management commands - ghostty() { - case "$1" in - ssh-cache-list) "$_CACHE" list ;; - ssh-cache-clear) "$_CACHE" clear ;; - *) builtin command ghostty "$@" ;; - esac - } fi # SSH wrapper ssh() { - local e=() o=() c=() # Removed 't' from here + local env=() opts=() ctrl=() # Set up env vars first so terminfo installation inherits them if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then @@ -123,35 +115,35 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then ) for v in "${vars[@]}"; do builtin export "${v?}" - o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") 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 t - t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + builtin local target + target=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - if [[ -n "$t" ]] && "$_CACHE" chk "$t"; then - e+=(TERM=xterm-ghostty) + if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then + env+=(TERM=xterm-ghostty) elif builtin command -v infocmp >/dev/null 2>&1; then - builtin local ti - ti=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n "$ti" ]]; 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 cp - cp="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" - case $(builtin echo "$ti" | builtin command ssh "${o[@]}" -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s "$@" ' + builtin local cpath + cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" + 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 ') in OK) builtin echo "Terminfo setup complete." >&2 - [[ -n "$t" ]] && "$_CACHE" add "$t" - e+=(TERM=xterm-ghostty) - c+=(-o "ControlPath=$cp") + [[ -n "$target" ]] && "$_CACHE" add "$target" + env+=(TERM=xterm-ghostty) + ctrl+=(-o "ControlPath=$cpath") ;; *) builtin echo "Warning: Failed to install terminfo." >&2 ;; esac @@ -163,14 +155,14 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then # Fallback TERM only if terminfo didn't set it if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - [[ $TERM == xterm-ghostty && ! " ${e[*]} " =~ " TERM=" ]] && e+=(TERM=xterm-256color) + [[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) fi # Execute - if [[ ${#e[@]} -gt 0 ]]; then - env "${e[@]}" ssh "${o[@]}" "${c[@]}" "$@" + if [[ ${#env[@]} -gt 0 ]]; then + env "${env[@]}" ssh "${opts[@]}" "${ctrl[@]}" "$@" else - builtin command ssh "${o[@]}" "${c[@]}" "$@" + builtin command ssh "${opts[@]}" "${ctrl[@]}" "$@" fi } fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 09aa09f31..7048d56b3 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -100,85 +100,68 @@ # SSH Integration use str - use path - use re - if (re:match 'ssh-(env|terminfo)' $E:GHOSTTY_SHELL_FEATURES) { - if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { - var _cache_script = (path:join $E:GHOSTTY_RESOURCES_DIR shell-integration shared ghostty-ssh-cache) - - # Wrap ghostty command to provide cache management commands - fn ghostty {|@args| - if (eq $args[0] ssh-cache-list) { - (external $_cache_script) list - } elif (eq $args[0] ssh-cache-clear) { - (external $_cache_script) clear - } else { - (external ghostty) $@args - } - } + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) or (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { - edit:add-var ghostty~ $ghostty~ + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { + var _CACHE = $E:GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache } # SSH wrapper fn ssh {|@args| - var e = [] - var o = [] - var c = [] + var env = [] + var opts = [] + var ctrl = [] # Set up env vars first so terminfo installation inherits them - if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { - set-env COLORTERM (or $E:COLORTERM truecolor) - set-env TERM_PROGRAM (or $E:TERM_PROGRAM ghostty) - if (has-env GHOSTTY_VERSION) { - set-env TERM_PROGRAM_VERSION $E:GHOSTTY_VERSION - } - - var vars = [COLORTERM=truecolor TERM_PROGRAM=ghostty] - if (has-env GHOSTTY_VERSION) { + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + var vars = [ + COLORTERM=truecolor + TERM_PROGRAM=ghostty + ] + if (not-eq $E:GHOSTTY_VERSION '') { set vars = [$@vars TERM_PROGRAM_VERSION=$E:GHOSTTY_VERSION] } + for v $vars { - var varname = (str:split &max=2 '=' $v | take 1) - set o = [$@o -o "SendEnv "$varname -o "SetEnv "$v] + set-env (str:split = $v | take 1) (str:split = $v | drop 1 | str:join =) + var varname = (str:split = $v | take 1) + set opts = [$@opts -o 'SendEnv '$varname -o 'SetEnv '$v] } } # Install terminfo if needed, reuse control connection for main session - if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { - # Get target (only when needed for terminfo) - var t = "" + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { + # Get target + var target = '' try { - set t = (e:ssh -G $@args 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@' | str:trim-space) - } catch e { - # Ignore errors - } + set target = (e:ssh -G $@args 2>/dev/null | e:awk '/^(user|hostname) /{print $2}' | e:paste -sd'@') + } catch { } - if (and (not-eq $t "") (try { (external $_cache_script) chk $t } catch e { put $false })) { - set e = [$@e TERM=xterm-ghostty] + if (and (not-eq $target '') ($_CACHE chk $target)) { + set env = [$@env TERM=xterm-ghostty] } elif (has-external infocmp) { - var ti = "" + var tinfo = '' try { - set ti = (infocmp -x xterm-ghostty 2>/dev/null | slurp) - } catch e { + set tinfo = (e:infocmp -x xterm-ghostty 2>/dev/null) + } catch { echo "Warning: xterm-ghostty terminfo not found locally." >&2 } - if (not-eq $ti "") { + + if (not-eq $tinfo '') { echo "Setting up Ghostty terminfo on remote host..." >&2 - var cp = "/tmp/ghostty-ssh-"$E:USER"-"(randint 10000)"-"(date +%s | str:trim-space) - var result = (echo $ti | e:ssh $@o -o ControlMaster=yes -o ControlPath=$cp -o ControlPersist=60s $@args ' + var cpath = '/tmp/ghostty-ssh-'$E:USER'-'(randint 0 32767)'-'(date +%s) + var result = (echo $tinfo | e:ssh $@opts -o ControlMaster=yes -o ControlPath=$cpath -o ControlPersist=60s $@args ' 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 - ' | str:trim-space) + ') + if (eq $result OK) { echo "Terminfo setup complete." >&2 - if (not-eq $t "") { - (external $_cache_script) add $t - } - set e = [$@e TERM=xterm-ghostty] - set c = [$@c -o ControlPath=$cp] + if (not-eq $target '') { $_CACHE add $target } + set env = [$@env TERM=xterm-ghostty] + set ctrl = [$@ctrl -o ControlPath=$cpath] } else { echo "Warning: Failed to install terminfo." >&2 } @@ -189,22 +172,19 @@ } # Fallback TERM only if terminfo didn't set it - if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { - if (and (eq $E:TERM xterm-ghostty) (not (re:match 'TERM=' (str:join ' ' $e)))) { - set e = [$@e TERM=xterm-256color] + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + if (and (eq $E:TERM xterm-ghostty) (not (str:contains (str:join ' ' $env) 'TERM='))) { + set env = [$@env TERM=xterm-256color] } } # Execute - if (> (count $e) 0) { - e:env $@e e:ssh $@o $@c $@args + if (> (count $env) 0) { + e:env $@env e:ssh $@opts $@ctrl $@args } else { - e:ssh $@o $@c $@args + e:ssh $@opts $@ctrl $@args } } - - # Export ssh function for global use - set edit:add-var[ssh] = $ssh~ } defer { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 55ce34985..9b82f5fa2 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -87,89 +87,86 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end # SSH Integration - if string match -qr 'ssh-(env|terminfo)' "$GHOSTTY_SHELL_FEATURES" - if string match -qr ssh-terminfo "$GHOSTTY_SHELL_FEATURES" - set -g _cache_script "$GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache" + if string match -qr 'ssh-(env|terminfo)' $GHOSTTY_SHELL_FEATURES - # Wrap ghostty command to provide cache management commands - function ghostty -d "Wrap ghostty to provide cache management commands" - switch "$argv[1]" - case ssh-cache-list - command "$_cache_script" list - case ssh-cache-clear - command "$_cache_script" clear - case "*" - command ghostty $argv - end - end + if string match -q '*ssh-terminfo*' $GHOSTTY_SHELL_FEATURES + set -g _CACHE "$GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache" end # SSH wrapper function ssh - set -l e - set -l o - set -l c + set -l env + set -l opts + set -l ctrl # Set up env vars first so terminfo installation inherits them - if string match -qr ssh-env "$GHOSTTY_SHELL_FEATURES" - set -gx COLORTERM (test -n "$COLORTERM" && echo "$COLORTERM" || echo "truecolor") - set -gx TERM_PROGRAM (test -n "$TERM_PROGRAM" && echo "$TERM_PROGRAM" || echo "ghostty") - test -n "$GHOSTTY_VERSION" && set -gx TERM_PROGRAM_VERSION "$GHOSTTY_VERSION" + if string match -q '*ssh-env*' $GHOSTTY_SHELL_FEATURES + set -l vars \ + COLORTERM=truecolor \ + TERM_PROGRAM=ghostty - set -l vars COLORTERM=truecolor TERM_PROGRAM=ghostty - test -n "$GHOSTTY_VERSION" && set vars $vars "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION" + if test -n "$GHOSTTY_VERSION" + set -a vars "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION" + end for v in $vars - set -l varname (string split -m1 '=' "$v")[1] - set o $o -o "SendEnv $varname" -o "SetEnv $v" + set -l parts (string split = $v) + set -gx $parts[1] $parts[2] + set -a opts -o "SendEnv $parts[1]" -o "SetEnv $v" end end # Install terminfo if needed, reuse control connection for main session - if string match -qr ssh-terminfo "$GHOSTTY_SHELL_FEATURES" - # Get target (only when needed for terminfo) - set -l t (builtin command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + if string match -q '*ssh-terminfo*' $GHOSTTY_SHELL_FEATURES + # Get target + set -l target (command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - if test -n "$t" && command "$_cache_script" chk "$t" - set e $e TERM=xterm-ghostty + if test -n "$target" -a ("$_CACHE" chk "$target") + set -a env TERM=xterm-ghostty else if command -v infocmp >/dev/null 2>&1 - set -l ti - set ti (infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if test -n "$ti" - builtin echo "Setting up Ghostty terminfo on remote host..." >&2 - set -l cp "/tmp/ghostty-ssh-$USER-"(random)"-"(date +%s) - set -l result (builtin echo "$ti" | builtin command ssh $o -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s $argv ' + set -l tinfo (infocmp -x xterm-ghostty 2>/dev/null) + set -l status_code $status + + if test $status_code -ne 0 + echo "Warning: xterm-ghostty terminfo not found locally." >&2 + end + + if test -n "$tinfo" + echo "Setting up Ghostty terminfo on remote host..." >&2 + set -l cpath "/tmp/ghostty-ssh-$USER-"(random)"-"(date +%s) + set -l result (echo "$tinfo" | command ssh $opts -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s $argv ' 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 ') + switch $result case OK - builtin echo "Terminfo setup complete." >&2 - test -n "$t" && command "$_cache_script" add "$t" - set e $e TERM=xterm-ghostty - set c $c -o "ControlPath=$cp" + echo "Terminfo setup complete." >&2 + test -n "$target" && "$_CACHE" add "$target" + set -a env TERM=xterm-ghostty + set -a ctrl -o "ControlPath=$cpath" case '*' - builtin echo "Warning: Failed to install terminfo." >&2 + echo "Warning: Failed to install terminfo." >&2 end end else - builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 end end # Fallback TERM only if terminfo didn't set it - if string match -qr ssh-env "$GHOSTTY_SHELL_FEATURES" - if test "$TERM" = xterm-ghostty && not string match -q '*TERM=*' "$e" - set e $e TERM=xterm-256color + if string match -q '*ssh-env*' $GHOSTTY_SHELL_FEATURES + if test "$TERM" = xterm-ghostty -a ! (string join ' ' $env | string match -q '*TERM=*') + set -a env TERM=xterm-256color end end # Execute - if test (count $e) -gt 0 - env $e ssh $o $c $argv + if test (count $env) -gt 0 + env $env command ssh $opts $ctrl $argv else - builtin command ssh $o $c $argv + command ssh $opts $ctrl $argv end end end diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 4305ab61f..4d4461775 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -246,80 +246,76 @@ _ghostty_deferred_init() { # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - readonly _cache_script="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" - - # Wrap ghostty command to provide cache management commands - ghostty() { - case "$1" in - ssh-cache-list) "$_cache_script" list ;; - ssh-cache-clear) "$_cache_script" clear ;; - *) builtin command ghostty "$@" ;; - esac - } + readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" fi # SSH wrapper ssh() { - local -a e o c - + local -a env opts ctrl + env=() + opts=() + ctrl=() + # Set up env vars first so terminfo installation inherits them if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local vars=( + local -a vars + vars=( COLORTERM=truecolor TERM_PROGRAM=ghostty ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} ) for v in "${vars[@]}"; do - builtin export "${v?}" - o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + export "${v?}" + opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") 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 t - t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + local target + target=$(command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - if [[ -n $t ]] && "$_cache_script" chk "$t"; then - e+=(TERM=xterm-ghostty) - elif builtin command -v infocmp >/dev/null 2>&1; then - local ti - ti=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n $ti ]]; then - builtin echo "Setting up Ghostty terminfo on remote host..." >&2 - local cp - cp="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" - case $(builtin echo "$ti" | builtin command ssh "${o[@]}" -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s "$@" ' + if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then + env+=(TERM=xterm-ghostty) + elif command -v infocmp >/dev/null 2>&1; then + local tinfo + tinfo=$(infocmp -x xterm-ghostty 2>/dev/null) || echo "Warning: xterm-ghostty terminfo not found locally." >&2 + if [[ -n "$tinfo" ]]; then + echo "Setting up Ghostty terminfo on remote host..." >&2 + local cpath + cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" + case $(echo "$tinfo" | 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 ') in OK) - builtin echo "Terminfo setup complete." >&2 - [[ -n $t ]] && "$_cache_script" add "$t" - e+=(TERM=xterm-ghostty) - c+=(-o "ControlPath=$cp") + echo "Terminfo setup complete." >&2 + [[ -n "$target" ]] && "$_CACHE" add "$target" + env+=(TERM=xterm-ghostty) + ctrl+=(-o "ControlPath=$cpath") ;; - *) builtin echo "Warning: Failed to install terminfo." >&2 ;; + *) echo "Warning: Failed to install terminfo." >&2 ;; esac fi else - builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + 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 && ! " ${(j: :)e} " =~ " TERM=" ]] && e+=(TERM=xterm-256color) + [[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) fi # Execute - if (( ${#e} > 0 )); then - env "${e[@]}" ssh "${o[@]}" "${c[@]}" "$@" + if [[ ${#env[@]} -gt 0 ]]; then + env "${env[@]}" command ssh "${opts[@]}" "${ctrl[@]}" "$@" else - builtin command ssh "${o[@]}" "${c[@]}" "$@" + command ssh "${opts[@]}" "${ctrl[@]}" "$@" fi } fi