diff --git a/docs/changelog.rst b/docs/changelog.rst index 39815e2a0..27be6b045 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ To update |kitty|, :doc:`follow the instructions `. - 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] --------------------- diff --git a/docs/conf.py b/docs/conf.py index 68e814038..bf5bf4d87 100644 --- a/docs/conf.py +++ b/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) diff --git a/docs/index.rst b/docs/index.rst index c2b0364e8..f9675be81 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -486,3 +486,4 @@ See :doc:`changelog`. * kittens/* + generated/rc diff --git a/docs/rc_protocol.rst b/docs/rc_protocol.rst new file mode 100644 index 000000000..0af13120c --- /dev/null +++ b/docs/rc_protocol.rst @@ -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:: + + P@kitty-cmd\ + +Where ```` is the byte ``0x1b``. The JSON object has the form:: + + { + 'cmd': "command name", + 'version': "kitty version", + 'no_response': Optional Boolean, + 'payload': , + } + +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 diff --git a/docs/remote-control.rst b/docs/remote-control.rst index 47dce3fda..83df27dd2 100644 --- a/docs/remote-control.rst +++ b/docs/remote-control.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 diff --git a/kitty/cli.py b/kitty/cli.py index 227c4b9d9..14ca30dce 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -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`. diff --git a/kitty/cmds.py b/kitty/cmds.py index 3ab68c18d..c7ac536fb 100644 --- a/kitty/cmds.py +++ b/kitty/cmds.py @@ -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: diff --git a/kitty/remote_control.py b/kitty/remote_control.py index b5bfcd6d9..b38706742 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -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