mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 11:35:48 +00:00
nushell: provide shell ssh integration (#9604)
Closes #7877. Small disclaimer: First Ghostty PR and Zig PR, but looking forward to contributing to the project. This PR supports shell-integration-features `ssh-env` and `ssh-terminfo` as per other shells, but not the rest as this is what the issue states. That being said, with this PR, then you would see this: - `warning(io_exec): shell could not be detected, no automatic shell integration will be injected`, but given that the default mode is `detect` it will pick up the executable and if ssh features are enabled it will integrate it. This might be confusing for users. - I decided to not add `nu` to `pub const Shell` because if we do so, then from what I understand from the code, then the code flow would imply that "shell integration will be injected" but it will only do so if those `ssh-*` features are enabled, which may be misleading. But on the other hand, providing `ssh` shell integration but returning `null` for `?!ShellIntegration` does not seem very correct either. - I dont like that I added `features` argument to `setupshell`, just to check them if `nu` was used. The reasoning is because the way Nushell works, if we autoload the `nushell` directory (by `setupXdgDatadirs()`) even if no `ssh` features were present, it will wrap the `ssh` function and I think that is not desirable, even if we end up just forwarding the arguments. Sorry for the long wall of text, but I think it was worth to add some of the doubts I have had myself, plus the ones that you folks may add. I am very happy to iterate on this, even if its a very "Easy" one, so I much welcome the feedback. > [!NOTE] > > Used `GPT` for helping with nushell variable naming verification/improvement > Used `Gemini` for helping with understanding the `Zsh` ssh integration so that I could replicate the logic with nushell. Just because I find `zsh` language very difficult to understand in detail.
This commit is contained in:
@@ -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`,
|
||||
`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:
|
||||
|
||||
@@ -4,22 +4,120 @@ export module ghostty {
|
||||
$feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',')
|
||||
}
|
||||
|
||||
# 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<string>
|
||||
ssh_args: list<string>
|
||||
]: [nothing -> record<ssh_term: string, ssh_opts: list<string>>] {
|
||||
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 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}
|
||||
}
|
||||
|
||||
print $"Setting up xterm-ghostty terminfo on ($ssh_cfg.hostname)..."
|
||||
|
||||
let ctrl_path = (
|
||||
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
|
||||
|
||||
($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
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
with-env {TERM: $session.ssh_term} {
|
||||
^ssh ...($session.ssh_opts ++ $ssh_args)
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user