From 45ead5ea99bdf08686d3b7cd2e0e2c005ca91c4a Mon Sep 17 00:00:00 2001 From: David Matos Date: Sun, 16 Nov 2025 09:20:29 +0100 Subject: [PATCH] Provide shell ssh integration for nushell --- src/shell-integration/README.md | 13 ++ .../vendor/autoload/ghostty-integration.nu | 114 ++++++++++++++++++ src/termio/shell_integration.zig | 30 +++-- 3 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index 1fd11091d..5ef1106af 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -88,3 +88,16 @@ if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration fi ``` + +### Nushell + +For `nushell` Ghostty prepends to the `XDG_DATA_DIRS` directory. Nushell automatically +loads configuration files in `/nushell/vendor/autoload/*.nu` on startup. These +directories are represented in `Nu` by `$nu.vendor-autoload-dirs`. For more details see + +[Nushell documentation](https://www.nushell.sh/book/configuration.html#configuration-overview) + +> [!NOTE] +> +> Ghostty only prepends to `XDG_DATA_DIRS` in the case where the `ssh-*` features are enabled. +> There is no shell integration for the other currently supported features. diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu b/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu new file mode 100644 index 000000000..167f9ca21 --- /dev/null +++ b/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu @@ -0,0 +1,114 @@ +# Enables SSH environment variable compatibility. +# Converts TERM from xterm-ghostty to xterm-256color +# and propagates COLORTERM, TERM_PROGRAM, and TERM_PROGRAM_VERSION +# check your sshd_config on remote host to see if these variables are accepted +def set_ssh_env []: nothing -> record> { + return {ssh_term: "xterm-256color", ssh_opts: ["-o", "SetEnv COLORTERM=truecolor", "-o", "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"]} +} + +# Enables 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. +# Caches installations to avoid repeat installations. +def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { + mut ssh_opts = $ssh_opts + let ssh_cfg = ( + run-external "ssh" "-G" ...($ssh_args) + | lines + | each {|e| if $e =~ '\buser\b' or $e =~ '\bhostname\b' {split row ' '}} + ) + let ssh_user = $ssh_cfg.0.1 + let ssh_hostname = $ssh_cfg.1.1 + let ssh_id = $"($ssh_user)@($ssh_hostname)" + let ghostty_bin = $env.GHOSTTY_BIN_DIR + "/ghostty" + let check_cache_cmd = [$ghostty_bin, "+ssh-cache", $"--host=($ssh_id)"] + + let is_cached = ( + run-external ...$check_cache_cmd + | complete + | get exit_code + | $in == 0 + ) + + if not $is_cached { + let ssh_opts_copy = $ssh_opts + let terminfo_data = try {infocmp -0 -x xterm-ghostty} catch { + print "Warning: Could not generate terminfo data." + return {ssh_term: "xterm-256color", ssh_opts: $ssh_opts_copy} + } + + print $"Setting up xterm-ghostty terminfo on ($ssh_hostname)..." + + let tmp_dir = $"/tmp/ghostty-ssh-($ssh_user).XXXXXX" + let mktemp_cmd = ["mktemp", "-d", $tmp_dir] + let ctrl_dir = try { + run-external ...$mktemp_cmd + | str trim + } catch { + $"/tmp/ghostty-ssh-($ssh_user).($nu.pid)" + } + let ctrl_path = $"($ctrl_dir)/socket" + + let master_parts = $ssh_opts ++ ["-o", "ControlMaster=yes", "-o", $"ControlPath=($ctrl_path)", "-o", "ControlPersist=60s"] ++ $ssh_args + + let infocmp_cmd = ["ssh"] ++ $master_parts ++ ["infocmp", "xterm-ghostty"] + + let terminfo_present = ( + run-external ...$infocmp_cmd + | complete + | get exit_code + | $in == 0 + ) + + if (not $terminfo_present) { + let install_terminfo_cmd = ["ssh"] ++ $master_parts ++ ["mkdir", "-p", "~/.terminfo", "&&", "tic", "-x", "-"] + + ($terminfo_data | run-external ...$install_terminfo_cmd) | complete | get exit_code | if $in != 0 { + print "Warning: Failed to install terminfo." + return {ssh_term: "xterm-256color", ssh_opts: $ssh_opts} + } + let state_dir = try { $env.XDG_STATE_HOME } catch { $env.HOME | path join ".local/state" } + let ghostty_state_dir = $state_dir | path join "ghostty" + + let cache_add_cmd = [$ghostty_bin, "+ssh-cache", $"--add=($ssh_id)"] + # Bug?: If I dont add TMPDIR, it complains about renameacrossmountpoints + with-env { TMPDIR: $ghostty_state_dir } { + run-external ...$cache_add_cmd o+e>| ignore + } + } + $ssh_opts ++= ["-o", $"ControlPath=($ctrl_path)"] + } + + return {ssh_term: "xterm-ghostty", ssh_opts: $ssh_opts} +} + +# SSH Integration +export def --wrapped ssh [...ssh_args: string] { + if ($ssh_args | is-empty) { + run-external "ssh" + } + mut session = {ssh_term: "", ssh_opts: []} + let shell_features = $env.GHOSTTY_SHELL_FEATURES | split row ',' + + if "ssh-env" in $shell_features { + $session = set_ssh_env + } + if "ssh-terminfo" in $shell_features { + $session = set_ssh_terminfo $session.ssh_opts $ssh_args + } + + let ssh_parts = $session.ssh_opts ++ $ssh_args + with-env { TERM: $session.ssh_term } { + run-external "ssh" ...$ssh_parts + } +} + +# Removes Ghostty's data directory from XDG_DATA_DIRS +let ghostty_data_dir = $env.GHOSTTY_SHELL_INTEGRATION_XDG_DIR +$env.XDG_DATA_DIRS = $env.XDG_DATA_DIRS + | split row ':' + | where ( + | $it !~ $ghostty_data_dir + ) + | str join ':' diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 8b2648dbd..afe1f547f 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -68,6 +68,7 @@ pub fn setup( command, env, exe, + features, ); // Setup our feature env vars @@ -76,13 +77,15 @@ pub fn setup( return result; } -fn setupShell( - alloc_arena: Allocator, - resource_dir: []const u8, - command: config.Command, - env: *EnvMap, - exe: []const u8, -) !?ShellIntegration { +fn setupShell(alloc_arena: Allocator, resource_dir: []const u8, command: config.Command, env: *EnvMap, exe: []const u8, features: config.ShellIntegrationFeatures) !?ShellIntegration { + if (std.mem.eql(u8, "nu", exe)) { + try setupNu(alloc_arena, resource_dir, env, features); + return null; + // // return .{ + // // .shell = .nu, + // // .command = try command.clone(alloc_arena), + // // }; + } if (std.mem.eql(u8, "bash", exe)) { // Apple distributes their own patched version of Bash 3.2 // on macOS that disables the ENV-based POSIX startup path. @@ -652,6 +655,19 @@ test "xdg: existing XDG_DATA_DIRS" { try testing.expectEqualStrings("./shell-integration:/opt/share", env.get("XDG_DATA_DIRS").?); } +/// Setup the nushell shell integration. This works by setting +/// XDG_DATA_DIRS so that it can be picked automatically by +/// nushell on startup. +/// Only implements `ssh-*` shell features. Rest are not supported. +fn setupNu(alloc_arena: Allocator, resource_dir: []const u8, env: *EnvMap, features: config.ShellIntegrationFeatures) !void { + // This makes sure that `Nu` loads our integration file + // and wraps the `ssh` function only if the `ssh` features + // are enabled. + // Otherwise, it does not do anything. + if (features.@"ssh-env" or features.@"ssh-terminfo") { + try setupXdgDataDirs(alloc_arena, resource_dir, env); + } +} /// Setup the zsh automatic shell integration. This works by setting /// ZDOTDIR to our resources dir so that zsh will load our config. This /// config then loads the true user config.