diff --git a/README.asciidoc b/README.asciidoc index fe0b634d1..5e608db63 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -522,6 +522,14 @@ have ssh do this automatically when connecting to a server, so that all 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? Apple does not want you to use command line options with GUI applications. To diff --git a/kitty/boss.py b/kitty/boss.py index 12b7bea90..fd6499fab 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -126,24 +126,35 @@ class Boss: '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): try: field, exp = match.split(':', 1) except ValueError: return pat = re.compile(exp) - for tm in self.os_window_map.values(): - for tab in tm: - for window in tab: - if window.matches(field, pat): - yield window + for window in self.all_windows: + if window.matches(field, pat): + yield window def tab_for_window(self, window): - for tm in self.os_window_map.values(): - for tab in tm: - for w in tab: - if w.id == window.id: - return tab + for tab in self.all_tabs: + for w in tab: + if w.id == window.id: + return tab def match_tabs(self, match): try: @@ -151,14 +162,12 @@ class Boss: except ValueError: return pat = re.compile(exp) - tms = tuple(self.os_window_map.values()) found = False if field in ('title', 'id'): - for tm in tms: - for tab in tm: - if tab.matches(field, pat): - yield tab - found = True + for tab in self.all_tabs: + if tab.matches(field, pat): + yield tab + found = True if not found: tabs = {self.tab_for_window(w) for w in self.match_windows(match)} for tab in tabs: @@ -687,3 +696,12 @@ class Boss: tm = self.active_tab_manager if tm is not None: 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) diff --git a/kitty/colors.c b/kitty/colors.c index 67231cb77..07dd56066 100644 --- a/kitty/colors.c +++ b/kitty/colors.c @@ -98,6 +98,43 @@ update_ansi_color_table(ColorProfile *self, PyObject *val) { 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 colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval) { color_type t = entry & 0xFF, r; @@ -247,6 +284,7 @@ PyTypeObject ColorProfile_Type = { static PyMethodDef module_methods[] = { METHODB(default_color_table, METH_NOARGS), + METHODB(patch_color_profiles, METH_VARARGS), {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/kitty/remote_control.py b/kitty/remote_control.py index f4bfc79d7..10a39ce18 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -3,6 +3,7 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal import json +import os import re import socket import sys @@ -10,7 +11,7 @@ import types from functools import partial 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 .tabs import SpecialWindow 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')} diff --git a/kitty/rgb.py b/kitty/rgb.py index 1af967f48..1030292e8 100644 --- a/kitty/rgb.py +++ b/kitty/rgb.py @@ -31,6 +31,10 @@ def color_from_int(x): 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): # See man XParseColor x = raw.strip().lower() diff --git a/kitty/tabs.py b/kitty/tabs.py index 8cf1c7c16..5ac76ff77 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -247,6 +247,7 @@ class TabBar: # {{{ def __init__(self, os_window_id, opts): self.os_window_id = os_window_id + self.opts = opts self.num_tabs = 1 self.cell_width = 1 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_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): central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id) if tab_bar.width < 2: