Add a remote control command to change colors

Also add a FAQ entry about changing colors in a running kitty instance.
I'm tired of all the bug reports asking for this feature. Apparently,
people find it hard to google for the existing escape codes based
solution.
This commit is contained in:
Kovid Goyal 2018-04-02 10:45:52 +05:30
parent 2efa83bc4d
commit 1fd84612a8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 160 additions and 17 deletions

View File

@ -522,6 +522,14 @@ have ssh do this automatically when connecting to a server, so that all
terminals work transparently. terminals work transparently.
=== How do I change the colors in a running kitty instance?
You can either use the
link:http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands[OSC
terminal escape codes] to set colors or you can enable link:remote-control.asciidoc[remote control]
for kitty and use `kitty @ set-colors --help`.
=== How do I specify command line options for kitty on macOS? === How do I specify command line options for kitty on macOS?
Apple does not want you to use command line options with GUI applications. To Apple does not want you to use command line options with GUI applications. To

View File

@ -126,24 +126,35 @@ class Boss:
'tabs': list(tm.list_tabs()), 'tabs': list(tm.list_tabs()),
} }
@property
def all_tab_managers(self):
yield from self.os_window_map.values()
@property
def all_tabs(self):
for tm in self.all_tab_managers:
yield from tm
@property
def all_windows(self):
for tab in self.all_tabs:
yield from tab
def match_windows(self, match): def match_windows(self, match):
try: try:
field, exp = match.split(':', 1) field, exp = match.split(':', 1)
except ValueError: except ValueError:
return return
pat = re.compile(exp) pat = re.compile(exp)
for tm in self.os_window_map.values(): for window in self.all_windows:
for tab in tm: if window.matches(field, pat):
for window in tab: yield window
if window.matches(field, pat):
yield window
def tab_for_window(self, window): def tab_for_window(self, window):
for tm in self.os_window_map.values(): for tab in self.all_tabs:
for tab in tm: for w in tab:
for w in tab: if w.id == window.id:
if w.id == window.id: return tab
return tab
def match_tabs(self, match): def match_tabs(self, match):
try: try:
@ -151,14 +162,12 @@ class Boss:
except ValueError: except ValueError:
return return
pat = re.compile(exp) pat = re.compile(exp)
tms = tuple(self.os_window_map.values())
found = False found = False
if field in ('title', 'id'): if field in ('title', 'id'):
for tm in tms: for tab in self.all_tabs:
for tab in tm: if tab.matches(field, pat):
if tab.matches(field, pat): yield tab
yield tab found = True
found = True
if not found: if not found:
tabs = {self.tab_for_window(w) for w in self.match_windows(match)} tabs = {self.tab_for_window(w) for w in self.match_windows(match)}
for tab in tabs: for tab in tabs:
@ -687,3 +696,12 @@ class Boss:
tm = self.active_tab_manager tm = self.active_tab_manager
if tm is not None: if tm is not None:
tm.move_tab(-1) tm.move_tab(-1)
def patch_colors(self, spec, configured=False):
from .rgb import color_from_int
if configured:
for k, v in spec.items():
if hasattr(self.opts, k):
setattr(self.opts, k, color_from_int(v))
for tm in self.all_tab_managers:
tm.tab_bar.patch_colors(spec)

View File

