diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index 72d925559..ab9018808 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -22,6 +22,7 @@ from ..typing import Protocol from ..utils import expandvars, log_error, shlex_split key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$') +number_unit_pat = re.compile(r'\s*([-+]?\d+\.?\d*)\s*([^\d\s]*)?') ItemParser = Callable[[str, str, dict[str, Any]], bool] T = TypeVar('T') @@ -66,6 +67,19 @@ def unit_float(x: ConvertibleToNumbers) -> float: return max(0, min(float(x), 1)) +def number_with_unit(x: str, default_unit: str = '') -> tuple[float, str]: + mat = number_unit_pat.match(x) + exception = None + if mat is not None: + try: + value, unit = float(mat.group(1)), mat.group(2) + unit = default_unit if not unit else unit + return value, unit + except Exception as e: + exception = e + raise ValueError(f'Invalid number with unit config option provided: {x if exception is None else exception}') + + def to_bool(x: str) -> bool: return x.lower() in ('y', 'yes', 'true') diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 624c4f814..7a28d1764 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -265,21 +265,21 @@ Then adjust the second parameter until it looks good. Then switch to a light the and adjust the first parameter until the perceived thickness matches the dark theme. ''') -opt('text_fg_override_threshold', 0, option_type='float', long_text=''' -A setting to prevent low contrast scenarios, configurable in two different modes (positive and negative value). +opt('text_fg_override_threshold', '0 %', option_type='number_with_unit', long_text=''' +A setting to prevent low contrast scenarios, configurable in two different modes (suffix :code:` %` and suffix :code:` ratio`). The default value is :code:`0`, which means no overriding is performed. Useful when working with applications that use colors that do not contrast well with your preferred color scheme. -A positive value represents the minimum accepted difference in luminance between the foreground and background +A value with the suffix :code:` %` represents the minimum accepted difference in luminance between the foreground and background color, below which kitty will override the foreground color. It is percentage -ranging from :code:`0` to :code:`100`. If the difference in luminance of the +ranging from :code:`0 %` to :code:`100 %`. If the difference in luminance of the foreground and background is below this threshold, the foreground color will be set to white if the background is dark or black if the background is light. -A negative value represents the minimum accepted contrast ratio between the foreground and background color. -Possible values range from :code:`-0.0` to :code:`-21.0`. +A value with the suffix :code:` ratio` represents the minimum accepted contrast ratio between the foreground and background color. +Possible values range from :code:`0.0 ratio` to :code:`21.0 ratio`. To for example meet :link:`WCAG level AA ` -a value of :code:`-4.5` can be provided. +a value of :code:`4.5 ratio` can be provided. The algorithm is implemented using :link:`HSLuv ` which enables it to change the perceived lightness of a color just as much as needed without really changing its hue and saturation. diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 25d6c0232..a45b730b4 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -4,8 +4,16 @@ import typing import collections.abc # noqa: F401, RUF100 from kitty.conf.utils import ( - merge_dicts, positive_float, positive_int, python_string, to_bool, to_cmdline, to_color, - to_color_or_none, unit_float + merge_dicts, + positive_float, + positive_int, + python_string, + number_with_unit, + to_bool, + to_cmdline, + to_color, + to_color_or_none, + unit_float, ) from kitty.options.utils import ( action_alias, active_tab_title_template, allow_hyperlinks, bell_on_tab, box_drawing_scale, @@ -1324,7 +1332,10 @@ class Parser: ans['text_composition_strategy'] = str(val) def text_fg_override_threshold(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['text_fg_override_threshold'] = float(val) + value, unit = number_with_unit(val, default_unit='%') + if unit not in ['%', 'ratio']: + raise ValueError(f'The unit {unit} is not a valid choice for text_fg_override_threshold') + ans['text_fg_override_threshold'] = value, unit def touch_scroll_multiplier(self, val: str, ans: dict[str, typing.Any]) -> None: ans['touch_scroll_multiplier'] = float(val) diff --git a/kitty/options/types.py b/kitty/options/types.py index ae04efde0..1fef0f042 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -612,7 +612,7 @@ class Options: term: str = 'xterm-kitty' terminfo_type: choices_for_terminfo_type = 'path' text_composition_strategy: str = 'platform' - text_fg_override_threshold: float = 0.0 + text_fg_override_threshold: tuple[float, str] = 0.0, '%' touch_scroll_multiplier: float = 1.0 transparent_background_colors: tuple[tuple[kitty.fast_data_types.Color, float], ...] = () undercurl_style: choices_for_undercurl_style = 'thin-sparse' diff --git a/kitty/shaders.py b/kitty/shaders.py index b75c3ef7e..92b1cebff 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -131,8 +131,8 @@ null_replacer = MultiReplacer() class LoadShaderPrograms: - text_fg_override_threshold: float = 0 + text_fg_override_threshold_unit: str = '%' text_old_gamma: bool = False semi_transparent: bool = False cell_program_replacer: MultiReplacer = null_replacer @@ -140,7 +140,10 @@ class LoadShaderPrograms: @property def needs_recompile(self) -> bool: opts = get_options() - return opts.text_fg_override_threshold != self.text_fg_override_threshold or (opts.text_composition_strategy == 'legacy') != self.text_old_gamma + return ( + opts.text_fg_override_threshold != (self.text_fg_override_threshold, self.text_fg_override_threshold_unit) + or (opts.text_composition_strategy == 'legacy') != self.text_old_gamma + ) def recompile_if_needed(self) -> None: if self.needs_recompile: @@ -150,10 +153,14 @@ class LoadShaderPrograms: self.semi_transparent = semi_transparent opts = get_options() self.text_old_gamma = opts.text_composition_strategy == 'legacy' - if opts.text_fg_override_threshold < 0: - self.text_fg_override_threshold = opts.text_fg_override_threshold - else: - self.text_fg_override_threshold = max(0, min(opts.text_fg_override_threshold, 100)) * 0.01 + + self.text_fg_override_threshold, self.text_fg_override_threshold_unit = opts.text_fg_override_threshold + match self.text_fg_override_threshold_unit: + case '%': + self.text_fg_override_threshold = max(0, min(self.text_fg_override_threshold, 100.0)) * 0.01 + case 'ratio': + self.text_fg_override_threshold = max(0, min(self.text_fg_override_threshold, 21.0)) + cell = program_for('cell') if self.cell_program_replacer is null_replacer: self.cell_program_replacer = MultiReplacer( @@ -171,9 +178,9 @@ class LoadShaderPrograms: r['WHICH_PHASE'] = f'PHASE_{which}' r['TRANSPARENT'] = '1' if semi_transparent else '0' r['FG_OVERRIDE_THRESHOLD'] = str(self.text_fg_override_threshold) - r['FG_OVERRIDE'] = '1' if self.text_fg_override_threshold > 0 else '0' - r['MIN_CONTRAST_RATIO'] = str(-self.text_fg_override_threshold) - r['APPLY_MIN_CONTRAST_RATIO'] = '1' if self.text_fg_override_threshold < 0 else '0' + r['FG_OVERRIDE'] = str(int(bool(self.text_fg_override_threshold_unit == '%'))) + r['MIN_CONTRAST_RATIO'] = str(self.text_fg_override_threshold) + r['APPLY_MIN_CONTRAST_RATIO'] = str(int(bool(self.text_fg_override_threshold_unit == 'ratio'))) r['TEXT_NEW_GAMMA'] = '0' if self.text_old_gamma else '1' return self.cell_program_replacer(src)