From c1fb18a6ef80598656a2e57050e4c8dbf8ea45f2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 24 Sep 2024 19:01:35 +0530 Subject: [PATCH] Implement changing transparent background colors via remote control --- kitty/boss.py | 3 +- kitty/colors.c | 63 ++++++++++++++++++++++++++------------ kitty/fast_data_types.pyi | 5 +-- kitty/launch.py | 4 +-- kitty/rc/get_colors.py | 2 +- kitty/rc/set_colors.py | 33 ++++++++++++++------ kitty/window.py | 2 +- tools/cmd/at/set_colors.go | 2 ++ 8 files changed, 78 insertions(+), 36 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index 4afac6258..c484f0c0e 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -2617,7 +2617,7 @@ class Boss: window.screen.disable_ligatures = strategy window.refresh() - def patch_colors(self, spec: dict[str, Optional[int]], configured: bool = False) -> None: + def patch_colors(self, spec: dict[str, Optional[int]], transparent_background_colors: tuple[tuple[Color, float], ...], configured: bool = False) -> None: opts = get_options() if configured: for k, v in spec.items(): @@ -2627,6 +2627,7 @@ class Boss: setattr(opts, k, None) else: setattr(opts, k, color_from_int(v)) + opts.transparent_background_colors = transparent_background_colors for tm in self.all_tab_managers: tm.tab_bar.patch_colors(spec) tm.tab_bar.layout() diff --git a/kitty/colors.c b/kitty/colors.c index e2433ad9d..24986d574 100644 --- a/kitty/colors.c +++ b/kitty/colors.c @@ -60,6 +60,17 @@ create_256_color_table(void) { return ans; } +static void +set_transparent_background_colors(TransparentDynamicColor *dest, PyObject *src) { + memset(dest, 0, sizeof(((ColorProfile*)0)->configured_transparent_colors)); + for (Py_ssize_t i = 0; i < MIN(PyTuple_GET_SIZE(src), (Py_ssize_t)arraysz(((ColorProfile*)0)->configured_transparent_colors)); i++) { + PyObject *e = PyTuple_GET_ITEM(src, i); + dest[i].color = ((Color*)(PyTuple_GET_ITEM(e, 0)))->color.val & 0xffffff; + dest[i].opacity = (float)PyFloat_AsDouble(PyTuple_GET_ITEM(e, 1)); + dest[i].is_set = true; + } +} + static bool set_configured_colors(ColorProfile *self, PyObject *opts) { #define n(which, attr) { \ @@ -82,15 +93,9 @@ set_configured_colors(ColorProfile *self, PyObject *opts) { n(highlight_fg, selection_foreground); n(highlight_bg, selection_background); n(visual_bell_color, visual_bell_color); #undef n - memset(self->configured_transparent_colors, 0, sizeof(self->configured_transparent_colors)); RAII_PyObject(src, PyObject_GetAttrString(opts, "transparent_background_colors")); if (!src) { PyErr_SetString(PyExc_TypeError, "No transparent_background_colors on opts object"); return false; } - for (Py_ssize_t i = 0; i < MIN(PyTuple_GET_SIZE(src), (Py_ssize_t)arraysz(self->configured_transparent_colors)); i++) { - PyObject *e = PyTuple_GET_ITEM(src, i); - self->configured_transparent_colors[i].color = ((Color*)(PyTuple_GET_ITEM(e, 0)))->color.val & 0xffffff; - self->configured_transparent_colors[i].opacity = (float)PyFloat_AsDouble(PyTuple_GET_ITEM(e, 1)); - self->configured_transparent_colors[i].is_set = true; - } + set_transparent_background_colors(self->configured_transparent_colors, src); return PyErr_Occurred() ? false : true; } @@ -204,8 +209,8 @@ patch_color_table(const char *key, PyObject *profiles, PyObject *spec, size_t wh 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; + PyObject *spec, *transparent_background_colors, *profiles, *v; ColorProfile *self; int change_configured; + if (!PyArg_ParseTuple(args, "O!O!O!p", &PyDict_Type, &spec, &PyTuple_Type, &transparent_background_colors, &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); @@ -239,7 +244,12 @@ patch_color_profiles(PyObject *module UNUSED, PyObject *args) { S(cursor_text_color, cursor_text_color); S(visual_bell_color, visual_bell_color); #undef SI #undef S - // TODO: Patch transparent_colors + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) { + self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i); + set_transparent_background_colors(self->overriden_transparent_colors, transparent_background_colors); + if (change_configured) set_transparent_background_colors(self->configured_transparent_colors, transparent_background_colors); + } + if (PyErr_Occurred()) return NULL; Py_RETURN_NONE; } @@ -293,20 +303,21 @@ colorprofile_to_color_with_fallback(ColorProfile *self, DynamicColor entry, Dyna } return entry.rgb; } +static Color* alloc_color(unsigned char r, unsigned char g, unsigned char b, unsigned a); static PyObject* as_dict(ColorProfile *self, PyObject *args UNUSED) { #define as_dict_doc "Return all colors as a dictionary of color_name to integer or None (names are the same as used in kitty.conf)" - PyObject *ans = PyDict_New(); + RAII_PyObject(ans, PyDict_New()); if (ans == NULL) return PyErr_NoMemory(); for (unsigned i = 0; i < arraysz(self->color_table); i++) { static char buf[32] = {0}; snprintf(buf, sizeof(buf) - 1, "color%u", i); PyObject *val = PyLong_FromUnsignedLong(self->color_table[i]); - if (!val) { Py_CLEAR(ans); return PyErr_NoMemory(); } + if (!val) { return PyErr_NoMemory(); } int ret = PyDict_SetItemString(ans, buf, val); Py_CLEAR(val); - if (ret != 0) { Py_CLEAR(ans); return NULL; } + if (ret != 0) { return NULL; } } #define D(attr, name) { \ if (self->overridden.attr.type != COLOR_NOT_SET) { \ @@ -317,18 +328,33 @@ as_dict(ColorProfile *self, PyObject *args UNUSED) { color_type c = colorprofile_to_color(self, self->overridden.attr, self->configured.attr).rgb; \ val = PyLong_FromUnsignedLong(c); \ } \ - if (!val) { Py_CLEAR(ans); return NULL; } \ + if (!val) { return NULL; } \ ret = PyDict_SetItemString(ans, #name, val); \ Py_CLEAR(val); \ - if (ret != 0) { Py_CLEAR(ans); return NULL; } \ + if (ret != 0) { return NULL; } \ }} D(default_fg, foreground); D(default_bg, background); D(cursor_color, cursor); D(cursor_text_color, cursor_text); D(highlight_fg, selection_foreground); D(highlight_bg, selection_background); D(visual_bell_color, visual_bell_color); - + RAII_PyObject(transparent_background_colors, PyList_New(0)); + if (!transparent_background_colors) return NULL; + for (size_t i = 0; i < arraysz(self->overriden_transparent_colors); i++) { + TransparentDynamicColor *c = NULL; + if (self->overriden_transparent_colors[i].is_set) c = self->overriden_transparent_colors + i; + else if (self->configured_transparent_colors[i].is_set) c = self->configured_transparent_colors + i; + if (c) { + RAII_PyObject(t, Py_BuildValue("Nf", alloc_color((c->color >> 16) & 0xff, (c->color >> 8) & 0xff, c->color & 0xff, 0), c->opacity)); + if (!t) return NULL; + if (PyList_Append(transparent_background_colors, t) != 0) return NULL; + } + } + if (PyList_GET_SIZE(transparent_background_colors)) { + RAII_PyObject(t, PyList_AsTuple(transparent_background_colors)); + if (!t) return NULL; + if (PyDict_SetItemString(ans, "transparent_background_colors", t) != 0) return NULL; + } #undef D - // TODO: Add transparent_colors - return ans; + return Py_NewRef(ans); } static PyObject* @@ -478,7 +504,6 @@ default_color_table(PyObject *self UNUSED, PyObject *args UNUSED) { // Boilerplate {{{ -static Color* alloc_color(unsigned char r, unsigned char g, unsigned char b, unsigned a); #define CGETSET(name, nullable) \ static PyObject* name##_get(ColorProfile *self, void UNUSED *closure) { \ DynamicColor ans = colorprofile_to_color(self, self->overridden.name, self->configured.name); \ diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 93b521a0e..0f703b615 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -811,7 +811,7 @@ class ColorProfile: def __init__(self, opts: Optional[Options] = None): ... - def as_dict(self) -> Dict[str, Optional[int]]: + def as_dict(self) -> Dict[str, int | None | tuple[tuple[Color, float], ...]]: pass def as_color(self, val: int) -> Optional[Color]: @@ -833,7 +833,8 @@ class ColorProfile: def patch_color_profiles( - spec: Dict[str, Optional[int]], profiles: Tuple[ColorProfile, ...], change_configured: bool + spec: Dict[str, Optional[int]], transparent_background_colors: tuple[tuple[Color, float], ...], + profiles: Tuple[ColorProfile, ...], change_configured: bool ) -> None: pass diff --git a/kitty/launch.py b/kitty/launch.py index e9a28ef8c..73d6a6972 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -478,9 +478,9 @@ class LaunchKwds(TypedDict): def apply_colors(window: Window, spec: Sequence[str]) -> None: from kitty.rc.set_colors import parse_colors - colors = parse_colors(spec) + colors, transparent_background_colors = parse_colors(spec) profiles = window.screen.color_profile, - patch_color_profiles(colors, profiles, True) + patch_color_profiles(colors, transparent_background_colors, profiles, True) def parse_var(defn: Iterable[str]) -> Iterator[tuple[str, str]]: diff --git a/kitty/rc/get_colors.py b/kitty/rc/get_colors.py index 2631bd1d2..f38b4edde 100644 --- a/kitty/rc/get_colors.py +++ b/kitty/rc/get_colors.py @@ -49,7 +49,7 @@ configured colors. for k, v in windows[0].current_colors.items(): if v is None: ans.pop(k, None) - else: + elif isinstance(v, int): ans[k] = color_from_int(v) tab = windows[0].tabref() tm = None if tab is None else tab.tab_manager_ref() diff --git a/kitty/rc/set_colors.py b/kitty/rc/set_colors.py index 7d231f43f..0f7db8804 100644 --- a/kitty/rc/set_colors.py +++ b/kitty/rc/set_colors.py @@ -27,16 +27,19 @@ if TYPE_CHECKING: from kitty.cli_stub import SetColorsRCOptions as CLIOptions -def parse_colors(args: Iterable[str]) -> Dict[str, Optional[int]]: +def parse_colors(args: Iterable[str]) -> tuple[Dict[str, Optional[int]], tuple[tuple[Color, float], ...]]: from kitty.options.types import nullable_colors colors: Dict[str, Optional[Color]] = {} nullable_color_map: Dict[str, Optional[int]] = {} + transparent_background_colors = () for spec in args: if '=' in spec: - colors.update(parse_config((spec.replace('=', ' '),))) + conf = parse_config((spec.replace('=', ' '),)) else: with open(os.path.expanduser(spec), encoding='utf-8', errors='replace') as f: - colors.update(parse_config(f)) + conf = parse_config(f) + transparent_background_colors = conf.pop('transparent_background_colors', ()) + colors.update(conf) for k in nullable_colors: q = colors.pop(k, False) if q is not False: @@ -44,13 +47,13 @@ def parse_colors(args: Iterable[str]) -> Dict[str, Optional[int]]: nullable_color_map[k] = val ans: Dict[str, Optional[int]] = {k: int(v) for k, v in colors.items() if isinstance(v, Color)} ans.update(nullable_color_map) - return ans + return ans, transparent_background_colors class SetColors(RemoteCommand): protocol_spec = __doc__ = ''' - colors+/dict.colors: An object mapping names to colors as 24-bit RGB integers or null for nullable colors + colors+/dict.colors: An object mapping names to colors as 24-bit RGB integers or null for nullable colors. Or a string for transparent_background_colors. match_window/str: Window to change colors in match_tab/str: Tab to change colors in all/bool: Boolean indicating change colors everywhere or not @@ -88,14 +91,18 @@ this option, any color arguments are ignored and :option:`kitten @ set-colors -- completion=RemoteCommand.CompletionSpec.from_string('type:file group:"CONF files", ext:conf')) def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: - final_colors: Dict[str, Optional[int]] = {} + final_colors: Dict[str, int | None | str] = {} + transparent_background_colors: tuple[tuple[Color, float], ...] = () if not opts.reset: try: - final_colors = parse_colors(args) + fc, transparent_background_colors = parse_colors(args) except FileNotFoundError as err: raise ParsingOfArgsFailed(f'The colors configuration file {emph(err.filename)} was not found.') from err except Exception as err: raise ParsingOfArgsFailed(str(err)) from err + final_colors.update(fc) + if transparent_background_colors: + final_colors['transparent_background_colors'] = ' '.join(f'{c.as_sharp}@{f}' for c, f in transparent_background_colors) ans = { 'match_window': opts.match, 'match_tab': opts.match_tab, 'all': opts.all or opts.reset, 'configured': opts.configured or opts.reset, @@ -105,12 +112,18 @@ this option, any color arguments are ignored and :option:`kitten @ set-colors -- def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: windows = self.windows_for_payload(boss, window, payload_get) - colors: Dict[str, Optional[int]] = payload_get('colors') + colors: Dict[str, int | None] = payload_get('colors') + tbc = colors.get('transparent_background_colors') if payload_get('reset'): colors = {k: None if v is None else int(v) for k, v in boss.color_settings_at_startup.items()} profiles = tuple(w.screen.color_profile for w in windows if w) - patch_color_profiles(colors, profiles, payload_get('configured')) - boss.patch_colors(colors, payload_get('configured')) + if tbc: + from kitty.options.utils import transparent_background_colors + parsed_tbc = transparent_background_colors(str(tbc)) + else: + parsed_tbc = () + patch_color_profiles(colors, parsed_tbc, profiles, payload_get('configured')) + boss.patch_colors(colors, parsed_tbc, payload_get('configured')) default_bg_changed = 'background' in colors for w in windows: if w: diff --git a/kitty/window.py b/kitty/window.py index 5c82419d3..d8a241677 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -805,7 +805,7 @@ class Window: return tab.overlay_parent(self) @property - def current_colors(self) -> dict[str, Optional[int]]: + def current_colors(self) -> dict[str, int | tuple[tuple[Color, float], ...] | None]: return self.screen.color_profile.as_dict() @property diff --git a/tools/cmd/at/set_colors.go b/tools/cmd/at/set_colors.go index 0dbe5ddc0..12a9de1de 100644 --- a/tools/cmd/at/set_colors.go +++ b/tools/cmd/at/set_colors.go @@ -35,6 +35,8 @@ func set_color_in_color_map(key, val string, ans map[string]any, check_nullable, return fmt.Errorf("The color %s cannot be set to none", key) } ans[key] = nil + } else if key == "transparent_background_colors" { + ans[key] = val } else { col, err := style.ParseColor(val) if err != nil {