Framework for remote control

This commit is contained in:
Kovid Goyal 2018-01-07 15:17:29 +05:30
parent 85a3da057f
commit f3cb68ee40
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
10 changed files with 164 additions and 16 deletions

View File

@ -15,21 +15,35 @@ def list_fonts(args):
main(args) main(args)
def remote_control(args):
from kitty.remote_control import main
main(args)
def namespaced(args):
func = namespaced_entry_points[args[0]]
func(args[1:])
entry_points = {
'icat': icat,
'list-fonts': list_fonts,
'+icat': icat,
'+list-fonts': list_fonts,
'@': remote_control,
'+': namespaced,
}
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
def main(): def main():
first_arg = '' if len(sys.argv) < 2 else sys.argv[1] first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
if first_arg in ('icat', '+icat'): func = entry_points.get(first_arg)
icat(sys.argv[1:]) if func is None:
elif first_arg in ('list-fonts', '+list-fonts'):
list_fonts(sys.argv[1:])
elif first_arg == '+' and len(sys.argv) > 2:
q = sys.argv[2]
if q == 'icat':
icat(sys.argv[2:])
elif q == 'list-fonts':
list_fonts(sys.argv[2:])
else:
from kitty.main import main from kitty.main import main
main() main()
else:
func(sys.argv[1:])
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -125,6 +125,16 @@ class Boss:
else: else:
safe_print('Unknown message received from peer, ignoring') safe_print('Unknown message received from peer, ignoring')
def handle_remote_cmd(self, cmd, window=None):
response = None
if self.opts.allow_remote_control:
pass
else:
response = {'ok': False, 'err': 'NOT_ALLOWED'}
if response is not None:
if window is not None:
window.send_cmd_response(response)
def on_child_death(self, window_id): def on_child_death(self, window_id):
window = self.window_id_map.pop(window_id, None) window = self.window_id_map.pop(window_id, None)
if window is None: if window is None:

View File

@ -206,7 +206,7 @@ def prettify(text):
def sub(m): def sub(m):
t = m.group(2) t = m.group(2)
for ch in m.group(1): for ch in m.group(1):
t = {'C': cyan, '_': italic, '*': bold}[ch](t) t = {'C': cyan, '_': italic, '*': bold, 'G': green, 'T': title}[ch](t)
return t return t
text = re.sub(r'[|]([a-zA-Z_*]+?) (.+?)[|]', sub, text) text = re.sub(r'[|]([a-zA-Z_*]+?) (.+?)[|]', sub, text)
@ -263,17 +263,24 @@ def print_help_for_seq(seq, usage, message, appname):
if leading_indent is None: if leading_indent is None:
leading_indent = indent leading_indent = indent
j = '\n' + (' ' * indent) j = '\n' + (' ' * indent)
a((' ' * leading_indent) + j.join(wrap(text, limit=linesz - indent))) lines = []
for l in text.splitlines():
if l:
lines.extend(wrap(l, limit=linesz - indent))
else:
lines.append('')
a((' ' * leading_indent) + j.join(lines))
usage = usage or '[program-to-run ...]' usage = usage or '[program-to-run ...]'
a('{}: {} [options] {}'.format(title('Usage'), bold(yellow(appname)), usage)) a('{}: {} [options] {}'.format(title('Usage'), bold(yellow(appname)), usage))
a('') a('')
message = message or ( message = message or (
'Run the |_ {appname}| terminal emulator. You can also specify the |_ program| to run inside |_ {appname}| as normal' 'Run the |G {appname}| terminal emulator. You can also specify the |_ program| to run inside |_ {appname}| as normal'
' arguments following the |_ options|. For example: {appname} /bin/sh' ' arguments following the |_ options|. For example: {appname} /bin/sh'
).format(appname=appname) ).format(appname=appname)
wa(prettify(message)) wa(prettify(message))
a('') a('')
if seq:
a('{}:'.format(title('Options'))) a('{}:'.format(title('Options')))
for opt in seq: for opt in seq:
if isinstance(opt, str): if isinstance(opt, str):

View File

@ -256,6 +256,7 @@ url_style.map = dict(((v, i) for i, v in enumerate('none single double curly'.sp
type_map = { type_map = {
'allow_remote_control': to_bool,
'adjust_line_height': adjust_line_height, 'adjust_line_height': adjust_line_height,
'scrollback_lines': positive_int, 'scrollback_lines': positive_int,
'scrollback_pager': shlex.split, 'scrollback_pager': shlex.split,

View File

@ -153,6 +153,13 @@ open_url_with default
# rectangular block with the mouse) # rectangular block with the mouse)
rectangle_select_modifiers ctrl+alt rectangle_select_modifiers ctrl+alt
# Allow other programs to control kitty. If you turn this on other programs can
# control all aspects of kitty, including sending text to kitty windows,
# opening new windows, closing windows, reading the content of windows, etc.
# Note that this even works over ssh connections.
allow_remote_control no
# Choose whether to use the system implementation of wcwidth() (used to # Choose whether to use the system implementation of wcwidth() (used to
# control how many cells a character is rendered in). If you use the system # control how many cells a character is rendered in). If you use the system
# implementation, then kitty and any programs running in it will agree. The # implementation, then kitty and any programs running in it will agree. The

View File

@ -727,6 +727,17 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
// }}} // }}}
// DCS mode {{{ // DCS mode {{{
static inline bool
startswith(const uint32_t *string, size_t sz, const char *prefix) {
size_t l = strlen(prefix);
if (sz < l) return false;
for (size_t i = 0; i < l; i++) {
if (string[i] != (unsigned char)prefix[i]) return false;
}
return true;
}
static inline void static inline void
dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
if (screen->parser_buf_pos < 2) return; if (screen->parser_buf_pos < 2) return;
@ -739,11 +750,23 @@ dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
REPORT_OSC2(screen_request_capabilities, (char)screen->parser_buf[0], string); REPORT_OSC2(screen_request_capabilities, (char)screen->parser_buf[0], string);
screen_request_capabilities(screen, (char)screen->parser_buf[0], string); screen_request_capabilities(screen, (char)screen->parser_buf[0], string);
Py_DECREF(string); Py_DECREF(string);
} } else PyErr_Clear();
} else { } else {
REPORT_ERROR("Unrecognized DCS %c code: 0x%x", (char)screen->parser_buf[0], screen->parser_buf[1]); REPORT_ERROR("Unrecognized DCS %c code: 0x%x", (char)screen->parser_buf[0], screen->parser_buf[1]);
} }
break; break;
case '@':
if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 2, "kitty-cmd{")) {
PyObject *cmd = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + 11, screen->parser_buf_pos - 11);
if (cmd != NULL) {
REPORT_OSC2(screen_handle_cmd, (char)screen->parser_buf[0], cmd);
screen_handle_cmd(screen, cmd);
Py_DECREF(cmd);
} else PyErr_Clear();
} else {
REPORT_ERROR("Unrecognized DCS @ code: 0x%x", screen->parser_buf[1]);
}
break;
default: default:
REPORT_ERROR("Unrecognized DCS code: 0x%x", screen->parser_buf[0]); REPORT_ERROR("Unrecognized DCS code: 0x%x", screen->parser_buf[0]);
break; break;

73
kitty/remote_control.py Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import json
import sys
from functools import partial
from kitty.cli import emph, parse_args
from kitty.constants import appname, version
def cmd(short_desc, desc=None, options_spec=None):
def w(func):
func.short_desc = short_desc
func.desc = desc or short_desc
func.name = func.__name__[4:]
func.options_spec = options_spec
return func
return w
def parse_subcommand_cli(func, args):
opts, items = parse_args(args[1:], func.options_spec or '\n'.format, '...', func.desc, '{} @ {}'.format(appname, func.name))
return opts, items
@cmd('List all windows')
def cmd_ls(global_opts, opts, args):
pass
global_options_spec = partial('''\
'''.format, appname=appname)
def main(args):
cmap = {k[4:]: v for k, v in globals().items() if k.startswith('cmd_')}
all_commands = tuple(sorted(cmap))
cmds = (' |G {}|\n {}'.format(cmap[c].name, cmap[c].short_desc) for c in all_commands)
msg = (
'Control {appname} by sending it commands. Add'
' |_ allow_remote_control yes| to kitty.conf for this'
' to work.\n\n|T Commands|:\n{cmds}\n\n'
'You can get help for each individual command by using:\n'
'{appname} @ |_ command| -h'
).format(appname=appname, cmds='\n'.join(cmds))
global_opts, items = parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname))
if not items:
raise SystemExit('You must specify a command')
cmd = items[0]
try:
func = cmap[cmd]
except KeyError:
raise SystemExit('{} is not a known command. Known commands are: {}'.format(
emph(cmd), ', '.join(all_commands)))
opts, items = parse_subcommand_cli(func, items)
payload = func(global_opts, opts, items)
send = {
'cmd': cmd,
'version': version,
}
if payload is not None:
send['payload'] = payload
send = ('@kitty-cmd' + json.dumps(send)).encode('ascii')
if not sys.stdout.isatty():
raise SystemExit('stdout is not a terminal')
sys.stdout.buffer.write(b'\x1bP' + send + b'\x1b\\')
sys.stdout.flush()

View File

@ -1141,6 +1141,11 @@ set_color_table_color(Screen *self, unsigned int code, PyObject *color) {
else { CALLBACK("set_color_table_color", "IO", code, color); } else { CALLBACK("set_color_table_color", "IO", code, color); }
} }
void
screen_handle_cmd(Screen *self, PyObject *cmd) {
CALLBACK("handle_remote_cmd", "O", cmd);
}
void void
screen_request_capabilities(Screen *self, char c, PyObject *q) { screen_request_capabilities(Screen *self, char c, PyObject *q) {
static char buf[128]; static char buf[128];

View File

@ -106,6 +106,7 @@ void screen_delete_characters(Screen *self, unsigned int count);
void screen_erase_characters(Screen *self, unsigned int count); void screen_erase_characters(Screen *self, unsigned int count);
void screen_set_margins(Screen *self, unsigned int top, unsigned int bottom); void screen_set_margins(Screen *self, unsigned int top, unsigned int bottom);
void screen_change_charset(Screen *, uint32_t to); void screen_change_charset(Screen *, uint32_t to);
void screen_handle_cmd(Screen *, PyObject *cmd);
void screen_designate_charset(Screen *, uint32_t which, uint32_t as); void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
void screen_use_latin1(Screen *, bool); void screen_use_latin1(Screen *, bool);
void set_title(Screen *self, PyObject*); void set_title(Screen *self, PyObject*);

View File

@ -2,6 +2,7 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import json
import sys import sys
import weakref import weakref
from collections import deque from collections import deque
@ -258,6 +259,12 @@ class Window:
def request_capabilities(self, q): def request_capabilities(self, q):
self.screen.send_escape_code_to_child(DCS, get_capabilities(q)) self.screen.send_escape_code_to_child(DCS, get_capabilities(q))
def handle_remote_cmd(self, cmd):
get_boss().handle_remote_cmd(cmd, self)
def send_cmd_response(self, response):
self.screen.send_escape_code_to_child(DCS, '@kitty-cmd' + json.dumps(response))
# }}} # }}}
def text_for_selection(self): def text_for_selection(self):