From 81641e56b1c471f9e110ebe7ec8372b75f366ed7 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 24 Jun 2025 08:58:00 -0700 Subject: [PATCH] ssh-integration: replace levels with flags, optimize implementation Rewrote shell functions to support the two new flags for shell-integration-features: - ssh-env: TERM compatibility + best effort environment variable propagation (anything beyond TERM will depend on what the remote host allows) - ssh-terminfo: automatic terminfo installation with control socket orchestration - Flags work independently or combined Implementation optimizations: - ~65% code reduction through unified execution path - Eliminated GHOSTTY_SSH_INTEGRATION environment variable system - Replaced complex function dispatch with direct flag detection - Consolidated 4 cache helper functions into single _ghst_cache() utility - Simplified control socket management (removed multi-step orchestration) - Subsequent connections to cached hosts are now directly executed and more reliable New additions: - If ssh-terminfo is enabled, ghostty will be wrapped to provide users with convenient commands to invoke either of the two utility functions: `ghostty ssh-cache-list` and `ghostty ssh-cache-clear` --- src/shell-integration/bash/ghostty.bash | 315 +++++------------- .../elvish/lib/ghostty-integration.elv | 311 +++++++++-------- .../ghostty-shell-integration.fish | 240 ++++++------- src/shell-integration/zsh/ghostty-integration | 222 +++++------- 4 files changed, 420 insertions(+), 668 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 46734525d..6e09ab193 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -95,252 +95,103 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then } fi -# SSH -if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then - # Cache configuration - _ghostty_cache_dir="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty" - _ghostty_cache_file="$_ghostty_cache_dir/terminfo_hosts" +# SSH Integration +if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + # Only define cache functions and variable if ssh-terminfo is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + _cache="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" - # Create cache directory with proper permissions - [[ ! -d "$_ghostty_cache_dir" ]] && mkdir -p "$_ghostty_cache_dir" && chmod 700 "$_ghostty_cache_dir" - - # Extract SSH target from arguments - _ghostty_get_ssh_target() { - local target="" - local skip_next=false - local args=("$@") - - for ((i=0; i<${#args[@]}; i++)); do - local arg="${args[i]}" - - # Skip if we're processing a flag's argument - [[ "$skip_next" == "true" ]] && { skip_next=false; continue; } - - # Handle flags that take arguments - if [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw]$ ]]; then - skip_next=true - continue - fi - - # Handle combined short flags with values (e.g., -p22) - [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw].+ ]] && continue - - # Skip other flags - [[ "$arg" =~ ^- ]] && continue - - # This should be our target - target="$arg" - break - done - - # Handle user@host format - echo "${target##*@}" - } - - # Check if host has terminfo cached - _ghostty_host_has_terminfo() { - local host="$1" - [[ -f "$_ghostty_cache_file" ]] && grep -qFx "$host" "$_ghostty_cache_file" 2>/dev/null - } - - # Add host to cache atomically - _ghostty_cache_host() { - local host="$1" - local temp_file - temp_file="$_ghostty_cache_file.$$" - - # Merge existing cache with new host - { - [[ -f "$_ghostty_cache_file" ]] && cat "$_ghostty_cache_file" - echo "$host" - } | sort -u > "$temp_file" - - # Atomic replace with proper permissions - mv -f "$temp_file" "$_ghostty_cache_file" && chmod 600 "$_ghostty_cache_file" - } - - # Remove host from cache (for maintenance) - _ghostty_uncache_host() { - local host="$1" - [[ -f "$_ghostty_cache_file" ]] || return 0 - - local temp_file="$_ghostty_cache_file.$$" - grep -vFx "$host" "$_ghostty_cache_file" > "$temp_file" 2>/dev/null || true - mv -f "$temp_file" "$_ghostty_cache_file" - } - - # Main SSH wrapper - ssh() { - case "$GHOSTTY_SSH_INTEGRATION" in - term-only) _ghostty_ssh_term_only "$@" ;; - basic) _ghostty_ssh_basic "$@" ;; - full) _ghostty_ssh_full "$@" ;; - *) _ghostty_ssh_basic "$@" ;; # Default to 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 propagation - _ghostty_ssh_basic() { - local term_value - term_value=$([[ "$TERM" == "xterm-ghostty" ]] && echo "xterm-256color" || echo "$TERM") - - builtin command ssh "$@" " - # Set environment for this session - export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' - export TERM='$term_value' - - # Start interactive shell - exec \$SHELL -l - " - } - - # Level: full - Complete integration with terminfo - _ghostty_ssh_full() { - local target - target="$(_ghostty_get_ssh_target "$@")" - - # Quick path for cached hosts - if [[ -n "$target" ]] && _ghostty_host_has_terminfo "$target"; then - # Direct connection with full ghostty support - builtin command ssh -t "$@" " - export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' - export TERM='xterm-ghostty' - exec \$SHELL -l - " - return $? - fi - - # Check if we can export terminfo - if ! builtin command -v infocmp >/dev/null 2>&1; then - echo "Warning: infocmp not found locally. Using basic integration." >&2 - _ghostty_ssh_basic "$@" - return $? - fi - - # Generate terminfo data - local terminfo_data - terminfo_data="$(infocmp -x xterm-ghostty 2>/dev/null)" || { - echo "Warning: xterm-ghostty terminfo not found locally. Using basic integration." >&2 - _ghostty_ssh_basic "$@" - return $? + # Cache operations and utilities + _ghst_cache() { + case $2 in + chk) [[ -f $_cache ]] && grep -qFx "$1" "$_cache" 2>/dev/null ;; + add) + mkdir -p "${_cache%/*}" + { + [[ -f $_cache ]] && cat "$_cache" + builtin echo "$1" + } | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" + ;; + esac } - echo "Setting up Ghostty terminal support on remote host..." >&2 + function ghostty_ssh_cache_clear() { + rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." + } - # Create control socket path - local control_path="/tmp/ghostty-ssh-${USER}-$" - trap "rm -f '$control_path'" EXIT + function ghostty_ssh_cache_list() { + [[ -s $_cache ]] && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." + } + fi - # Start control master and check/install terminfo - local setup_script=' - if ! infocmp xterm-ghostty >/dev/null 2>&1; then - if command -v tic >/dev/null 2>&1; then - mkdir -p "$HOME/.terminfo" 2>/dev/null - echo "NEEDS_INSTALL" - else - echo "NO_TIC" + # SSH wrapper + ssh() { + local e=() o=() c=() t + + # Get target + t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + + # Set up env vars first so terminfo installation inherits them + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + builtin export COLORTERM=${COLORTERM:-truecolor} TERM_PROGRAM=${TERM_PROGRAM:-ghostty} ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} + for v in COLORTERM=truecolor TERM_PROGRAM=ghostty ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION}; do + o+=(-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 + if [[ -n $t ]] && _ghst_cache "$t" chk; then + e+=(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 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 "$@" ' + 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 ]] && _ghst_cache "$t" add + e+=(TERM=xterm-ghostty) + c+=(-o "ControlPath=$cp") + ;; + *) builtin echo "Warning: Failed to install terminfo." >&2 ;; + esac fi else - echo "ALREADY_INSTALLED" + builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 fi - ' - - # First connection: Start control master and check status - local install_status - install_status=$(builtin command ssh -o ControlMaster=yes \ - -o ControlPath="$control_path" \ - -o ControlPersist=30s \ - "$@" "$setup_script") - - case "$install_status" in - "NEEDS_INSTALL") - echo "Installing xterm-ghostty terminfo..." >&2 - # Send terminfo through existing control connection - if echo "$terminfo_data" | builtin command ssh -o ControlPath="$control_path" "$@" \ - 'tic -x - 2>/dev/null && echo "SUCCESS"' | grep -q "SUCCESS"; then - echo "Terminfo installed successfully." >&2 - [[ -n "$target" ]] && _ghostty_cache_host "$target" - else - echo "Warning: Failed to install terminfo. Using basic integration." >&2 - ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true - _ghostty_ssh_basic "$@" - return $? - fi - ;; - "ALREADY_INSTALLED") - [[ -n "$target" ]] && _ghostty_cache_host "$target" - ;; - "NO_TIC") - echo "Warning: tic not found on remote host. Using basic integration." >&2 - ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true - _ghostty_ssh_basic "$@" - return $? - ;; - esac - - # Now use the existing control connection for interactive session - echo "Connecting with full Ghostty support..." >&2 - - # Pass environment through and start login shell to show MOTD - builtin command ssh -t -o ControlPath="$control_path" "$@" " - # Set up Ghostty environment - export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' - export TERM='xterm-ghostty' - - # Display MOTD if this is a fresh connection - if [[ '$install_status' == 'NEEDS_INSTALL' ]]; then - # Try to display MOTD manually - if [[ -f /etc/motd ]]; then - cat /etc/motd 2>/dev/null || true - fi - # Run update-motd if available (Ubuntu/Debian) - if [[ -d /etc/update-motd.d ]]; then - run-parts /etc/update-motd.d 2>/dev/null || true - fi - fi - - # Force a login shell - exec \$SHELL -l - " - - local exit_code=$? - - # Clean up control socket - ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true - - return $exit_code - } - - # Utility function to clear cache for a specific host - ghostty_ssh_reset() { - local host="${1:-}" - if [[ -z "$host" ]]; then - echo "Usage: ghostty_ssh_reset " >&2 - return 1 fi - _ghostty_uncache_host "$host" - echo "Cleared Ghostty terminfo cache for: $host" - } + # 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) + fi - # Utility function to list cached hosts - ghostty_ssh_list_cached() { - if [[ -f "$_ghostty_cache_file" ]]; then - echo "Hosts with cached Ghostty terminfo:" - cat "$_ghostty_cache_file" + # Execute + if [[ ${#e[@]} -gt 0 ]]; then + env "${e[@]}" ssh "${o[@]}" "${c[@]}" "$@" else - echo "No hosts cached yet." + builtin command ssh "${o[@]}" "${c[@]}" "$@" fi } + + # If 'ssh-terminfo' flag is enabled, wrap ghostty to provide 'ghostty ssh-cache-list' and `ghostty ssh-cache-clear` utility commands + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + ghostty() { + case "$1" in + ssh-cache-list) ghostty_ssh_cache_list ;; + ssh-cache-clear) ghostty_ssh_cache_clear ;; + *) builtin command ghostty "$@" ;; + esac + } + fi fi # Import bash-preexec, safe to do multiple times diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index deb258ae7..1e0c08732 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -99,176 +99,167 @@ } # SSH Integration - # Cache file for tracking hosts with terminfo installed - var ghostty-cache-file = (if (has-env GHOSTTY_RESOURCES_DIR) { put $E:GHOSTTY_RESOURCES_DIR"/terminfo_hosts" } else { put $E:HOME"/.config/ghostty/terminfo_hosts" }) + use str + use path + use re - # Extract target host from SSH arguments - fn ghostty-get-ssh-target {|@args| - var target = "" - var skip-next = $false + if (re:match 'ssh-(env|terminfo)' $E:GHOSTTY_SHELL_FEATURES) { + # Only define cache functions and variable if ssh-terminfo is enabled + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + var _cache = (path:join (or $E:XDG_STATE_HOME $E:HOME/.local/state) ghostty terminfo_hosts) - for arg $args { - if (eq $skip-next $true) { - set skip-next = $false - continue + # Cache operations and utilities + fn _ghst_cache {|target action| + if (eq $action chk) { + if (path:is-regular $_cache) { + try { + grep -qFx $target $_cache 2>/dev/null + } catch e { + fail + } + } else { + fail + } + } elif (eq $action add) { + mkdir -p (path:dir $_cache) + var tmpfile = $_cache.tmp + { + if (path:is-regular $_cache) { + cat $_cache + } + echo $target + } | sort -u > $tmpfile + mv $tmpfile $_cache + chmod 600 $_cache + } + } + + fn ghostty_ssh_cache_clear { + try { + rm -f $_cache 2>/dev/null + echo "Ghostty SSH terminfo cache cleared." + } catch e { + echo "No Ghostty SSH terminfo cache found." + } + } + + fn ghostty_ssh_cache_list { + if (and (path:is-regular $_cache) (> (wc -c < $_cache | str:trim-space) 0)) { + echo "Hosts with Ghostty terminfo installed:" + cat $_cache + } else { + echo "No cached hosts found." + } + } } - # Skip flags that take arguments - if (re:match '^-[bcDEeFIiJLlmOopQRSWw]$' $arg) { - set skip-next = $true - continue + # SSH wrapper + fn ssh {|@args| + var e = [] + var o = [] + var c = [] + var t = "" + + # Get target (only if ssh-terminfo enabled for caching) + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + 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 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) { + 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] + } + } + + # Install terminfo if needed, reuse control connection for main session + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + if (and (not-eq $t "") (try { _ghst_cache $t chk } catch e { put $false })) { + set e = [$@e TERM=xterm-ghostty] + } elif (has-external infocmp) { + var ti = "" + try { + set ti = (infocmp -x xterm-ghostty 2>/dev/null | slurp) + } catch e { + echo "Warning: xterm-ghostty terminfo not found locally." >&2 + } + if (not-eq $ti "") { + 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 ' + 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 "") { + _ghst_cache $t add + } + set e = [$@e TERM=xterm-ghostty] + set c = [$@c -o ControlPath=$cp] + } else { + echo "Warning: Failed to install terminfo." >&2 + } + } + } else { + echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + } + } + + # 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] + } + } + + # Execute + if (> (count $e) 0) { + e:env $@e e:ssh $@o $@c $@args + } else { + e:ssh $@o $@c $@args + } } - # Skip other flags - if (re:match '^-' $arg) { - continue + # Wrap ghostty command only if ssh-terminfo is enabled + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + fn ghostty {|@args| + if (eq $args[0] ssh-cache-list) { + ghostty_ssh_cache_list + } elif (eq $args[0] ssh-cache-clear) { + ghostty_ssh_cache_clear + } else { + (external ghostty) $@args + } + } + + edit:add-var ghostty~ $ghostty~ + + # Export cache functions for global use + set edit:add-var[ghostty_ssh_cache_clear] = $ghostty_ssh_cache_clear~ + set edit:add-var[ghostty_ssh_cache_list] = $ghostty_ssh_cache_list~ } - # This should be the target - set target = $arg - break - } - - put $target + # Export ssh function for global use + set edit:add-var[ssh] = $ssh~ } - # Check if host has terminfo installed - fn ghostty-host-has-terminfo {|target| - and (path:is-regular $ghostty-cache-file) ?(grep -qFx $target $ghostty-cache-file 2>/dev/null) - } - - # Add host to terminfo cache - fn ghostty-cache-host {|target| - var cache-dir = (path:dir $ghostty-cache-file) - - # Create cache directory if needed - if (not (path:is-dir $cache-dir)) { - mkdir -p $cache-dir - } - - # Atomic write to cache file - var temp-file = $ghostty-cache-file".tmp" - - { - if (path:is-regular $ghostty-cache-file) { - cat $ghostty-cache-file - } - echo $target - } | sort -u > $temp-file - - mv $temp-file $ghostty-cache-file - - # Secure permissions - ?chmod 600 $ghostty-cache-file 2>/dev/null - } - - fn ssh-with-ghostty-integration {|@args| - if (has-env 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) { - (external env) TERM=xterm-256color 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 = (conj $env-vars TERM=xterm-256color) - } - - # Propagate Ghostty shell integration environment variables - if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { - set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) - } - - # Execute with environment variables if any were set - if (> (count $env-vars) 0) { - (external env) $@env-vars ssh $@args - } else { - (external ssh) $@args - } - } - - fn ssh-full {|@args| - var target = (ghostty-get-ssh-target $@args) - - # Check if we already know this host has terminfo - if (and (not-eq "" $target) (ghostty-host-has-terminfo $target)) { - # Direct connection with xterm-ghostty - var env-vars = [TERM=xterm-ghostty] - - # Propagate Ghostty shell integration environment variables - if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { - set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) - } - - (external env) $@env-vars ssh $@args - return - } - - # Full integration: Install terminfo if needed - if (has-external infocmp) { - try { - # Install terminfo only if needed - infocmp -x xterm-ghostty 2>/dev/null | (external ssh) $@args ' - if ! infocmp xterm-ghostty >/dev/null 2>&1; then - echo "Installing Ghostty terminfo..." >&2 - tic -x - 2>/dev/null - fi - ' - echo "Connecting with full Ghostty support..." >&2 - - # Cache this host for future connections - if (not-eq "" $target) { - ghostty-cache-host $target - } - - # Connect with xterm-ghostty since terminfo is available - var env-vars = [TERM=xterm-ghostty] - - # Propagate Ghostty shell integration environment variables - if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { - set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) - } - - # Normal SSH connection with Ghostty terminfo available - (external env) $@env-vars ssh $@args - return - } catch e { - echo "Terminfo installation failed. Using basic integration." >&2 - } - } - - # Fallback to basic integration - ssh-basic $@args - } - - # Register SSH integration if enabled - if (and (has-env GHOSTTY_SSH_INTEGRATION) (has-external ssh)) { - edit:add-var ssh~ $ssh-with-ghostty-integration~ - } - defer { mark-prompt-start report-pwd 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 e01eabac3..73ccf9874 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 @@ -86,156 +86,116 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # SSH integration - if test -n "$GHOSTTY_SSH_INTEGRATION" - # Cache file for tracking hosts with terminfo installed - set --local _ghostty_cache_file (string join / (test -n "$GHOSTTY_RESOURCES_DIR"; and echo "$GHOSTTY_RESOURCES_DIR"; or echo "$HOME/.config/ghostty") "terminfo_hosts") + # SSH Integration + if string match -qr 'ssh-(env|terminfo)' "$GHOSTTY_SHELL_FEATURES" + # Only define cache functions and variable if ssh-terminfo is enabled + if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" + set -g _cache (test -n "$XDG_STATE_HOME" && echo "$XDG_STATE_HOME" || echo "$HOME/.local/state")/ghostty/terminfo_hosts - # Extract target host from SSH arguments - function _ghostty_get_ssh_target - set --local target "" - set --local skip_next "false" - - for arg in $argv - if test "$skip_next" = "true" - set skip_next "false" - continue + # Cache operations and utilities + function _ghst_cache + switch $argv[2] + case chk + test -f $_cache && grep -qFx "$argv[1]" "$_cache" 2>/dev/null + case add + mkdir -p (dirname "$_cache") + begin + test -f $_cache && cat "$_cache" + builtin echo "$argv[1]" + end | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" end + end - # Skip flags that take arguments - if string match -qr -- '^-[bcDEeFIiJLlmOopQRSWw]$' "$arg" - set skip_next "true" - continue + function ghostty_ssh_cache_clear -d "Clear Ghostty SSH terminfo cache" + rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." + end + + function ghostty_ssh_cache_list -d "List hosts with Ghostty terminfo installed" + test -s $_cache && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." + end + end + + # SSH wrapper + function ssh + set -l e + set -l o + set -l c + set -l t + + # Get target (only if ssh-terminfo enabled for caching) + if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" + set t (builtin command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + end + + # 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" + + for v in COLORTERM=truecolor TERM_PROGRAM=ghostty (test -n "$GHOSTTY_VERSION" && echo "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION") + set -l varname (string split -m1 '=' "$v")[1] + set o $o -o "SendEnv $varname" -o "SetEnv $v" end - - # Skip other flags - if string match -q -- '-*' "$arg" - continue - end - - # This should be the target - set target "$arg" - break end - echo "$target" - end - - # Check if host has terminfo installed - function _ghostty_host_has_terminfo - set --local target "$argv[1]" - test -f "$_ghostty_cache_file"; and grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null - end - - # Add host to terminfo cache - function _ghostty_cache_host - set --local target "$argv[1]" - set --local cache_dir (dirname "$_ghostty_cache_file") - - # Create cache directory if needed - test -d "$cache_dir"; or mkdir -p "$cache_dir" - - # Atomic write to cache file - begin - if test -f "$_ghostty_cache_file" - cat "$_ghostty_cache_file" - end - echo "$target" - end | sort -u > "$_ghostty_cache_file.tmp"; and mv "$_ghostty_cache_file.tmp" "$_ghostty_cache_file" - - # Secure permissions - chmod 600 "$_ghostty_cache_file" 2>/dev/null - end - - # Wrap `ssh` command to provide Ghostty SSH integration - 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 builtin command ssh $argv - else - builtin 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 --append env_vars TERM=xterm-256color - end - - # Propagate Ghostty shell integration environment variables - if test -n "$GHOSTTY_SHELL_FEATURES" - set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" - end - - # Execute with environment variables if any were set - if test (count $env_vars) -gt 0 - env $env_vars ssh $argv - else - builtin command ssh $argv - end - end - - # Level: full - All features - function _ghostty_ssh_full - set --local target (_ghostty_get_ssh_target $argv) - - # Check if we already know this host has terminfo - if test -n "$target"; and _ghostty_host_has_terminfo "$target" - # Direct connection with xterm-ghostty - set --local env_vars TERM=xterm-ghostty - if test -n "$GHOSTTY_SHELL_FEATURES" - set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" - end - env $env_vars ssh $argv - return 0 - end - - # Full integration: Install terminfo if needed - if type -q infocmp - # Install terminfo only if needed - if infocmp -x xterm-ghostty 2>/dev/null | ssh $argv ' - if ! infocmp xterm-ghostty >/dev/null 2>&1 - echo "Installing Ghostty terminfo..." >&2 - tic -x - 2>/dev/null + # Install terminfo if needed, reuse control connection for main session + if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" + if test -n "$t" && _ghst_cache "$t" chk + set e $e 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 ' + 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" && _ghst_cache "$t" add + set e $e TERM=xterm-ghostty + set c $c -o "ControlPath=$cp" + case '*' + builtin echo "Warning: Failed to install terminfo." >&2 + end end - ' - echo "Connecting with full Ghostty support..." >&2 - - # Cache this host for future connections - test -n "$target"; and _ghostty_cache_host "$target" - - # Connect with xterm-ghostty since terminfo is available - set --local env_vars TERM=xterm-ghostty - if test -n "$GHOSTTY_SHELL_FEATURES" - set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" - end - env $env_vars ssh $argv - builtin return 0 + else + builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 end - echo "Terminfo installation failed. Using basic integration." >&2 end - # Fallback to basic integration - _ghostty_ssh_basic $argv + # 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 + end + end + + # Execute + if test (count $e) -gt 0 + env $e ssh $o $c $argv + else + builtin command ssh $o $c $argv + end + end + + # Wrap ghostty command only if ssh-terminfo is enabled + if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" + function ghostty -d "Wrap ghostty to provide cache management commands" + switch "$argv[1]" + case ssh-cache-list + ghostty_ssh_cache_list + case ssh-cache-clear + ghostty_ssh_cache_clear + case "*" + command ghostty $argv + end + end end end diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index c172fc02d..b3c604c83 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -244,156 +244,106 @@ _ghostty_deferred_init() { } fi - # SSH - if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then - # Cache file for tracking hosts with terminfo installed - _ghostty_cache_file="${GHOSTTY_RESOURCES_DIR:-$HOME/.config/ghostty}/terminfo_hosts" + # SSH Integration + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + # Only define cache functions and variable if ssh-terminfo is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + _cache="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" - # Extract target host from SSH arguments - _ghostty_get_ssh_target() { - local target="" - local skip_next=false + # Cache operations and utilities + _ghst_cache() { + case $2 in + chk) [[ -f $_cache ]] && grep -qFx "$1" "$_cache" 2>/dev/null ;; + add) + mkdir -p "${_cache:h}" + { + [[ -f $_cache ]] && cat "$_cache" + builtin echo "$1" + } | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" + ;; + esac + } - for arg in "$@"; do - if [[ "$skip_next" == "true" ]]; then - skip_next=false - continue - fi + ghostty_ssh_cache_clear() { + rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." + } - # Skip flags that take arguments - if [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw]$ ]]; then - skip_next=true - continue - fi + ghostty_ssh_cache_list() { + [[ -s $_cache ]] && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." + } + fi - # Skip other flags - if [[ "$arg" =~ ^- ]]; then - continue - fi - - # This should be the target - target="$arg" - break - done - - echo "$target" - } - - # Check if host has terminfo installed - _ghostty_host_has_terminfo() { - local target="$1" - [[ -f "$_ghostty_cache_file" ]] && grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null - } - - # Add host to terminfo cache - _ghostty_cache_host() { - local target="$1" - local cache_dir - cache_dir="$(dirname "$_ghostty_cache_file")" - - # Create cache directory if needed - [[ ! -d "$cache_dir" ]] && mkdir -p "$cache_dir" - - # Atomic write to cache file - { - if [[ -f "$_ghostty_cache_file" ]]; then - cat "$_ghostty_cache_file" - fi - echo "$target" - } | sort -u > "$_ghostty_cache_file.tmp" && mv "$_ghostty_cache_file.tmp" "$_ghostty_cache_file" - - # Secure permissions - chmod 600 "$_ghostty_cache_file" 2>/dev/null - } - - # Wrap `ssh` command to provide Ghostty SSH integration + # SSH wrapper 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 - } + local -a e o c + local t - # 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() { - local env_vars=() - - # Fix TERM compatibility - if [[ "$TERM" == "xterm-ghostty" ]]; then - env_vars+=("TERM=xterm-256color") + # Get target (only if ssh-terminfo enabled for caching) + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') fi - # Propagate Ghostty shell integration environment variables - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - - # 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() { - local target - target="$(_ghostty_get_ssh_target "$@")" - - # Check if we already know this host has terminfo - if [[ -n "$target" ]] && _ghostty_host_has_terminfo "$target"; then - # Direct connection with xterm-ghostty - local env_vars=("TERM=xterm-ghostty") - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - env "${env_vars[@]}" ssh "$@" - return 0 + # Set up env vars first so terminfo installation inherits them + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + builtin export COLORTERM=${COLORTERM:-truecolor} TERM_PROGRAM=${TERM_PROGRAM:-ghostty} ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} + for v in COLORTERM=truecolor TERM_PROGRAM=ghostty ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION}; do + o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + done fi - # Full integration: Install terminfo if needed - if builtin command -v infocmp >/dev/null 2>&1; then - # Install terminfo only if needed - if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" ' - if ! infocmp xterm-ghostty >/dev/null 2>&1; then - echo "Installing Ghostty terminfo..." >&2 - tic -x - 2>/dev/null - fi - '; then - echo "Connecting with full Ghostty support..." >&2 - - # Cache this host for future connections - [[ -n "$target" ]] && _ghostty_cache_host "$target" - - # Connect with xterm-ghostty since terminfo is available - local env_vars=("TERM=xterm-ghostty") - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - env "${env_vars[@]}" ssh "$@" - builtin return 0 + # Install terminfo if needed, reuse control connection for main session + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + if [[ -n $t ]] && _ghst_cache "$t" chk; 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 "$@" ' + 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 ]] && _ghst_cache "$t" add + e+=(TERM=xterm-ghostty) + c+=(-o "ControlPath=$cp") + ;; + *) builtin echo "Warning: Failed to install terminfo." >&2 ;; + esac + fi + else + builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 fi - echo "Terminfo installation failed. Using basic integration." >&2 fi - # Fallback to basic integration - _ghostty_ssh_basic "$@" + # 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) + fi + + # Execute + if (( ${#e} > 0 )); then + env "${e[@]}" ssh "${o[@]}" "${c[@]}" "$@" + else + builtin command ssh "${o[@]}" "${c[@]}" "$@" + fi } + + # Wrap ghostty command only if ssh-terminfo is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + ghostty() { + case "$1" in + ssh-cache-list) ghostty_ssh_cache_list ;; + ssh-cache-clear) ghostty_ssh_cache_clear ;; + *) builtin command ghostty "$@" ;; + esac + } + fi fi # Some zsh users manually run `source ~/.zshrc` in order to apply rc file