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?
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!
Most helpful comment
I'm not particularly interested in creating a standalone client, but I
am definitely willing to document the protocol.