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 ..typing import Protocol
from ..utils import expandvars, log_error, shlex_split from ..utils import expandvars, log_error, shlex_split
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$') 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] ItemParser = Callable[[str, str, dict[str, Any]], bool]
T = TypeVar('T') T = TypeVar('T')
@@ -66,6 +67,19 @@ def unit_float(x: ConvertibleToNumbers) -> float:
return max(0, min(float(x), 1)) 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: def to_bool(x: str) -> bool:
return x.lower() in ('y', 'yes', 'true') return x.lower() in ('y', 'yes', 'true')

View File

@@ -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. and adjust the first parameter until the perceived thickness matches the dark theme.
''') ''')
opt('text_fg_override_threshold', 0, option_type='float', long_text=''' 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 (positive and negative value). 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 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. 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 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 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. 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. 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` to :code:`-21.0`. 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>` 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 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. 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 typing
import collections.abc # noqa: F401, RUF100 import collections.abc # noqa: F401, RUF100
from kitty.conf.utils import ( from kitty.conf.utils import (
merge_dicts, positive_float, positive_int, python_string, to_bool, to_cmdline, to_color, merge_dicts,
to_color_or_none, unit_float 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 ( from kitty.options.utils import (
action_alias, active_tab_title_template, allow_hyperlinks, bell_on_tab, box_drawing_scale, 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) ans['text_composition_strategy'] = str(val)
def text_fg_override_threshold(self, val: str, ans: dict[str, typing.Any]) -> None: 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: def touch_scroll_multiplier(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['touch_scroll_multiplier'] = float(val) ans['touch_scroll_multiplier'] = float(val)

View File

@@ -612,7 +612,7 @@ class Options:
term: str = 'xterm-kitty' term: str = 'xterm-kitty'
terminfo_type: choices_for_terminfo_type = 'path' terminfo_type: choices_for_terminfo_type = 'path'
text_composition_strategy: str = 'platform' 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 touch_scroll_multiplier: float = 1.0
transparent_background_colors: tuple[tuple[kitty.fast_data_types.Color, float], ...] = () transparent_background_colors: tuple[tuple[kitty.fast_data_types.Color, float], ...] = ()
undercurl_style: choices_for_undercurl_style = 'thin-sparse' undercurl_style: choices_for_undercurl_style = 'thin-sparse'

View File

@@ -131,8 +131,8 @@ null_replacer = MultiReplacer()
class LoadShaderPrograms: class LoadShaderPrograms:
text_fg_override_threshold: float = 0 text_fg_override_threshold: float = 0
text_fg_override_threshold_unit: str = '%'
text_old_gamma: bool = False text_old_gamma: bool = False
semi_transparent: bool = False semi_transparent: bool = False
cell_program_replacer: MultiReplacer = null_replacer cell_program_replacer: MultiReplacer = null_replacer
@@ -140,7 +140,10 @@ class LoadShaderPrograms:
@property @property
def needs_recompile(self) -> bool: def needs_recompile(self) -> bool:
opts = get_options() 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: def recompile_if_needed(self) -> None:
if self.needs_recompile: if self.needs_recompile:
@@ -150,10 +153,14 @@ class LoadShaderPrograms:
self.semi_transparent = semi_transparent self.semi_transparent = semi_transparent
opts = get_options() opts = get_options()
self.text_old_gamma = opts.text_composition_strategy == 'legacy' 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 self.text_fg_override_threshold, self.text_fg_override_threshold_unit = opts.text_fg_override_threshold
else: match self.text_fg_override_threshold_unit:
self.text_fg_override_threshold = max(0, min(opts.text_fg_override_threshold, 100)) * 0.01 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') cell = program_for('cell')
if self.cell_program_replacer is null_replacer: if self.cell_program_replacer is null_replacer:
self.cell_program_replacer = MultiReplacer( self.cell_program_replacer = MultiReplacer(
@@ -171,9 +178,9 @@ class LoadShaderPrograms:
r['WHICH_PHASE'] = f'PHASE_{which}' r['WHICH_PHASE'] = f'PHASE_{which}'
r['TRANSPARENT'] = '1' if semi_transparent else '0' r['TRANSPARENT'] = '1' if semi_transparent else '0'
r['FG_OVERRIDE_THRESHOLD'] = str(self.text_fg_override_threshold) r['FG_OVERRIDE_THRESHOLD'] = str(self.text_fg_override_threshold)
r['FG_OVERRIDE'] = '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['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['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' r['TEXT_NEW_GAMMA'] = '0' if self.text_old_gamma else '1'
return self.cell_program_replacer(src) return self.cell_program_replacer(src)