Implement symbol maps

A config option to use special fonts for specified unicode characters.
Useful for things like Powerline without needing patched fonts.
This commit is contained in:
Kovid Goyal
2017-02-09 21:15:53 +05:30
parent a4715de5dc
commit 6c6f000229
5 changed files with 130 additions and 42 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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))

View File

@@ -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):

View File

@@ -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+<code point
# in hexadecimal>. 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