Document the kitty remote control protocol

Fixes #1646
This commit is contained in:
Kovid Goyal 2019-06-12 13:12:53 +05:30
parent 15e8f6ad8a
commit 658be9405f
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 149 additions and 9 deletions

View File

@ -10,6 +10,8 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- Fix an out of bounds read causing a crash when selecting text with the mouse
in the alternate screen mode (:iss:`1578`)
- Document the kitty remote control protocol (:iss:`1646`)
0.14.2 [2019-06-09]
---------------------

View File

@ -276,6 +276,48 @@ def write_cli_docs(all_kitten_names):
# }}}
def write_remote_control_protocol_docs(): # {{{
from kitty.cmds import cmap
field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)')
def format_cmd(p, name, cmd):
p(name)
p('-' * 80)
lines = cmd.__doc__.strip().splitlines()
fields = []
for line in lines:
m = field_pat.match(line)
if m is None:
p(line)
else:
fields.append((m.group(1), m.group(2)))
if fields:
p('\nFields are:\n')
for (name, desc) in fields:
if '+' in name:
title = name.replace('+', ' (required)')
else:
title = name
defval = cmd.get_default(name.replace('-', '_'), cmd)
if defval is not cmd:
title = f'{title} (default: {defval})'
else:
title = f'{title} (optional)'
p(f':code:`{title}`')
p(' ', desc), p()
p(), p()
with open(f'generated/rc.rst', 'w') as f:
p = partial(print, file=f)
for name in sorted(cmap):
cmd = cmap[name]
if not cmd.__doc__:
continue
name = name.replace('_', '-')
format_cmd(p, name, cmd)
# }}}
# config file docs {{{
class ConfLexer(RegexLexer):
@ -537,6 +579,7 @@ def setup(app):
from kittens.runner import all_kitten_names
all_kitten_names = all_kitten_names()
write_cli_docs(all_kitten_names)
write_remote_control_protocol_docs()
write_conf_docs(app, all_kitten_names)
app.add_lexer('session', SessionLexer())
app.add_role('link', link_role)

View File

