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)
|
||||
|
||||
|
||||
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():
|
||||
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
|
||||
if first_arg in ('icat', '+icat'):
|
||||
icat(sys.argv[1:])
|
||||
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:
|
||||
func = entry_points.get(first_arg)
|
||||
if func is None:
|
||||
from kitty.main import main
|
||||
main()
|
||||
else:
|
||||
func(sys.argv[1:])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -125,6 +125,16 @@ class Boss:
|
||||
else:
|
||||
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):
|
||||
window = self.window_id_map.pop(window_id, None)
|
||||
if window is None:
|
||||
|
||||
13
kitty/cli.py
13
kitty/cli.py
@ -206,7 +206,7 @@ def prettify(text):
|
||||
def sub(m):
|
||||
t = m.group(2)
|
||||
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
|
||||
|
||||
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:
|
||||
leading_indent = 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 ...]'
|
||||
a('{}: {} [options] {}'.format(title('Usage'), bold(yellow(appname)), usage))
|
||||
a('')
|
||||
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'
|
||||
).format(appname=appname)
|
||||
wa(prettify(message))
|
||||
a('')
|
||||
if seq:
|
||||
a('{}:'.format(title('Options')))
|
||||
for opt in seq:
|
||||
if isinstance(opt, str):
|
||||
|
||||
@ -256,6 +256,7 @@ url_style.map = dict(((v, i) for i, v in enumerate('none single double curly'.sp
|
||||
|
||||
|
||||
type_map = {
|
||||
'allow_remote_control': to_bool,
|
||||
'adjust_line_height': adjust_line_height,
|
||||
'scrollback_lines': positive_int,
|
||||
'scrollback_pager': shlex.split,
|
||||
|
||||
@ -153,6 +153,13 @@ open_url_with default
|
||||
# rectangular block with the mouse)
|
||||
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
|
||||
# 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
|
||||
|
||||
@ -727,6 +727,17 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
// }}}
|
||||
|
||||
// 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
|
||||
dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
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);
|
||||
screen_request_capabilities(screen, (char)screen->parser_buf[0], string);
|
||||
Py_DECREF(string);
|
||||
}
|
||||
} else PyErr_Clear();
|
||||
} else {
|
||||
REPORT_ERROR("Unrecognized DCS %c code: 0x%x", (char)screen->parser_buf[0], screen->parser_buf[1]);
|
||||
}
|
||||
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:
|
||||
REPORT_ERROR("Unrecognized DCS code: 0x%x", screen->parser_buf[0]);
|
||||
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); }
|
||||
}
|
||||
|
||||
void
|
||||
screen_handle_cmd(Screen *self, PyObject *cmd) {
|
||||
CALLBACK("handle_remote_cmd", "O", cmd);
|
||||
}
|
||||
|
||||
void
|
||||
screen_request_capabilities(Screen *self, char c, PyObject *q) {
|
||||
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_set_margins(Screen *self, unsigned int top, unsigned int bottom);
|
||||
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_use_latin1(Screen *, bool);
|
||||
void set_title(Screen *self, PyObject*);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import json
|
||||
import sys
|
||||
import weakref
|
||||
from collections import deque
|
||||
@ -258,6 +259,12 @@ class Window:
|
||||
def request_capabilities(self, 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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user