From 1db613e95b2ee03f3e42783cea68d4bf6c9aba7e Mon Sep 17 00:00:00 2001 From: Fredrick Brennan Date: Sat, 4 Jan 2020 14:36:20 +0800 Subject: [PATCH 1/4] Add `font_feature_settings` Close #2247 --- kitty/config.py | 15 ++++++++++++++- kitty/config_data.py | 42 ++++++++++++++++++++++++++++++++++++++++++ kitty/fonts.c | 34 +++++++++++++++++++++++++++------- kitty/fonts/render.py | 2 +- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/kitty/config.py b/kitty/config.py index fdac97d4b..131265ca6 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -410,6 +410,19 @@ def handle_symbol_map(key, val, ans): ans['symbol_map'].update(parse_symbol_map(val)) +@special_handler +def handle_font_feature_settings(key, val, ans): + parts = val.split(':', maxsplit=2) + if len(parts) != 2: + if parts[0] == "none": + return + else: + log_error("Ignoring invalid font_feature_settings for font {}".format(parts[0])) + else: + font, features = parts + ans['font_feature_settings'].update({font.strip(): features.strip().split()}) + + @special_handler def handle_kitten_alias(key, val, ans): parts = val.split(maxsplit=2) @@ -495,7 +508,7 @@ def option_names_for_completion(): def parse_config(lines, check_keys=True, accumulate_bad_lines=None): - ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [], 'env': {}, 'kitten_aliases': {}} + ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [], 'env': {}, 'kitten_aliases': {}, 'font_feature_settings': {}} parse_config_base( lines, defaults, diff --git a/kitty/config_data.py b/kitty/config_data.py index 721bef543..c792d31cd 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -276,6 +276,48 @@ or by defining shortcuts for it in kitty.conf, for example:: map alt+1 disable_ligatures_in active always map alt+2 disable_ligatures_in all never map alt+3 disable_ligatures_in tab cursor + +Note: If font_feature_settings is enabled, this feature has no effect. +''')) + +o('font_feature_settings', 'none', long_text=_(''' +Choose exactly which OpenType features to enable or disable. This is useful as +some fonts might have many features worthwhile in a terminal—for example, Fira +Code Retina includes a discretionary feature, :code:`zero`, which in that font +changes the appearance of the zero (0), to make it more easily distinguishable +from Ø. Fira Code Retina also includes other discretionary features known as +Stylistic Sets which have the tags :code:`ss01` through :code:`ss20`. + +Note that this code is indexed by PostScript name, and not TTF name or font +family; this allows you to define very precise feature settings; e.g. you can +disable a feature in the italic font but not in the regular font. + +Note that this feature ignores the value of :code:`disable_ligatures`, because +it assumes you want to fine tune exactly which OpenType tags are +enabled/disabled. See examples below. + +To get the PostScript name for a font, ask Fontconfig for it, using your family +name: + + $ fc-match "Fira Code" postscriptname + :postscriptname=FiraCode-Regular + $ fc-match "Fira Code Retina" postscriptname + :postscriptname=FiraCode-Retina + $ fc-match "TT2020Base:style=italic" postscriptname + :postscriptname=TT2020Base-Italic + +Enable alternate zero and oldstyle numerals: + + font_feature_settings FiraCode-Retina: +zero +onum + +Enable only alternate zero: + + font_feature_settings FiraCode-Retina: +zero + +Disable the normal ligatures, but keep the :code:`calt` feature which (in this +font) breaks up monotony: + + font_feature_settings TT2020StyleB-Regular: -liga +calt ''')) diff --git a/kitty/fonts.c b/kitty/fonts.c index bb2f01f4e..917a48eb1 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -62,6 +62,7 @@ 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; @@ -71,16 +72,16 @@ typedef struct { static SymbolMap *symbol_maps = NULL; static size_t num_symbol_maps = 0; - - typedef struct { PyObject *face; // Map glyphs to sprite map co-ords SpritePosition sprite_map[1024]; hb_feature_t hb_features[8]; + hb_feature_t* ffs_hb_features; size_t num_hb_features; + size_t num_ffs_hb_features; SpecialGlyphCache special_glyph_cache[SPECIAL_GLYPH_CACHE_SIZE]; - bool bold, italic, emoji_presentation; + bool bold, italic, emoji_presentation, ffs_set; } Font; typedef struct { @@ -363,6 +364,22 @@ init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentati copy_hb_feature(f, LIGA_FEATURE); copy_hb_feature(f, DLIG_FEATURE); } copy_hb_feature(f, CALT_FEATURE); + if (font_feature_settings != NULL){ + const char* face = postscript_name_for_face(f->face); + + PyObject* o = PyDict_GetItemString(font_feature_settings, face); + if (o != NULL) { + long len = PyList_Size(o); + hb_feature_t* hb_feat = calloc(len, sizeof(hb_feature_t)); + for (long i = len-1; i >= 0; i--) { + PyObject* item = PyList_GetItem(o, i); + const char* feat = PyUnicode_AsUTF8(item); + hb_feature_from_string(feat, -1, &hb_feat[i]); + } + f->ffs_hb_features = hb_feat; + f->num_ffs_hb_features = len; + } + } return true; } @@ -776,7 +793,10 @@ shape(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, hb group_state.last_gpu_cell = first_gpu_cell + (num_cells ? num_cells - 1 : 0); load_hb_buffer(first_cpu_cell, first_gpu_cell, num_cells); - hb_shape(font, harfbuzz_buffer, fobj->hb_features, fobj->num_hb_features - (disable_ligature ? 0 : 1)); + if (fobj->num_ffs_hb_features > 0) + hb_shape(font, harfbuzz_buffer, fobj->ffs_hb_features, fobj->num_ffs_hb_features); + else + hb_shape(font, harfbuzz_buffer, fobj->hb_features, fobj->num_hb_features - (disable_ligature ? 0 : 1)); unsigned int info_length, positions_length; group_state.info = hb_buffer_get_glyph_infos(harfbuzz_buffer, &info_length); @@ -1184,11 +1204,11 @@ static PyObject* set_font_data(PyObject UNUSED *m, PyObject *args) { PyObject *sm; Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); - if (!PyArg_ParseTuple(args, "OOOIIIIO!d", + 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, &global_state.font_sz_in_pts)) return NULL; - Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); + &PyTuple_Type, &sm, &global_state.font_sz_in_pts, &font_feature_settings)) return NULL; + Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings); free_font_groups(); clear_symbol_maps(); num_symbol_maps = PyTuple_GET_SIZE(sm); diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index d8f463672..6b0527346 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -103,7 +103,7 @@ def set_font_family(opts=None, override_font_size=None, debug_font_matching=Fals set_font_data( render_box_drawing, prerender_function, descriptor_for_idx, indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts, - sm, sz + sm, sz, opts['font_feature_settings'] ) From b479ea410d1cba9a4027997df56f2a6e3b9a85a6 Mon Sep 17 00:00:00 2001 From: Fredrick Brennan Date: Sat, 4 Jan 2020 15:34:39 +0800 Subject: [PATCH 2/4] Prevent leaks Thanks @martinetd --- kitty/fonts.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kitty/fonts.c b/kitty/fonts.c index 917a48eb1..95bb9472e 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -1203,7 +1203,7 @@ DescriptorIndices descriptor_indices = {0}; static PyObject* set_font_data(PyObject UNUSED *m, PyObject *args) { PyObject *sm; - Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); + Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings); 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, @@ -1310,6 +1310,7 @@ finalize(void) { Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); + Py_CLEAR(font_feature_settings); free_font_groups(); if (harfbuzz_buffer) { hb_buffer_destroy(harfbuzz_buffer); harfbuzz_buffer = NULL; } free(group_state.groups); group_state.groups = NULL; group_state.groups_capacity = 0; From dbd0dab154c26823fc742edb80e36ecc9506fa51 Mon Sep 17 00:00:00 2001 From: Fredrick Brennan Date: Sat, 4 Jan 2020 15:37:05 +0800 Subject: [PATCH 3/4] Revert addition of ffs_set to Font struct It was from an earlier stage of development and is not needed --- kitty/fonts.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kitty/fonts.c b/kitty/fonts.c index 95bb9472e..b91125ad6 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -81,7 +81,7 @@ typedef struct { size_t num_hb_features; size_t num_ffs_hb_features; SpecialGlyphCache special_glyph_cache[SPECIAL_GLYPH_CACHE_SIZE]; - bool bold, italic, emoji_presentation, ffs_set; + bool bold, italic, emoji_presentation; } Font; typedef struct { From d250555cd0d3b0dfef8ba83bb1500cab4908c67b Mon Sep 17 00:00:00 2001 From: Fredrick Brennan Date: Sat, 4 Jan 2020 20:11:34 +0800 Subject: [PATCH 4/4] Make `font_feature_settings` respect `disable_ligatures` --- kitty/config_data.py | 15 ++++++--------- kitty/fonts.c | 24 +++++++++++++----------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/kitty/config_data.py b/kitty/config_data.py index c792d31cd..48d6df8eb 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -277,7 +277,8 @@ or by defining shortcuts for it in kitty.conf, for example:: map alt+2 disable_ligatures_in all never map alt+3 disable_ligatures_in tab cursor -Note: If font_feature_settings is enabled, this feature has no effect. +Note: This function is equivalent to setting :code:`font_feature_settings` to +:code:`-liga -dlig -calt`. ''')) o('font_feature_settings', 'none', long_text=_(''' @@ -292,12 +293,8 @@ Note that this code is indexed by PostScript name, and not TTF name or font family; this allows you to define very precise feature settings; e.g. you can disable a feature in the italic font but not in the regular font. -Note that this feature ignores the value of :code:`disable_ligatures`, because -it assumes you want to fine tune exactly which OpenType tags are -enabled/disabled. See examples below. - To get the PostScript name for a font, ask Fontconfig for it, using your family -name: +name:: $ fc-match "Fira Code" postscriptname :postscriptname=FiraCode-Regular @@ -306,16 +303,16 @@ name: $ fc-match "TT2020Base:style=italic" postscriptname :postscriptname=TT2020Base-Italic -Enable alternate zero and oldstyle numerals: +Enable alternate zero and oldstyle numerals:: font_feature_settings FiraCode-Retina: +zero +onum -Enable only alternate zero: +Enable only alternate zero:: font_feature_settings FiraCode-Retina: +zero Disable the normal ligatures, but keep the :code:`calt` feature which (in this -font) breaks up monotony: +font) breaks up monotony:: font_feature_settings TT2020StyleB-Regular: -liga +calt ''')) diff --git a/kitty/fonts.c b/kitty/fonts.c index b91125ad6..9deffa669 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -360,24 +360,24 @@ init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentati f->bold = bold; f->italic = italic; f->emoji_presentation = emoji_presentation; f->num_hb_features = 0; const char *psname = postscript_name_for_face(face); - if (strstr(psname, "NimbusMonoPS-") == psname) { - copy_hb_feature(f, LIGA_FEATURE); copy_hb_feature(f, DLIG_FEATURE); - } + copy_hb_feature(f, LIGA_FEATURE); + copy_hb_feature(f, DLIG_FEATURE); copy_hb_feature(f, CALT_FEATURE); if (font_feature_settings != NULL){ - const char* face = postscript_name_for_face(f->face); - - PyObject* o = PyDict_GetItemString(font_feature_settings, face); + PyObject* o = PyDict_GetItemString(font_feature_settings, psname); if (o != NULL) { long len = PyList_Size(o); - hb_feature_t* hb_feat = calloc(len, sizeof(hb_feature_t)); + if (len==0) return true; + f->num_ffs_hb_features = len + f->num_hb_features; + hb_feature_t* hb_feat = calloc(f->num_ffs_hb_features, sizeof(hb_feature_t)); for (long i = len-1; i >= 0; i--) { PyObject* item = PyList_GetItem(o, i); const char* feat = PyUnicode_AsUTF8(item); hb_feature_from_string(feat, -1, &hb_feat[i]); } + for (size_t i = 0; i < f->num_hb_features; i++) + hb_feat[len+i] = hb_features[i]; f->ffs_hb_features = hb_feat; - f->num_ffs_hb_features = len; } } return true; @@ -386,6 +386,8 @@ init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentati static inline void del_font(Font *f) { Py_CLEAR(f->face); + if (f->num_ffs_hb_features > 0) + free(f->ffs_hb_features); free_maps(f); f->bold = false; f->italic = false; } @@ -794,9 +796,9 @@ shape(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, hb load_hb_buffer(first_cpu_cell, first_gpu_cell, num_cells); if (fobj->num_ffs_hb_features > 0) - hb_shape(font, harfbuzz_buffer, fobj->ffs_hb_features, fobj->num_ffs_hb_features); + hb_shape(font, harfbuzz_buffer, fobj->ffs_hb_features, fobj->num_ffs_hb_features - (disable_ligature ? 0 : fobj->num_hb_features)); else - hb_shape(font, harfbuzz_buffer, fobj->hb_features, fobj->num_hb_features - (disable_ligature ? 0 : 1)); + hb_shape(font, harfbuzz_buffer, fobj->hb_features, fobj->num_hb_features - (disable_ligature ? 0 : fobj->num_hb_features)); unsigned int info_length, positions_length; group_state.info = hb_buffer_get_glyph_infos(harfbuzz_buffer, &info_length); @@ -1079,7 +1081,7 @@ render_run(FontGroup *fg, CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, inde default: shape_run(first_cpu_cell, first_gpu_cell, num_cells, &fg->fonts[font_idx], disable_ligature_strategy == DISABLE_LIGATURES_ALWAYS); if (pua_space_ligature) merge_groups_for_pua_space_ligature(); - else if (cursor_offset > -1) { + else if (cursor_offset > -1) { // false if DISABLE_LIGATURES_NEVER index_type left, right; split_run_at_offset(cursor_offset, &left, &right); if (right > left) {