diff --git a/kittens/choose_fonts/backend.py b/kittens/choose_fonts/backend.py index e65a6e3ca..867247a81 100644 --- a/kittens/choose_fonts/backend.py +++ b/kittens/choose_fonts/backend.py @@ -20,7 +20,7 @@ from kitty.fonts.common import ( get_variable_data_for_descriptor, get_variable_data_for_face, is_variable, - spec_for_descriptor, + spec_for_face, ) from kitty.fonts.list import create_family_groups from kitty.fonts.render import display_bitmap @@ -134,6 +134,11 @@ def render_family_sample( ResolvedFace = Dict[Literal['family', 'spec'], str] +def spec_for_descriptor(d: Descriptor) -> str: + face = face_from_descriptor(d) + return spec_for_face(d['family'], face).as_setting + + def resolved_faces(opts: Options) -> Dict[OptNames, ResolvedFace]: font_files = get_font_files(opts) ans: Dict[OptNames, ResolvedFace] = {} diff --git a/kittens/choose_fonts/face.go b/kittens/choose_fonts/face.go index 90e05c494..937377bfb 100644 --- a/kittens/choose_fonts/face.go +++ b/kittens/choose_fonts/face.go @@ -11,6 +11,7 @@ import ( "kitty/tools/tui" "kitty/tools/tui/loop" "kitty/tools/utils" + "kitty/tools/utils/shlex" "kitty/tools/wcswidth" ) @@ -28,7 +29,8 @@ type face_panel struct { } func (self *face_panel) variable_spec(named_style string, axis_overrides map[string]float64) string { - ans := fmt.Sprintf(`family="%s" variable_name="%s"`, self.family, self.current_preview.Variable_data.Variations_postscript_name_prefix) + vname := self.current_preview.Variable_data.Variations_postscript_name_prefix + ans := fmt.Sprintf(`family=%s variable_name=%s`, shlex.Quote(self.family), shlex.Quote(vname)) if axis_overrides != nil { axis_values := self.current_preview.current_axis_values() maps.Copy(axis_values, axis_overrides) @@ -36,7 +38,7 @@ func (self *face_panel) variable_spec(named_style string, axis_overrides map[str ans += fmt.Sprintf(" %s=%g", tag, val) } } else if named_style != "" { - ans += fmt.Sprintf(" style=\"%s\"", named_style) + ans += fmt.Sprintf(" style=%s", shlex.Quote(named_style)) } return ans } diff --git a/kitty/fonts/__init__.py b/kitty/fonts/__init__.py index d59c6a539..68fa19bb9 100644 --- a/kitty/fonts/__init__.py +++ b/kitty/fonts/__init__.py @@ -1,5 +1,5 @@ from enum import Enum, IntEnum, auto -from typing import TYPE_CHECKING, Dict, List, Literal, NamedTuple, Sequence, Tuple, TypedDict, TypeVar, Union +from typing import TYPE_CHECKING, Dict, List, Literal, NamedTuple, Optional, Sequence, Tuple, TypedDict, TypeVar, Union from kitty.types import run_once from kitty.typing import CoreTextFont, FontConfigPattern @@ -136,13 +136,13 @@ class FontModification(NamedTuple): class FontSpec(NamedTuple): - family: str = '' - style: str = '' - postscript_name: str = '' - full_name: str = '' - system: str = '' + family: Optional[str] = None + style: Optional[str] = None + postscript_name: Optional[str] = None + full_name: Optional[str] = None + system: Optional[str] = None axes: Tuple[Tuple[str, float], ...] = () - variable_name: str = '' + variable_name: Optional[str] = None created_from_string: str = '' @property @@ -153,6 +153,33 @@ class FontSpec(NamedTuple): def is_auto(self) -> bool: return self.system == 'auto' + @property + def as_setting(self) -> str: + if self.created_from_string: + return self.created_from_string + if self.system: + return self.system + ans = [] + from shlex import quote + def a(key: str, val: str) -> None: + ans.append(f'{key}={quote(val)}') + + if self.family is not None: + a('family', self.family) + if self.postscript_name is not None: + a('postscript_name', self.postscript_name) + if self.full_name is not None: + a('full_name', self.full_name) + if self.variable_name is not None: + a('variable_name', self.variable_name) + if self.style is not None: + a('style', self.style) + if self.axes: + for (key, val) in self.axes: + a(key, f'{val:g}') + return ' '.join(ans) + + Descriptor = Union[FontConfigPattern, CoreTextFont] DescriptorVar = TypeVar('DescriptorVar', FontConfigPattern, CoreTextFont, Descriptor) diff --git a/kitty/fonts/common.py b/kitty/fonts/common.py index 848b4633e..db86db320 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -157,7 +157,7 @@ def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool) -> De def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced: bool, candidates: List[Descriptor]) -> Descriptor: - if spec.variable_name: + if spec.variable_name is not None: q = spec.variable_name.lower() for font in candidates: vd = get_variable_data_for_descriptor(font) @@ -177,7 +177,7 @@ def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced def get_fine_grained_font( - spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), + spec: FontSpec, bold: bool = False, italic: bool = False, resolved_medium_font: Optional[Descriptor] = None, monospaced: bool = True, match_is_more_specific_than_family: Event = Event() ) -> Descriptor: font_map = all_fonts_map(monospaced) @@ -241,13 +241,13 @@ def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> Tuple[Descrip def get_font_from_spec( - spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), + spec: FontSpec, bold: bool = False, italic: bool = False, resolved_medium_font: Optional[Descriptor] = None, match_is_more_specific_than_family: Event = Event() ) -> Descriptor: if not spec.is_system: - return get_fine_grained_font(spec, bold, italic, medium_font_spec, resolved_medium_font, + return get_fine_grained_font(spec, bold, italic, resolved_medium_font, match_is_more_specific_than_family=match_is_more_specific_than_family) - family = spec.system + family = spec.system or '' if family == 'auto': if bold or italic: assert resolved_medium_font is not None @@ -288,7 +288,7 @@ def get_font_files(opts: Options) -> FontFiles: kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'} for (bold, italic), attr in attr_map.items(): 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) + font = get_font_from_spec(getattr(opts, attr), bold, italic, resolved_medium_font=medium_font) else: font = medium_font key = kd[(bold, italic)] @@ -382,21 +382,19 @@ def get_axis_map(face_or_descriptor: Union[Face, Descriptor]) -> Dict[str, float return base_axis_map -def spec_for_descriptor(descriptor: Descriptor) -> str: - from shlex import quote as q - if is_variable(descriptor): - vd = get_variable_data_for_descriptor(descriptor) - spec = f'family={q(descriptor["family"])}' - if vd['variations_postscript_name_prefix']: - spec += f' variable_name={q(vd["variations_postscript_name_prefix"])}' - ns = get_named_style(descriptor) - if ns is None: - for key, val in get_axis_map(descriptor).items(): - spec += f' {key}={val:g}' - else: - spec = f'{spec} style={q(ns["psname"])}' - return spec - return descriptor['postscript_name'] +def spec_for_face(family: str, face: Face) -> FontSpec: + v = face.get_variation() + if v is None: + return FontSpec(family=family, postscript_name=face.postscript_name()) + vd = face.get_variable_data() + varname = vd['variations_postscript_name_prefix'] + ns = get_named_style(face) + if ns is None: + axes = [] + for key, val in get_axis_map(face).items(): + axes.append((key, val)) + return FontSpec(family=family, variable_name=varname, axes=tuple(axes)) + return FontSpec(family=family, variable_name=varname, style=ns['psname'] or ns['name']) if __name__ == '__main__': diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index e749d8851..9c2c982b6 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -8,11 +8,10 @@ from functools import lru_cache from typing import Dict, Generator, Iterable, List, NamedTuple, Optional, Sequence, Tuple from kitty.fast_data_types import CTFace, coretext_all_fonts -from kitty.options.types import Options from kitty.typing import CoreTextFont from kitty.utils import log_error -from . import Descriptor, DescriptorVar, FontSpec, ListedFont, Score, Scorer, VariableData, family_name_to_key +from . import Descriptor, DescriptorVar, ListedFont, Score, Scorer, VariableData, family_name_to_key attr_map = {(False, False): 'font_family', (True, False): 'bold_font', @@ -202,36 +201,6 @@ def find_best_match( return candidates[0] -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 = 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', font['family']) - return ans - - def font_for_family(family: str) -> Tuple[CoreTextFont, bool, bool]: ans = find_best_match(family, monospaced=False) return ans, ans['bold'], ans['italic'] diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index 52f293fa3..5f68fe617 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -8,10 +8,18 @@ import unittest from functools import partial from kitty.constants import is_macos, read_kitty_resource -from kitty.fast_data_types import DECAWM, get_fallback_font, sprite_map_set_layout, sprite_map_set_limits, test_render_line, test_sprite_position_for, wcwidth +from kitty.fast_data_types import ( + DECAWM, + get_fallback_font, + sprite_map_set_layout, + sprite_map_set_limits, + test_render_line, + test_sprite_position_for, + wcwidth, +) from kitty.fonts import family_name_to_key from kitty.fonts.box_drawing import box_chars -from kitty.fonts.common import all_fonts_map, face_from_descriptor, get_font_files, get_named_style +from kitty.fonts.common import FontSpec, all_fonts_map, face_from_descriptor, get_font_files, get_named_style, spec_for_face from kitty.fonts.render import coalesce_symbol_maps, render_string, setup_for_testing, shape_string from kitty.options.types import Options from kitty.options.utils import parse_font_spec @@ -93,6 +101,18 @@ class Selection(BaseTest): d = get_font_files(opts)['medium'] face = face_from_descriptor(d) self.ae(get_named_style(face)['name'], 'Black') + if has('cascadia code'): + opts = Options() + opts.font_family = parse_font_spec('family="cascadia code"') + opts.italic_font = parse_font_spec('family="cascadia code" variable_name= style="Light Italic"') + ff = get_font_files(opts) + + def t(x, **kw): + kw['family'] = 'Cascadia Code' + face = face_from_descriptor(ff[x]) + self.ae(FontSpec(**kw).as_setting, spec_for_face('Cascadia Code', face).as_setting) + t('medium', variable_name='CascadiaCodeRoman', style='Regular') + t('italic', variable_name='', style='Light Italic') class Rendering(BaseTest): diff --git a/tools/utils/shlex/shlex.go b/tools/utils/shlex/shlex.go index 1aaf85fa3..faf4b1520 100644 --- a/tools/utils/shlex/shlex.go +++ b/tools/utils/shlex/shlex.go @@ -17,6 +17,7 @@ package shlex import ( "fmt" + "kitty/tools/utils" "strings" "unicode/utf8" ) @@ -182,6 +183,16 @@ func Split(s string) (ans []string, err error) { return } +func Quote(s string) string { + if s == "" { + return s + } + if utils.MustCompile(`[^\w@%+=:,./-]`).MatchString(s) { + return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" + } + return s +} + // SplitForCompletion partitions a string into a slice of strings. It differs from Split in being // more relaxed about errors and also adding an empty string at the end if s ends with a Space. func SplitForCompletion(s string) (argv []string, position_of_last_arg int) {