diff --git a/docs/changelog.rst b/docs/changelog.rst index 7acdf5576..f336534c3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -77,6 +77,8 @@ Detailed list of changes 0.36.3 [future] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- The option ``second_transparent_bg`` has been removed and replaced by :opt:`transparent_background_colors` which allows setting up to seven additional colors that will be transparent, with individual opacities per color (:iss:`7646`) + - Fix a regression in the previous release that broke use of the ``cd`` command in session files (:iss:`7829`) - macOS: Fix shortcuts that become entries in the global menubar being reported as removed shortcuts in the debug output diff --git a/docs/color-stack.rst b/docs/color-stack.rst index 937a1fe65..b3feafd6f 100644 --- a/docs/color-stack.rst +++ b/docs/color-stack.rst @@ -69,8 +69,11 @@ selection_foreground The foreground color of selections cursor The color of the text cursor Foreground color cursor_text The color of text under the cursor Background color visual_bell The color of a visual bell Automatic color selection based on current screen colors -second_transparent_background A color that might be rendered semi-transparent No second color is made transparent - in addition to the default background color +transparent_background_color1..8 A background color that is rendered Unset + with the specified opacity in cells that have + the specified background color. An opacity + value less than zero means, use the + :opt:`background_opacity` value. ================================= =============================================== =============================== In this table the third column shows what effect setting the color to *dynamic* @@ -160,8 +163,19 @@ RGB colors are encoded in one of three forms: ``rgbi://`` red, green, and blue are floating-point values between 0.0 and 1.0, inclusive. The input format for these values is an optional - sign, a string of numbers possibly containing a decimal point, and an optional exponent field containing an E or e followed by a possibly - signed integer string. + sign, a string of numbers possibly containing a decimal point, and an optional exponent field containing an E or e followed by a possibly + signed integer string. Values outside the ``0 - 1`` range must be clipped to be within the range. + +If a color should have an alpha component, it must be suffixed to the color +specification in the form :code:`@number between zero and one`. For example:: + + red@0.5 rgb:ff0000@0.1 #ff0000@0.3 + +The syntax for the floating point alpha component is the same as used for the +components of ``rgbi`` defined above. When not specified, the default alpha +value is ``1.0``. Values outside the range ``0 - 1`` must be clipped +to be within the range, negative values may have special context dependent +meaning. In addition, the following color names are accepted (case-insensitively) corresponding to the specified RGB values. diff --git a/kitty/colors.c b/kitty/colors.c index 605df0d9d..e2433ad9d 100644 --- a/kitty/colors.c +++ b/kitty/colors.c @@ -249,10 +249,12 @@ colorprofile_to_transparent_color(const ColorProfile *self, unsigned index, colo if (index < arraysz(self->configured_transparent_colors)) { if (self->overriden_transparent_colors[index].is_set) { *color = self->overriden_transparent_colors[index].color; *opacity = self->overriden_transparent_colors[index].opacity; + if (*opacity < 0) *opacity = OPT(background_opacity); return true; } if (self->configured_transparent_colors[index].is_set) { *color = self->configured_transparent_colors[index].color; *opacity = self->configured_transparent_colors[index].opacity; + if (*opacity < 0) *opacity = OPT(background_opacity); return true; } } @@ -537,6 +539,36 @@ reload_from_opts(ColorProfile *self, PyObject *args UNUSED) { Py_RETURN_NONE; } +static PyObject* +get_transparent_background_color(ColorProfile *self, PyObject *index) { + if (!PyLong_Check(index)) { PyErr_SetString(PyExc_TypeError, "index must be an int"); return NULL; } + unsigned long idx = PyLong_AsUnsignedLong(index); + if (PyErr_Occurred()) return NULL; + if (idx >= arraysz(self->configured_transparent_colors)) Py_RETURN_NONE; + TransparentDynamicColor *c = self->overriden_transparent_colors[idx].is_set ? self->overriden_transparent_colors + idx : self->configured_transparent_colors + idx; + if (!c->is_set) Py_RETURN_NONE; + float opacity = c->opacity >= 0 ? c->opacity : OPT(background_opacity); + return (PyObject*)alloc_color((c->color >> 16) & 0xff, (c->color >> 8) & 0xff, c->color & 0xff, (unsigned)(255.f * opacity)); +} + +static PyObject* +set_transparent_background_color(ColorProfile *self, PyObject *const *args, Py_ssize_t nargs) { + if (nargs < 1) { PyErr_SetString(PyExc_TypeError, "must specify index"); return NULL; } + if (!PyLong_Check(args[0])) { PyErr_SetString(PyExc_TypeError, "index must be an int"); return NULL; } + unsigned long idx = PyLong_AsUnsignedLong(args[0]); + if (PyErr_Occurred()) return NULL; + if (idx >= arraysz(self->configured_transparent_colors)) Py_RETURN_NONE; + if (nargs < 2) { self->overriden_transparent_colors[idx].is_set = false; Py_RETURN_NONE; } + if (!PyObject_TypeCheck(args[1], &Color_Type)) { PyErr_SetString(PyExc_TypeError, "color must be Color object"); return NULL; } + Color *c = (Color*)args[1]; + float opacity = (float)(c->color.alpha) / 255.f; + if (nargs > 2 && PyFloat_Check(args[2])) opacity = (float)PyFloat_AsDouble(args[2]); + self->overriden_transparent_colors[idx].is_set = true; + self->overriden_transparent_colors[idx].color = c->color.rgb; + self->overriden_transparent_colors[idx].opacity = MAX(-1.f, MIN(opacity, 1.f)); + Py_RETURN_NONE; +} + static PyMethodDef cp_methods[] = { METHOD(reset_color_table, METH_NOARGS) METHOD(as_dict, METH_NOARGS) @@ -544,7 +576,9 @@ static PyMethodDef cp_methods[] = { METHOD(as_color, METH_O) METHOD(reset_color, METH_O) METHOD(set_color, METH_VARARGS) + METHODB(get_transparent_background_color, METH_O), METHODB(reload_from_opts, METH_VARARGS), + {"set_transparent_background_color", (PyCFunction)(void(*)(void))set_transparent_background_color, METH_FASTCALL, ""}, {NULL} /* Sentinel */ }; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index a31b647bf..93b521a0e 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -828,6 +828,9 @@ class ColorProfile: def reload_from_opts(self, opts: Optional[Options] = None) -> None: ... + def get_transparent_background_color(self, index: int) -> Color | None: ... + def set_transparent_background_color(self, index: int, color: Color | None = None, opacity: float | None = None) -> None: ... + def patch_color_profiles( spec: Dict[str, Optional[int]], profiles: Tuple[ColorProfile, ...], change_configured: bool diff --git a/kitty/window.py b/kitty/window.py index 3657d0077..5c82419d3 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -447,13 +447,41 @@ def process_remote_print(msg: memoryview) -> str: return replace_c0_codes_except_nl_space_tab(base64_decode(msg)).decode('utf-8', 'replace') +def transparent_background_color_control(cp: ColorProfile, responses: dict[str, str], index: int, key: str, sep: str, val: str) -> None: + if sep == '=': + if val == '?': + if index > 8: + responses[key] = '?' + else: + c = cp.get_transparent_background_color(index - 1) + if c is None: + responses[key] = '' + else: + opacity = max(0, min(c.alpha / 255.0, 1)) + responses[key] = f'rgb:{c.red:02x}/{c.green:02x}/{c.blue:02x}@{opacity:.4f}' + elif index <= 8: + col, _, o = val.partition('@') + try: + opacity = float(o) + except Exception: + opacity = -1.0 + c = to_color(col) + if c is not None: + cp.set_transparent_background_color(index - 1, c, opacity) + elif index <= 8: + cp.set_transparent_background_color(index - 1) + + def color_control(cp: ColorProfile, code: int, value: Union[str, bytes, memoryview] = '') -> str: if isinstance(value, (bytes, memoryview)): value = str(value, 'utf-8', 'replace') - responses = {} + responses: dict[str, str] = {} for rec in value.split(';'): key, sep, val = rec.partition('=') - # TODO: Add transparent_colors + if key.startswith('transparent_background_color'): + index = int(key[len('transparent_background_color'):]) + transparent_background_color_control(cp, responses, index, key, sep, val) + continue attr = { 'foreground': 'default_fg', 'background': 'default_bg', 'selection_background': 'highlight_bg', 'selection_foreground': 'highlight_fg', @@ -481,6 +509,7 @@ def serialize_color(c: Optional[Color]) -> str: else: if attr: if val: + val = val.partition('@')[0] col = to_color(val) if col is not None: setattr(cp, attr, col) @@ -489,6 +518,7 @@ def serialize_color(c: Optional[Color]) -> str: setattr(cp, attr, None) else: if 0 <= colnum <= 255: + val = val.partition('@')[0] col = to_color(val) if col is not None: cp.set_color(colnum, color_as_int(col)) diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py index b60a50324..810d54ef9 100644 --- a/kitty_tests/__init__.py +++ b/kitty_tests/__init__.py @@ -59,6 +59,8 @@ def color_control(self, code, data) -> None: response = color_control(self.color_profile, code, data) if response: def p(x): + if '@' in x: + return (to_color(x.partition('@')[0]), int(255 * float(x.partition('@')[2]))) ans = to_color(x) if ans is None: ans = x diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index e3460ae8f..6328d5bdf 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -1280,3 +1280,10 @@ def q(send, expected=None): q({'selection_background': ''}) self.assertIsNone(s.color_profile.highlight_bg) q({'selection_background': '?'}, {'selection_background': ''}) + s.color_profile.reload_from_opts(defaults) + q({'transparent_background_color9': '?'}, {'transparent_background_color9': '?'}) + q({'transparent_background_color2': '?'}, {'transparent_background_color2': ''}) + q({'transparent_background_color2': 'red@0.5'}) + q({'transparent_background_color2': '?'}, {'transparent_background_color2': (Color(255, 0, 0), 126)}) + q({'transparent_background_color2': '#ffffff@-1'}) + q({'transparent_background_color2': '?'}, {'transparent_background_color2': (Color(255, 255, 255), 255)})