diff --git a/kitty/core_text.m b/kitty/core_text.m index 85148eba6..00f8e4e9e 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -22,18 +22,19 @@ typedef struct { unsigned int units_per_em; float ascent, descent, leading, underline_position, underline_thickness, point_sz, scaled_point_sz; CTFontRef font; - PyObject *family_name, *full_name, *postscript_name; + PyObject *family_name, *full_name, *postscript_name, *path; } Face; -static PyObject* -convert_cfstring(CFStringRef src) { +static inline PyObject* +convert_cfstring(CFStringRef src, int free_src) { #define SZ 2048 static char buf[SZ+2] = {0}; - const char *p = CFStringGetCStringPtr(src, kCFStringEncodingUTF8); - if (p != NULL) return PyUnicode_FromString(buf); - if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) { PyErr_SetString(PyExc_ValueError, "Failed to convert CFString"); return NULL; } - return PyUnicode_FromString(buf); + PyObject *ans = NULL; + if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) PyErr_SetString(PyExc_ValueError, "Failed to convert CFString"); + else ans = PyUnicode_FromString(buf); + if (free_src) CFRelease(src); + return ans; #undef SZ } @@ -70,6 +71,34 @@ font_descriptor_to_python(CTFontDescriptorRef descriptor) { return ans; } +static CTFontDescriptorRef +font_descriptor_from_python(PyObject *src) { + CTFontSymbolicTraits symbolic_traits = 0; + NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; + PyObject *t = PyDict_GetItemString(src, "traits"); + if (t == NULL) { + symbolic_traits = ( + (PyDict_GetItemString(src, "bold") == Py_True) ? kCTFontBoldTrait : 0 | + (PyDict_GetItemString(src, "italic") == Py_True) ? kCTFontItalicTrait : 0 | + (PyDict_GetItemString(src, "monospace") == Py_True) ? kCTFontMonoSpaceTrait : 0); + } else { + symbolic_traits = PyLong_AsUnsignedLong(t); + } + NSDictionary *traits = @{(id)kCTFontSymbolicTrait:[NSNumber numberWithUnsignedInt:symbolic_traits]}; + attrs[(id)kCTFontTraitsAttribute] = traits; + +#define SET(x, attr) \ + t = PyDict_GetItemString(src, #x); \ + if (t) attrs[(id)attr] = [NSString stringWithUTF8String:PyUnicode_AsUTF8(t)]; + + SET(family, kCTFontFamilyNameAttribute); + SET(style, kCTFontStyleNameAttribute); + SET(postscript_name, kCTFontNameAttribute); +#undef SET + + return CTFontDescriptorCreateWithAttributes((CFDictionaryRef) attrs); +} + PyObject* coretext_all_fonts(PyObject UNUSED *_self) { static CTFontCollectionRef collection = NULL; @@ -89,23 +118,17 @@ coretext_all_fonts(PyObject UNUSED *_self) { static PyObject* new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { Face *self; - int bold, italic, monospace; - char *cfamily; + PyObject *descriptor; float point_sz, dpi; - if(!PyArg_ParseTuple(args, "spppff", &cfamily, &bold, &italic, &monospace, &point_sz, &dpi)) return NULL; - NSString *family = [[NSString alloc] initWithCString:cfamily encoding:NSUTF8StringEncoding]; - if (family == NULL) return PyErr_NoMemory(); + if(!PyArg_ParseTuple(args, "Off", &descriptor, &point_sz, &dpi)) return NULL; self = (Face *)type->tp_alloc(type, 0); if (self) { - CTFontSymbolicTraits symbolic_traits = (bold ? kCTFontBoldTrait : 0) | (italic ? kCTFontItalicTrait : 0) | (monospace ? kCTFontMonoSpaceTrait : 0); - NSDictionary *font_traits = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:symbolic_traits] forKey:(NSString *)kCTFontSymbolicTrait]; - NSDictionary *font_attributes = [NSDictionary dictionaryWithObjectsAndKeys:family, kCTFontFamilyNameAttribute, font_traits, kCTFontTraitsAttribute, nil]; - CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)font_attributes); - if (descriptor) { + CTFontDescriptorRef desc = font_descriptor_from_python(descriptor); + if (desc) { self->point_sz = point_sz; self->scaled_point_sz = (dpi / 72.0) * point_sz; - self->font = CTFontCreateWithFontDescriptor(descriptor, self->scaled_point_sz, NULL); - CFRelease(descriptor); + self->font = CTFontCreateWithFontDescriptor(desc, self->scaled_point_sz, NULL); + CFRelease(desc); if (!self->font) { Py_CLEAR(self); PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); } else { self->units_per_em = CTFontGetUnitsPerEm(self->font); @@ -115,17 +138,19 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { self->underline_position = CTFontGetUnderlinePosition(self->font); self->underline_thickness = CTFontGetUnderlineThickness(self->font); self->scaled_point_sz = CTFontGetSize(self->font); - self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font)); - self->full_name = convert_cfstring(CTFontCopyFullName(self->font)); - self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font)); - if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL) { Py_CLEAR(self); } + self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font), 1); + self->full_name = convert_cfstring(CTFontCopyFullName(self->font), 1); + self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font), 1); + NSURL *url = (NSURL*)CTFontCopyAttribute(self->font, kCTFontURLAttribute); + self->path = PyUnicode_FromString([[url path] UTF8String]); + [url release]; + if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); } } } else { Py_CLEAR(self); PyErr_NoMemory(); } } - [ family release ]; return (PyObject*)self; } @@ -133,7 +158,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { static void dealloc(Face* self) { if (self->font) CFRelease(self->font); - Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name); + Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name); Py_CLEAR(self->path); Py_TYPE(self)->tp_free((PyObject*)self); } @@ -248,8 +273,8 @@ repr(Face *self) { snprintf(buf, sizeof(buf)/sizeof(buf[0]), "ascent=%.1f, descent=%.1f, leading=%.1f, point_sz=%.1f, scaled_point_sz=%.1f, underline_position=%.1f underline_thickness=%.1f", (self->ascent), (self->descent), (self->leading), (self->point_sz), (self->scaled_point_sz), (self->underline_position), (self->underline_thickness)); return PyUnicode_FromFormat( - "Face(family=%U, full_name=%U, postscript_name=%U, units_per_em=%u, %s)", - self->family_name, self->full_name, self->postscript_name, self->units_per_em, buf + "Face(family=%U, full_name=%U, postscript_name=%U, path=%U, units_per_em=%u, %s)", + self->family_name, self->full_name, self->postscript_name, self->path, self->units_per_em, buf ); } @@ -267,6 +292,7 @@ static PyMemberDef members[] = { MEM(underline_position, T_FLOAT), MEM(underline_thickness, T_FLOAT), MEM(family_name, T_OBJECT), + MEM(path, T_OBJECT), MEM(full_name, T_OBJECT), MEM(postscript_name, T_OBJECT), {NULL} /* Sentinel */ diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index 8932a72a3..7cc58775b 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -3,18 +3,67 @@ # License: GPL v3 Copyright: 2017, Kovid Goyal import ctypes -from kitty.fast_data_types import CTFace as Face -from kitty.utils import get_logical_dpi, wcwidth, ceil_int +import re + +from kitty.fast_data_types import CTFace as Face, coretext_all_fonts +from kitty.utils import ceil_int, get_logical_dpi, wcwidth main_font = {} symbol_map = {} cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None +attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'} -def install_symbol_map(val, font_size, dpi): +def create_font_map(all_fonts): + ans = {'family_map': {}, 'ps_map': {}, 'full_map': {}} + for x in all_fonts: + f = (x['family'] or '').lower() + s = (x['style'] or '').lower() + ps = (x['postscript_name'] or '').lower() + ans['family_map'].setdefault(f, []).append(x) + ans['ps_map'].setdefault(ps, []).append(x) + ans['full_map'].setdefault(f + ' ' + s, []).append(x) + return ans + + +def find_best_match(font_map, family, bold, italic): + q = re.sub(r'\s+', ' ', family.lower()) + + def matches(candidate, monospace): + return candidate['bold'] == bold and candidate['italic'] == italic and candidate['monospace'] == monospace + + for monospace in (True, False): + for selector in 'ps_map full_map family_map'.split(): + candidates = font_map[selector].get(q, ()) + if candidates: + for candidate in candidates: + if matches(candidate, monospace): + return candidate + if selector != 'family_map': + return candidates[0] + + +def get_face(font_map, family, main_family, font_size, dpi, bold=False, italic=False): + def resolve_family(f): + if f.lower() == 'monospace': + f = 'Menlo' + if (bold or italic) and f == 'auto': + f = main_family + return f + family = resolve_family(family) + descriptor = find_best_match(font_map, family, bold, italic) or { + 'monospace': True, + 'bold': bold, + 'italic': italic, + 'family': family + } + return Face(descriptor, font_size, dpi) + + +def install_symbol_map(all_fonts, val, font_size, dpi): global symbol_map symbol_map = {} - family_map = {f: Face(f, False, False, False, font_size, dpi) for f in set(val.values())} + family_map = {f: get_face(all_fonts, f, 'Menlo', font_size, dpi) for f in set(val.values())} for ch, family in val.items(): symbol_map[ch] = family_map[family] @@ -28,7 +77,11 @@ def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False): raise dpi = (72, 72) # Happens when running via develop() in an ssh session dpi = sum(dpi) / 2.0 - attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'} + font_size = override_font_size or opts.font_size + all_fonts = create_font_map(coretext_all_fonts()) + + for (bold, italic), attr in attr_map.items(): + main_font[(bold, italic)] = get_face(all_fonts, getattr(opts, attr), opts.font_family, font_size, dpi, bold, italic) def get_family(bold, italic): ans = getattr(opts, attr_map[(bold, italic)]) @@ -37,12 +90,8 @@ def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False): if ans == 'auto' and (bold or italic): ans = get_family(False, False) return ans - font_size = override_font_size or opts.font_size - for bold in (False, True): - for italic in (False, True): - main_font[(bold, italic)] = Face(get_family(bold, italic), bold, italic, True, font_size, dpi) - install_symbol_map(opts.symbol_map, font_size, dpi) + install_symbol_map(all_fonts, opts.symbol_map, font_size, dpi) mf = main_font[(False, False)] cell_width, cell_height = mf.cell_size() CellTexture = ctypes.c_ubyte * (cell_width * cell_height) @@ -53,6 +102,12 @@ def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False): return cell_width, cell_height +def test_font_matching(name='Menlo', bold=False, italic=False, dpi=72.0, font_size=11.0): + all_fonts = create_font_map(coretext_all_fonts()) + face = get_face(all_fonts, name, 'Menlo', font_size, dpi, bold, italic) + return face + + def current_cell(): return CellTexture, cell_width, cell_height, baseline, underline_thickness, underline_position diff --git a/kitty/kitty.conf b/kitty/kitty.conf index 516d0608d..61f79ea06 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -7,6 +7,12 @@ # font_family Operator Mono Book # bold_font Operator Mono Thick # bold_italic_font Operator Mono Medium +# or +# font_family SF Mono Medium +# bold_font SF Mono Semibold +# bold_italic_font SF Mono Semibold +# Note that you should use the full family name but do not add Bold or Italic qualifiers +# to the name. font_family monospace italic_font auto bold_font auto