diff --git a/docs/changelog.rst b/docs/changelog.rst index 949587cd7..2c9caf9aa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,9 @@ To update |kitty|, :doc:`follow the instructions `. - diff kitten: Implement recursive diff over SSH (:iss:`3268`) +- ssh kitten: Allow using python instead of the shell on the server, useful if + the shell used is a non-POSIX compliant one, such as fish (:iss:`3277`) + - Add support for the color settings stack that XTerm copied from us without acknowledgement and decided to use incompatible escape codes for. diff --git a/docs/faq.rst b/docs/faq.rst index 2076267e5..8226a2eba 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -54,7 +54,12 @@ type it each time:: alias ssh="kitty +kitten ssh" If for some reason that does not work (typically because the server is using a -non POSIX compliant shell), you can use the following one-liner instead (it +non POSIX compliant shell), you can try using it with python instead:: + + kitty +kitten ssh use-python myserver + +If that also fails, perhaps because python is not installed on the remote +server, use the following one-liner instead (it is slower as it needs to ssh into the server twice, but will work with most servers):: diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index a47f34d79..7882df87b 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -46,6 +46,33 @@ exec -a "-$shell_name" "$0" ''' +PYTHON_SCRIPT = '''\ +#!/usr/bin/env python +from __future__ import print_function +from tempfile import NamedTemporaryFile +import subprocess, os, sys, pwd, binascii, json + +# macOS ships with an ancient version of tic that cannot read from stdin, so we +# create a temp file for it +with NamedTemporaryFile() as tmp: + tmp.write(binascii.unhexlify('{terminfo}')) + p = subprocess.Popen(['tic', '-x', '-o', os.path.expanduser('~/.terminfo'), tmp.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if p.wait() != 0: + getattr(sys.stderr, 'buffer', sys.stderr).write(stdout + stderr) + raise SystemExit('Failed to compile terminfo using tic') +command_to_execute = json.loads(binascii.unhexlify('{command_to_execute}')) +if command_to_execute: + os.execlp(command_to_execute[0], *command_to_execute) +try: + shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh' +except KeyError: + shell_path = '/bin/sh' +shell_name = '-' + os.path.basename(shell_path) +os.execlp(shell_path, shell_name) +''' + + def get_ssh_cli() -> Tuple[Set[str], Set[str]]: other_ssh_args: List[str] = [] boolean_ssh_args: List[str] = [] @@ -150,20 +177,44 @@ def quote(x: str) -> str: return x +def get_posix_cmd(terminfo: str, server_args: List[str]) -> List[str]: + sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1) + if len(server_args) > 1: + command_to_executeg = (quote(c) for c in server_args[1:]) + command_to_execute = 'exec ' + ' '.join(command_to_executeg) + else: + command_to_execute = '' + sh_script = sh_script.replace('EXEC_CMD', command_to_execute) + return ['-t', server_args[0], sh_script] + server_args[1:] + + +def get_python_cmd(terminfo: str, server_args: List[str]) -> List[str]: + import json + hostname = server_args[0] + command_to_execute = server_args[1:] + script = PYTHON_SCRIPT.format( + terminfo=terminfo.encode('utf-8').hex(), + command_to_execute=json.dumps(command_to_execute).encode('utf-8').hex() + ) + return ['-t', hostname, f'python -c "{script}"'] + + def main(args: List[str]) -> NoReturn: - ssh_args, server_args, passthrough = parse_ssh_args(args[1:]) + args = args[1:] + use_posix = True + if args and args[0] == 'use-python': + args = args[1:] + use_posix = False + ssh_args, server_args, passthrough = parse_ssh_args(args) if passthrough: cmd = ['ssh'] + ssh_args + server_args else: terminfo = subprocess.check_output(['infocmp']).decode('utf-8') - sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1) - if len(server_args) > 1: - command_to_executeg = (quote(c) for c in server_args[1:]) - command_to_execute = 'exec ' + ' '.join(command_to_executeg) + cmd = ['ssh'] + ssh_args + if use_posix: + cmd += get_posix_cmd(terminfo, server_args) else: - command_to_execute = '' - sh_script = sh_script.replace('EXEC_CMD', command_to_execute) - cmd = ['ssh'] + ssh_args + ['-t', server_args[0], sh_script] + server_args[1:] + cmd += get_python_cmd(terminfo, server_args) os.execvp('ssh', cmd)