@ -98,6 +98,43 @@ update_ansi_color_table(ColorProfile *self, PyObject *val) {
Py_RETURN_NONE; Py_RETURN_NONE;
} }
static PyObject*
patch_color_profiles(PyObject *module UNUSED, PyObject *args) {
PyObject *spec, *profiles, *v; ColorProfile *self; int change_configured;
if (!PyArg_ParseTuple(args, "O!O!p", &PyDict_Type, &spec, &PyTuple_Type, &profiles, &change_configured)) return NULL;
char key[32] = {0};
for (size_t i = 0; i < arraysz(FG_BG_256); i++) {
snprintf(key, sizeof(key) - 1, "color%zu", i);
v = PyDict_GetItemString(spec, key);
if (v) {
color_type color = PyLong_AsUnsignedLong(v);
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) {
self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i);
self->color_table[i] = color;
if (change_configured) self->orig_color_table[i] = color;
self->dirty = true;
}
}
}
#define S(config_name, profile_name) { \
v = PyDict_GetItemString(spec, #config_name); \
if (v) { \
color_type color = PyLong_AsUnsignedLong(v); \
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) { \
self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i); \
self->overridden.profile_name = (color << 8) | 2; \
if (change_configured) self->configured.profile_name = color; \
self->dirty = true; \
} \
} \
}
S(foreground, default_fg); S(background, default_bg); S(cursor, cursor_color);
S(selection_foreground, highlight_fg); S(selection_background, highlight_bg);
#undef S
Py_RETURN_NONE;
}
color_type color_type
colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval) { colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval) {
color_type t = entry & 0xFF, r; color_type t = entry & 0xFF, r;
@ -247,6 +284,7 @@ PyTypeObject ColorProfile_Type = {
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
METHODB(default_color_table, METH_NOARGS), METHODB(default_color_table, METH_NOARGS),
METHODB(patch_color_profiles, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };

View File

@ -3,6 +3,7 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import json import json
import os
import re import re
import socket import socket
import sys import sys
@ -10,7 +11,7 @@ import types
from functools import partial from functools import partial
from .cli import emph, parse_args from .cli import emph, parse_args
from .config import parse_send_text_bytes from .config import parse_config, parse_send_text_bytes
from .constants import appname, version from .constants import appname, version
from .tabs import SpecialWindow from .tabs import SpecialWindow
from .utils import non_blocking_read, parse_address_spec, read_with_timeout from .utils import non_blocking_read, parse_address_spec, read_with_timeout
@ -467,6 +468,69 @@ def get_text(boss, window, payload):
# }}} # }}}
# set_colors {{{
@cmd(
'Set terminal colors',
'Set the terminal colors for the specified windows/tabs (defaults to active window). You can either specify the path to a conf file'
' (in the same format as kitty.conf) to read the colors from or you can specify individual colors,'
' for example: kitty @ set-colors foreground=red background=white',
options_spec='''\
--all -a
type=bool-set
By default, colors are only changed for the currently active window. This option will
cause colors to be changed in all windows.
--configured -c
type=bool-set
Also change the configured colors (i.e. the colors kitty will use for new
windows or after a reset).
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t'),
argspec='COLOR_OR_FILE ...'
)
def cmd_set_colors(global_opts, opts, args):
from .rgb import color_as_int, Color
colors = {}
for spec in args:
if '=' in spec:
colors.update(parse_config((spec.replace('=', ' '),)))
else:
with open(os.path.expanduser(spec), encoding='utf-8', errors='replace') as f:
colors.update(parse_config(f))
colors = {k: color_as_int(v) for k, v in colors.items() if isinstance(v, Color)}
return {
'title': ' '.join(args), 'match_window': opts.match, 'match_tab': opts.match_tab,
'all': opts.all, 'configured': opts.configured, 'colors': colors
}
def set_colors(boss, window, payload):
if payload['all']:
windows = tuple(boss.all_windows)
else:
windows = (window or boss.active_window,)
if payload['match_window']:
windows = tuple(boss.match_windows(payload['match_window']))
if not windows:
raise MatchError(payload['match_window'])
if payload['match_tab']:
tabs = tuple(boss.match_tabs(payload['match_tab']))
if not tabs:
raise MatchError(payload['match_tab'], 'tabs')
for tab in tabs:
windows += tuple(tab)
profiles = tuple(w.screen.color_profile for w in windows)
from .fast_data_types import patch_color_profiles
patch_color_profiles(payload['colors'], profiles, payload['configured'])
boss.patch_colors(payload['colors'], payload['configured'])
default_bg_changed = 'background' in payload['colors']
for w in windows:
if default_bg_changed:
boss.default_bg_changed_for(w.id)
w.refresh()
# }}}
cmap = {v.name: v for v in globals().values() if hasattr(v, 'is_cmd')} cmap = {v.name: v for v in globals().values() if hasattr(v, 'is_cmd')}

4
kitty/rgb.py generated
View File

@ -31,6 +31,10 @@ def color_from_int(x):
return Color((x >> 16) & 255, (x >> 8) & 255, x & 255) return Color((x >> 16) & 255, (x >> 8) & 255, x & 255)
def color_as_int(x):
return x.red << 16 | x.green << 8 | x.blue
def to_color(raw, validate=False): def to_color(raw, validate=False):
# See man XParseColor # See man XParseColor
x = raw.strip().lower() x = raw.strip().lower()

View File

@ -247,6 +247,7 @@ class TabBar: # {{{
def __init__(self, os_window_id, opts): def __init__(self, os_window_id, opts):
self.os_window_id = os_window_id self.os_window_id = os_window_id
self.opts = opts
self.num_tabs = 1 self.num_tabs = 1
self.cell_width = 1 self.cell_width = 1
self.data_buffer_size = 0 self.data_buffer_size = 0
@ -277,6 +278,16 @@ class TabBar: # {{{
self.active_bg = as_rgb(color_as_int(opts.active_tab_background)) self.active_bg = as_rgb(color_as_int(opts.active_tab_background))
self.active_fg = as_rgb(color_as_int(opts.active_tab_foreground)) self.active_fg = as_rgb(color_as_int(opts.active_tab_foreground))
def patch_colors(self, spec):
if 'active_tab_foreground' in spec:
self.active_fg = (spec['active_tab_foreground'] << 8) | 2
if 'active_tab_background' in spec:
self.active_bg = (spec['active_tab_background'] << 8) | 2
self.screen.color_profile.set_configured_colors(
spec.get('inactive_tab_foreground', color_as_int(self.opts.inactive_tab_foreground)),
spec.get('inactive_tab_background', color_as_int(self.opts.inactive_tab_background))
)
def layout(self): def layout(self):
central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id) central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id)
if tab_bar.width < 2: if tab_bar.width < 2: