Framework for remote control
This commit is contained in:
parent
85a3da057f
commit
f3cb68ee40
36
__main__.py
36
__main__.py
@ -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__':
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
15
kitty/cli.py
15
kitty/cli.py
@ -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,18 +263,25 @@ 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('')
|
||||||
a('{}:'.format(title('Options')))
|
if seq:
|
||||||
|
a('{}:'.format(title('Options')))
|
||||||
for opt in seq:
|
for opt in seq:
|
||||||
if isinstance(opt, str):
|
if isinstance(opt, str):
|
||||||
a('{}:'.format(title(opt)))
|
a('{}:'.format(title(opt)))
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
73
kitty/remote_control.py
Normal 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()
|
||||||
@ -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];
|
||||||
|
|||||||
@ -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*);
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user