diff --git a/kittens/diff/options/types.py b/kittens/diff/options/types.py index b2998dc9e..3d4c5fe69 100644 --- a/kittens/diff/options/types.py +++ b/kittens/diff/options/types.py @@ -3,8 +3,8 @@ import typing from kitty.conf.utils import KeyAction, KittensKeyMap import kitty.conf.utils -from kitty.rgb import Color -import kitty.rgb +from kitty.fast_data_types import Color +import kitty.fast_data_types from kitty.types import ParsedShortcut import kitty.types @@ -39,31 +39,31 @@ option_names = ( # {{{ class Options: - added_bg: Color = Color(red=230, green=255, blue=237) - added_margin_bg: Color = Color(red=205, green=255, blue=216) - background: Color = Color(red=255, green=255, blue=255) + added_bg: Color = Color(230, 255, 237) + added_margin_bg: Color = Color(205, 255, 216) + background: Color = Color(255, 255, 255) diff_cmd: str = 'auto' - filler_bg: Color = Color(red=250, green=251, blue=252) - foreground: Color = Color(red=0, green=0, blue=0) - highlight_added_bg: Color = Color(red=172, green=242, blue=189) - highlight_removed_bg: Color = Color(red=253, green=184, blue=192) - hunk_bg: Color = Color(red=241, green=248, blue=255) - hunk_margin_bg: Color = Color(red=219, green=237, blue=255) - margin_bg: Color = Color(red=250, green=251, blue=252) - margin_fg: Color = Color(red=170, green=170, blue=170) - margin_filler_bg: typing.Optional[kitty.rgb.Color] = None + filler_bg: Color = Color(250, 251, 252) + foreground: Color = Color(0, 0, 0) + highlight_added_bg: Color = Color(172, 242, 189) + highlight_removed_bg: Color = Color(253, 184, 192) + hunk_bg: Color = Color(241, 248, 255) + hunk_margin_bg: Color = Color(219, 237, 255) + margin_bg: Color = Color(250, 251, 252) + margin_fg: Color = Color(170, 170, 170) + margin_filler_bg: typing.Optional[kitty.fast_data_types.Color] = None num_context_lines: int = 3 pygments_style: str = 'default' - removed_bg: Color = Color(red=255, green=238, blue=240) - removed_margin_bg: Color = Color(red=255, green=220, blue=224) + removed_bg: Color = Color(255, 238, 240) + removed_margin_bg: Color = Color(255, 220, 224) replace_tab_by: str = ' ' - search_bg: Color = Color(red=68, green=68, blue=68) - search_fg: Color = Color(red=255, green=255, blue=255) - select_bg: Color = Color(red=180, green=213, blue=254) - select_fg: typing.Optional[kitty.rgb.Color] = Color(red=0, green=0, blue=0) + search_bg: Color = Color(68, 68, 68) + search_fg: Color = Color(255, 255, 255) + select_bg: Color = Color(180, 213, 254) + select_fg: typing.Optional[kitty.fast_data_types.Color] = Color(0, 0, 0) syntax_aliases: typing.Dict[str, str] = {'pyj': 'py', 'pyi': 'py', 'recipe': 'py'} - title_bg: Color = Color(red=255, green=255, blue=255) - title_fg: Color = Color(red=0, green=0, blue=0) + title_bg: Color = Color(255, 255, 255) + title_fg: Color = Color(0, 0, 0) map: typing.List[typing.Tuple[kitty.types.ParsedShortcut, kitty.conf.utils.KeyAction]] = [] key_definitions: KittensKeyMap = {} config_paths: typing.Tuple[str, ...] = () diff --git a/kittens/themes/collection.py b/kittens/themes/collection.py index b92f58c33..282aab1b5 100644 --- a/kittens/themes/collection.py +++ b/kittens/themes/collection.py @@ -18,7 +18,7 @@ from urllib.request import Request, urlopen from kitty.config import atomic_save, parse_config from kitty.constants import cache_dir, config_dir from kitty.options.types import Options as KittyOptions -from kitty.rgb import Color +from kitty.fast_data_types import Color from kitty.utils import reload_conf_in_all_kitties from ..choose.match import match diff --git a/kittens/tui/operations.py b/kittens/tui/operations.py index 8e752fc92..3bae316f2 100644 --- a/kittens/tui/operations.py +++ b/kittens/tui/operations.py @@ -6,10 +6,11 @@ from contextlib import contextmanager from enum import Enum, auto from functools import wraps from typing import ( - IO, Any, Callable, Dict, Generator, Optional, Tuple, TypeVar, Union + IO, Any, Callable, Dict, Generator, Optional, TypeVar, Union ) -from kitty.rgb import Color, color_as_sharp, to_color +from kitty.fast_data_types import Color +from kitty.rgb import color_as_sharp, to_color from kitty.typing import GraphicsCommandType, HandlerType, ScreenSize from .operations_stub import CMD @@ -170,7 +171,7 @@ UNDERLINE_STYLES = {name: i + 1 for i, name in enumerate( 'straight double curly'.split())} -ColorSpec = Union[int, str, Tuple[int, int, int]] +ColorSpec = Union[int, str, Color] def color_code(color: ColorSpec, intense: bool = False, base: int = 30) -> str: @@ -179,7 +180,7 @@ def color_code(color: ColorSpec, intense: bool = False, base: int = 30) -> str: elif isinstance(color, int): e = f'{base + 8}:5:{max(0, min(color, 255))}' else: - e = '{}:2:{}:{}:{}'.format(base + 8, *color) + e = f'{base + 8}{color.as_sgr}' return e @@ -454,7 +455,7 @@ def as_type_stub() -> str: ans = [ 'from typing import * # noqa', 'from kitty.typing import GraphicsCommandType, ScreenSize', - 'from kitty.rgb import Color', + 'from kitty.fast_data_types import Color', 'import kitty.rgb', 'import kittens.tui.operations', ] diff --git a/kitty/boss.py b/kitty/boss.py index 75220082b..e9424d2a9 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -36,7 +36,7 @@ from .fast_data_types import ( os_window_font_size, patch_global_colors, redirect_mouse_handling, ring_bell, safe_pipe, set_application_quit_request, set_background_image, set_boss, set_clipboard_string, set_in_sequence_mode, set_options, - set_os_window_size, thread_write, toggle_fullscreen, toggle_maximized + set_os_window_size, thread_write, toggle_fullscreen, toggle_maximized, Color ) from .key_encoding import get_name_to_functional_number_map from .keys import get_shortcut, shortcut_matches @@ -45,7 +45,7 @@ from .notify import notification_activated from .options.types import Options from .options.utils import MINIMUM_FONT_SIZE, SubSequenceMap from .os_window_size import initial_window_size_func -from .rgb import Color, color_from_int +from .rgb import color_from_int from .session import Session, create_sessions, get_os_window_sizing_data from .tabs import ( SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager diff --git a/kitty/colors.c b/kitty/colors.c index 4fa76d92c..406633012 100644 --- a/kitty/colors.c +++ b/kitty/colors.c @@ -7,8 +7,8 @@ #include "state.h" #include +#include "colors.h" -PyTypeObject ColorProfile_Type; static uint32_t FG_BG_256[256] = { 0x000000, // 0 @@ -60,7 +60,7 @@ PyObject* create_256_color_table() { } static PyObject * -new(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED *kwds) { +new_cp(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED *kwds) { ColorProfile *self; self = (ColorProfile *)type->tp_alloc(type, 0); @@ -77,14 +77,14 @@ new(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED *kwds) { } static void -dealloc(ColorProfile* self) { +dealloc_cp(ColorProfile* self) { if (self->color_stack) free(self->color_stack); Py_TYPE(self)->tp_free((PyObject*)self); } ColorProfile* alloc_color_profile() { - return (ColorProfile*)new(&ColorProfile_Type, NULL, NULL); + return (ColorProfile*)new_cp(&ColorProfile_Type, NULL, NULL); } @@ -248,7 +248,6 @@ as_color(ColorProfile *self, PyObject *val) { unsigned int t = entry & 0xFF; uint8_t r; uint32_t col = 0; - PyObject *ans = NULL; switch(t) { case 1: r = (entry >> 8) & 0xff; @@ -258,10 +257,11 @@ as_color(ColorProfile *self, PyObject *val) { col = entry >> 8; break; default: - ans = Py_None; Py_INCREF(Py_None); + Py_RETURN_NONE; } - if (ans == NULL) ans = Py_BuildValue("BBB", (unsigned char)(col >> 16), (unsigned char)((col >> 8) & 0xFF), (unsigned char)(col & 0xFF)); - return ans; + Color *ans = PyObject_New(Color, &Color_Type); + if (ans) ans->color.rgb = col; + return (PyObject*)ans; } static PyObject* @@ -412,8 +412,9 @@ CGETSET(cursor_color) CGETSET(cursor_text_color) CGETSET(highlight_fg) CGETSET(highlight_bg) +#undef CGETSET -static PyGetSetDef getsetters[] = { +static PyGetSetDef cp_getsetters[] = { GETSET(default_fg) GETSET(default_bg) GETSET(cursor_color) @@ -424,11 +425,11 @@ static PyGetSetDef getsetters[] = { }; -static PyMemberDef members[] = { +static PyMemberDef cp_members[] = { {NULL} }; -static PyMethodDef methods[] = { +static PyMethodDef cp_methods[] = { METHOD(update_ansi_color_table, METH_O) METHOD(reset_color_table, METH_NOARGS) METHOD(as_dict, METH_NOARGS) @@ -445,14 +446,151 @@ PyTypeObject ColorProfile_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "fast_data_types.ColorProfile", .tp_basicsize = sizeof(ColorProfile), - .tp_dealloc = (destructor)dealloc, + .tp_dealloc = (destructor)dealloc_cp, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = "ColorProfile", - .tp_members = members, - .tp_methods = methods, - .tp_getset = getsetters, - .tp_new = new, + .tp_members = cp_members, + .tp_methods = cp_methods, + .tp_getset = cp_getsetters, + .tp_new = new_cp, }; +// }}} + + +static PyObject * +new_color(PyTypeObject *type, PyObject *args, PyObject *kwds) { + static const char* kwlist[] = {"red", "green", "blue", "alpha", NULL}; + Color *self; + unsigned char r = 0, g = 0, b = 0, a = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|BBBB", (char**)kwlist, &r, &g, &b, &a)) return NULL; + + self = (Color *)type->tp_alloc(type, 0); + if (self != NULL) { + self->color.r = r; self->color.g = g; self->color.b = b; self->color.a = a; + } + return (PyObject*) self; +} + +static PyObject* +color_as_int(Color *self) { + return PyLong_FromUnsignedLong(self->color.val); +} + +static PyObject* +color_truediv(Color *self, PyObject *divisor) { + DECREF_AFTER_FUNCTION PyObject *o = PyNumber_Float(divisor); + if (o == NULL) return NULL; + double r = self->color.r, g = self->color.g, b = self->color.b, a = self->color.a; + double d = PyFloat_AS_DOUBLE(o) * 255.; + return Py_BuildValue("dddd", r/d, g/d, b/d, a/d); +} + +static PyNumberMethods color_number_methods = { + .nb_int = (unaryfunc)color_as_int, + .nb_true_divide = (binaryfunc)color_truediv, +}; + +#define CGETSET(name) \ + static PyObject* name##_get(Color *self, void UNUSED *closure) { return PyLong_FromUnsignedLong(self->color.name); } +CGETSET(red) +CGETSET(green) +CGETSET(blue) +CGETSET(alpha) +#undef CGETSET + +static PyObject* +rgb_get(Color *self, void *closure UNUSED) { + return PyLong_FromUnsignedLong(self->color.rgb); +} + +static PyObject* +luminance_get(Color *self, void *closure UNUSED) { + return PyFloat_FromDouble(rgb_luminance(self->color)); +} + +static PyObject* +sgr_get(Color* self, void *closure UNUSED) { + char buf[32]; + int sz = snprintf(buf, sizeof(buf), ":2:%u:%u:%u", self->color.r, self->color.g, self->color.b); + return PyUnicode_FromStringAndSize(buf, sz); +} + +static PyObject* +sharp_get(Color* self, void *closure UNUSED) { + char buf[32]; + int sz; + if (self->color.alpha) sz = snprintf(buf, sizeof(buf), "#%02x%02x%02x%02x", self->color.a, self->color.r, self->color.g, self->color.b); + else sz = snprintf(buf, sizeof(buf), "#%02x%02x%02x", self->color.r, self->color.g, self->color.b); + return PyUnicode_FromStringAndSize(buf, sz); +} + +static PyObject* +color_cmp(PyObject *self, PyObject *other, int op) { + if (op != Py_EQ && op != Py_NE) return Py_NotImplemented; + if (!PyObject_TypeCheck(other, &Color_Type)) { + if (op == Py_EQ) Py_RETURN_FALSE; + Py_RETURN_TRUE; + } + Color *a = (Color*)self, *b = (Color*)other; + switch (op) { + case Py_EQ: { if (a->color.val == b->color.val) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } + case Py_NE: { if (a->color.val != b->color.val) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } + default: + return Py_NotImplemented; + } +} + +static PyGetSetDef color_getsetters[] = { + {"rgb", (getter) rgb_get, NULL, "rgb", NULL}, + {"red", (getter) red_get, NULL, "red", NULL}, + {"green", (getter) green_get, NULL, "green", NULL}, + {"blue", (getter) blue_get, NULL, "blue", NULL}, + {"alpha", (getter) alpha_get, NULL, "alpha", NULL}, + {"luminance", (getter) luminance_get, NULL, "luminance", NULL}, + {"as_sgr", (getter) sgr_get, NULL, "as_sgr", NULL}, + {"as_sharp", (getter) sharp_get, NULL, "as_sharp", NULL}, + {NULL} /* Sentinel */ +}; + +static PyObject* +contrast(Color* self, PyObject *o) { + if (!PyObject_TypeCheck(o, &Color_Type)) { PyErr_SetString(PyExc_TypeError, "Not a Color"); return NULL; } + Color *other = (Color*) o; + return PyFloat_FromDouble(rgb_contrast(self->color, other->color)); +} + +static PyMethodDef color_methods[] = { + METHODB(contrast, METH_O), + {NULL} /* Sentinel */ +}; + + +static PyObject * +repr(Color *self) { + if (self->color.alpha) return PyUnicode_FromFormat("Color(red=%u, green=%u, blue=%u, alpha=%u)", self->color.r, self->color.g, self->color.b, self->color.a); + return PyUnicode_FromFormat("Color(%u, %u, %u)", self->color.r, self->color.g, self->color.b); +} + +static Py_hash_t +color_hash(PyObject *x) { + return ((Color*)x)->color.val; +} + +PyTypeObject Color_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "kitty.fast_data_types.Color", + .tp_basicsize = sizeof(Color), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "Color", + .tp_new = new_color, + .tp_getset = color_getsetters, + .tp_as_number = &color_number_methods, + .tp_methods = color_methods, + .tp_repr = (reprfunc)repr, + .tp_hash = color_hash, + .tp_richcompare = color_cmp, +}; + static PyMethodDef module_methods[] = { METHODB(default_color_table, METH_NOARGS), @@ -465,6 +603,11 @@ int init_ColorProfile(PyObject *module) {\ if (PyType_Ready(&ColorProfile_Type) < 0) return 0; if (PyModule_AddObject(module, "ColorProfile", (PyObject *)&ColorProfile_Type) != 0) return 0; Py_INCREF(&ColorProfile_Type); + + if (PyType_Ready(&Color_Type) < 0) return 0; + if (PyModule_AddObject(module, "Color", (PyObject *)&Color_Type) != 0) return 0; + Py_INCREF(&Color_Type); + if (PyModule_AddFunctions(module, module_methods) != 0) return false; return 1; } diff --git a/kitty/colors.h b/kitty/colors.h new file mode 100644 index 000000000..426370f0a --- /dev/null +++ b/kitty/colors.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#pragma once + +#include "data-types.h" + +typedef union ARGB32 { + color_type val; + struct { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + uint8_t b: 8; + uint8_t g: 8; + uint8_t r: 8; + uint8_t a: 8; +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + uint8_t a: 8; + uint8_t r: 8; + uint8_t g: 8; + uint8_t b: 8; +#else +#error "Unsupported endianness" +#endif + }; + struct { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + uint8_t blue: 8; + uint8_t green: 8; + uint8_t red: 8; + uint8_t alpha: 8; +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + uint8_t alpha: 8; + uint8_t red: 8; + uint8_t green: 8; + uint8_t blue: 8; +#else +#error "Unsupported endianness" +#endif + }; + struct { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + color_type rgb: 24; + uint8_t _ignore_me: 8; +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + uint8_t _ignore_me: 8; + color_type rgb: 24; +#else +#error "Unsupported endianness" +#endif + }; +} ARGB32; + +typedef struct { + PyObject_HEAD + + ARGB32 color; +} Color; + +extern PyTypeObject ColorProfile_Type; +extern PyTypeObject Color_Type; + +static inline double +rgb_luminance(ARGB32 c) { + return 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue; +} + +static inline double +rgb_contrast(ARGB32 a, ARGB32 b) { + double al = rgb_luminance(a), bl = rgb_luminance(b); + if (al < bl) SWAP(al, bl); + return (al + 0.05) / (bl + 0.05); +} diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index d6b93b57e..67a8cc8d3 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -9,7 +9,8 @@ from typing import ( Sequence, Set, Tuple, TypeVar, Union, Generic ) -from ..rgb import Color, to_color as as_color +from ..rgb import to_color as as_color +from ..fast_data_types import Color from ..types import ConvertibleToNumbers, ParsedShortcut from ..typing import Protocol from ..utils import expandvars, log_error diff --git a/kitty/data-types.h b/kitty/data-types.h index d361835a6..91cf1f504 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -30,6 +30,7 @@ #define MIN(x, y) __extension__ ({ \ __typeof__ (x) a = (x); __typeof__ (y) b = (y); \ a < b ? a : b;}) +#define SWAP(x, y) do { __typeof__(x) _sw_ = y; y = x; x = _sw_; } while(0) #define xstr(s) str(s) #define str(s) #s #define arraysz(x) (sizeof(x)/sizeof(x[0])) diff --git a/kitty/debug_config.py b/kitty/debug_config.py index 943405d1b..c096ab966 100644 --- a/kitty/debug_config.py +++ b/kitty/debug_config.py @@ -20,10 +20,10 @@ from .conf.utils import KeyAction from .constants import ( extensions_dir, is_macos, is_wayland, kitty_base_dir, kitty_exe, shell_path ) -from .fast_data_types import num_users +from .fast_data_types import num_users, Color from .options.types import Options as KittyOpts, defaults from .options.utils import MouseMap -from .rgb import Color, color_as_sharp +from .rgb import color_as_sharp from .types import MouseEvent, SingleKey from .typing import SequenceMap diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index adb5bc8a9..0ed94730a 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -619,6 +619,61 @@ def patch_global_colors(spec: Dict[str, Optional[int]], configured: bool) -> Non pass +class Color: + @property + def rgb(self) -> int: + pass + + @property + def red(self) -> int: + pass + + @property + def green(self) -> int: + pass + + @property + def blue(self) -> int: + pass + + @property + def alpha(self) -> int: + pass + + @property + def luminance(self) -> float: + pass + + @property + def as_sgr(self) -> str: + pass + + @property + def as_sharp(self) -> str: + pass + + def __init__(self, red: int = 0, green: int = 0, blue: int = 0, alpha: int = 0) -> None: + pass + + def __truediv__(self, divisor: float) -> Tuple[float, float, float, float]: # (r, g, b, a) + pass + + def __int__(self) -> int: + pass + + def __hash__(self) -> int: + pass + + def __eq__(self, other: Any) -> bool: + pass + + def __ne__(self, other: Any) -> bool: + pass + + def contrast(self, other: 'Color') -> float: + pass + + class ColorProfile: default_bg: int @@ -626,7 +681,7 @@ class ColorProfile: def as_dict(self) -> Dict[str, int]: pass - def as_color(self, val: int) -> Tuple[int, int, int]: + def as_color(self, val: int) -> Optional[Color]: pass def set_color(self, num: int, val: int) -> None: diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index 562878d9b..cbbd84334 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -7,6 +7,7 @@ #pragma once #include "../state.h" +#include "../colors.h" static inline float PyFloat_AsFloat(PyObject *o) { @@ -15,10 +16,9 @@ PyFloat_AsFloat(PyObject *o) { static inline color_type color_as_int(PyObject *color) { - if (!PyTuple_Check(color)) { PyErr_SetString(PyExc_TypeError, "Not a color tuple"); return 0; } -#define I(n, s) ((PyLong_AsUnsignedLong(PyTuple_GET_ITEM(color, n)) & 0xff) << s) - return (I(0, 16) | I(1, 8) | I(2, 0)) & 0xffffff; -#undef I + if (!PyObject_TypeCheck(color, &Color_Type)) { PyErr_SetString(PyExc_TypeError, "Not a Color object"); return 0; } + Color *c = (Color*)color; + return c->color.val & 0xffffff; } static inline color_type diff --git a/kitty/options/types.py b/kitty/options/types.py index 511f2bccc..dba237933 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -6,10 +6,10 @@ from kitty.conf.utils import KeyAction import kitty.conf.utils from kitty.constants import is_macos import kitty.constants +from kitty.fast_data_types import Color +import kitty.fast_data_types from kitty.options.utils import KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, TabBarMarginHeight import kitty.options.utils -from kitty.rgb import Color -import kitty.rgb from kitty.types import FloatEdges, SingleKey import kitty.types @@ -441,23 +441,23 @@ option_names = ( # {{{ class Options: - active_border_color: typing.Optional[kitty.rgb.Color] = Color(red=0, green=255, blue=0) - active_tab_background: Color = Color(red=238, green=238, blue=238) + active_border_color: typing.Optional[kitty.fast_data_types.Color] = Color(0, 255, 0) + active_tab_background: Color = Color(238, 238, 238) active_tab_font_style: typing.Tuple[bool, bool] = (True, True) - active_tab_foreground: Color = Color(red=0, green=0, blue=0) + active_tab_foreground: Color = Color(0, 0, 0) active_tab_title_template: typing.Optional[str] = None adjust_baseline: typing.Union[int, float] = 0 adjust_column_width: typing.Union[int, float] = 0 adjust_line_height: typing.Union[int, float] = 0 allow_hyperlinks: int = 1 allow_remote_control: str = 'n' - background: Color = Color(red=0, green=0, blue=0) + background: Color = Color(0, 0, 0) background_image: typing.Optional[str] = None background_image_layout: choices_for_background_image_layout = 'tiled' background_image_linear: bool = False background_opacity: float = 1.0 background_tint: float = 0 - bell_border_color: Color = Color(red=255, green=90, blue=0) + bell_border_color: Color = Color(255, 90, 0) bell_on_tab: bool = True bell_path: typing.Optional[str] = None bold_font: str = 'auto' @@ -472,12 +472,12 @@ class Options: command_on_bell: typing.List[str] = ['none'] confirm_os_window_close: int = 0 copy_on_select: str = '' - cursor: typing.Optional[kitty.rgb.Color] = Color(red=204, green=204, blue=204) + cursor: typing.Optional[kitty.fast_data_types.Color] = Color(204, 204, 204) cursor_beam_thickness: float = 1.5 cursor_blink_interval: float = -1.0 cursor_shape: int = 1 cursor_stop_blinking_after: float = 15.0 - cursor_text_color: typing.Optional[kitty.rgb.Color] = Color(red=17, green=17, blue=17) + cursor_text_color: typing.Optional[kitty.fast_data_types.Color] = Color(17, 17, 17) cursor_underline_thickness: float = 2.0 default_pointer_shape: choices_for_default_pointer_shape = 'beam' detect_urls: bool = True @@ -493,12 +493,12 @@ class Options: font_family: str = 'monospace' font_size: float = 11.0 force_ltr: bool = False - foreground: Color = Color(red=221, green=221, blue=221) + foreground: Color = Color(221, 221, 221) hide_window_decorations: int = 0 - inactive_border_color: Color = Color(red=204, green=204, blue=204) - inactive_tab_background: Color = Color(red=153, green=153, blue=153) + inactive_border_color: Color = Color(204, 204, 204) + inactive_tab_background: Color = Color(153, 153, 153) inactive_tab_font_style: typing.Tuple[bool, bool] = (False, False) - inactive_tab_foreground: Color = Color(red=68, green=68, blue=68) + inactive_tab_foreground: Color = Color(68, 68, 68) inactive_text_alpha: float = 1.0 initial_window_height: typing.Tuple[int, str] = (400, 'px') initial_window_width: typing.Tuple[int, str] = (640, 'px') @@ -516,12 +516,12 @@ class Options: macos_titlebar_color: int = 0 macos_traditional_fullscreen: bool = False macos_window_resizable: bool = True - mark1_background: Color = Color(red=152, green=211, blue=203) - mark1_foreground: Color = Color(red=0, green=0, blue=0) - mark2_background: Color = Color(red=242, green=220, blue=211) - mark2_foreground: Color = Color(red=0, green=0, blue=0) - mark3_background: Color = Color(red=242, green=116, blue=188) - mark3_foreground: Color = Color(red=0, green=0, blue=0) + mark1_background: Color = Color(152, 211, 203) + mark1_foreground: Color = Color(0, 0, 0) + mark2_background: Color = Color(242, 220, 211) + mark2_foreground: Color = Color(0, 0, 0) + mark3_background: Color = Color(242, 116, 188) + mark3_foreground: Color = Color(0, 0, 0) mouse_hide_wait: float = 0.0 if is_macos else 3.0 open_url_with: typing.List[str] = ['default'] placement_strategy: choices_for_placement_strategy = 'center' @@ -537,8 +537,8 @@ class Options: scrollback_pager: typing.List[str] = ['less', '--chop-long-lines', '--RAW-CONTROL-CHARS', '+INPUT_LINE_NUMBER'] scrollback_pager_history_size: int = 0 select_by_word_characters: str = '@-./_~?&=%+#' - selection_background: Color = Color(red=255, green=250, blue=205) - selection_foreground: typing.Optional[kitty.rgb.Color] = Color(red=0, green=0, blue=0) + selection_background: Color = Color(255, 250, 205) + selection_foreground: typing.Optional[kitty.fast_data_types.Color] = Color(0, 0, 0) shell: str = '.' shell_integration: str = 'enabled' single_window_margin_width: FloatEdges = FloatEdges(left=-1.0, top=-1.0, right=-1.0, bottom=-1.0) @@ -547,9 +547,9 @@ class Options: sync_to_monitor: bool = True tab_activity_symbol: typing.Optional[str] = None tab_bar_align: choices_for_tab_bar_align = 'left' - tab_bar_background: typing.Optional[kitty.rgb.Color] = None + tab_bar_background: typing.Optional[kitty.fast_data_types.Color] = None tab_bar_edge: int = 3 - tab_bar_margin_color: typing.Optional[kitty.rgb.Color] = None + tab_bar_margin_color: typing.Optional[kitty.fast_data_types.Color] = None tab_bar_margin_height: TabBarMarginHeight = TabBarMarginHeight(outer=0, inner=0) tab_bar_margin_width: float = 0 tab_bar_min_tabs: int = 2 @@ -562,7 +562,7 @@ class Options: term: str = 'xterm-kitty' touch_scroll_multiplier: float = 1.0 update_check_interval: float = 24.0 - url_color: Color = Color(red=0, green=135, blue=189) + url_color: Color = Color(0, 135, 189) url_excluded_characters: str = '' url_prefixes: typing.Tuple[str, ...] = ('http', 'https', 'file', 'ftp', 'gemini', 'irc', 'gopher', 'mailto', 'news', 'git') url_style: int = 3 diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 9823f8b72..94b605344 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -16,13 +16,13 @@ from kitty.conf.utils import ( python_string, to_bool, to_cmdline, to_color, uniq, unit_float ) from kitty.constants import config_dir, is_macos -from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE +from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, Color from kitty.fonts import FontFeature from kitty.key_names import ( character_key_name_aliases, functional_key_name_aliases, get_key_name_lookup ) -from kitty.rgb import Color, color_as_int +from kitty.rgb import color_as_int from kitty.types import FloatEdges, MouseEvent, SingleKey from kitty.utils import expandvars, log_error diff --git a/kitty/rc/get_colors.py b/kitty/rc/get_colors.py index 4e36270f3..3fc177bbd 100644 --- a/kitty/rc/get_colors.py +++ b/kitty/rc/get_colors.py @@ -3,7 +3,8 @@ from typing import TYPE_CHECKING, Optional -from kitty.rgb import Color, color_as_sharp, color_from_int +from kitty.fast_data_types import Color +from kitty.rgb import color_as_sharp, color_from_int from kitty.utils import natsort_ints from .base import ( diff --git a/kitty/rc/set_colors.py b/kitty/rc/set_colors.py index 917365f53..ae689ff6e 100644 --- a/kitty/rc/set_colors.py +++ b/kitty/rc/set_colors.py @@ -6,8 +6,7 @@ import os from typing import TYPE_CHECKING, Dict, Iterable, Optional from kitty.config import parse_config -from kitty.fast_data_types import patch_color_profiles -from kitty.rgb import Color +from kitty.fast_data_types import patch_color_profiles, Color from .base import ( MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, ParsingOfArgsFailed, diff --git a/kitty/rgb.py b/kitty/rgb.py index c06ebbbac..ff0e9b3e6 100644 --- a/kitty/rgb.py +++ b/kitty/rgb.py @@ -3,37 +3,8 @@ import re from contextlib import suppress -from typing import NamedTuple, Optional, Tuple - - -class Color(NamedTuple): - red: int = 0 - green: int = 0 - blue: int = 0 - - def __truediv__(self, denom: float) -> Tuple[float, float, float]: - return self.red / denom, self.green / denom, self.blue / denom - - def as_sgr(self) -> str: - return ':2:{}:{}:{}'.format(*self) - - def luminance(self) -> float: - return 0.299 * self.red + 0.587 * self.green + 0.114 * self.blue - - def contrast(self, other: 'Color') -> float: - a = self.luminance() - b = other.luminance() - if a < b: - a, b = b, a - return (a + 0.05) / (b + 0.05) - - def __int__(self) -> int: - return self.red << 16 | self.green << 8 | self.blue - - def as_bytearray(self, alpha: Optional[int] = None) -> bytearray: - if alpha is None: - return bytearray((self.red, self.green, self.blue)) - return bytearray((self.red, self.green, self.blue, alpha)) +from typing import Optional +from .fast_data_types import Color def alpha_blend_channel(top_color: int, bottom_color: int, alpha: float) -> int: @@ -78,11 +49,11 @@ def color_as_int(x: Color) -> int: def color_as_sharp(x: Color) -> str: - return '#{:02x}{:02x}{:02x}'.format(*x) + return x.as_sharp def color_as_sgr(x: Color) -> str: - return x.as_sgr() + return x.as_sgr def to_color(raw: str, validate: bool = False) -> Optional[Color]: diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py index 132bb40b6..a0f46858c 100644 --- a/kitty/tab_bar.py +++ b/kitty/tab_bar.py @@ -12,9 +12,9 @@ from .config import build_ansi_color_table from .constants import config_dir from .fast_data_types import ( DECAWM, Region, Screen, cell_size_for_window, get_options, pt_to_px, - set_tab_bar_render_data, viewport_for_window + set_tab_bar_render_data, viewport_for_window, Color ) -from .rgb import Color, alpha_blend, color_as_sgr, color_from_int, to_color +from .rgb import alpha_blend, color_as_sgr, color_from_int, to_color from .types import WindowGeometry, run_once from .typing import EdgeLiteral, PowerlineStyle from .utils import color_as_int, log_error diff --git a/kitty/utils.py b/kitty/utils.py index f99fba36e..150545dfe 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -21,9 +21,10 @@ from .constants import ( appname, is_macos, is_wayland, read_kitty_resource, shell_path, supports_primary_selection ) -from .rgb import Color, to_color +from .rgb import to_color from .types import run_once from .typing import AddressFamily, PopenType, Socket, StartupCtx +from .fast_data_types import Color if TYPE_CHECKING: from .fast_data_types import OSWindowSize @@ -95,8 +96,8 @@ def sanitize_title(x: str) -> str: return re.sub(r'\s+', ' ', re.sub(r'[\0-\x19\x80-\x9f]', '', x)) -def color_as_int(val: Tuple[int, int, int]) -> int: - return val[0] << 16 | val[1] << 8 | val[2] +def color_as_int(val: Color) -> int: + return int(val) & 0xffffff def color_from_int(val: int) -> Color: @@ -118,8 +119,7 @@ def parse_color_set(raw: str) -> Generator[Tuple[int, Optional[int]], None, None else: q = to_color(spec) if q is not None: - r, g, b = q - yield c, r << 16 | g << 8 | b + yield c, int(q) & 0xffffff except Exception: continue diff --git a/kitty/window.py b/kitty/window.py index 37617b3b4..267cce634 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -22,7 +22,7 @@ from .constants import appname, is_macos, wakeup from .fast_data_types import ( BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK, - CURSOR_UNDERLINE, DCS, DECORATION, DIM, GLFW_MOD_CONTROL, + CURSOR_UNDERLINE, DCS, DECORATION, DIM, GLFW_MOD_CONTROL, Color, GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM, MARK, MARK_MASK, NO_CURSOR_SHAPE, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, SEVEN_SEGMENT_PROGRAM, STRIKETHROUGH, TINT_PROGRAM, KeyEvent, @@ -36,7 +36,7 @@ from .fast_data_types import ( from .keys import keyboard_mode_name, mod_mask from .notify import NotificationCommand, handle_notification_cmd from .options.types import Options -from .rgb import Color, to_color +from .rgb import to_color from .terminfo import get_capabilities from .types import MouseEvent, ScreenGeometry, WindowGeometry, ac from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict @@ -813,7 +813,9 @@ class Window: changed = False for c, val in parse_color_set(value): if val is None: # color query - self.report_color(f'4;{c}', *self.screen.color_profile.as_color((c << 8) | 1)) + qc = self.screen.color_profile.as_color((c << 8) | 1) + assert qc is not None + self.report_color(f'4;{c}', qc.red, qc.green, qc.blue) else: changed = True cp.set_color(c, val) diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index 99fd7b984..f41c659cf 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -6,7 +6,7 @@ import tempfile from kitty.config import build_ansi_color_table, defaults from kitty.fast_data_types import ( - ColorProfile, Cursor as C, HistoryBuf, LineBuf, + ColorProfile, Cursor as C, HistoryBuf, LineBuf, Color, parse_input_from_terminal, truncate_point_for_length, wcswidth, wcwidth ) from kitty.rgb import to_color @@ -32,8 +32,12 @@ class TestDataTypes(BaseTest): for x in 'xxx #12 #1234 rgb:a/b'.split(): self.assertIsNone(to_color(x)) - def c(spec, r=0, g=0, b=0): - self.ae(tuple(to_color(spec)), (r, g, b)) + def c(spec, r=0, g=0, b=0, a=0): + c = to_color(spec) + self.ae(c.red, r) + self.ae(c.green, g) + self.ae(c.blue, b) + self.ae(c.alpha, a) c('#eee', 0xee, 0xee, 0xee) c('#234567', 0x23, 0x45, 0x67) @@ -42,6 +46,15 @@ class TestDataTypes(BaseTest): c('rgb:23/45/67', 0x23, 0x45, 0x67) c('rgb:abc/abc/def', 0xab, 0xab, 0xde) c('red', 0xff) + self.ae(int(Color(1, 2, 3)), 0x10203) + base = Color(12, 12, 12) + a = Color(23, 23, 23) + b = Color(100, 100, 100) + self.assertLess(base.contrast(a), base.contrast(b)) + self.ae(Color(1, 2, 3).as_sgr, ':2:1:2:3') + self.ae(Color(1, 2, 3).as_sharp, '#010203') + self.ae(Color(1, 2, 3, 4).as_sharp, '#04010203') + self.ae(Color(1, 2, 3, 4).rgb, 0x10203) def test_linebuf(self): old = filled_line_buf(2, 3, filled_cursor()) @@ -423,8 +436,8 @@ class TestDataTypes(BaseTest): c.update_ansi_color_table(build_ansi_color_table()) for i in range(8): col = getattr(defaults, f'color{i}') - self.assertEqual(c.as_color(i << 8 | 1), (col[0], col[1], col[2])) - self.ae(c.as_color(255 << 8 | 1), (0xee, 0xee, 0xee)) + self.assertEqual(c.as_color(i << 8 | 1), col) + self.ae(c.as_color(255 << 8 | 1), Color(0xee, 0xee, 0xee)) def test_historybuf(self): lb = filled_line_buf() diff --git a/kitty_tests/options.py b/kitty_tests/options.py index a1db02e58..508bc8c30 100644 --- a/kitty_tests/options.py +++ b/kitty_tests/options.py @@ -5,6 +5,7 @@ from . import BaseTest from kitty.utils import log_error from kitty.options.utils import DELETE_ENV_VAR +from kitty.fast_data_types import Color class TestConfParsing(BaseTest): @@ -37,7 +38,7 @@ class TestConfParsing(BaseTest): opts = p('font_size 11.37', 'clear_all_shortcuts y', 'color23 red') self.ae(opts.font_size, 11.37) self.ae(opts.mouse_hide_wait, 0 if is_macos else 3) - self.ae(tuple(opts.color23), (255, 0, 0)) + self.ae(opts.color23, Color(255, 0, 0)) self.assertFalse(opts.keymap) opts = p('clear_all_shortcuts y', 'map f1 next_window') self.ae(len(opts.keymap), 1)