parent
15e8f6ad8a
commit
658be9405f
@ -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]
|
||||
---------------------
|
||||
|
||||
43
docs/conf.py
43
docs/conf.py
@ -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)
|
||||
|
||||
@ -486,3 +486,4 @@ See :doc:`changelog`.
|
||||
|
||||
*
|
||||
kittens/*
|
||||
generated/rc
|
||||
|
||||
29
docs/rc_protocol.rst
Normal file
29
docs/rc_protocol.rst
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user