Kitty: Feature request: minimal cli for remote control

Created on 25 May 2019  Â·  5Comments  Â·  Source: kovidgoyal/kitty

I use Kitty on my laptop, and I often ssh into other servers. In order to make use of the remote-control features, I need to install all of Kitty on those instances (including python, etc). I'd love to have a tiny CLI that I can install instead, which doesn't have the Kitty terminal features, but only knows how to send remote-control commands. Ideally, this CLI would be statically compiled for easy installation.

Alternatively, could you publish the remote-control protocol so we might be able to make such a CLI ourselves?

Most helpful comment

I'm not particularly interested in creating a standalone client, but I
am definitely willing to document the protocol.

All 5 comments

I'm not particularly interested in creating a standalone client, but I
am definitely willing to document the protocol.

I have already created such a beast in pure Shell (bash or busybox sh – not too heavy on advanced features, so it could be ported to dash). Currently I can't grab the response, and awk (available even on embedded platforms) might not be enough to parse it, but the sending part works. There is a simple little language, which creates the cumbersome json-message and echoes it to kitty. The args are any of key, key+, key-, key.literal, key:string, key{, }
which map to null, true, false, the exact literal, the escaped string and a nested json object:

kitty_() {
    [ $# = 0 ] && { echo usage: kitty_ cmd '[key-value ...]
    Where key-value is one of: key key+ key- key.literal key:string key{ }
    for: null, true, false, exact literal, escaped string, nested json object' >&2; return 1; }
    local noresp=+ stty
#    [ "x$1" = x-r ] && { noresp=-; shift; trap "stty $(stty -g); trap - RETURN INT" RETURN INT; stty -echo; }

    local cmd=$1
    shift

    [ $# = 0 ] || set payload\{ "$@" \} # wrap into outer parts of json msg
    set cmd:$cmd version.'[0, 14, 1]' "$@" no_response$noresp

    local json sep='' arg key val
    for arg; do
        key=${arg%%[^a-z0-9_]*}     # can't do this in case, e.g. *.* wrongly grabs key:a.b
        val=${arg#$key}             # cut off rest
        case $val in
            '') val=null;;
            +) val=true;;
            -) val=false;;
            .*) val=${val#.};;
            :*) val=${val#:}; val=${val//\\/'\\\\'}; val=\"${val//\"/\\\"}\";; # in dash redo last 2 in sed
            \{) json="$json$sep\"$key\": {"; sep=''; continue;;
            \}) json="$json}"; sep=', '; continue;;
            *) echo "wrong key-value '$arg'" >&2; kitty_; return;;
        esac
        json="$json$sep\"$key\": $val"
        sep=', '
    done
    cmd=-en; [ -z "$kitty_debug" ] || cmd=-E
    echo $cmd "\eP@kitty-cmd{$json}\e\\"
#    [ $noresp = - ] &&
#   awk 'BEGIN { RS="\33\\\\" }
#   { sub( /^\33P@kitty-cmd{"ok": ?(false|true), "data": /, "AA", $0 ); print; exit }' </dev/tty
#    :
}

This can be used as

ktitle() {
    kitty_ set-window-title title:"$1" match temporary+
}

or

# kbgcolor 4f8f00
kbgcolor() {
    kitty_ set-colors title:"background=#$1" match_window match_tab all- configured- colors\{ background.$(( 0x$1 )) \} reset-
}

I even have a poor man's scp which types a file into another window. While this seems cumbersome at first, it's great for following you across chained ssh to directly unreachable hosts, sudo etc. Usage is kcp file target-window-id, or - for stdin. It switches you to the receiving window (as a safeguard to not send MBs to wrong window). There it already typed in the receive command for you, which you complete with a filename or pipe to another command. It encodes the content, so as to not send nasties like ^C or ^D. Base85 would be the best encoding, but it's not widely available, so use base64:

kcp() {
    [ "$2" -gt 0 ] 2>&- || { echo usage: kcp '{infile|-}' target-window-id >&2; return 1; }
    local id="id:$2" st1 st2
    st1=$(kitty_ send-text match:$id is_binary- match_tab- text:X)
    st2=${st1#*X} # reuse cached message, varying only the sent string
    st1=${st1%X*}
    echo -n "${st1}kcp_receive ${KITTY_WINDOW_ID:-0} $st2"
    kitty_ focus-window match:$id
    read -p "Go & start kcp_receive on $id!  Come back & hit return to send / ^C to cancel " </dev/tty
    base64 -w 1023 "$1" |
        while read; do echo -n "$st1$REPLY\n$st2"; done
    echo -n "$st1\\\\4$st2"
}

kcp_receive() {
    [ "$1" -ge 0 ] 2>&- || { echo usage: kcp_receive origin-window-id '[outfile]' >&2; return 1; }
    local stty=$(stty -g)
    trap "stty $stty; trap - INT QUIT TERM" INT QUIT TERM # only bash has RETURN
    stty -echo
    local id
    [ "$1" = 0 ] || id="id:$1"
    echo "Go back to kcp${id+ on $id}!  There hit return to send / ^C to cancel" >&2
    [ "$id" ] && kitty_ focus-window match:"$id" >/dev/tty
    if [ $# -gt 1 ]; then
        base64 -d > "$2"
    else
        base64 -d
    fi
    stty $stty
}

That didn't answer the question about the protocol. The part of the protocol we need to care about, is the inner payload json object. This reflects all the possible options of a given command. Sadly on this level they are not optional. That means as new options appear (and the version gets bumped on line 9 of kitty_) the following may need to be repeated.

There are 2 ways of sniffing and roughly transforming what a kitty @ command does. Either you run it through strace with long strings:

strace -s 9999 -e trace=write   kitty @ send-text aha

Then you pipe what kitty writes through this command.

# mostly transform STRACE STRING to kitty_ syntax
sed -E 's/\\"([a-z][a-z0-9_]*)\\": /\1\cA/g;
        s/\cAnull,*//g;
        s/\cAtrue,*/+/g;
        s/\cAfalse,*/-/g;
        s/\cA([-+0-9][-+0-9.e]*),*/.\1/g;
        s/\cA\\"([^"]*)\\",*/:"\1"/g;
        s/\cA\{/\\{ /g;
        s/\cA/:/g;
        s/\\\\/\\/g'

Or, if you have Emacs M-x shell, where what kitty @ writes just appears as output (without the above leaning toothpick syndrome of escaped doublequotes), use this:

# mostly transform OUTPUT to kitty_ syntax
sed -E 's/"([a-z][a-z0-9_]*)": /\1\cA/g;
        s/\cAnull,*//g;
        s/\cAtrue,*/+/g;
        s/\cAfalse,*/-/g;
        s/\cA([-+0-9][-+0-9.e]*),*/.\1/g;
        s/\cA("[^"]*"),*/:\1/g;
        s/\cA\{/\\{ /g;
        s/\cA/:/g'

Rather than my few functions with hardwired options, one could of course write more complex functions with getopt, which behave like the originals. I had no need for such luxury.

There is no need for them not be optional, I haven't really designed the
current protocol with an eye towards interoperability, it can be easily
modified for that goal.

Note that I have not completed the documentation for individual commands, contributions are welcome!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Askannz picture Askannz  Â·  3Comments

hdriqi picture hdriqi  Â·  3Comments

bewzaalex picture bewzaalex  Â·  3Comments

metalelf0 picture metalelf0  Â·  4Comments

drandreaskrueger picture drandreaskrueger  Â·  4Comments