diff --git a/kitty/config.py b/kitty/config.py index ea5a046d3..fce0137f0 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -80,6 +80,38 @@ def parse_key(val, keymap): keymap[(mods, key)] = action +def parse_symbol_map(val): + parts = val.split(' ') + symbol_map = {} + + def abort(): + safe_print('Symbol map: {} is invalid, ignoring'.format(val), file=sys.stderr) + return {} + + if len(parts) < 2: + return abort() + family = ' '.join(parts[1:]) + + def to_chr(x): + if not x.startswith('U+'): + raise ValueError() + x = int(x[2:], 16) + return x + + for x in parts[0].split(','): + a, b = x.partition('-')[::2] + b = b or a + try: + a, b = map(to_chr, (a, b)) + except Exception: + return abort() + if b < a or max(a, b) > sys.maxunicode or min(a, b) < 1: + return abort() + for y in range(a, b+1): + symbol_map[chr(y)] = family + return symbol_map + + def to_open_url_modifiers(val): return parse_mods(val.split('+')) @@ -125,7 +157,7 @@ for a in ('active', 'inactive'): def parse_config(lines): - ans = {'keymap': {}} + ans = {'keymap': {}, 'symbol_map': {}} for line in lines: line = line.strip() if not line or line.startswith('#'): @@ -136,6 +168,9 @@ def parse_config(lines): if key == 'map': parse_key(val, ans['keymap']) continue + if key == 'symbol_map': + ans['symbol_map'].update(parse_symbol_map(val)) + continue tm = type_map.get(key) if tm is not None: val = tm(val) diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index 10c9cc0be..90c0d257a 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -7,9 +7,18 @@ from kitty.fast_data_types import CTFace as Face from kitty.utils import get_logical_dpi, wcwidth, ceil_int main_font = {} +symbol_map = {} cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None +def install_symbol_map(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())} + for ch, family in val.items(): + symbol_map[ch] = family_map[family] + + def set_font_family(opts, ignore_dpi_failure=False): global cell_width, cell_height, baseline, CellTexture, WideCellTexture, underline_thickness, underline_position try: @@ -32,6 +41,7 @@ def set_font_family(opts, ignore_dpi_failure=False): for bold in (False, True): for italic in (False, True): main_font[(bold, italic)] = Face(get_family(bold, italic), bold, italic, True, opts.font_size, dpi) + install_symbol_map(opts.symbol_map, opts.font_size, dpi) mf = main_font[(False, False)] cell_width, cell_height = mf.cell_size() CellTexture = ctypes.c_ubyte * (cell_width * cell_height) @@ -57,8 +67,9 @@ def split(buf, cell_width, cell_height): def render_cell(text=' ', bold=False, italic=False): - width = wcwidth(text[0]) - face = main_font[(bold, italic)] + ch = text[0] + width = wcwidth(ch) + face = symbol_map.get(ch) or main_font[(bold, italic)] if width == 2: buf, width = WideCellTexture(), cell_width * 2 else: diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index 0ca1d2613..165bb5a50 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -162,3 +162,8 @@ def get_font_files(opts): do('bold'), do('italic'), do('bi') return ans + + +def font_for_family(family): + ans = get_font_information(family) + return ans._replace(face=Face(ans.face, ans.index)) diff --git a/kitty/fonts/freetype.py b/kitty/fonts/freetype.py index 4b15798d1..a1cae4d6e 100644 --- a/kitty/fonts/freetype.py +++ b/kitty/fonts/freetype.py @@ -7,17 +7,21 @@ import sys import unicodedata from collections import namedtuple from functools import lru_cache +from itertools import chain from threading import Lock from kitty.fast_data_types import FT_PIXEL_MODE_GRAY, Face from kitty.fonts.box_drawing import render_missing_glyph from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth -from .fontconfig import find_font_for_character, get_font_files, FontNotFound +from .fontconfig import ( + FontNotFound, find_font_for_character, font_for_family, get_font_files +) current_font_family = current_font_family_name = cff_size = cell_width = cell_height = baseline = None CharTexture = underline_position = underline_thickness = None alt_face_cache = {} +symbol_map = {} def set_char_size(face, width=0, height=0, hres=0, vres=0): @@ -55,6 +59,14 @@ def font_for_char(char, bold=False, italic=False, allow_bitmaped_fonts=False): ) +def install_symbol_map(val): + global symbol_map + symbol_map = {} + family_map = {f: font_for_family(f) for f in set(val.values())} + for ch, family in val.items(): + symbol_map[ch] = family_map[family] + + def font_units_to_pixels(x, units_per_em, size_in_pts, dpi): return ceil_int(x * ((size_in_pts * dpi) / (72 * units_per_em))) @@ -73,7 +85,8 @@ def set_font_family(opts): 'hres': int(dpi[0]), 'vres': int(dpi[1]) } - for fobj in current_font_family.values(): + install_symbol_map(opts.symbol_map) + for fobj in chain(current_font_family.values(), symbol_map.values()): set_char_size(fobj.face, **cff_size) face = current_font_family['regular'].face cell_width = calc_cell_width(current_font_family['regular'], face) @@ -114,51 +127,60 @@ def render_to_bitmap(font, face, text): return bitmap +def render_using_face(font, face, text, width, italic, bold): + bitmap = render_to_bitmap(font, face, text) + if width == 1 and bitmap.width > cell_width: + extra = bitmap.width - cell_width + if italic and extra < cell_width // 2: + bitmap = face.trim_to_width(bitmap, cell_width) + elif extra > max(2, 0.3 * cell_width) and face.is_scalable: + # rescale the font size so that the glyph is visible in a single + # cell and hope somebody updates libc's wcwidth + sz = cff_size.copy() + sz['width'] = int(sz['width'] * cell_width / bitmap.width) + # Preserve aspect ratio + sz['height'] = int(sz['height'] * cell_width / bitmap.width) + try: + set_char_size(face, **sz) + bitmap = render_to_bitmap(font, face, text) + finally: + set_char_size(face, **cff_size) + m = face.glyph_metrics() + return CharBitmap( + bitmap.buffer, + ceil_int(abs(m.horiBearingX) / 64), + ceil_int(abs(m.horiBearingY) / 64), + ceil_int(m.horiAdvance / 64), bitmap.rows, bitmap.width + ) + + def render_char(text, bold=False, italic=False, width=1): key = 'regular' if bold: key = 'bi' if italic else 'bold' elif italic: key = 'italic' + ch = text[0] with freetype_lock: - font = current_font_family.get(key) or current_font_family['regular'] - face = font.face - if not face.get_char_index(text[0]): - try: - font = font_for_char(text[0], bold, italic) - except FontNotFound: - font = font_for_char( - text[0], bold, italic, allow_bitmaped_fonts=True - ) - face = alt_face_cache.get(font) - if face is None: - face = alt_face_cache[font] = Face(font.face, font.index) - if face.is_scalable: - set_char_size(face, **cff_size) - bitmap = render_to_bitmap(font, face, text) - if width == 1 and bitmap.width > cell_width: - extra = bitmap.width - cell_width - if italic and extra < cell_width // 2: - bitmap = face.trim_to_width(bitmap, cell_width) - elif extra > max(2, 0.3 * cell_width) and face.is_scalable: - # rescale the font size so that the glyph is visible in a single - # cell and hope somebody updates libc's wcwidth - sz = cff_size.copy() - sz['width'] = int(sz['width'] * cell_width / bitmap.width) - # Preserve aspect ratio - sz['height'] = int(sz['height'] * cell_width / bitmap.width) + font = symbol_map.get(ch) + if font is None or not font.face.get_char_index(ch): + font = current_font_family.get(key) or current_font_family['regular'] + face = font.face + if not face.get_char_index(ch): try: - set_char_size(face, **sz) - bitmap = render_to_bitmap(font, face, text) - finally: - set_char_size(face, **cff_size) - m = face.glyph_metrics() - return CharBitmap( - bitmap.buffer, - ceil_int(abs(m.horiBearingX) / 64), - ceil_int(abs(m.horiBearingY) / 64), - ceil_int(m.horiAdvance / 64), bitmap.rows, bitmap.width - ) + font = font_for_char(ch, bold, italic) + except FontNotFound: + font = font_for_char( + ch, bold, italic, allow_bitmaped_fonts=True + ) + face = alt_face_cache.get(font) + if face is None: + face = alt_face_cache[font] = Face(font.face, font.index) + if face.is_scalable: + set_char_size(face, **cff_size) + else: + face = font.face + return render_using_face(font, face, text, width, italic, bold) def place_char_in_cell(bitmap_char): diff --git a/kitty/kitty.conf b/kitty/kitty.conf index 8a8574721..fae39e739 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -207,3 +207,18 @@ map ctrl+shift+q close_tab map ctrl+shift+l next_layout map ctrl+shift+. move_tab_forward map ctrl+shift+, move_tab_backward + + +# Symbol mapping (special font for specified unicode code points). Map the +# specified unicode codepoints to a particular font. Useful if you need special +# rendering for some symbols, such as for Powerline. Avoids the need for +# patched fonts. Each unicode code point is specified in the form U+. You can specify multiple code points, separated by commas +# and ranges separated by hyphens. symbol_map itself can be specified multiple times. +# Syntax is: +# +# symbol_map codepoints Font Family Name +# +# For example: +# +# symbol_map U+E0A0-U+E0A2,U+E0B0-U+E0B3 PowerlineSymbols