diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index 015db4198..a4fdb241b 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -156,6 +156,8 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st env['KITTY_LOGIN_SHELL'] = ssh_opts.login_shell if ssh_opts.cwd: env['KITTY_LOGIN_CWD'] = ssh_opts.cwd + if ssh_opts.remote_kitty != 'no': + env['KITTY_REMOTE'] = ssh_opts.remote_kitty env_script = serialize_env(env, base_env) buf = io.BytesIO() with tarfile.open(mode=f'w:{compression}', fileobj=buf, encoding='utf-8') as tf: @@ -171,9 +173,10 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st f'{arcname}/ssh/*', # bootstrap files are sent as command line args f'{arcname}/zsh/kitty.zsh', # present for legacy compat not needed by ssh kitten )) - arcname = 'home/' + rd + '/kitty' - add_data_as_file(tf, arcname + '/version', str_version.encode('ascii')) - tf.add(shell_integration_dir + '/ssh/kitty', arcname=arcname + '/bin/kitty', filter=normalize_tarinfo) + if ssh_opts.remote_kitty != 'no': + arcname = 'home/' + rd + '/kitty' + add_data_as_file(tf, arcname + '/version', str_version.encode('ascii')) + tf.add(shell_integration_dir + '/ssh/kitty', arcname=arcname + '/bin/kitty', filter=normalize_tarinfo) tf.add(f'{terminfo_dir}/kitty.terminfo', arcname='home/.terminfo/kitty.terminfo', filter=normalize_tarinfo) tf.add(glob.glob(f'{terminfo_dir}/*/xterm-kitty')[0], arcname='home/.terminfo/x/xterm-kitty', filter=normalize_tarinfo) return buf.getvalue() diff --git a/kittens/ssh/options/definition.py b/kittens/ssh/options/definition.py index 556e6c8b3..6b218150e 100644 --- a/kittens/ssh/options/definition.py +++ b/kittens/ssh/options/definition.py @@ -102,6 +102,19 @@ The working directory on the remote host to change to. Env vars in this value are expanded. The default is empty so no changing is done, which usually means the home directory is used. ''') + +opt('remote_kitty', 'if-needed', choices=('if-needed', 'no', 'yes'), long_text=''' +Make kitty available on the remote server. Useful to run kittens such as the +icat kitten to display images or the transfer file kitten to transfer files. +Only works if the remote server has an architecture for which pre-compiled +kitty binaries are available. Note that kitty is not actually copied to the +remote server, instead a small bootstrap script is copied which will download +and run kitty when kitty is first executed on the remote server. A value of +:code:`needed` means kitty is installed only if not already present in the +system-wide PATH. A value of :code:`yes` means that kitty is installed even if +already present, and the installed kitty takes precedence. Finally, :code:`no` +means no kitty is installed on the remote machine. +''') egr() # }}} agr('ssh', 'SSH configuration') # {{{ diff --git a/kittens/ssh/options/parse.py b/kittens/ssh/options/parse.py index 11221378b..4c8c8c3dd 100644 --- a/kittens/ssh/options/parse.py +++ b/kittens/ssh/options/parse.py @@ -38,6 +38,14 @@ class Parser: def remote_dir(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['remote_dir'] = relative_dir(val) + def remote_kitty(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_remote_kitty: + raise ValueError(f"The value {val} is not a valid choice for remote_kitty") + ans["remote_kitty"] = val + + choices_for_remote_kitty = frozenset(('if-needed', 'no', 'yes')) + def share_connections(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['share_connections'] = to_bool(val) diff --git a/kittens/ssh/options/types.py b/kittens/ssh/options/types.py index 8c1a8ed92..ee5520f38 100644 --- a/kittens/ssh/options/types.py +++ b/kittens/ssh/options/types.py @@ -5,8 +5,10 @@ import kittens.ssh.copy if typing.TYPE_CHECKING: choices_for_askpass = typing.Literal['unless-set', 'ssh', 'native'] + choices_for_remote_kitty = typing.Literal['if-needed', 'no', 'yes'] else: choices_for_askpass = str + choices_for_remote_kitty = str option_names = ( # {{{ 'askpass', @@ -17,6 +19,7 @@ option_names = ( # {{{ 'interpreter', 'login_shell', 'remote_dir', + 'remote_kitty', 'share_connections', 'shell_integration') # }}} @@ -28,6 +31,7 @@ class Options: interpreter: str = 'sh' login_shell: str = '' remote_dir: str = '.local/share/kitty-ssh-kitten' + remote_kitty: choices_for_remote_kitty = 'if-needed' share_connections: bool = True shell_integration: str = 'inherited' copy: typing.Dict[str, kittens.ssh.copy.CopyInstruction] = {} diff --git a/shell-integration/ssh/bootstrap-utils.sh b/shell-integration/ssh/bootstrap-utils.sh index c6931c4b8..490cbaaa9 100644 --- a/shell-integration/ssh/bootstrap-utils.sh +++ b/shell-integration/ssh/bootstrap-utils.sh @@ -169,6 +169,24 @@ fi' > "$sh_script" exec "$login_shell" } +install_kitty_bootstrap() { + case "$(command uname)" in + Linux) ;; + Darwin) ;; + *) return ;; + esac + kitty_exists="n" + command -v kitty 2> /dev/null > /dev/null && kitty_exists="y" + if [ "$kitty_remote" = "yes" -o "$kitty_remote-$kitty_exists" = "if-needed-n" ]; then + kitty_dir="$data_dir/kitty/bin" + if [ "$kitty_exists" = "y" ]; then + export PATH="$kitty_dir:$PATH" + else + export PATH="$PATH:$kitty_dir" + fi + fi +} + prepare_for_exec() { if [ -n "$leading_data" ]; then # clear current line as it might have things echoed on it from leading_data @@ -177,6 +195,7 @@ prepare_for_exec() { printf "\r\033[K" > /dev/tty fi [ -f "$HOME/.terminfo/kitty.terminfo" ] || die "Incomplete extraction of ssh data" + install_kitty_bootstrap [ -n "$login_shell" ] || using_getent || using_id || using_python || using_perl || using_passwd || using_shell_env || login_shell="sh" shell_name=$(command basename $login_shell) diff --git a/shell-integration/ssh/bootstrap.py b/shell-integration/ssh/bootstrap.py index 294ec0382..5032ce7f2 100644 --- a/shell-integration/ssh/bootstrap.py +++ b/shell-integration/ssh/bootstrap.py @@ -258,6 +258,19 @@ def exec_with_shell_integration(): exec_bash_with_integration() +def install_kitty_bootstrap(): + kitty_remote = os.environ.pop('KITTY_REMOTE', '') + if os.uname().sysname not in ('Linux', 'Darwin'): + return + kitty_exists = shutil.which('kitty') + if kitty_remote == 'yes' or (kitty_remote == 'if-needed' and not kitty_exists): + kitty_dir = os.path.join(data_dir, 'kitty', 'bin') + if kitty_exists: + os.environ['PATH'] = kitty_dir + os.pathsep + os.environ['PATH'] + else: + os.environ['PATH'] = os.environ['PATH'] + os.pathsep + kitty_dir + + def main(): global tty_file_obj, login_shell # the value of O_CLOEXEC below is on macOS which is most likely to not have @@ -271,6 +284,7 @@ def main(): finally: cleanup() cwd = os.environ.pop('KITTY_LOGIN_CWD', '') + install_kitty_bootstrap() if cwd: os.chdir(cwd) ksi = frozenset(filter(None, os.environ.get('KITTY_SHELL_INTEGRATION', '').split())) diff --git a/shell-integration/ssh/bootstrap.sh b/shell-integration/ssh/bootstrap.sh index cf633aeb0..106ce4536 100644 --- a/shell-integration/ssh/bootstrap.sh +++ b/shell-integration/ssh/bootstrap.sh @@ -106,6 +106,8 @@ untar_and_read_env() { unset KITTY_LOGIN_SHELL login_cwd="$KITTY_LOGIN_CWD" unset KITTY_LOGIN_CWD + kitty_remote="$KITTY_REMOTE" + unset KITTY_REMOTE compile_terminfo "$tdir/home" mv_files_and_dirs "$tdir/home" "$HOME" [ -e "$tdir/root" ] && mv_files_and_dirs "$tdir/root" ""