mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-24 01:08:10 +02:00
Refactor font selection code to share more between fontconfig and CoreText
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import termios
|
import termios
|
||||||
from ctypes import Array, c_ubyte
|
from ctypes import Array, c_ubyte
|
||||||
from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, NotRequired, Optional, Tuple, TypedDict, Union, overload
|
from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Tuple, TypedDict, Union, overload
|
||||||
|
|
||||||
from kitty.boss import Boss
|
from kitty.boss import Boss
|
||||||
from kitty.fonts import FontFeature, VariableData
|
from kitty.fonts import FontFeature, VariableData
|
||||||
@@ -8,7 +8,7 @@ from kitty.fonts.render import FontObject
|
|||||||
from kitty.marks import MarkerFunc
|
from kitty.marks import MarkerFunc
|
||||||
from kitty.options.types import Options
|
from kitty.options.types import Options
|
||||||
from kitty.types import LayerShellConfig, SignalInfo
|
from kitty.types import LayerShellConfig, SignalInfo
|
||||||
from kitty.typing import EdgeLiteral
|
from kitty.typing import EdgeLiteral, NotRequired
|
||||||
|
|
||||||
# Constants {{{
|
# Constants {{{
|
||||||
GLFW_LAYER_SHELL_NONE: int
|
GLFW_LAYER_SHELL_NONE: int
|
||||||
@@ -447,6 +447,10 @@ class CoreTextFont(TypedDict):
|
|||||||
slant: float
|
slant: float
|
||||||
traits: int
|
traits: int
|
||||||
|
|
||||||
|
# The following two are used by C code to get a face from the pattern
|
||||||
|
named_style: NotRequired[int]
|
||||||
|
axes: NotRequired[List[float]]
|
||||||
|
|
||||||
|
|
||||||
class CTFace:
|
class CTFace:
|
||||||
def __init__(self, descriptor: Optional[CoreTextFont] = None, path: str = ''): ...
|
def __init__(self, descriptor: Optional[CoreTextFont] = None, path: str = ''): ...
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from enum import Enum, IntEnum, auto
|
from enum import Enum, IntEnum, auto
|
||||||
from typing import Dict, NamedTuple, Tuple, TypedDict, Union
|
from typing import Callable, Dict, NamedTuple, Tuple, TypedDict, Union
|
||||||
|
|
||||||
from kitty.typing import CoreTextFont, FontConfigPattern
|
from kitty.typing import CoreTextFont, FontConfigPattern
|
||||||
|
|
||||||
@@ -99,3 +99,18 @@ class FontSpec(NamedTuple):
|
|||||||
return self.system == 'auto'
|
return self.system == 'auto'
|
||||||
|
|
||||||
|
|
||||||
|
class Score(NamedTuple):
|
||||||
|
variable_score: int
|
||||||
|
style_score: int
|
||||||
|
monospace_score: int
|
||||||
|
width_score: int
|
||||||
|
weight_distance_from_medium: float = 0
|
||||||
|
|
||||||
|
|
||||||
|
Descriptor = Union[FontConfigPattern, CoreTextFont]
|
||||||
|
Scorer = Callable[[Descriptor], Score]
|
||||||
|
|
||||||
|
|
||||||
|
def family_name_to_key(family: str) -> str:
|
||||||
|
import re
|
||||||
|
return re.sub(r'\s+', ' ', family.lower())
|
||||||
|
|||||||
@@ -1,28 +1,41 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Dict, Union
|
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Sequence, Union
|
||||||
|
|
||||||
from kitty.constants import is_macos
|
from kitty.constants import is_macos
|
||||||
|
from kitty.options.types import Options
|
||||||
|
|
||||||
from . import VariableData
|
from . import Descriptor, FontSpec, Scorer, VariableData, family_name_to_key
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from kitty.fast_data_types import CoreTextFont, CTFace, FontConfigPattern
|
from kitty.fast_data_types import CTFace
|
||||||
from kitty.fast_data_types import Face as FT_Face
|
from kitty.fast_data_types import Face as FT_Face
|
||||||
|
|
||||||
Descriptor = Union[FontConfigPattern, CoreTextFont]
|
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map']
|
||||||
|
FontMap = Dict[FontCollectionMapType, Dict[str, List[Descriptor]]]
|
||||||
def Face(descriptor: Descriptor) -> Union[FT_Face, CTFace]:
|
def Face(descriptor: Descriptor) -> Union[FT_Face, CTFace]:
|
||||||
pass
|
pass
|
||||||
|
def all_fonts_map(monospaced: bool) -> FontMap: ...
|
||||||
|
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer: ...
|
||||||
|
def find_best_match(
|
||||||
|
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True, ignore_face: Optional[Descriptor] = None
|
||||||
|
) -> Descriptor: ...
|
||||||
|
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> Descriptor: ...
|
||||||
else:
|
else:
|
||||||
Descriptor = object
|
FontCollectionMapType = FontMap = None
|
||||||
if is_macos:
|
if is_macos:
|
||||||
from kitty.fast_data_types import CTFace as Face
|
from kitty.fast_data_types import CTFace as Face
|
||||||
|
|
||||||
|
from .core_text import all_fonts_map, create_scorer, find_best_match, find_last_resort_text_font
|
||||||
else:
|
else:
|
||||||
from kitty.fast_data_types import Face
|
from kitty.fast_data_types import Face
|
||||||
|
|
||||||
|
from .fontconfig import all_fonts_map, create_scorer, find_best_match, find_last_resort_text_font
|
||||||
|
|
||||||
|
|
||||||
cache_for_variable_data_by_path: Dict[str, VariableData] = {}
|
cache_for_variable_data_by_path: Dict[str, VariableData] = {}
|
||||||
|
attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'}
|
||||||
|
|
||||||
|
|
||||||
def get_variable_data_for_descriptor(d: Descriptor) -> VariableData:
|
def get_variable_data_for_descriptor(d: Descriptor) -> VariableData:
|
||||||
@@ -32,3 +45,118 @@ def get_variable_data_for_descriptor(d: Descriptor) -> VariableData:
|
|||||||
if ans is None:
|
if ans is None:
|
||||||
ans = cache_for_variable_data_by_path[d['path']] = Face(descriptor=d).get_variable_data()
|
ans = cache_for_variable_data_by_path[d['path']] = Face(descriptor=d).get_variable_data()
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def find_best_match_in_candidates(
|
||||||
|
candidates: Sequence[Descriptor], scorer: Scorer, is_medium_face: bool, ignore_face: Optional[Descriptor] = None
|
||||||
|
) -> Optional[Descriptor]:
|
||||||
|
if len(candidates) == 1 and not is_medium_face and candidates[0].get('family') == candidates[0].get('full_name'):
|
||||||
|
# IBM Plex Mono does this, where the full name of the regular font
|
||||||
|
# face is the same as its family name
|
||||||
|
return None
|
||||||
|
candidates = sorted(candidates, key=scorer)
|
||||||
|
for x in candidates:
|
||||||
|
if ignore_face is None or x != ignore_face:
|
||||||
|
return x
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_fine_grained_font(
|
||||||
|
spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(),
|
||||||
|
resolved_medium_font: Optional[Descriptor] = None, monospaced: bool = True
|
||||||
|
) -> Descriptor:
|
||||||
|
font_map = all_fonts_map(monospaced)
|
||||||
|
is_medium_face = resolved_medium_font is None
|
||||||
|
prefer_variable = bool(spec.axes) or bool(spec.style)
|
||||||
|
scorer = create_scorer(bold, italic, monospaced, prefer_variable=prefer_variable)
|
||||||
|
if spec.postscript_name:
|
||||||
|
q = find_best_match_in_candidates(font_map['ps_map'].get(family_name_to_key(spec.postscript_name), []), scorer, is_medium_face)
|
||||||
|
if q:
|
||||||
|
return q
|
||||||
|
if spec.full_name:
|
||||||
|
q = find_best_match_in_candidates(font_map['full_map'].get(family_name_to_key(spec.full_name), []), scorer, is_medium_face)
|
||||||
|
if q:
|
||||||
|
return q
|
||||||
|
if spec.family:
|
||||||
|
candidates = font_map['family_map'].get(family_name_to_key(spec.family), [])
|
||||||
|
if spec.style:
|
||||||
|
qs = spec.style.lower()
|
||||||
|
candidates = [x for x in candidates if x['style'].lower() == qs]
|
||||||
|
q = find_best_match_in_candidates(candidates, scorer, is_medium_face)
|
||||||
|
if q:
|
||||||
|
return q
|
||||||
|
return find_last_resort_text_font(bold, italic, monospaced)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> Descriptor:
|
||||||
|
if not pat['variable']:
|
||||||
|
return pat
|
||||||
|
|
||||||
|
vd = Face(descriptor=pat).get_variable_data()
|
||||||
|
if spec.style:
|
||||||
|
q = spec.style.lower()
|
||||||
|
for i, ns in enumerate(vd['named_styles']):
|
||||||
|
if ns.get('psname') and ns['psname'].lower() == q:
|
||||||
|
pat['named_style'] = i
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for i, ns in enumerate(vd['named_styles']):
|
||||||
|
if ns['name'].lower() == q:
|
||||||
|
pat['named_style'] = i
|
||||||
|
break
|
||||||
|
tag_map, name_map = {}, {}
|
||||||
|
axes = [ax['default'] for ax in vd['axes']]
|
||||||
|
for i, ax in enumerate(vd['axes']):
|
||||||
|
tag_map[ax['tag']] = i
|
||||||
|
if ax['strid']:
|
||||||
|
name_map[ax['strid'].lower()] = i
|
||||||
|
changed = False
|
||||||
|
for axspec in spec.axes:
|
||||||
|
qname = axspec[0]
|
||||||
|
axis = tag_map.get(qname)
|
||||||
|
if axis is None:
|
||||||
|
axis = name_map.get(qname.lower())
|
||||||
|
if axis is not None:
|
||||||
|
axes[axis] = axspec[1]
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
pat['axes'] = axes
|
||||||
|
return pat
|
||||||
|
|
||||||
|
|
||||||
|
def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool) -> Optional[Descriptor]:
|
||||||
|
raise NotImplementedError('TODO: Implement me')
|
||||||
|
|
||||||
|
|
||||||
|
def get_font_from_spec(
|
||||||
|
spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(),
|
||||||
|
resolved_medium_font: Optional[Descriptor] = None
|
||||||
|
) -> Descriptor:
|
||||||
|
if not spec.is_system:
|
||||||
|
return apply_variation_to_pattern(get_fine_grained_font(spec, bold, italic, medium_font_spec, resolved_medium_font), spec)
|
||||||
|
family = spec.system
|
||||||
|
if family == 'auto':
|
||||||
|
if bold or italic:
|
||||||
|
assert resolved_medium_font is not None
|
||||||
|
family = resolved_medium_font['family']
|
||||||
|
if resolved_medium_font['variable']:
|
||||||
|
v = find_bold_italic_variant(resolved_medium_font, bold, italic)
|
||||||
|
if v is not None:
|
||||||
|
return v
|
||||||
|
else:
|
||||||
|
family = 'monospace'
|
||||||
|
return find_best_match(family, bold, italic, ignore_face=resolved_medium_font)
|
||||||
|
|
||||||
|
|
||||||
|
def get_font_files(opts: Options) -> Dict[str, Descriptor]:
|
||||||
|
ans: Dict[str, Descriptor] = {}
|
||||||
|
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():
|
||||||
|
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
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from kitty.options.types import Options
|
|||||||
from kitty.typing import CoreTextFont
|
from kitty.typing import CoreTextFont
|
||||||
from kitty.utils import log_error
|
from kitty.utils import log_error
|
||||||
|
|
||||||
from . import ListedFont
|
from . import Descriptor, ListedFont, Score, Scorer
|
||||||
|
|
||||||
attr_map = {(False, False): 'font_family',
|
attr_map = {(False, False): 'font_family',
|
||||||
(True, False): 'bold_font',
|
(True, False): 'bold_font',
|
||||||
@@ -50,13 +50,11 @@ def list_fonts() -> Generator[ListedFont, None, None]:
|
|||||||
'is_variable': fd['variable'], 'descriptor': fd}
|
'is_variable': fd['variable'], 'descriptor': fd}
|
||||||
|
|
||||||
|
|
||||||
def find_best_match(
|
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer:
|
||||||
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True, ignore_face: Optional[CoreTextFont] = None
|
|
||||||
) -> CoreTextFont:
|
|
||||||
q = re.sub(r'\s+', ' ', family.lower())
|
|
||||||
font_map = all_fonts_map(monospaced)
|
|
||||||
|
|
||||||
def score(candidate: CoreTextFont) -> Tuple[int, int, int, float]:
|
def score(candidate: Descriptor) -> Score:
|
||||||
|
assert candidate['descriptor_type'] == 'core_text'
|
||||||
|
variable_score = 0 if prefer_variable and candidate['variable'] else 1
|
||||||
style_match = 1 if candidate['bold'] == bold and candidate[
|
style_match = 1 if candidate['bold'] == bold and candidate[
|
||||||
'italic'
|
'italic'
|
||||||
] == italic else 0
|
] == italic else 0
|
||||||
@@ -65,13 +63,30 @@ def find_best_match(
|
|||||||
# prefer semi-bold to bold to heavy, less bold means less chance of
|
# prefer semi-bold to bold to heavy, less bold means less chance of
|
||||||
# overflow
|
# overflow
|
||||||
weight_distance_from_medium = abs(candidate['weight'])
|
weight_distance_from_medium = abs(candidate['weight'])
|
||||||
return style_match, monospace_match, 1 if is_regular_width else 0, 1 - weight_distance_from_medium
|
return Score(variable_score, 1 - style_match, 1 - monospace_match, 1 - is_regular_width, weight_distance_from_medium)
|
||||||
|
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> CoreTextFont:
|
||||||
|
font_map = all_fonts_map(monospaced)
|
||||||
|
candidates = font_map['family_map']['menlo']
|
||||||
|
scorer = create_scorer(bold, italic, monospaced)
|
||||||
|
return sorted(candidates, key=scorer)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def find_best_match(
|
||||||
|
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True, ignore_face: Optional[CoreTextFont] = None
|
||||||
|
) -> CoreTextFont:
|
||||||
|
q = re.sub(r'\s+', ' ', family.lower())
|
||||||
|
font_map = all_fonts_map(monospaced)
|
||||||
|
scorer = create_scorer(bold, italic, monospaced)
|
||||||
|
|
||||||
# First look for an exact match
|
# First look for an exact match
|
||||||
for selector in ('ps_map', 'full_map'):
|
for selector in ('ps_map', 'full_map'):
|
||||||
candidates = font_map[selector].get(q)
|
candidates = font_map[selector].get(q)
|
||||||
if candidates:
|
if candidates:
|
||||||
possible = sorted(candidates, key=score)[-1]
|
possible = sorted(candidates, key=scorer)[0]
|
||||||
if possible != ignore_face:
|
if possible != ignore_face:
|
||||||
return possible
|
return possible
|
||||||
|
|
||||||
@@ -81,7 +96,7 @@ def find_best_match(
|
|||||||
log_error(f'The font {family} was not found, falling back to Menlo')
|
log_error(f'The font {family} was not found, falling back to Menlo')
|
||||||
q = 'menlo'
|
q = 'menlo'
|
||||||
candidates = font_map['family_map'][q]
|
candidates = font_map['family_map'][q]
|
||||||
return sorted(candidates, key=score)[-1]
|
return sorted(candidates, key=scorer)[0]
|
||||||
|
|
||||||
|
|
||||||
def get_font_from_spec(
|
def get_font_from_spec(
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import re
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Callable, Dict, Generator, List, Literal, NamedTuple, Optional, Tuple, cast
|
from typing import Dict, Generator, List, Literal, Optional, Tuple, cast
|
||||||
|
|
||||||
from kitty.fast_data_types import (
|
from kitty.fast_data_types import (
|
||||||
FC_DUAL,
|
FC_DUAL,
|
||||||
@@ -13,20 +12,12 @@ from kitty.fast_data_types import (
|
|||||||
FC_WEIGHT_BOLD,
|
FC_WEIGHT_BOLD,
|
||||||
FC_WEIGHT_REGULAR,
|
FC_WEIGHT_REGULAR,
|
||||||
FC_WIDTH_NORMAL,
|
FC_WIDTH_NORMAL,
|
||||||
Face,
|
|
||||||
fc_list,
|
fc_list,
|
||||||
)
|
)
|
||||||
from kitty.fast_data_types import fc_match as fc_match_impl
|
from kitty.fast_data_types import fc_match as fc_match_impl
|
||||||
from kitty.options.types import Options
|
|
||||||
from kitty.typing import FontConfigPattern
|
from kitty.typing import FontConfigPattern
|
||||||
|
|
||||||
from . import FontSpec, ListedFont
|
from . import Descriptor, ListedFont, Score, Scorer, family_name_to_key
|
||||||
|
|
||||||
attr_map = {(False, False): 'font_family',
|
|
||||||
(True, False): 'bold_font',
|
|
||||||
(False, True): 'italic_font',
|
|
||||||
(True, True): 'bold_italic_font'}
|
|
||||||
|
|
||||||
|
|
||||||
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map']
|
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map']
|
||||||
FontMap = Dict[FontCollectionMapType, Dict[str, List[FontConfigPattern]]]
|
FontMap = Dict[FontCollectionMapType, Dict[str, List[FontConfigPattern]]]
|
||||||
@@ -46,7 +37,7 @@ def create_font_map(all_fonts: Tuple[FontConfigPattern, ...]) -> FontMap:
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache(maxsize=2)
|
||||||
def all_fonts_map(monospaced: bool = True) -> FontMap:
|
def all_fonts_map(monospaced: bool = True) -> FontMap:
|
||||||
if monospaced:
|
if monospaced:
|
||||||
ans = fc_list(spacing=FC_DUAL) + fc_list(spacing=FC_MONO)
|
ans = fc_list(spacing=FC_DUAL) + fc_list(spacing=FC_MONO)
|
||||||
@@ -73,64 +64,50 @@ def list_fonts(only_variable: bool = False) -> Generator[ListedFont, None, None]
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def family_name_to_key(family: str) -> str:
|
|
||||||
return re.sub(r'\s+', ' ', family.lower())
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
def fc_match(family: str, bold: bool, italic: bool, spacing: int = FC_MONO) -> FontConfigPattern:
|
def fc_match(family: str, bold: bool, italic: bool, spacing: int = FC_MONO) -> FontConfigPattern:
|
||||||
return fc_match_impl(family, bold, italic, spacing)
|
return fc_match_impl(family, bold, italic, spacing)
|
||||||
|
|
||||||
|
|
||||||
class Score(NamedTuple):
|
|
||||||
variable_score: int
|
|
||||||
style_score: int
|
|
||||||
monospace_score: int
|
|
||||||
width_score: int
|
|
||||||
|
|
||||||
Scorer = Callable[[FontConfigPattern], Score]
|
|
||||||
|
|
||||||
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer:
|
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer:
|
||||||
|
|
||||||
def score(candidate: FontConfigPattern) -> Score:
|
def score(candidate: Descriptor) -> Score:
|
||||||
|
assert candidate['descriptor_type'] == 'fontconfig'
|
||||||
variable_score = 0 if prefer_variable and candidate['variable'] else 1
|
variable_score = 0 if prefer_variable and candidate['variable'] else 1
|
||||||
bold_score = abs((FC_WEIGHT_BOLD if bold else FC_WEIGHT_REGULAR) - candidate.get('weight', 0))
|
bold_score = abs((FC_WEIGHT_BOLD if bold else FC_WEIGHT_REGULAR) - candidate['weight'])
|
||||||
italic_score = abs((FC_SLANT_ITALIC if italic else FC_SLANT_ROMAN) - candidate.get('slant', 0))
|
italic_score = abs((FC_SLANT_ITALIC if italic else FC_SLANT_ROMAN) - candidate['slant'])
|
||||||
monospace_match = 0
|
monospace_match = 0
|
||||||
if monospaced:
|
if monospaced:
|
||||||
monospace_match = 0 if candidate.get('spacing') == 'MONO' else 1
|
monospace_match = 0 if candidate.get('spacing') == 'MONO' else 1
|
||||||
width_score = abs(candidate.get('width', FC_WIDTH_NORMAL) - FC_WIDTH_NORMAL)
|
width_score = abs(candidate['width'] - FC_WIDTH_NORMAL)
|
||||||
|
|
||||||
return Score(variable_score, bold_score + italic_score, monospace_match, width_score)
|
return Score(variable_score, bold_score + italic_score, monospace_match, width_score)
|
||||||
|
|
||||||
return score
|
return score
|
||||||
|
|
||||||
|
|
||||||
def find_best_match_in_candidates(
|
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> FontConfigPattern:
|
||||||
candidates: List[FontConfigPattern], scorer: Scorer, is_medium_face: bool
|
# Use fc-match with a generic family
|
||||||
) -> Optional[FontConfigPattern]:
|
family = 'monospace' if monospaced else 'sans-serif'
|
||||||
if not candidates:
|
return fc_match(family, bold, italic)
|
||||||
return None
|
|
||||||
if len(candidates) == 1 and not is_medium_face and candidates[0].get('family') == candidates[0].get('full_name'):
|
|
||||||
# IBM Plex Mono does this, where the full name of the regular font
|
|
||||||
# face is the same as its family name
|
|
||||||
return None
|
|
||||||
candidates.sort(key=scorer)
|
|
||||||
return candidates[0]
|
|
||||||
|
|
||||||
|
|
||||||
def find_best_match(family: str, bold: bool = False, italic: bool = False, monospaced: bool = True) -> FontConfigPattern:
|
def find_best_match(
|
||||||
|
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True,
|
||||||
|
ignore_face: Optional[FontConfigPattern] = None
|
||||||
|
) -> FontConfigPattern:
|
||||||
|
from .common import find_best_match_in_candidates
|
||||||
q = family_name_to_key(family)
|
q = family_name_to_key(family)
|
||||||
font_map = all_fonts_map(monospaced)
|
font_map = all_fonts_map(monospaced)
|
||||||
scorer = create_scorer(bold, italic, monospaced)
|
scorer = create_scorer(bold, italic, monospaced)
|
||||||
is_medium_face = not bold and not italic
|
is_medium_face = not bold and not italic
|
||||||
# First look for an exact match
|
# First look for an exact match
|
||||||
exact_match = (
|
exact_match = (
|
||||||
find_best_match_in_candidates(font_map['ps_map'].get(q, []), scorer, is_medium_face) or
|
find_best_match_in_candidates(font_map['ps_map'].get(q, []), scorer, is_medium_face, ignore_face=ignore_face) or
|
||||||
find_best_match_in_candidates(font_map['full_map'].get(q, []), scorer, is_medium_face) or
|
find_best_match_in_candidates(font_map['full_map'].get(q, []), scorer, is_medium_face, ignore_face=ignore_face) or
|
||||||
find_best_match_in_candidates(font_map['family_map'].get(q, []), scorer, is_medium_face)
|
find_best_match_in_candidates(font_map['family_map'].get(q, []), scorer, is_medium_face, ignore_face=ignore_face)
|
||||||
)
|
)
|
||||||
if exact_match:
|
if exact_match:
|
||||||
|
assert exact_match['descriptor_type'] == 'fontconfig'
|
||||||
return exact_match
|
return exact_match
|
||||||
|
|
||||||
# Use fc-match to see if we can find a monospaced font that matches family
|
# Use fc-match to see if we can find a monospaced font that matches family
|
||||||
@@ -155,104 +132,7 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos
|
|||||||
if family_name_candidates and len(family_name_candidates) > 1:
|
if family_name_candidates and len(family_name_candidates) > 1:
|
||||||
candidates = family_name_candidates
|
candidates = family_name_candidates
|
||||||
return sorted(candidates, key=scorer)[0]
|
return sorted(candidates, key=scorer)[0]
|
||||||
|
return find_last_resort_text_font(bold, italic, monospaced)
|
||||||
# Use fc-match with a generic family
|
|
||||||
family = 'monospace' if monospaced else 'sans-serif'
|
|
||||||
return fc_match(family, bold, italic)
|
|
||||||
|
|
||||||
|
|
||||||
def get_fine_grained_font(
|
|
||||||
spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(),
|
|
||||||
resolved_medium_font: Optional[FontConfigPattern] = None, monospaced: bool = True
|
|
||||||
) -> FontConfigPattern:
|
|
||||||
font_map = all_fonts_map(monospaced)
|
|
||||||
is_medium_face = resolved_medium_font is None
|
|
||||||
prefer_variable = bool(spec.axes) or bool(spec.style)
|
|
||||||
if resolved_medium_font and resolved_medium_font['variable']:
|
|
||||||
prefer_variable = True
|
|
||||||
scorer = create_scorer(bold, italic, monospaced, prefer_variable=prefer_variable)
|
|
||||||
if spec.postscript_name:
|
|
||||||
q = find_best_match_in_candidates(font_map['ps_map'].get(family_name_to_key(spec.postscript_name), []), scorer, is_medium_face)
|
|
||||||
if q:
|
|
||||||
return q
|
|
||||||
if spec.full_name:
|
|
||||||
q = find_best_match_in_candidates(font_map['full_map'].get(family_name_to_key(spec.full_name), []), scorer, is_medium_face)
|
|
||||||
if q:
|
|
||||||
return q
|
|
||||||
if spec.family:
|
|
||||||
candidates = font_map['family_map'].get(family_name_to_key(spec.family), [])
|
|
||||||
if spec.style:
|
|
||||||
qs = spec.style.lower()
|
|
||||||
candidates = [x for x in candidates if x['style'].lower() == qs]
|
|
||||||
q = find_best_match_in_candidates(candidates, scorer, is_medium_face)
|
|
||||||
if q:
|
|
||||||
return q
|
|
||||||
# Use fc-match with a generic family
|
|
||||||
family = 'monospace' if monospaced else 'sans-serif'
|
|
||||||
return fc_match(family, bold, italic)
|
|
||||||
|
|
||||||
|
|
||||||
def apply_variation_to_pattern(pat: FontConfigPattern, spec: FontSpec) -> FontConfigPattern:
|
|
||||||
if not pat['variable']:
|
|
||||||
return pat
|
|
||||||
|
|
||||||
vd = Face(descriptor=pat).get_variable_data()
|
|
||||||
if spec.style:
|
|
||||||
q = spec.style.lower()
|
|
||||||
for i, ns in enumerate(vd['named_styles']):
|
|
||||||
if ns.get('psname') and ns['psname'].lower() == q:
|
|
||||||
pat['named_style'] = i
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for i, ns in enumerate(vd['named_styles']):
|
|
||||||
if ns['name'].lower() == q:
|
|
||||||
pat['named_style'] = i
|
|
||||||
break
|
|
||||||
tag_map, name_map = {}, {}
|
|
||||||
axes = [ax['default'] for ax in vd['axes']]
|
|
||||||
for i, ax in enumerate(vd['axes']):
|
|
||||||
tag_map[ax['tag']] = i
|
|
||||||
if ax['strid']:
|
|
||||||
name_map[ax['strid'].lower()] = i
|
|
||||||
changed = False
|
|
||||||
for axspec in spec.axes:
|
|
||||||
qname = axspec[0]
|
|
||||||
axis = tag_map.get(qname)
|
|
||||||
if axis is None:
|
|
||||||
axis = name_map.get(qname.lower())
|
|
||||||
if axis is not None:
|
|
||||||
axes[axis] = axspec[1]
|
|
||||||
changed = True
|
|
||||||
if changed:
|
|
||||||
pat['axes'] = axes
|
|
||||||
return pat
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
return apply_variation_to_pattern(get_fine_grained_font(spec, bold, italic, medium_font_spec, resolved_medium_font), spec)
|
|
||||||
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():
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def font_for_family(family: str) -> Tuple[FontConfigPattern, bool, bool]:
|
def font_for_family(family: str) -> Tuple[FontConfigPattern, bool, bool]:
|
||||||
|
|||||||
@@ -29,23 +29,17 @@ from kitty.types import _T
|
|||||||
from kitty.typing import CoreTextFont, FontConfigPattern
|
from kitty.typing import CoreTextFont, FontConfigPattern
|
||||||
from kitty.utils import log_error
|
from kitty.utils import log_error
|
||||||
|
|
||||||
|
from .common import get_font_files
|
||||||
|
|
||||||
if is_macos:
|
if is_macos:
|
||||||
from .core_text import font_for_family as font_for_family_macos
|
from .core_text import font_for_family as font_for_family_macos
|
||||||
from .core_text import get_font_files as get_font_files_coretext
|
|
||||||
else:
|
else:
|
||||||
from .fontconfig import font_for_family as font_for_family_fontconfig
|
from .fontconfig import font_for_family as font_for_family_fontconfig
|
||||||
from .fontconfig import get_font_files as get_font_files_fontconfig
|
|
||||||
|
|
||||||
FontObject = Union[CoreTextFont, FontConfigPattern]
|
FontObject = Union[CoreTextFont, FontConfigPattern]
|
||||||
current_faces: List[Tuple[FontObject, bool, bool]] = []
|
current_faces: List[Tuple[FontObject, bool, bool]] = []
|
||||||
|
|
||||||
|
|
||||||
def get_font_files(opts: Options) -> Dict[str, Any]:
|
|
||||||
if is_macos:
|
|
||||||
return get_font_files_coretext(opts)
|
|
||||||
return get_font_files_fontconfig(opts)
|
|
||||||
|
|
||||||
|
|
||||||
def font_for_family(family: str) -> Tuple[FontObject, bool, bool]:
|
def font_for_family(family: str) -> Tuple[FontObject, bool, bool]:
|
||||||
if is_macos:
|
if is_macos:
|
||||||
return font_for_family_macos(family)
|
return font_for_family_macos(family)
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ PowerlineStyle = str
|
|||||||
MatchType = str
|
MatchType = str
|
||||||
Protocol = object
|
Protocol = object
|
||||||
OptionsProtocol = object
|
OptionsProtocol = object
|
||||||
|
NotRequired = object
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from socket import socket as Socket
|
|||||||
from subprocess import CompletedProcess as CompletedProcess
|
from subprocess import CompletedProcess as CompletedProcess
|
||||||
from subprocess import Popen as PopenType
|
from subprocess import Popen as PopenType
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
from typing import NotRequired as NotRequired
|
||||||
from typing import Protocol as Protocol
|
from typing import Protocol as Protocol
|
||||||
from typing import TypedDict as TypedDict
|
from typing import TypedDict as TypedDict
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ __all__ = (
|
|||||||
'EdgeLiteral', 'MatchType', 'GRT_a', 'GRT_f', 'GRT_t', 'GRT_o', 'GRT_m', 'GRT_d',
|
'EdgeLiteral', 'MatchType', 'GRT_a', 'GRT_f', 'GRT_t', 'GRT_o', 'GRT_m', 'GRT_d',
|
||||||
'GraphicsCommandType', 'HandlerType', 'AbstractEventLoop', 'AddressFamily', 'Socket', 'CompletedProcess',
|
'GraphicsCommandType', 'HandlerType', 'AbstractEventLoop', 'AddressFamily', 'Socket', 'CompletedProcess',
|
||||||
'PopenType', 'Protocol', 'TypedDict', 'MarkType', 'ImageManagerType', 'Debug', 'LoopType', 'MouseEvent',
|
'PopenType', 'Protocol', 'TypedDict', 'MarkType', 'ImageManagerType', 'Debug', 'LoopType', 'MouseEvent',
|
||||||
'TermManagerType', 'BossType', 'ChildType', 'BadLineType', 'MouseButton',
|
'TermManagerType', 'BossType', 'ChildType', 'BadLineType', 'MouseButton', 'NotRequired',
|
||||||
'KeyActionType', 'KeyMap', 'KittyCommonOpts', 'AliasMap', 'CoreTextFont', 'WindowSystemMouseEvent',
|
'KeyActionType', 'KeyMap', 'KittyCommonOpts', 'AliasMap', 'CoreTextFont', 'WindowSystemMouseEvent',
|
||||||
'FontConfigPattern', 'ScreenType', 'StartupCtx', 'KeyEventType', 'LayoutType', 'PowerlineStyle',
|
'FontConfigPattern', 'ScreenType', 'StartupCtx', 'KeyEventType', 'LayoutType', 'PowerlineStyle',
|
||||||
'RemoteCommandType', 'SessionType', 'SessionTab', 'SpecialWindowInstance', 'TabType', 'ScreenSize', 'WindowType'
|
'RemoteCommandType', 'SessionType', 'SessionTab', 'SpecialWindowInstance', 'TabType', 'ScreenSize', 'WindowType'
|
||||||
|
|||||||
Reference in New Issue
Block a user