From 6e3c9856d1b3affc0c8904abc363e3d1c276923c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 24 Apr 2024 19:21:23 +0530 Subject: [PATCH] Wire up parsing of font specs --- kitty/fonts/__init__.py | 19 +++++++++++++++++++ kitty/fonts/core_text.py | 38 +++++++++++++++++++++---------------- kitty/fonts/fontconfig.py | 30 ++++++++++++++++++----------- kitty/fonts/render.py | 3 ++- kitty/options/definition.py | 18 +++++++++--------- kitty/options/parse.py | 21 ++++++++++---------- kitty/options/types.py | 9 +++++---- kitty/options/utils.py | 22 ++++++++++++++++++++- 8 files changed, 107 insertions(+), 53 deletions(-) diff --git a/kitty/fonts/__init__.py b/kitty/fonts/__init__.py index f247fdd43..253bb232a 100644 --- a/kitty/fonts/__init__.py +++ b/kitty/fonts/__init__.py @@ -80,3 +80,22 @@ class FontModification(NamedTuple): def __repr__(self) -> str: fn = f' {self.font_name}' if self.font_name else '' return f'{self.mod_type.name}{fn} {self.mod_value}' + + +class FontSpec(NamedTuple): + family: str = '' + style: str = '' + postscript_name: str = '' + full_name: str = '' + system: str = '' + axes: Tuple[Tuple[str, float], ...] = () + + @property + def is_system(self) -> bool: + return bool(self.system) + + @property + def is_auto(self) -> bool: + return self.system == 'auto' + + diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index ce164babb..9b26d2617 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -5,7 +5,7 @@ import re from typing import Dict, Generator, Iterable, List, Optional, Tuple from kitty.fast_data_types import CTFace, coretext_all_fonts -from kitty.fonts import FontFeature, VariableData +from kitty.fonts import FontFeature, FontSpec, VariableData from kitty.options.types import Options from kitty.typing import CoreTextFont from kitty.utils import log_error @@ -89,32 +89,38 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, ignor return sorted(candidates, key=score)[-1] -def resolve_family(f: str, main_family: str, bold: bool = False, italic: bool = False) -> str: - if (bold or italic) and f == 'auto': - f = main_family - if f.lower() == 'monospace': - f = 'Menlo' - return f +def get_font_from_spec( + spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), + resolved_medium_font: Optional[CoreTextFont] = None +) -> CoreTextFont: + if not spec.is_system: + raise NotImplementedError('TODO: Implement me') + family = spec.system + if family == 'auto' and (bold or italic): + assert resolved_medium_font is not None + family = resolved_medium_font['family'] + return find_best_match(family, bold, italic, ignore_face=resolved_medium_font) def get_font_files(opts: Options) -> Dict[str, CoreTextFont]: + medium_font = get_font_from_spec(opts.font_family) ans: Dict[str, CoreTextFont] = {} + kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'} for (bold, italic) in sorted(attr_map): attr = attr_map[(bold, italic)] - key = {(False, False): 'medium', - (True, False): 'bold', - (False, True): 'italic', - (True, True): 'bi'}[(bold, italic)] - ignore_face = None if key == 'medium' else ans['medium'] - face = find_best_match(resolve_family(getattr(opts, attr), opts.font_family, bold, italic), bold, italic, ignore_face=ignore_face) - ans[key] = face + key = kd[(bold, italic)] + if bold or italic: + font = get_font_from_spec(getattr(opts, attr), bold, italic, medium_font_spec=opts.font_family, resolved_medium_font=medium_font) + else: + font = medium_font + ans[key] = font if key == 'medium': - setattr(get_font_files, 'medium_family', face['family']) + setattr(get_font_files, 'medium_family', font['family']) return ans def font_for_family(family: str) -> Tuple[CoreTextFont, bool, bool]: - ans = find_best_match(resolve_family(family, getattr(get_font_files, 'medium_family'))) + ans = find_best_match(family) return ans, ans['bold'], ans['italic'] diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index ac13c2760..58d0af614 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -23,7 +23,7 @@ from kitty.options.types import Options from kitty.typing import FontConfigPattern from kitty.utils import log_error -from . import FontFeature, ListedFont, VariableData +from . import FontFeature, FontSpec, ListedFont, VariableData attr_map = {(False, False): 'font_family', (True, False): 'bold_font', @@ -153,21 +153,29 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos return fc_match(family, bold, italic) -def resolve_family(f: str, main_family: str, bold: bool, italic: bool) -> str: - if (bold or italic) and f == 'auto': - f = main_family - return f +def get_font_from_spec( + spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), + resolved_medium_font: Optional[FontConfigPattern] = None +) -> FontConfigPattern: + if not spec.is_system: + raise NotImplementedError('TODO: Implement me') + family = spec.system + if family == 'auto' and (bold or italic): + assert resolved_medium_font is not None + family = resolved_medium_font['family'] + return find_best_match(family, bold, italic) def get_font_files(opts: Options) -> Dict[str, FontConfigPattern]: ans: Dict[str, FontConfigPattern] = {} + medium_font = get_font_from_spec(opts.font_family) + kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'} for (bold, italic), attr in attr_map.items(): - rf = resolve_family(getattr(opts, attr), opts.font_family, bold, italic) - font = find_best_match(rf, bold, italic) - key = {(False, False): 'medium', - (True, False): 'bold', - (False, True): 'italic', - (True, True): 'bi'}[(bold, italic)] + if bold or italic: + font = get_font_from_spec(getattr(opts, attr), bold, italic, medium_font_spec=opts.font_family, resolved_medium_font=medium_font) + else: + font = medium_font + key = kd[(bold, italic)] ans[key] = font return ans diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index 96ac420ed..0f7c760ea 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -23,6 +23,7 @@ from kitty.fast_data_types import ( ) from kitty.fonts.box_drawing import BufType, distribute_dots, render_box_char, render_missing_glyph from kitty.options.types import Options, defaults +from kitty.options.utils import parse_font_spec from kitty.types import _T from kitty.typing import CoreTextFont, FontConfigPattern from kitty.utils import log_error @@ -431,7 +432,7 @@ class setup_for_testing: self.family, self.size, self.dpi = family, size, dpi def __enter__(self) -> Tuple[Dict[Tuple[int, int, int], bytes], int, int]: - opts = defaults._replace(font_family=self.family, font_size=self.size) + opts = defaults._replace(font_family=parse_font_spec(self.family), font_size=self.size) set_options(opts) sprites = {} diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 07ce50001..de49a714a 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -32,15 +32,15 @@ kitty has very powerful font management. You can configure individual font faces and even specify special fonts for particular characters. ''') -opt('font_family', 'monospace', +opt('font_family', 'monospace', option_type='parse_font_spec', long_text=''' You can specify different fonts for the bold/italic/bold-italic variants. To get a full list of supported fonts use the ``kitty +list-fonts`` command. -By default they are derived automatically, by the OSes font system. When -:opt:`bold_font` or :opt:`bold_italic_font` is set to :code:`auto` on macOS, the -priority of bold fonts is semi-bold, bold, heavy. Setting them manually is -useful for font families that have many weight variants like Book, Medium, -Thick, etc. +By default, they are derived automatically, via the Operating System's font +management. When :opt:`bold_font` or :opt:`bold_italic_font` is set to +:code:`auto` on macOS, the priority of bold fonts is semi-bold, bold, heavy. +Setting them manually is useful for font families that have many weight variants +like Book, Medium, Thick, etc. For example:: font_family Operator Mono Book @@ -50,11 +50,11 @@ For example:: ''' ) -opt('bold_font', 'auto') +opt('bold_font', 'auto', option_type='parse_font_spec') -opt('italic_font', 'auto') +opt('italic_font', 'auto', option_type='parse_font_spec') -opt('bold_italic_font', 'auto') +opt('bold_italic_font', 'auto', option_type='parse_font_spec') opt('font_size', '11.0', option_type='to_font_size', ctype='double', diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 2c8b7aa4e..7f109bad1 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -13,13 +13,12 @@ from kitty.options.utils import ( deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias, deprecated_send_text, disable_ligatures, edge_width, env, font_features, hide_window_decorations, macos_option_as_alt, macos_titlebar_color, menu_map, modify_font, narrow_symbols, - notify_on_cmd_finish, optional_edge_width, parse_map, parse_mouse_map, paste_actions, - remote_control_password, resize_debounce_time, scrollback_lines, scrollback_pager_history_size, - shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, - tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, - tab_title_template, titlebar_color, to_cursor_shape, to_cursor_unfocused_shape, to_font_size, - to_layout_names, to_modifiers, url_prefixes, url_style, visual_window_select_characters, - window_border_width, window_logo_scale, window_size + notify_on_cmd_finish, optional_edge_width, parse_font_spec, parse_map, parse_mouse_map, + paste_actions, remote_control_password, resize_debounce_time, scrollback_lines, + scrollback_pager_history_size, shell_integration, store_multiple, symbol_map, tab_activity_symbol, + tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, + tab_title_template, titlebar_color, to_cursor_shape, to_font_size, to_layout_names, to_modifiers, + url_prefixes, url_style, visual_window_select_characters, window_border_width, window_size ) @@ -102,10 +101,10 @@ class Parser: ans['bell_path'] = config_or_absolute_path(val) def bold_font(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: - ans['bold_font'] = str(val) + ans['bold_font'] = parse_font_spec(val) def bold_italic_font(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: - ans['bold_italic_font'] = str(val) + ans['bold_italic_font'] = parse_font_spec(val) def box_drawing_scale(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['box_drawing_scale'] = box_drawing_scale(val) @@ -979,7 +978,7 @@ class Parser: ans['focus_follows_mouse'] = to_bool(val) def font_family(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: - ans['font_family'] = str(val) + ans['font_family'] = parse_font_spec(val) def font_features(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: for k, v in font_features(val): @@ -1025,7 +1024,7 @@ class Parser: ans['input_delay'] = positive_int(val) def italic_font(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: - ans['italic_font'] = str(val) + ans['italic_font'] = parse_font_spec(val) def kitten_alias(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: for k, v in action_alias(val): diff --git a/kitty/options/types.py b/kitty/options/types.py index b9262c860..01ead072f 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -7,6 +7,7 @@ from kitty.constants import is_macos import kitty.constants from kitty.fast_data_types import Color, SingleKey import kitty.fast_data_types +from kitty.fonts import FontSpec import kitty.fonts from kitty.options.utils import ( AliasMap, KeyDefinition, KeyboardModeMap, MouseMap, MouseMapping, NotifyOnCmdFinish, @@ -487,8 +488,8 @@ class Options: bell_border_color: Color = Color(255, 90, 0) bell_on_tab: str = '🔔 ' bell_path: typing.Optional[str] = None - bold_font: str = 'auto' - bold_italic_font: str = 'auto' + bold_font: FontSpec = FontSpec(family='', style='', postscript_name='', full_name='', system='auto', axes=()) + bold_italic_font: FontSpec = FontSpec(family='', style='', postscript_name='', full_name='', system='auto', axes=()) box_drawing_scale: typing.Tuple[float, float, float, float] = (0.001, 1.0, 1.5, 2.0) clear_all_mouse_actions: bool = False clear_all_shortcuts: bool = False @@ -519,7 +520,7 @@ class Options: enabled_layouts: typing.List[str] = ['fat', 'grid', 'horizontal', 'splits', 'stack', 'tall', 'vertical'] file_transfer_confirmation_bypass: str = '' focus_follows_mouse: bool = False - font_family: str = 'monospace' + font_family: FontSpec = FontSpec(family='', style='', postscript_name='', full_name='', system='monospace', axes=()) font_size: float = 11.0 force_ltr: bool = False foreground: Color = Color(221, 221, 221) @@ -533,7 +534,7 @@ class Options: initial_window_height: typing.Tuple[int, str] = (400, 'px') initial_window_width: typing.Tuple[int, str] = (640, 'px') input_delay: int = 3 - italic_font: str = 'auto' + italic_font: FontSpec = FontSpec(family='', style='', postscript_name='', full_name='', system='auto', axes=()) kitty_mod: int = 5 linux_bell_theme: str = '__custom' linux_display_server: choices_for_linux_display_server = 'auto' diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 099a05f16..0f936cf5e 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -45,7 +45,7 @@ from kitty.conf.utils import ( ) from kitty.constants import is_macos from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, NO_CURSOR_SHAPE, Color, Shlex, SingleKey -from kitty.fonts import FontFeature, FontModification, ModificationType, ModificationUnit, ModificationValue +from kitty.fonts import FontFeature, FontModification, FontSpec, ModificationType, ModificationUnit, ModificationValue from kitty.key_names import character_key_name_aliases, functional_key_name_aliases, get_key_name_lookup from kitty.rgb import color_as_int from kitty.types import FloatEdges, MouseEvent @@ -1390,6 +1390,26 @@ def parse_mouse_map(val: str) -> Iterable[MouseMapping]: yield MouseMapping(button, mods, count, mode == 'grabbed', definition=action) +def parse_font_spec(spec: str) -> FontSpec: + if spec == 'auto': + return FontSpec(system='auto') + items = tuple(shlex_split(spec)) + if '=' not in items[0]: + return FontSpec(system=spec) + axes = {} + defined = {} + for item in items: + k, sep, v = item.partition('=') + if sep != '=': + raise ValueError(f'The font specification: {spec} is not valid as {item} does not contain an =') + if k in ('family', 'style', 'full_name', 'postscript_name'): + defined[k] = v + else: + try: + axes[k] = float(v) + except Exception: + raise ValueError(f'The font specification: {spec} is not valid as {v} is not a number') + return FontSpec(axes=tuple(axes.items()), **defined) def deprecated_hide_window_decorations_aliases(key: str, val: str, ans: Dict[str, Any]) -> None: if not hasattr(deprecated_hide_window_decorations_aliases, key): setattr(deprecated_hide_window_decorations_aliases, key, True)