Add support for RemoteCommand option in kitten ssh

This commit is contained in:
zhaolei 2026-04-14 11:11:51 +08:00
parent c60ed85cdd
commit 9b02a59c4e
4 changed files with 73 additions and 4 deletions

View file

@ -10,7 +10,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/kovidgoyal/kitty"
"io"
"io/fs"
"maps"
@ -28,6 +27,8 @@ import (
"syscall"
"time"
"github.com/kovidgoyal/kitty"
"github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/themes"
@ -177,6 +178,7 @@ func set_askpass(hostname_for_match, uname string, overrides []string) (need_to_
type connection_data struct {
remote_args []string
host_opts *Config
ssh_config *SSHConfig
hostname_for_match string
username string
echo_on bool
@ -408,6 +410,18 @@ func prepare_exec_cmd(cd *connection_data) string {
return "unset KITTY_SHELL_INTEGRATION; exec \"$login_shell\" -c '" + strings.Join(args, " ") + "'"
}
func prepare_remote_cmd(cd *connection_data) string {
if cd.ssh_config == nil || cd.ssh_config.RemoteCommand == "" {
return ""
}
remote_command := cd.ssh_config.RemoteCommand
if cd.script_type == "py" {
return base64.RawStdEncoding.EncodeToString(utils.UnsafeStringToBytes(remote_command))
}
return remote_command
}
var data_shm shm.MMap
func prepare_script(script string, replacements map[string]string) string {
@ -417,6 +431,9 @@ func prepare_script(script string, replacements map[string]string) string {
if _, found := replacements["EXPORT_HOME_CMD"]; !found {
replacements["EXPORT_HOME_CMD"] = ""
}
if _, found := replacements["REMOTE_CMD"]; !found {
replacements["REMOTE_CMD"] = ""
}
keys := utils.Keys(replacements)
for i, key := range keys {
keys[i] = "\\b" + key + "\\b"
@ -434,6 +451,8 @@ func bootstrap_script(cd *connection_data) (err error) {
if len(cd.remote_args) > 0 {
exec_cmd = prepare_exec_cmd(cd)
}
remote_cmd := prepare_remote_cmd(cd)
pw, err := secrets.TokenHex()
if err != nil {
return err
@ -467,6 +486,7 @@ func bootstrap_script(cd *connection_data) (err error) {
replacements := map[string]string{
"EXPORT_HOME_CMD": export_home_cmd,
"EXEC_CMD": exec_cmd,
"REMOTE_CMD": remote_cmd,
"TEST_SCRIPT": cd.test_script,
}
add_bool := func(ok bool, key string) {
@ -600,7 +620,7 @@ func change_colors(color_scheme string) (ans string, err error) {
return
}
func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err error) {
func run_ssh(ssh_args, server_args, found_extra_args []string, ssh_config *SSHConfig) (rc int, err error) {
go shell_integration.Data()
go RelevantKittyOpts()
defer func() {
@ -610,11 +630,14 @@ func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err erro
}
}()
cmd := append([]string{SSHExe()}, ssh_args...)
cd := connection_data{remote_args: server_args[1:]}
cd := connection_data{remote_args: server_args[1:], ssh_config: ssh_config}
hostname := server_args[0]
if len(cd.remote_args) == 0 {
cmd = append(cmd, "-t")
}
if cd.ssh_config != nil && cd.ssh_config.RemoteCommand != "" {
cmd = append(cmd, "-o", "RemoteCommand=none")
}
insertion_point := len(cmd)
cmd = append(cmd, "--", hostname)
uname, hostname_for_match := get_destination(hostname)
@ -833,13 +856,17 @@ func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
if passthrough {
return 1, unix.Exec(SSHExe(), utils.Concat([]string{"ssh"}, ssh_args, server_args), os.Environ())
}
ssh_config, err := LoadSSHConfig(server_args[0])
if err != nil {
return 1, err
}
if os.Getenv("KITTY_WINDOW_ID") == "" || os.Getenv("KITTY_PID") == "" {
return 1, fmt.Errorf("The SSH kitten is meant to run inside a kitty window")
}
if !tty.IsTerminal(os.Stdin.Fd()) {
return 1, fmt.Errorf("The SSH kitten is meant for interactive use only, STDIN must be a terminal")
}
return run_ssh(ssh_args, server_args, found_extra_args)
return run_ssh(ssh_args, server_args, found_extra_args, ssh_config)
}
func EntryPoint(parent *cli.Command) {

View file

@ -3,6 +3,7 @@
package ssh
import (
"bufio"
"bytes"
"fmt"
"os/exec"
@ -194,6 +195,40 @@ func ParseSSHArgs(args []string, extra_args ...string) (ssh_args []string, serve
return
}
type SSHConfig struct {
RemoteCommand string
}
func LoadSSHConfig(hostname string) (config *SSHConfig, err error) {
cmd_args := []string{SSHExe(), hostname, "-G"}
cmd := exec.Command(cmd_args[0], cmd_args[1:]...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
_ = cmd.Run()
text := stdout.String()
scanner := bufio.NewScanner(strings.NewReader(text))
config = &SSHConfig{}
for scanner.Scan() {
line := scanner.Text()
i := strings.IndexByte(line, ' ')
if i <= 0 {
continue
}
key, val := line[:i], line[i+1:]
switch key {
case "remotecommand":
if val != "none" {
config.RemoteCommand = val
}
}
}
return config, nil
}
type SSHVersion struct{ Major, Minor int }
func (self SSHVersion) SupportsAskpassRequire() bool {

View file

@ -321,6 +321,11 @@ def main():
os.environ.pop('KITTY_SHELL_INTEGRATION', None)
cmd = base64.standard_b64decode(exec_cmd).decode('utf-8')
exec_with_better_error(login_shell, os.path.basename(login_shell), '-c', cmd)
remote_cmd = b'REMOTE_CMD'
if remote_cmd:
os.environ.pop('KITTY_SHELL_INTEGRATION', None)
cmd = base64.standard_b64decode(remote_cmd).decode('utf-8')
exec_with_better_error(login_shell, os.path.basename(login_shell), '-c', cmd)
TEST_SCRIPT # noqa
if ksi and 'no-rc' not in ksi:
exec_with_shell_integration()

View file

@ -158,6 +158,8 @@ prepare_for_exec
# If a command was passed to SSH execute it here
EXEC_CMD
REMOTE_CMD
# Used in the tests
TEST_SCRIPT