diff --git a/kitty/core_text.m b/kitty/core_text.m index 8d17c2338..14a0d6ce6 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -33,6 +33,7 @@ typedef struct { CTFontRef ct_font; hb_font_t *hb_font; PyObject *family_name, *full_name, *postscript_name, *path, *name_lookup_table; + FontFeatures font_features; } CTFace; PyTypeObject CTFace_Type; static CTFontRef window_title_font = nil; @@ -87,7 +88,7 @@ get_path_for_font_descriptor(CTFontDescriptorRef font) { static CTFace* -ct_face(CTFontRef font) { +ct_face(CTFontRef font, PyObject *features) { CTFace *self = (CTFace *)CTFace_Type.tp_alloc(&CTFace_Type, 0); if (self) { init_face(self, font); @@ -96,6 +97,9 @@ ct_face(CTFontRef font) { self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true); self->path = get_path_for_font(self->ct_font); if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); } + else { + if (!create_features_for_face(postscript_name_for_face((PyObject*)self), features, &self->font_features)) { Py_CLEAR(self); } + } } return self; } @@ -106,6 +110,7 @@ dealloc(CTFace* self) { if (self->ct_font) CFRelease(self->ct_font); self->hb_font = NULL; self->ct_font = NULL; + free(self->font_features.features); Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name); Py_CLEAR(self->path); Py_CLEAR(self->name_lookup_table); Py_TYPE(self)->tp_free((PyObject*)self); @@ -126,6 +131,8 @@ string_to_tag(const uint8_t *bytes) { return (((uint32_t)bytes[0]) << 24) | (((uint32_t)bytes[1]) << 16) | (((uint32_t)bytes[2]) << 8) | bytes[3]; } +FontFeatures* +features_for_face(PyObject *s) { return &((CTFace*)s)->font_features; } static void add_variation_pair(const void *key_, const void *value_, void *ctx) { @@ -444,7 +451,7 @@ create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic, break; } } - return ans ? ans : (PyObject*)ct_face(new_font); + return ans ? ans : (PyObject*)ct_face(new_font, NULL); } unsigned int @@ -590,7 +597,7 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) { if (!desc) return NULL; RAII_CoreFoundation(CTFontRef, font, CTFontCreateWithFontDescriptor(desc, fg ? scaled_point_sz(fg) : 12, NULL)); if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; } - return (PyObject*) ct_face(font); + return (PyObject*) ct_face(font, PyDict_GetItemString(descriptor, "features")); } PyObject* @@ -600,7 +607,7 @@ face_from_path(const char *path, int UNUSED index, FONTS_DATA_HANDLE fg UNUSED) RAII_CoreFoundation(CGDataProviderRef, dp, CGDataProviderCreateWithURL(url)); RAII_CoreFoundation(CGFontRef, cg_font, CGFontCreateWithDataProvider(dp)); RAII_CoreFoundation(CTFontRef, ct_font, CTFontCreateWithGraphicsFont(cg_font, 0.0, NULL, NULL)); - return (PyObject*) ct_face(ct_font); + return (PyObject*) ct_face(ct_font, NULL); } static PyObject* diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index a7b890cdc..3605b3040 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -3,7 +3,7 @@ from ctypes import Array, c_ubyte from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Tuple, TypedDict, Union, overload from kitty.boss import Boss -from kitty.fonts import FontFeature, VariableData +from kitty.fonts import VariableData from kitty.fonts.render import FontObject from kitty.marks import MarkerFunc from kitty.options.types import Options @@ -400,6 +400,7 @@ class FontConfigPattern(TypedDict): # The following two are used by C code to get a face from the pattern named_style: NotRequired[int] axes: NotRequired[Tuple[float, ...]] + features: NotRequired[Tuple[ParsedFontFeature, ...]] def fc_list(spacing: int = -1, allow_bitmapped_fonts: bool = False, only_variable: bool = False) -> Tuple[FontConfigPattern, ...]: @@ -464,6 +465,7 @@ class CoreTextFont(TypedDict): # The following is used by C code to get a face from the pattern axis_map: NotRequired[Dict[str, float]] + features: NotRequired[Tuple[ParsedFontFeature, ...]] class CTFace: @@ -482,6 +484,10 @@ def coretext_all_fonts(monospaced_only: bool) -> Tuple[CoreTextFont, ...]: pass +class ParsedFontFeature: + def __init__(self, s: str): ... + + def add_timer( callback: Callable[[Optional[int]], None], interval: float, @@ -600,10 +606,6 @@ def get_options() -> Options: pass -def parse_font_feature(ff: str) -> bytes: - pass - - def glfw_primary_monitor_size() -> Tuple[int, int]: pass @@ -1071,7 +1073,6 @@ def set_font_data( descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]], bold: int, italic: int, bold_italic: int, num_symbol_fonts: int, symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float, - font_feature_settings: Dict[str, Tuple[FontFeature, ...]], narrow_symbols: Tuple[Tuple[int, int, int], ...], ) -> None: pass diff --git a/kitty/fonts.c b/kitty/fonts.c index 263903878..7719c8f5a 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -7,6 +7,7 @@ */ #include "fonts.h" +#include "pyport.h" #include "state.h" #include "emoji.h" #include "unicode-data.h" @@ -43,7 +44,6 @@ static hb_feature_t hb_features[3] = {{0}}; static char_type shape_buffer[4096] = {0}; static size_t max_texture_size = 1024, max_array_len = 1024; typedef enum { LIGA_FEATURE, DLIG_FEATURE, CALT_FEATURE } HBFeature; -static PyObject* font_feature_settings = NULL; typedef struct { char_type left, right; @@ -290,40 +290,45 @@ desc_to_face(PyObject *desc, FONTS_DATA_HANDLE fg) { return ans; } +bool +create_features_for_face(const char *psname, PyObject *features, FontFeatures *output) { + size_t count_from_descriptor = features ? PyTuple_GET_SIZE(features): 0; + __typeof__(OPT(font_features).entries) from_opts = NULL; + if (psname) { + for (size_t i = 0; i < OPT(font_features).num && !from_opts; i++) { + __typeof__(OPT(font_features).entries) e = OPT(font_features).entries + i; + if (strcmp(e->psname, psname) == 0) from_opts = e; + } + } + size_t count_from_opts = from_opts ? from_opts->num : 0; + output->features = calloc(MAX(2u, count_from_opts + count_from_descriptor), sizeof(output->features[0])); + if (!output->features) { PyErr_NoMemory(); return false; } + for (size_t i = 0; i < count_from_opts; i++) { + output->features[output->count++] = from_opts->features[i]; + } + for (size_t i = 0; i < count_from_descriptor; i++) { + ParsedFontFeature *f = (ParsedFontFeature*)PyTuple_GET_ITEM(features, i); + output->features[output->count++] = f->feature; + } + if (!output->count) { + if (strstr(psname, "NimbusMonoPS-") == psname) { + output->features[output->count++] = hb_features[LIGA_FEATURE]; + output->features[output->count++] = hb_features[DLIG_FEATURE]; + } + } + return true; +} + static bool init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentation) { f->face = face; Py_INCREF(f->face); f->bold = bold; f->italic = italic; f->emoji_presentation = emoji_presentation; - f->num_ffs_hb_features = 0; - const char *psname = postscript_name_for_face(face); - if (font_feature_settings != NULL){ - PyObject* o = PyDict_GetItemString(font_feature_settings, psname); - if (o != NULL && PyTuple_Check(o)) { - Py_ssize_t len = PyTuple_GET_SIZE(o); - if (len > 0) { - f->num_ffs_hb_features = len + 1; - f->ffs_hb_features = calloc(f->num_ffs_hb_features, sizeof(hb_feature_t)); - if (!f->ffs_hb_features) return false; - for (Py_ssize_t i = 0; i < len; i++) { - PyObject* parsed = PyObject_GetAttrString(PyTuple_GET_ITEM(o, i), "parsed"); - if (parsed) { - memcpy(f->ffs_hb_features + i, PyBytes_AS_STRING(parsed), sizeof(hb_feature_t)); - Py_DECREF(parsed); - } - } - memcpy(f->ffs_hb_features + len, &hb_features[CALT_FEATURE], sizeof(hb_feature_t)); - } - } - } - if (!f->num_ffs_hb_features) { - f->ffs_hb_features = calloc(4, sizeof(hb_feature_t)); - if (!f->ffs_hb_features) return false; - if (strstr(psname, "NimbusMonoPS-") == psname) { - memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[LIGA_FEATURE], sizeof(hb_feature_t)); - memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[DLIG_FEATURE], sizeof(hb_feature_t)); - } - memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[CALT_FEATURE], sizeof(hb_feature_t)); - } + const FontFeatures *features = features_for_face(face); + f->ffs_hb_features = calloc(1 + features->count, sizeof(hb_feature_t)); + if (!f->ffs_hb_features) { PyErr_NoMemory(); return false; } + f->num_ffs_hb_features = features->count; + memcpy(f->ffs_hb_features, features->features, sizeof(hb_feature_t) * features->count); + memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[CALT_FEATURE], sizeof(hb_feature_t)); return true; } @@ -1433,12 +1438,12 @@ set_symbol_maps(SymbolMap **maps, size_t *num, const PyObject *sm) { static PyObject* set_font_data(PyObject UNUSED *m, PyObject *args) { PyObject *sm, *ns; - Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings); - if (!PyArg_ParseTuple(args, "OOOIIIIO!dOO!", + Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); + if (!PyArg_ParseTuple(args, "OOOIIIIO!dO!", &box_drawing_function, &prerender_function, &descriptor_for_idx, &descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts, - &PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings, &PyTuple_Type, &ns)) return NULL; - Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings); + &PyTuple_Type, &sm, &OPT(font_size), &PyTuple_Type, &ns)) return NULL; + Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); free_font_groups(); clear_symbol_maps(); set_symbol_maps(&symbol_maps, &num_symbol_maps, sm); @@ -1539,7 +1544,6 @@ finalize(void) { Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); - Py_CLEAR(font_feature_settings); free_font_groups(); free(ligature_types); if (harfbuzz_buffer) { hb_buffer_destroy(harfbuzz_buffer); harfbuzz_buffer = NULL; } @@ -1710,26 +1714,89 @@ free_font_data(PyObject *self UNUSED, PyObject *args UNUSED) { Py_RETURN_NONE; } -static PyObject* -parse_font_feature(PyObject *self UNUSED, PyObject *feature) { - if (!PyUnicode_Check(feature)) { - PyErr_SetString(PyExc_TypeError, "feature must be a unicode object"); - return NULL; +static PyObject * +parsed_font_feature_new(PyTypeObject *type, PyObject *args, PyObject *kwds UNUSED) { + const char *s; + if (!PyArg_ParseTuple(args, "s", &s)) return NULL; + ParsedFontFeature *self = (ParsedFontFeature *)type->tp_alloc(type, 0); + if (self != NULL) { + if (!hb_feature_from_string(s, -1, &self->feature)) { + PyErr_Format(PyExc_ValueError, "%s is not a valid font feature", s); + Py_CLEAR(self); + } } - PyObject *ans = PyBytes_FromStringAndSize(NULL, sizeof(hb_feature_t)); - if (!ans) return NULL; - if (!hb_feature_from_string(PyUnicode_AsUTF8(feature), -1, (hb_feature_t*)PyBytes_AS_STRING(ans))) { - Py_CLEAR(ans); - PyErr_Format(PyExc_ValueError, "%U is not a valid font feature", feature); - return NULL; - } - return ans; + return (PyObject*) self; } +static PyObject* +parsed_font_feature_str(PyObject *self_) { + char buf[128]; + hb_feature_to_string(&((ParsedFontFeature*)self_)->feature, buf, arraysz(buf)); + return PyUnicode_FromString(buf); +} + +static PyObject* +parsed_font_feature_repr(PyObject *self_) { + RAII_PyObject(s, parsed_font_feature_str(self_)); + return s ? PyObject_Repr(s) : NULL; +} + + +PyTypeObject ParsedFontFeature_Type; + +static PyObject* +parsed_font_feature_cmp(PyObject *self, PyObject *other, int op) { + if (op != Py_EQ && op != Py_NE) return Py_NotImplemented; + if (!PyObject_TypeCheck(other, &ParsedFontFeature_Type)) { + if (op == Py_EQ) Py_RETURN_FALSE; + Py_RETURN_TRUE; + } + ParsedFontFeature *a = (ParsedFontFeature*)self, *b = (ParsedFontFeature*)other; + PyObject *ret = Py_True; + if (memcmp(&a->feature, &b->feature, sizeof(hb_feature_t)) == 0) { + if (op == Py_NE) ret = Py_False; + } else { + if (op == Py_EQ) ret = Py_False; + } + Py_INCREF(ret); return ret; +} + +static Py_hash_t +parsed_font_feature_hash(PyObject *s) { + ParsedFontFeature *self = (ParsedFontFeature*)s; + if (self->hash_computed) return self->hashval; + self->hash_computed = true; + HASH_FUNCTION(&self->feature, sizeof(hb_feature_t), self->hashval); + return self->hashval; +} + +static PyObject* +parsed_font_feature_call(PyObject *s, PyObject *args, PyObject *kwargs UNUSED) { + ParsedFontFeature *self = (ParsedFontFeature*)s; + void *dest = PyLong_AsVoidPtr(args); + memcpy(dest, &self->feature, sizeof(hb_feature_t)); + Py_RETURN_NONE; +} + +PyTypeObject ParsedFontFeature_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "kitty.fast_data_types.ParsedFontFeature", + .tp_basicsize = sizeof(ParsedFontFeature), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "FontFeature", + .tp_new = parsed_font_feature_new, + .tp_str = parsed_font_feature_str, + .tp_repr = parsed_font_feature_repr, + .tp_richcompare = parsed_font_feature_cmp, + .tp_hash = parsed_font_feature_hash, + .tp_call = parsed_font_feature_call, +}; + + + static PyMethodDef module_methods[] = { METHODB(set_font_data, METH_VARARGS), METHODB(free_font_data, METH_NOARGS), - METHODB(parse_font_feature, METH_O), METHODB(create_test_font_group, METH_VARARGS), METHODB(sprite_map_set_layout, METH_VARARGS), METHODB(test_sprite_position_for, METH_VARARGS), @@ -1757,5 +1824,9 @@ init_fonts(PyObject *module) { create_feature("-calt", CALT_FEATURE); #undef create_feature if (PyModule_AddFunctions(module, module_methods) != 0) return false; + if (PyType_Ready(&ParsedFontFeature_Type) < 0) return 0; + if (PyModule_AddObject(module, "ParsedFontFeature", (PyObject *)&ParsedFontFeature_Type) != 0) return 0; + Py_INCREF(&ParsedFontFeature_Type); + return true; } diff --git a/kitty/fonts.h b/kitty/fonts.h index 807ba0c2d..cfcde2f15 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -18,6 +18,20 @@ typedef struct { size_t width, height; } StringCanvas; +typedef struct FontFeatures { + size_t count; + hb_feature_t *features; +} FontFeatures; + +typedef struct ParsedFontFeature { + PyObject_HEAD + + hb_feature_t feature; + Py_hash_t hashval; + bool hash_computed; +} ParsedFontFeature; + + // API that font backends need to implement unsigned int glyph_id_for_codepoint(PyObject *, char_type); int get_glyph_width(PyObject *, glyph_index); @@ -54,6 +68,8 @@ bool read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output); bool read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output); +FontFeatures* features_for_face(PyObject *); +bool create_features_for_face(const char* psname, PyObject *features, FontFeatures* output); static inline void right_shift_canvas(pixel *canvas, size_t width, size_t height, size_t amt) { diff --git a/kitty/fonts/__init__.py b/kitty/fonts/__init__.py index 8afb4cba1..e4f27f2cf 100644 --- a/kitty/fonts/__init__.py +++ b/kitty/fonts/__init__.py @@ -1,6 +1,7 @@ from enum import Enum, IntEnum, auto from typing import TYPE_CHECKING, Dict, List, Literal, NamedTuple, Optional, Sequence, Tuple, TypedDict, TypeVar, Union +from kitty.fast_data_types import ParsedFontFeature from kitty.types import run_once from kitty.typing import CoreTextFont, FontConfigPattern from kitty.utils import shlex_split @@ -88,18 +89,6 @@ class VariableData(TypedDict): multi_axis_styles: Tuple[MultiAxisStyle, ...] -class FontFeature: - - __slots__ = 'name', 'parsed' - - def __init__(self, name: str, parsed: bytes): - self.name = name - self.parsed = parsed - - def __repr__(self) -> str: - return repr(self.name) - - class ModificationType(Enum): underline_position = auto() underline_thickness = auto() @@ -144,6 +133,7 @@ class FontSpec(NamedTuple): system: Optional[str] = None axes: Tuple[Tuple[str, float], ...] = () variable_name: Optional[str] = None + features: Tuple[ParsedFontFeature, ...] = () created_from_string: str = '' @classmethod @@ -155,19 +145,21 @@ class FontSpec(NamedTuple): return FontSpec(system=spec, created_from_string=spec) axes = {} defined = {} + features: Tuple[ParsedFontFeature, ...] = () 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', 'variable_name'): defined[k] = v + elif k == 'features': + features += tuple(ParsedFontFeature(x) for x in v.split()) 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()), created_from_string=spec, **defined) - + return FontSpec(axes=tuple(axes.items()), created_from_string=spec, features=features, **defined) @property def is_system(self) -> bool: @@ -198,6 +190,8 @@ class FontSpec(NamedTuple): a('variable_name', self.variable_name) if self.style is not None: a('style', self.style) + if self.features: + a('features', ' '.join(str(f) for f in self.features)) if self.axes: for (key, val) in self.axes: a(key, f'{val:g}') diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index c2cdf82f4..2db1c3dcb 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -189,7 +189,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional set_font_data( render_box_drawing, prerender_function, descriptor_for_idx, indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts, - sm, sz, opts.font_features.copy(), ns + sm, sz, ns ) diff --git a/kitty/freetype.c b/kitty/freetype.c index 7ef9d106c..216a9d4b9 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -48,6 +48,7 @@ typedef struct { free_extra_data_func free_extra_data; float apple_leading; PyObject *name_lookup_table; + FontFeatures font_features; } Face; PyTypeObject Face_Type; @@ -233,8 +234,7 @@ init_ft_face(Face *self, PyObject *path, int hinting, int hintstyle, FONTS_DATA_ self->strikethrough_thickness = os2->yStrikeoutSize; } - self->path = path; - Py_INCREF(self->path); + self->path = path; Py_INCREF(self->path); self->space_glyph_id = glyph_id_for_codepoint((PyObject*)self, ' '); return true; } @@ -299,11 +299,15 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) { } if ((error = FT_Set_Var_Design_Coordinates(self->face, sz, coords))) return set_load_error(path, error); } + if (!create_features_for_face(postscript_name_for_face((PyObject*)self), PyDict_GetItemString(descriptor, "features"), &self->font_features)) return NULL; } Py_XINCREF(retval); return retval; } +FontFeatures* +features_for_face(PyObject *s) { return &((Face*)s)->font_features; } + static PyObject* new(PyTypeObject *type UNUSED, PyObject *args, PyObject *kw) { const char *path = NULL; @@ -343,6 +347,7 @@ dealloc(Face* self) { if (self->harfbuzz_font) hb_font_destroy(self->harfbuzz_font); if (self->face) FT_Done_Face(self->face); if (self->extra_data && self->free_extra_data) self->free_extra_data(self->extra_data); + free(self->font_features.features); Py_CLEAR(self->path); Py_CLEAR(self->name_lookup_table); Py_TYPE(self)->tp_free((PyObject*)self); diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 1880122b4..21ad24e7c 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -132,10 +132,8 @@ Note that this refers to programming ligatures, typically implemented using the ''' ) -opt('+font_features', 'none', - option_type='font_features', - add_to_default=False, - long_text=''' +opt('+font_features', 'none', option_type='font_features', ctype='!font_features', + add_to_default=False, long_text=''' Choose exactly which OpenType features to enable or disable. This is useful as some fonts might have features worthwhile in a terminal. For example, Fira Code includes a discretionary feature, :code:`zero`, which in that font changes the diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 7f109bad1..93dbd9899 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -18,7 +18,7 @@ from kitty.options.utils import ( 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 + url_prefixes, url_style, visual_window_select_characters, window_border_width, window_size, window_logo_scale ) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 6aec2ea6d..0cc1f640d 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -44,6 +44,19 @@ convert_from_opts_disable_ligatures(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_font_features(PyObject *val, Options *opts) { + font_features(val, opts); +} + +static void +convert_from_opts_font_features(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "font_features"); + if (ret == NULL) return; + convert_from_python_font_features(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_modify_font(PyObject *val, Options *opts) { modify_font(val, opts); @@ -1170,6 +1183,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_disable_ligatures(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_font_features(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_modify_font(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_text_composition_strategy(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index 00434564f..4ee81ed04 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -8,6 +8,7 @@ #include "../state.h" #include "../colors.h" +#include "../fonts.h" static inline float PyFloat_AsFloat(PyObject *o) { @@ -44,7 +45,7 @@ parse_ms_long_to_monotonic_t(PyObject *val) { return ms_to_monotonic_t(PyLong_AsUnsignedLong(val)); } -static WindowTitleIn +static inline WindowTitleIn window_title_in(PyObject *title_in) { const char *in = PyUnicode_AsUTF8(title_in); switch(in[0]) { @@ -57,7 +58,7 @@ window_title_in(PyObject *title_in) { return ALL; } -static UnderlineHyperlinks +static inline UnderlineHyperlinks underline_hyperlinks(PyObject *x) { const char *in = PyUnicode_AsUTF8(x); switch(in[0]) { @@ -67,7 +68,7 @@ underline_hyperlinks(PyObject *x) { } } -static BackgroundImageLayout +static inline BackgroundImageLayout bglayout(PyObject *layout_name) { const char *name = PyUnicode_AsUTF8(layout_name); switch(name[0]) { @@ -82,7 +83,7 @@ bglayout(PyObject *layout_name) { return TILING; } -static ImageAnchorPosition +static inline ImageAnchorPosition bganchor(PyObject *anchor_name) { const char *name = PyUnicode_AsUTF8(anchor_name); ImageAnchorPosition anchor = {0.5f, 0.5f, 0.5f, 0.5f}; @@ -108,16 +109,16 @@ bganchor(PyObject *anchor_name) { if (opts->name) memcpy(opts->name, s, sz); \ } -static void +static inline void background_image(PyObject *src, Options *opts) { STR_SETTER(background_image); } -static void +static inline void bell_path(PyObject *src, Options *opts) { STR_SETTER(bell_path); } -static void +static inline void bell_theme(PyObject *src, Options *opts) { STR_SETTER(bell_theme); } -static void +static inline void window_logo_path(PyObject *src, Options *opts) { STR_SETTER(default_window_logo); } #undef STR_SETTER @@ -132,7 +133,7 @@ parse_font_mod_size(PyObject *val, float *sz, AdjustmentUnit *unit) { } } -static void +static inline void modify_font(PyObject *mf, Options *opts) { #define S(which) { PyObject *v = PyDict_GetItemString(mf, #which); if (v) parse_font_mod_size(v, &opts->which.val, &opts->which.unit); } S(underline_position); S(underline_thickness); S(strikethrough_thickness); S(strikethrough_position); @@ -140,7 +141,45 @@ modify_font(PyObject *mf, Options *opts) { #undef S } -static MouseShape +static inline void +free_font_features(Options *opts) { + if (opts->font_features.entries) { + for (size_t i = 0; i < opts->font_features.num; i++) { + free((void*)opts->font_features.entries[i].psname); + free((void*)opts->font_features.entries[i].features); + } + free(opts->font_features.entries); + } + memset(&opts->font_features, 0, sizeof(opts->font_features)); +} + +static inline void +font_features(PyObject *mf, Options *opts) { + free_font_features(opts); + opts->font_features.num = PyDict_GET_SIZE(mf); + if (!opts->font_features.num) return; + opts->font_features.entries = calloc(opts->font_features.num, sizeof(opts->font_features.entries[0])); + if (!opts->font_features.entries) { PyErr_NoMemory(); return; } + PyObject *key, *value; + Py_ssize_t pos = 0, i = 0; + while (PyDict_Next(mf, &pos, &key, &value)) { + __typeof__(opts->font_features.entries) e = opts->font_features.entries + i++; + Py_ssize_t psname_sz; const char *psname = PyUnicode_AsUTF8AndSize(key, &psname_sz); + e->psname = strndup(psname, psname_sz); + if (!e->psname) { PyErr_NoMemory(); return; } + e->num = PyTuple_GET_SIZE(value); + if (e->num) { + e->features = calloc(e->num, sizeof(e->features[0])); + if (!e->features) { PyErr_NoMemory(); return; } + for (size_t n = 0; n < e->num; n++) { + ParsedFontFeature *f = (ParsedFontFeature*)PyTuple_GET_ITEM(value, n); + e->features[n] = f->feature; + } + } + } +} + +static inline MouseShape pointer_shape(PyObject *shape_name) { const char *name = PyUnicode_AsUTF8(shape_name); if (!name) return TEXT_POINTER; @@ -181,7 +220,7 @@ pointer_shape(PyObject *shape_name) { return TEXT_POINTER; } -static int +static inline int macos_colorspace(PyObject *csname) { if (PyUnicode_CompareWithASCIIString(csname, "srgb") == 0) return 1; if (PyUnicode_CompareWithASCIIString(csname, "displayp3") == 0) return 2; @@ -198,7 +237,7 @@ free_url_prefixes(Options *opts) { } } -static void +static inline void url_prefixes(PyObject *up, Options *opts) { if (!PyTuple_Check(up)) { PyErr_SetString(PyExc_TypeError, "url_prefixes must be a tuple"); return; } free_url_prefixes(opts); @@ -233,7 +272,7 @@ free_menu_map(Options *opts) { opts->global_menu.count = 0; } -static void +static inline void menu_map(PyObject *entry_dict, Options *opts) { if (!PyDict_Check(entry_dict)) { PyErr_SetString(PyExc_TypeError, "menu_map entries must be a dict"); return; } free_menu_map(opts); @@ -261,7 +300,7 @@ menu_map(PyObject *entry_dict, Options *opts) { } } -static void +static inline void text_composition_strategy(PyObject *val, Options *opts) { if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_rendering_strategy must be a string"); return; } opts->text_old_gamma = false; @@ -305,30 +344,30 @@ list_of_chars(PyObject *chars) { return ans; } -static void +static inline void url_excluded_characters(PyObject *chars, Options *opts) { free(opts->url_excluded_characters); opts->url_excluded_characters = list_of_chars(chars); } -static void +static inline void select_by_word_characters(PyObject *chars, Options *opts) { free(opts->select_by_word_characters); opts->select_by_word_characters = list_of_chars(chars); } -static void +static inline void select_by_word_characters_forward(PyObject *chars, Options *opts) { free(opts->select_by_word_characters_forward); opts->select_by_word_characters_forward = list_of_chars(chars); } -static void +static inline void tab_bar_style(PyObject *val, Options *opts) { opts->tab_bar_hidden = PyUnicode_CompareWithASCIIString(val, "hidden") == 0 ? true: false; } -static void +static inline void tab_bar_margin_height(PyObject *val, Options *opts) { if (!PyTuple_Check(val) || PyTuple_GET_SIZE(val) != 2) { PyErr_SetString(PyExc_TypeError, "tab_bar_margin_height is not a 2-item tuple"); @@ -344,16 +383,17 @@ window_logo_scale(PyObject *src, Options *opts) { opts->window_logo_scale.height = PyFloat_AsFloat(PyTuple_GET_ITEM(src, 1)); } -static void +static inline void resize_debounce_time(PyObject *src, Options *opts) { opts->resize_debounce_time.on_end = s_double_to_monotonic_t(PyFloat_AsDouble(PyTuple_GET_ITEM(src, 0))); opts->resize_debounce_time.on_pause = s_double_to_monotonic_t(PyFloat_AsDouble(PyTuple_GET_ITEM(src, 1))); } -static void +static inline void free_allocs_in_options(Options *opts) { free_menu_map(opts); free_url_prefixes(opts); + free_font_features(opts); #define F(x) free(opts->x); opts->x = NULL; F(select_by_word_characters); F(url_excluded_characters); F(select_by_word_characters_forward); F(background_image); F(bell_path); F(bell_theme); F(default_window_logo); diff --git a/kitty/options/types.py b/kitty/options/types.py index 01ead072f..20b8ac9f3 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -488,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: 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=()) + bold_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto') + bold_italic_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto') 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 @@ -520,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: FontSpec = FontSpec(family='', style='', postscript_name='', full_name='', system='monospace', axes=()) + font_family: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='monospace', axes=(), variable_name=None, features=(), created_from_string='monospace') font_size: float = 11.0 force_ltr: bool = False foreground: Color = Color(221, 221, 221) @@ -534,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: FontSpec = FontSpec(family='', style='', postscript_name='', full_name='', system='auto', axes=()) + italic_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto') kitty_mod: int = 5 linux_bell_theme: str = '__custom' linux_display_server: choices_for_linux_display_server = 'auto' @@ -631,7 +631,7 @@ class Options: action_alias: typing.Dict[str, str] = {} env: typing.Dict[str, str] = {} exe_search_path: typing.Dict[str, str] = {} - font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {} + font_features: typing.Dict[str, typing.Tuple[kitty.fast_data_types.ParsedFontFeature, ...]] = {} kitten_alias: typing.Dict[str, str] = {} menu_map: typing.Dict[typing.Tuple[str, ...], str] = {} modify_font: typing.Dict[str, kitty.fonts.FontModification] = {} diff --git a/kitty/options/utils.py b/kitty/options/utils.py index b61291252..f7de683f9 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, FontSpec, ModificationType, ModificationUnit, ModificationValue +from kitty.fonts import 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 @@ -879,7 +879,7 @@ def clear_all_shortcuts(val: str, dict_with_parse_results: Optional[Dict[str, An return ans -def font_features(val: str) -> Iterable[Tuple[str, Tuple[FontFeature, ...]]]: +def font_features(val: str) -> Iterable[Tuple[str, Tuple[defines.ParsedFontFeature, ...]]]: if val == 'none': return parts = val.split() @@ -890,11 +890,9 @@ def font_features(val: str) -> Iterable[Tuple[str, Tuple[FontFeature, ...]]]: features = [] for feat in parts[1:]: try: - parsed = defines.parse_font_feature(feat) + features.append(defines.ParsedFontFeature(feat)) except ValueError: log_error(f'Ignoring invalid font feature: {feat}') - else: - features.append(FontFeature(feat, parsed)) yield parts[0], tuple(features) diff --git a/kitty/state.h b/kitty/state.h index a539a88e8..d8db29da3 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -9,6 +9,10 @@ #include "screen.h" #include "monotonic.h" #include "window_logo.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#pragma GCC diagnostic pop #define OPT(name) global_state.opts.name #define debug_rendering(...) if (global_state.debug_rendering) { timed_debug_print(__VA_ARGS__); } @@ -106,6 +110,14 @@ typedef struct { unsigned long wayland_titlebar_color; struct { struct MenuItem *entries; size_t count; } global_menu; bool wayland_enable_ime; + struct { + size_t num; + struct { + const char *psname; + size_t num; + hb_feature_t *features; + } *entries; + } font_features; } Options; typedef struct WindowLogoRenderData {