config: number with unit for min contrast ratio

This commit is contained in:
arne314 2025-03-14 14:05:34 +01:00
parent a6fcdf1964
commit 40ef6b4f37
5 changed files with 52 additions and 20 deletions

View file

@ -22,6 +22,7 @@
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')

View file

@ -265,21 +265,21 @@
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 <https://en.wikipedia.org/wiki/Web_Content_Accessibility_Guidelines>`
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 <https://www.hsluv.org/>` which enables it to change
the perceived lightness of a color just as much as needed without really changing its hue and saturation.

17
kitty/options/parse.py generated
View file

@ -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 @@ def text_composition_strategy(self, val: str, ans: dict[str, typing.Any]) -> Non
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)

View file

@ -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'

View file

@ -131,8 +131,8 @@ def __call__(self, src: str) -> str:
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 @@ def __call__(self, semi_transparent: bool = False, allow_recompile: bool = False
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 @@ def resolve_cell_defines(which: str, src: str) -> str:
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)