@ -486,3 +486,4 @@ See :doc:`changelog`.
*
kittens/*
generated/rc

29
docs/rc_protocol.rst Normal file
View File

@ -0,0 +1,29 @@
Documentation for the kitty remote control protocol
======================================================
The kitty remote control protocol is a simple protocol that involves sending
data to kitty in the form of JSON. Any individual command ot kitty has the
form::
<ESC>P@kitty-cmd<JSON object><ESC>\
Where ``<ESC>`` is the byte ``0x1b``. The JSON object has the form::
{
'cmd': "command name",
'version': "kitty version",
'no_response': Optional Boolean,
'payload': <Optional JSON object>,
}
The ``version`` above is a string of the form :code:`0.14.2`. If you are developing a
standalone client, use the kitty version that you are developing against. Using
a version greater than the version of the kitty instance you are talking to,
will cause a failure.
Set ``no_response`` to True if you dont want a response from kitty.
The optional payload is a JSON object that is specific to the actual command being sent.
The fields in the object for every command are documented below.
.. include:: generated/rc.rst

View File

@ -134,5 +134,10 @@ still write to the pipes of any other program on the same computer and
therefore can control |kitty|. It can, however, be useful to block programs
running on other computers (for example, over ssh) or as other users.
Documentation for the remote control protocol
-----------------------------------------------
If you wish to develop your own client to talk to |kitty|, you
can use the :doc:`rc_protocol`.
.. include:: generated/cli-kitty-at.rst

View File

@ -212,6 +212,14 @@ def wrap(text, limit=80):
return reversed(lines)
def get_defaults_from_seq(seq):
ans = {}
for opt in seq:
if not isinstance(opt, str):
ans[opt['dest']] = defval_for_opt(opt)
return ans
default_msg = ('''\
Run the :italic:`{appname}` terminal emulator. You can also specify the :italic:`program`
to run inside :italic:`{appname}` as normal arguments following the :italic:`options`.

View File

@ -7,7 +7,7 @@ import os
import sys
from contextlib import suppress
from .cli import parse_args
from .cli import parse_args, parse_option_spec, get_defaults_from_seq
from .config import parse_config, parse_send_text_bytes
from .constants import appname
from .fast_data_types import focus_os_window
@ -36,7 +36,33 @@ class UnknownLayout(ValueError):
cmap = {}
def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='...', string_return_is_error=False, args_count=None):
def cmd(
short_desc,
desc=None,
options_spec=None,
no_response=False,
argspec='...',
string_return_is_error=False,
args_count=None,
):
if options_spec:
defaults = None
def get_defaut_value(name, missing=None):
nonlocal defaults
if defaults is None:
defaults = get_defaults_from_seq(parse_option_spec(options_spec)[0])
return defaults.get(name, missing)
else:
def get_defaut_value(name, missing=None):
return missing
def payload_get(payload, key, opt_name=None):
ans = payload.get(key, payload_get)
if ans is not payload_get:
return ans
return get_defaut_value(opt_name or key)
def w(func):
func.short_desc = short_desc
@ -49,6 +75,8 @@ def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='..
func.no_response = no_response
func.string_return_is_error = string_return_is_error
func.args_count = 0 if not argspec else args_count
func.get_default = get_defaut_value
func.payload_get = payload_get
cmap[func.name] = func
return func
return w
@ -109,6 +137,9 @@ def windows_for_payload(boss, window, payload):
argspec=''
)
def cmd_ls(global_opts, opts, args):
'''
No payload
'''
pass
@ -135,6 +166,11 @@ By default, the font size is only changed in the active OS window,
this option will cause it to be changed in all OS windows.
''')
def cmd_set_font_size(global_opts, opts, args):
'''
size+: The new font size in pts (a positive number)
all: Boolean whether to change font size in the current window or all windows
increment_op: The string ``+`` or ``-`` to interpret size as an increment
'''
if not args:
raise SystemExit('No font size specified')
fs = args[0]
@ -143,7 +179,9 @@ def cmd_set_font_size(global_opts, opts, args):
def set_font_size(boss, window, payload):
boss.change_font_size(payload['all'], payload['increment_op'], payload['size'])
boss.change_font_size(
cmd_set_font_size.payload_get(payload, 'all'),
payload.get('increment_op', None), payload['size'])
# }}}
@ -170,6 +208,12 @@ are sent as is, not interpreted for escapes.
argspec='[TEXT TO SEND]'
)
def cmd_send_text(global_opts, opts, args):
'''
text+: The text being sent
is_binary+: If False text is interpreted as a python string literal instead of plain text
match: A string indicating the window to send text to
match_tab: A string indicating the tab to send text to
'''
limit = 1024
ret = {'match': opts.match, 'is_binary': False, 'match_tab': opts.match_tab}
@ -235,12 +279,14 @@ def cmd_send_text(global_opts, opts, args):
def send_text(boss, window, payload):
windows = [boss.active_window]
match = payload['match']
pg = cmd_send_text.payload_get
match = pg(payload, 'match')
if match:
windows = tuple(boss.match_windows(match))
if payload['match_tab']:
mt = pg(payload, 'match_tab')
if mt:
windows = []
tabs = tuple(boss.match_tabs(payload['match_tab']))
tabs = tuple(boss.match_tabs(mt))
if not tabs:
raise MatchError(payload['match_tab'], 'tabs')
for tab in tabs:
@ -269,19 +315,25 @@ want to allow other programs to change it afterwards, use this option.
argspec='TITLE ...'
)
def cmd_set_window_title(global_opts, opts, args):
'''
title+: The new title
match: Which windows to change the title in
temporary: Boolean indicating if the change is temporary or permanent
'''
return {'title': ' '.join(args), 'match': opts.match, 'temporary': opts.temporary}
def set_window_title(boss, window, payload):
windows = [window or boss.active_window]
match = payload['match']
pg = cmd_set_window_title.payload_get
match = pg(payload, 'match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
for window in windows:
if window:
if payload['temporary']:
if pg(payload, 'temporary'):
window.override_title = None
window.title_changed(payload['title'])
else:

View File

@ -20,7 +20,7 @@ from .utils import TTYIO, parse_address_spec
def handle_cmd(boss, window, cmd):
cmd = json.loads(cmd)
v = cmd['version']
no_response = cmd['no_response']
no_response = cmd.get('no_response', False)
if tuple(v)[:2] > version[:2]:
if no_response:
return