From 45ead5ea99bdf08686d3b7cd2e0e2c005ca91c4a Mon Sep 17 00:00:00 2001 From: David Matos Date: Sun, 16 Nov 2025 09:20:29 +0100 Subject: [PATCH 01/10] 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. From 52f94f445d5ca3890da9c7821c09afc4330da3a1 Mon Sep 17 00:00:00 2001 From: David Matos Date: Tue, 18 Nov 2025 17:22:31 +0100 Subject: [PATCH 02/10] Use ^ssh directly rather than run-external Modify README --- src/shell-integration/README.md | 3 +- .../vendor/autoload/ghostty-integration.nu | 42 +++++++++---------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index 5ef1106af..8a01ed625 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -100,4 +100,5 @@ directories are represented in `Nu` by `$nu.vendor-autoload-dirs`. For more deta > [!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. +> Nushell supports most features out of the box, so other shell integration features are not +> necessary. diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu b/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu index 167f9ca21..1a1b377de 100644 --- a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu +++ b/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu @@ -13,19 +13,20 @@ def set_ssh_env []: nothing -> record> # 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) + let ssh_cfg = ^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 + | split column -n 2 " " key value + | where key == "user" or key == "hostname" + | transpose -r + | into record + let ssh_user = $ssh_cfg.user + let ssh_hostname = $ssh_cfg.hostname 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 check_cache_cmd = ["+ssh-cache", $"--host=($ssh_id)"] let is_cached = ( - run-external ...$check_cache_cmd + ^$ghostty_bin ...$check_cache_cmd | complete | get exit_code | $in == 0 @@ -40,41 +41,40 @@ def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { 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 + mktemp -td $"ghostty-ssh-($ssh_user).XXXXXX" } catch { - $"/tmp/ghostty-ssh-($ssh_user).($nu.pid)" + $"/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 infocmp_cmd = $master_parts ++ ["infocmp", "xterm-ghostty"] let terminfo_present = ( - run-external ...$infocmp_cmd + ^ssh ...$infocmp_cmd | complete | get exit_code | $in == 0 ) if (not $terminfo_present) { - let install_terminfo_cmd = ["ssh"] ++ $master_parts ++ ["mkdir", "-p", "~/.terminfo", "&&", "tic", "-x", "-"] + let install_terminfo_cmd = $master_parts ++ ["mkdir", "-p", "~/.terminfo", "&&", "tic", "-x", "-"] - ($terminfo_data | run-external ...$install_terminfo_cmd) | complete | get exit_code | if $in != 0 { + ($terminfo_data | ^ssh ...$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)"] + let cache_add_cmd = ["+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 + ^$ghostty_bin ...$cache_add_cmd o+e>| ignore } } $ssh_opts ++= ["-o", $"ControlPath=($ctrl_path)"] @@ -86,7 +86,7 @@ def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { # SSH Integration export def --wrapped ssh [...ssh_args: string] { if ($ssh_args | is-empty) { - run-external "ssh" + return (^ssh) } mut session = {ssh_term: "", ssh_opts: []} let shell_features = $env.GHOSTTY_SHELL_FEATURES | split row ',' @@ -100,7 +100,7 @@ export def --wrapped ssh [...ssh_args: string] { let ssh_parts = $session.ssh_opts ++ $ssh_args with-env { TERM: $session.ssh_term } { - run-external "ssh" ...$ssh_parts + ^ssh ...$ssh_parts } } From a5218198823f7e6994cf3b2a47d2919d3f394691 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 3 Dec 2025 22:04:52 +0100 Subject: [PATCH 03/10] Address changes fixes logic bug when adding cache --- .../vendor/autoload/ghostty-integration.nu | 76 ++++++++----------- src/termio/shell_integration.zig | 13 ++-- 2 files changed, 38 insertions(+), 51 deletions(-) diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu b/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu index 1a1b377de..77f7fb64d 100644 --- a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu +++ b/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu @@ -14,22 +14,21 @@ def set_ssh_env []: nothing -> record> def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { mut ssh_opts = $ssh_opts let ssh_cfg = ^ssh -G ...($ssh_args) - | lines - | split column -n 2 " " key value - | where key == "user" or key == "hostname" - | transpose -r + | lines + | parse "{key} {value}" + | where key in ["user", "hostname"] + | select key value + | transpose -rd | into record - let ssh_user = $ssh_cfg.user - let ssh_hostname = $ssh_cfg.hostname - let ssh_id = $"($ssh_user)@($ssh_hostname)" - let ghostty_bin = $env.GHOSTTY_BIN_DIR + "/ghostty" - let check_cache_cmd = ["+ssh-cache", $"--host=($ssh_id)"] + | default { user: $env.USER, hostname: "localhost" } + + let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)" + let ghostty_bin = $env.GHOSTTY_BIN_DIR | path join "ghostty" let is_cached = ( - ^$ghostty_bin ...$check_cache_cmd + ^$ghostty_bin ...(["+ssh-cache", $"--host=($ssh_id)"]) | complete - | get exit_code - | $in == 0 + | $in.exit_code == 0 ) if not $is_cached { @@ -39,44 +38,36 @@ def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { return {ssh_term: "xterm-256color", ssh_opts: $ssh_opts_copy} } - print $"Setting up xterm-ghostty terminfo on ($ssh_hostname)..." + print $"Setting up xterm-ghostty terminfo on ($ssh_cfg.hostname)..." - let ctrl_dir = try { - mktemp -td $"ghostty-ssh-($ssh_user).XXXXXX" - } catch { - $"/tmp/ghostty-ssh-($ssh_user).($nu.pid)" - } - - let ctrl_path = $"($ctrl_dir)/socket" + let ctrl_path = ( + try { + mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX" + } catch { + $"/tmp/ghostty-ssh-($ssh_cfg.user).($nu.pid)" + } | path join "socket" + ) let master_parts = $ssh_opts ++ ["-o", "ControlMaster=yes", "-o", $"ControlPath=($ctrl_path)", "-o", "ControlPersist=60s"] ++ $ssh_args - let infocmp_cmd = $master_parts ++ ["infocmp", "xterm-ghostty"] - let terminfo_present = ( - ^ssh ...$infocmp_cmd + ^ssh ...($master_parts ++ ["infocmp", "xterm-ghostty"]) | complete - | get exit_code - | $in == 0 + | $in.exit_code == 0 ) if (not $terminfo_present) { - let install_terminfo_cmd = $master_parts ++ ["mkdir", "-p", "~/.terminfo", "&&", "tic", "-x", "-"] - - ($terminfo_data | ^ssh ...$install_terminfo_cmd) | complete | get exit_code | if $in != 0 { + ( + $terminfo_data + | ^ssh ...($master_parts ++ ["mkdir", "-p", "~/.terminfo", "&&", "tic", "-x", "-"]) + ) + | complete + | if $in.exit_code != 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 = ["+ssh-cache", $"--add=($ssh_id)"] - - # Bug?: If I dont add TMPDIR, it complains about renameacrossmountpoints - with-env { TMPDIR: $ghostty_state_dir } { - ^$ghostty_bin ...$cache_add_cmd o+e>| ignore } } + ^$ghostty_bin ...(["+ssh-cache", $"--add=($ssh_id)"]) o+e>| ignore $ssh_opts ++= ["-o", $"ControlPath=($ctrl_path)"] } @@ -99,16 +90,15 @@ export def --wrapped ssh [...ssh_args: string] { } let ssh_parts = $session.ssh_opts ++ $ssh_args - with-env { TERM: $session.ssh_term } { + with-env {TERM: $session.ssh_term} { ^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 +$env.XDG_DATA_DIRS = ( + $env.XDG_DATA_DIRS | split row ':' - | where ( - | $it !~ $ghostty_data_dir - ) + | where {|path| $path != $env.GHOSTTY_SHELL_INTEGRATION_XDG_DIR } | str join ':' +) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index afe1f547f..570ec85cc 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -78,14 +78,6 @@ pub fn setup( } 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. @@ -132,6 +124,11 @@ fn setupShell(alloc_arena: Allocator, resource_dir: []const u8, command: config. }; } + if (std.mem.eql(u8, "nu", exe)) { + try setupNu(alloc_arena, resource_dir, env, features); + return null; + } + if (std.mem.eql(u8, "zsh", exe)) { try setupZsh(resource_dir, env); return .{ From c0ce4ef44f9de914de31d94d4ca13b69517e1b03 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 3 Dec 2025 22:31:25 +0100 Subject: [PATCH 04/10] Retain original fmt --- src/termio/shell_integration.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index b93f2ea31..fcbdaef6a 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -77,7 +77,14 @@ pub fn setup( return result; } -fn setupShell(alloc_arena: Allocator, resource_dir: []const u8, command: config.Command, env: *EnvMap, exe: []const u8, features: config.ShellIntegrationFeatures) !?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, "bash", exe)) { // Apple distributes their own patched version of Bash 3.2 // on macOS that disables the ENV-based POSIX startup path. From d69e16c168d2270081d36b7c9def1a6c08538f8a Mon Sep 17 00:00:00 2001 From: David Matos Date: Thu, 4 Dec 2025 13:08:04 +0100 Subject: [PATCH 05/10] Use external cmd Remove redundant `into record` --- .../nushell/vendor/autoload/ghostty-integration.nu | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu b/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu index 77f7fb64d..f8e2c3e16 100644 --- a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu +++ b/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu @@ -19,7 +19,6 @@ def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { | where key in ["user", "hostname"] | select key value | transpose -rd - | into record | default { user: $env.USER, hostname: "localhost" } let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)" @@ -33,7 +32,7 @@ def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { if not $is_cached { let ssh_opts_copy = $ssh_opts - let terminfo_data = try {infocmp -0 -x xterm-ghostty} catch { + 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} } From 07f4ef8e4778fcc4ac962c5b3058ed070fd59bfc Mon Sep 17 00:00:00 2001 From: David Matos Date: Sat, 6 Dec 2025 00:48:14 +0100 Subject: [PATCH 06/10] Alternative approach by unconditionally setting xdgDataDirs and checking features with nu --- src/shell-integration/README.md | 26 ++++++----- ...egration.nu => ghostty-ssh-integration.nu} | 43 +++++++++++-------- .../vendor/autoload/bootstrap-integration.nu | 29 +++++++++++++ .../vendor/autoload/source-integration.nu | 11 +++++ src/termio/shell_integration.zig | 19 ++------ 5 files changed, 79 insertions(+), 49 deletions(-) rename src/shell-integration/nushell/{vendor/autoload/ghostty-integration.nu => ghostty-ssh-integration.nu} (73%) create mode 100644 src/shell-integration/nushell/vendor/autoload/bootstrap-integration.nu create mode 100644 src/shell-integration/nushell/vendor/autoload/source-integration.nu diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index 50c01344b..bd1702dde 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -76,6 +76,18 @@ allowing us to automatically integrate with the shell. For details on the Fish startup process, see the [Fish documentation](https://fishshell.com/docs/current/language.html). +### 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 implements concretely the `ssh-*` features. The rest of the features are supported mostly out of the box by Nushell. + ### Zsh For `zsh`, Ghostty sets `ZDOTDIR` so that it loads our configuration @@ -90,17 +102,3 @@ fi ``` Shell integration requires Zsh 5.1+. - -### 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. -> Nushell supports most features out of the box, so other shell integration features are not -> necessary. diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu b/src/shell-integration/nushell/ghostty-ssh-integration.nu similarity index 73% rename from src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu rename to src/shell-integration/nushell/ghostty-ssh-integration.nu index f8e2c3e16..495b96c78 100644 --- a/src/shell-integration/nushell/vendor/autoload/ghostty-integration.nu +++ b/src/shell-integration/nushell/ghostty-ssh-integration.nu @@ -3,7 +3,10 @@ # 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"]} + 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. @@ -11,7 +14,10 @@ def set_ssh_env []: nothing -> record> # 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] { +def set_ssh_terminfo [ + ssh_opts: list, + ssh_args: list +]: [nothing -> record>] { mut ssh_opts = $ssh_opts let ssh_cfg = ^ssh -G ...($ssh_args) | lines @@ -47,25 +53,24 @@ def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { } | path join "socket" ) - let master_parts = $ssh_opts ++ ["-o", "ControlMaster=yes", "-o", $"ControlPath=($ctrl_path)", "-o", "ControlPersist=60s"] ++ $ssh_args + let master_parts = $ssh_opts ++ ["-o" "ControlMaster=yes" "-o" $"ControlPath=($ctrl_path)" "-o" "ControlPersist=60s"] ++ $ssh_args - let terminfo_present = ( - ^ssh ...($master_parts ++ ["infocmp", "xterm-ghostty"]) - | complete - | $in.exit_code == 0 + ($terminfo_data) | ^ssh ...( + $master_parts ++ + [ + ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1' + ] ) - - if (not $terminfo_present) { - ( - $terminfo_data - | ^ssh ...($master_parts ++ ["mkdir", "-p", "~/.terminfo", "&&", "tic", "-x", "-"]) - ) - | complete - | if $in.exit_code != 0 { - print "Warning: Failed to install terminfo." - return {ssh_term: "xterm-256color", ssh_opts: $ssh_opts} - } + | complete + | if $in.exit_code != 0 { + print "Warning: Failed to install terminfo." + return {ssh_term: "xterm-256color" ssh_opts: $ssh_opts} } + ^$ghostty_bin ...(["+ssh-cache", $"--add=($ssh_id)"]) o+e>| ignore $ssh_opts ++= ["-o", $"ControlPath=($ctrl_path)"] } @@ -74,7 +79,7 @@ def set_ssh_terminfo [ssh_opts: list, ssh_args: list] { } # SSH Integration -export def --wrapped ssh [...ssh_args: string] { +export def --wrapped ssh [...ssh_args: string]: any -> any { if ($ssh_args | is-empty) { return (^ssh) } diff --git a/src/shell-integration/nushell/vendor/autoload/bootstrap-integration.nu b/src/shell-integration/nushell/vendor/autoload/bootstrap-integration.nu new file mode 100644 index 000000000..317ba62d3 --- /dev/null +++ b/src/shell-integration/nushell/vendor/autoload/bootstrap-integration.nu @@ -0,0 +1,29 @@ +let enable_integration = $env.GHOSTTY_SHELL_FEATURES | split row ',' + | where ($it in ["ssh-env" "ssh-terminfo"]) + | is-not-empty + +let ghostty_ssh_file = $env.GHOSTTY_RESOURCES_DIR + | path join "shell-integration" "nushell" "ghostty-ssh-integration.nu" + +let ssh_integration_file = $nu.data-dir | path join "ghostty-ssh-integration.nu" +let ssh_file_exists = $ssh_integration_file | path exists + +# TOD0: In case of an update to the `ghostty-ssh-integration.nu` file +# the file wont be updated here, so we need to support +# saving the new file once there is an update + +match [$enable_integration $ssh_file_exists] { + [true false] => { + # $nu.data-dir is not created by default + # https://www.nushell.sh/book/configuration.html#startup-variables + $nu.data-dir | path exists | if (not $in) { mkdir $nu.data-dir } + open $ghostty_ssh_file | save $ssh_integration_file + } + [false true] => { + # We need to check if the user disabled `ssh-integration` and thus + # the integration file needs to be removed so it doesnt get sourced by + # the `source-integration.nu` file + rm $ssh_integration_file + } + _ => { } +} diff --git a/src/shell-integration/nushell/vendor/autoload/source-integration.nu b/src/shell-integration/nushell/vendor/autoload/source-integration.nu new file mode 100644 index 000000000..1c21833a4 --- /dev/null +++ b/src/shell-integration/nushell/vendor/autoload/source-integration.nu @@ -0,0 +1,11 @@ +# Sourcing the `ghostty-integration.nu` cant be on the +# `bootstrap-integration.nu` file because it tries to resolve the `sourced` +# file at parsing time, which would make it source nothing. + +# But here we rely on the fact that `boostrap-integration.nu` gets parsed +# and executed first, and then we can count on `ssh_integration_file` being available + +#https://www.nushell.sh/book/thinking_in_nu.html#example-dynamically-generating-source + +const ssh_integration_file = $nu.data-dir | path join "ghostty-ssh-integration.nu" +source (if ($ssh_integration_file | path exists) { $ssh_integration_file } else { null }) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index fcbdaef6a..8a03945c9 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -68,7 +68,6 @@ pub fn setup( command, env, exe, - features, ); // Setup our feature env vars @@ -83,7 +82,6 @@ fn setupShell( command: config.Command, env: *EnvMap, exe: []const u8, - features: config.ShellIntegrationFeatures, ) !?ShellIntegration { if (std.mem.eql(u8, "bash", exe)) { // Apple distributes their own patched version of Bash 3.2 @@ -132,7 +130,9 @@ fn setupShell( } if (std.mem.eql(u8, "nu", exe)) { - try setupNu(alloc_arena, resource_dir, env, features); + // Sets up XDG_DATA_DIRS so that it can be picked automatically by + // nushell on startup. + try setupXdgDataDirs(alloc_arena, resource_dir, env); return null; } @@ -659,19 +659,6 @@ 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. From b87e8d81721ded4fcf2f5cdd3887480909022e7b Mon Sep 17 00:00:00 2001 From: David Matos Date: Wed, 21 Jan 2026 13:19:53 +0100 Subject: [PATCH 07/10] Update to new nu ssh ghostty integration --- .../nushell/ghostty-ssh-integration.nu | 108 ---------------- .../vendor/autoload/bootstrap-integration.nu | 29 ----- .../nushell/vendor/autoload/ghostty.nu | 120 ++++++++++++++++-- .../vendor/autoload/source-integration.nu | 11 -- 4 files changed, 111 insertions(+), 157 deletions(-) delete mode 100644 src/shell-integration/nushell/ghostty-ssh-integration.nu delete mode 100644 src/shell-integration/nushell/vendor/autoload/bootstrap-integration.nu delete mode 100644 src/shell-integration/nushell/vendor/autoload/source-integration.nu diff --git a/src/shell-integration/nushell/ghostty-ssh-integration.nu b/src/shell-integration/nushell/ghostty-ssh-integration.nu deleted file mode 100644 index 495b96c78..000000000 --- a/src/shell-integration/nushell/ghostty-ssh-integration.nu +++ /dev/null @@ -1,108 +0,0 @@ -# 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 -]: [nothing -> record>] { - mut ssh_opts = $ssh_opts - let ssh_cfg = ^ssh -G ...($ssh_args) - | lines - | parse "{key} {value}" - | where key in ["user", "hostname"] - | select key value - | transpose -rd - | default { user: $env.USER, hostname: "localhost" } - - let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)" - let ghostty_bin = $env.GHOSTTY_BIN_DIR | path join "ghostty" - - let is_cached = ( - ^$ghostty_bin ...(["+ssh-cache", $"--host=($ssh_id)"]) - | complete - | $in.exit_code == 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_cfg.hostname)..." - - let ctrl_path = ( - try { - mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX" - } catch { - $"/tmp/ghostty-ssh-($ssh_cfg.user).($nu.pid)" - } | path join "socket" - ) - - let master_parts = $ssh_opts ++ ["-o" "ControlMaster=yes" "-o" $"ControlPath=($ctrl_path)" "-o" "ControlPersist=60s"] ++ $ssh_args - - ($terminfo_data) | ^ssh ...( - $master_parts ++ - [ - ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1' - ] - ) - | complete - | if $in.exit_code != 0 { - print "Warning: Failed to install terminfo." - return {ssh_term: "xterm-256color" ssh_opts: $ssh_opts} - } - - ^$ghostty_bin ...(["+ssh-cache", $"--add=($ssh_id)"]) 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]: any -> any { - if ($ssh_args | is-empty) { - return (^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} { - ^ssh ...$ssh_parts - } -} - -# Removes Ghostty's data directory from XDG_DATA_DIRS -$env.XDG_DATA_DIRS = ( - $env.XDG_DATA_DIRS - | split row ':' - | where {|path| $path != $env.GHOSTTY_SHELL_INTEGRATION_XDG_DIR } - | str join ':' -) diff --git a/src/shell-integration/nushell/vendor/autoload/bootstrap-integration.nu b/src/shell-integration/nushell/vendor/autoload/bootstrap-integration.nu deleted file mode 100644 index 317ba62d3..000000000 --- a/src/shell-integration/nushell/vendor/autoload/bootstrap-integration.nu +++ /dev/null @@ -1,29 +0,0 @@ -let enable_integration = $env.GHOSTTY_SHELL_FEATURES | split row ',' - | where ($it in ["ssh-env" "ssh-terminfo"]) - | is-not-empty - -let ghostty_ssh_file = $env.GHOSTTY_RESOURCES_DIR - | path join "shell-integration" "nushell" "ghostty-ssh-integration.nu" - -let ssh_integration_file = $nu.data-dir | path join "ghostty-ssh-integration.nu" -let ssh_file_exists = $ssh_integration_file | path exists - -# TOD0: In case of an update to the `ghostty-ssh-integration.nu` file -# the file wont be updated here, so we need to support -# saving the new file once there is an update - -match [$enable_integration $ssh_file_exists] { - [true false] => { - # $nu.data-dir is not created by default - # https://www.nushell.sh/book/configuration.html#startup-variables - $nu.data-dir | path exists | if (not $in) { mkdir $nu.data-dir } - open $ghostty_ssh_file | save $ssh_integration_file - } - [false true] => { - # We need to check if the user disabled `ssh-integration` and thus - # the integration file needs to be removed so it doesnt get sourced by - # the `source-integration.nu` file - rm $ssh_integration_file - } - _ => { } -} diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty.nu b/src/shell-integration/nushell/vendor/autoload/ghostty.nu index 467e3f529..6a6f83629 100644 --- a/src/shell-integration/nushell/vendor/autoload/ghostty.nu +++ b/src/shell-integration/nushell/vendor/autoload/ghostty.nu @@ -4,26 +4,128 @@ export module ghostty { $feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',') } + # 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 + ]: [nothing -> record>] { + mut ssh_opts = $ssh_opts + let ssh_cfg = ^ssh -G ...($ssh_args) + | lines + | parse "{key} {value}" + | where key in ["user" "hostname"] + | select key value + | transpose -rd + | default {user: $env.USER hostname: "localhost"} + + let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)" + let ghostty_bin = $env.GHOSTTY_BIN_DIR | path join "ghostty" + + let is_cached = ( + ^$ghostty_bin ...(["+ssh-cache" $"--host=($ssh_id)"]) + | complete + | $in.exit_code == 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_cfg.hostname)..." + + let ctrl_path = ( + try { + mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX" + } catch { + $"/tmp/ghostty-ssh-($ssh_cfg.user).($nu.pid)" + } | path join "socket" + ) + + let master_parts = $ssh_opts ++ ["-o" "ControlMaster=yes" "-o" $"ControlPath=($ctrl_path)" "-o" "ControlPersist=60s"] ++ $ssh_args + + ($terminfo_data) | ^ssh ...( + $master_parts ++ + [ + ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1' + ] + ) + | complete + | if $in.exit_code != 0 { + print "Warning: Failed to install terminfo." + return {ssh_term: "xterm-256color" ssh_opts: $ssh_opts} + } + + ^$ghostty_bin ...(["+ssh-cache" $"--add=($ssh_id)"]) o+e>| ignore + $ssh_opts ++= ["-o" $"ControlPath=($ctrl_path)"] + } + + return {ssh_term: "xterm-ghostty" ssh_opts: $ssh_opts} + } + # Wrap `sudo` to preserve Ghostty's TERMINFO environment variable export def --wrapped sudo [ - ...args # Arguments to pass to `sudo` + ...args # Arguments to pass to `sudo` ] { mut sudo_args = $args if (has_feature "sudo") { - # Extract just the sudo options (before the command) - let sudo_options = ($args | take until {|arg| - not (($arg | str starts-with "-") or ($arg | str contains "=")) - }) - - # Prepend TERMINFO preservation flag if not using sudoedit - if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) { - $sudo_args = ($args | prepend "--preserve-env=TERMINFO") + # Extract just the sudo options (before the command) + let sudo_options = ( + $args | take until {|arg| + not (($arg | str starts-with "-") or ($arg | str contains "=")) } + ) + + # Prepend TERMINFO preservation flag if not using sudoedit + if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) { + $sudo_args = ($args | prepend "--preserve-env=TERMINFO") + } } ^sudo ...$sudo_args } + # Wrap `ssh` to provide ghostty `ssh-integration` + export def --wrapped ssh [...ssh_args: string]: any -> any { + if ($ssh_args | is-empty) { + return (^ssh) + } + mut session = {ssh_term: "" ssh_opts: []} + let shell_features = $env.GHOSTTY_SHELL_FEATURES | split row ',' + + if (has_feature "ssh-env") { + $session = set_ssh_env + } + if (has_feature "ssh-terminfo") { + $session = set_ssh_terminfo $session.ssh_opts $ssh_args + } + + let ssh_parts = $session.ssh_opts ++ $ssh_args + with-env {TERM: $session.ssh_term} { + ^ssh ...$ssh_parts + } + } } # Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR diff --git a/src/shell-integration/nushell/vendor/autoload/source-integration.nu b/src/shell-integration/nushell/vendor/autoload/source-integration.nu deleted file mode 100644 index 1c21833a4..000000000 --- a/src/shell-integration/nushell/vendor/autoload/source-integration.nu +++ /dev/null @@ -1,11 +0,0 @@ -# Sourcing the `ghostty-integration.nu` cant be on the -# `bootstrap-integration.nu` file because it tries to resolve the `sourced` -# file at parsing time, which would make it source nothing. - -# But here we rely on the fact that `boostrap-integration.nu` gets parsed -# and executed first, and then we can count on `ssh_integration_file` being available - -#https://www.nushell.sh/book/thinking_in_nu.html#example-dynamically-generating-source - -const ssh_integration_file = $nu.data-dir | path join "ghostty-ssh-integration.nu" -source (if ($ssh_integration_file | path exists) { $ssh_integration_file } else { null }) From 0a2b90ed64ae6a56e8f025e2af4d66610c670c63 Mon Sep 17 00:00:00 2001 From: David Matos Date: Wed, 21 Jan 2026 13:31:08 +0100 Subject: [PATCH 08/10] Expand Readme to reflect new changes --- src/shell-integration/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index 8809134d2..f3961599c 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -84,7 +84,8 @@ Nushell's vendor autoload mechanism. Ghostty then automatically imports the module using the `-e "use ghostty *"` flag when starting Nushell. Nushell provides many shell features itself, such as `title` and `cursor`, -so our integration focuses on Ghostty-specific features like `sudo`. +so our integration focuses on Ghostty-specific features like `sudo`. Additionally, +we also provide `ssh-integration` via the `ssh-env` and `ssh-terminfo` features. The shell integration is automatically enabled when running Nushell in Ghostty, but you can also load it manually is shell integration is disabled: From d70eef69f9ec99b45e57ccbbb8cfe3cf458409c2 Mon Sep 17 00:00:00 2001 From: David Matos Date: Tue, 27 Jan 2026 00:51:50 +0100 Subject: [PATCH 09/10] address changes --- src/shell-integration/README.md | 4 +- .../nushell/vendor/autoload/ghostty.nu | 77 +++++++++---------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index f3961599c..3484b0cdc 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -84,8 +84,8 @@ Nushell's vendor autoload mechanism. Ghostty then automatically imports the module using the `-e "use ghostty *"` flag when starting Nushell. Nushell provides many shell features itself, such as `title` and `cursor`, -so our integration focuses on Ghostty-specific features like `sudo`. Additionally, -we also provide `ssh-integration` via the `ssh-env` and `ssh-terminfo` features. +so our integration focuses on Ghostty-specific features like `sudo`, +`ssh-env`, and `ssh-terminfo`. The shell integration is automatically enabled when running Nushell in Ghostty, but you can also load it manually is shell integration is disabled: diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty.nu b/src/shell-integration/nushell/vendor/autoload/ghostty.nu index 6a6f83629..475a7a182 100644 --- a/src/shell-integration/nushell/vendor/autoload/ghostty.nu +++ b/src/shell-integration/nushell/vendor/autoload/ghostty.nu @@ -4,17 +4,6 @@ export module ghostty { $feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',') } - # 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. @@ -24,7 +13,6 @@ export module ghostty { ssh_opts: list ssh_args: list ]: [nothing -> record>] { - mut ssh_opts = $ssh_opts let ssh_cfg = ^ssh -G ...($ssh_args) | lines | parse "{key} {value}" @@ -43,20 +31,16 @@ export module ghostty { ) 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} + return {ssh_term: "xterm-256color" ssh_opts: $ssh_opts} } print $"Setting up xterm-ghostty terminfo on ($ssh_cfg.hostname)..." let ctrl_path = ( - try { - mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX" - } catch { - $"/tmp/ghostty-ssh-($ssh_cfg.user).($nu.pid)" - } | path join "socket" + mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX" + | path join "socket" ) let master_parts = $ssh_opts ++ ["-o" "ControlMaster=yes" "-o" $"ControlPath=($ctrl_path)" "-o" "ControlPersist=60s"] ++ $ssh_args @@ -78,12 +62,45 @@ export module ghostty { } ^$ghostty_bin ...(["+ssh-cache" $"--add=($ssh_id)"]) o+e>| ignore - $ssh_opts ++= ["-o" $"ControlPath=($ctrl_path)"] + + return {ssh_term: "xterm-ghostty" ssh_opts: ($ssh_opts ++ ["-o" $"ControlPath=($ctrl_path)"])} } return {ssh_term: "xterm-ghostty" ssh_opts: $ssh_opts} } + # Wrap `ssh` with Ghostty TERMINFO support + export def --wrapped ssh [...ssh_args: string]: any -> any { + if ($ssh_args | is-empty) { + return (^ssh) + } + # `ssh-env` 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 + let base_ssh_opts = if (has_feature "ssh-env") { + ["-o" "SetEnv COLORTERM=truecolor" "-o" "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"] + } else { + [] + } + let base_ssh_term = if (has_feature "ssh-env") { + "xterm-256color" + } else { + ($env.TERM? | default "") + } + + let session = if (has_feature "ssh-terminfo") { + set_ssh_terminfo $base_ssh_opts $ssh_args + } else { + {ssh_term: $base_ssh_term ssh_opts: $base_ssh_opts} + } + + let ssh_parts = $session.ssh_opts ++ $ssh_args + with-env {TERM: $session.ssh_term} { + ^ssh ...$ssh_parts + } + } + # Wrap `sudo` to preserve Ghostty's TERMINFO environment variable export def --wrapped sudo [ ...args # Arguments to pass to `sudo` @@ -106,26 +123,6 @@ export module ghostty { ^sudo ...$sudo_args } - # Wrap `ssh` to provide ghostty `ssh-integration` - export def --wrapped ssh [...ssh_args: string]: any -> any { - if ($ssh_args | is-empty) { - return (^ssh) - } - mut session = {ssh_term: "" ssh_opts: []} - let shell_features = $env.GHOSTTY_SHELL_FEATURES | split row ',' - - if (has_feature "ssh-env") { - $session = set_ssh_env - } - if (has_feature "ssh-terminfo") { - $session = set_ssh_terminfo $session.ssh_opts $ssh_args - } - - let ssh_parts = $session.ssh_opts ++ $ssh_args - with-env {TERM: $session.ssh_term} { - ^ssh ...$ssh_parts - } - } } # Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR From 675fa34e66ae6bdd7468f7c78b601fbd0c96a338 Mon Sep 17 00:00:00 2001 From: David Matos Date: Tue, 27 Jan 2026 00:59:39 +0100 Subject: [PATCH 10/10] unnecesary bind --- src/shell-integration/nushell/vendor/autoload/ghostty.nu | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty.nu b/src/shell-integration/nushell/vendor/autoload/ghostty.nu index 475a7a182..93e5fd909 100644 --- a/src/shell-integration/nushell/vendor/autoload/ghostty.nu +++ b/src/shell-integration/nushell/vendor/autoload/ghostty.nu @@ -95,9 +95,8 @@ export module ghostty { {ssh_term: $base_ssh_term ssh_opts: $base_ssh_opts} } - let ssh_parts = $session.ssh_opts ++ $ssh_args with-env {TERM: $session.ssh_term} { - ^ssh ...$ssh_parts + ^ssh ...($session.ssh_opts ++ $ssh_args) } }