1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213 |
- #!/usr/bin/env python
- # License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
- import array
- import os
- import tempfile
- import unittest
- from collections.abc import Iterable
- from functools import lru_cache, partial
- from itertools import repeat
- from math import ceil
- from kitty.constants import is_macos, read_kitty_resource
- from kitty.fast_data_types import (
- DECAWM,
- ParsedFontFeature,
- get_fallback_font,
- set_allow_use_of_box_fonts,
- sprite_idx_to_pos,
- sprite_map_set_layout,
- sprite_map_set_limits,
- test_render_line,
- test_sprite_position_increment,
- wcwidth,
- )
- from kitty.fonts import family_name_to_key
- from kitty.fonts.common import FontSpec, all_fonts_map, face_from_descriptor, get_font_files, get_named_style, spec_for_face
- from kitty.fonts.render import coalesce_symbol_maps, create_face, render_string, setup_for_testing, shape_string
- from kitty.options.types import Options
- from . import BaseTest, draw_multicell
- def parse_font_spec(spec):
- return FontSpec.from_setting(spec)
- @lru_cache(maxsize=64)
- def testing_font_data(name):
- return read_kitty_resource(name, __name__.rpartition('.')[0])
- class Selection(BaseTest):
- def test_font_selection(self):
- self.set_options({'font_features': {'LiberationMono': (ParsedFontFeature('-dlig'),)}})
- opts = Options()
- fonts_map = all_fonts_map(True)
- names = set(fonts_map['family_map']) | set(fonts_map['variable_map'])
- del fonts_map
- def s(family: str, *expected: str, alternate=None) -> None:
- opts.font_family = parse_font_spec(family)
- ff = get_font_files(opts)
- actual = tuple(face_from_descriptor(ff[x]).postscript_name() for x in ('medium', 'bold', 'italic', 'bi')) # type: ignore
- del ff
- for x in actual:
- if '/' in x: # Old FreeType failed to generate postscript name for a variable font probably
- return
- with self.subTest(spec=family):
- try:
- self.ae(expected, actual)
- except AssertionError:
- if alternate:
- self.ae(alternate, actual)
- else:
- raise
- def both(family: str, *expected: str, alternate=None) -> None:
- for family in (family, f'family="{family}"'):
- s(family, *expected, alternate=alternate)
- def has(family, allow_missing_in_ci=False):
- ans = family_name_to_key(family) in names
- if self.is_ci and not allow_missing_in_ci and not ans:
- raise AssertionError(f'The family: {family} is not available')
- return ans
- def t(family, psprefix, bold='Bold', italic='Italic', bi='', reg='Regular', allow_missing_in_ci=False, alternate=None):
- if has(family, allow_missing_in_ci=allow_missing_in_ci):
- bi = bi or bold + italic
- if reg:
- reg = '-' + reg
- both(family, f'{psprefix}{reg}', f'{psprefix}-{bold}', f'{psprefix}-{italic}', f'{psprefix}-{bi}', alternate=alternate)
- t('Source Code Pro', 'SourceCodePro', 'Semibold', 'It')
- t('sourcecodeVf', 'SourceCodeVF', 'Semibold')
- # The Arch ttf-fira-code package excludes the variable fonts for some reason
- t('fira code', 'FiraCodeRoman', 'SemiBold', 'Regular', 'SemiBold', alternate=(
- 'FiraCode-Regular', 'FiraCode-SemiBold', 'FiraCode-Retina', 'FiraCode-SemiBold'))
- t('hack', 'Hack')
- # some ubuntu systems (such as the build VM) have only the regular and
- # bold faces of DejaVu Sans Mono installed.
- # t('DejaVu Sans Mono', 'DejaVuSansMono', reg='', italic='Oblique')
- t('ubuntu mono', 'UbuntuMono')
- t('liberation mono', 'LiberationMono', reg='')
- t('ibm plex mono', 'IBMPlexMono', 'SmBld', reg='')
- t('iosevka fixed', 'Iosevka-Fixed', 'Semibold', reg='', bi='Semibold-Italic', allow_missing_in_ci=True)
- t('iosevka term', 'Iosevka-Term', 'Semibold', reg='', bi='Semibold-Italic', allow_missing_in_ci=True)
- t('fantasque sans mono', 'FantasqueSansMono')
- t('jetbrains mono', 'JetBrainsMono', 'SemiBold')
- t('consolas', 'Consolas', reg='', allow_missing_in_ci=True)
- if has('cascadia code'):
- if is_macos:
- both('cascadia code', 'CascadiaCode-Regular', 'CascadiaCode-Regular_SemiBold', 'CascadiaCode-Italic', 'CascadiaCode-Italic_SemiBold-Italic')
- else:
- both('cascadia code', 'CascadiaCodeRoman-Regular', 'CascadiaCodeRoman-SemiBold', 'CascadiaCode-Italic', 'CascadiaCode-SemiBoldItalic')
- if has('cascadia mono'):
- if is_macos:
- both('cascadia mono', 'CascadiaMono-Regular', 'CascadiaMono-Regular_SemiBold', 'CascadiaMono-Italic', 'CascadiaMono-Italic_SemiBold-Italic')
- else:
- both('cascadia mono', 'CascadiaMonoRoman-Regular', 'CascadiaMonoRoman-SemiBold', 'CascadiaMono-Italic', 'CascadiaMono-SemiBoldItalic')
- if has('operator mono', allow_missing_in_ci=True):
- both('operator mono', 'OperatorMono-Medium', 'OperatorMono-Bold', 'OperatorMono-MediumItalic', 'OperatorMono-BoldItalic')
- # Test variable font selection
- if has('SourceCodeVF'):
- opts = Options()
- opts.font_family = parse_font_spec('family="SourceCodeVF" variable_name="SourceCodeUpright" style="Bold"')
- ff = get_font_files(opts)
- face = face_from_descriptor(ff['medium'])
- self.ae(get_named_style(face)['name'], 'Bold')
- face = face_from_descriptor(ff['italic'])
- self.ae(get_named_style(face)['name'], 'Bold Italic')
- face = face_from_descriptor(ff['bold'])
- self.ae(get_named_style(face)['name'], 'Black')
- face = face_from_descriptor(ff['bi'])
- self.ae(get_named_style(face)['name'], 'Black Italic')
- opts.font_family = parse_font_spec('family=SourceCodeVF variable_name=SourceCodeUpright wght=470')
- opts.italic_font = parse_font_spec('family=SourceCodeVF variable_name=SourceCodeItalic style=Black')
- ff = get_font_files(opts)
- self.assertFalse(get_named_style(ff['medium']))
- self.ae(get_named_style(ff['italic'])['name'], 'Black Italic')
- if has('cascadia code'):
- opts = Options()
- opts.font_family = parse_font_spec('family="cascadia code"')
- opts.italic_font = parse_font_spec('family="cascadia code" variable_name= style="Light Italic"')
- ff = get_font_files(opts)
- def t(x, **kw):
- if 'spec' in kw:
- fs = FontSpec.from_setting('family="Cascadia Code" ' + kw['spec'])._replace(created_from_string='')
- else:
- kw['family'] = 'Cascadia Code'
- fs = FontSpec(**kw)
- face = face_from_descriptor(ff[x])
- self.ae(fs.as_setting, spec_for_face('Cascadia Code', face).as_setting)
- t('medium', variable_name='CascadiaCodeRoman', style='Regular')
- t('italic', variable_name='', style='Light Italic')
- opts = Options()
- opts.font_family = parse_font_spec('family="cascadia code" variable_name=CascadiaCodeRoman wght=455')
- opts.italic_font = parse_font_spec('family="cascadia code" variable_name= wght=405')
- opts.bold_font = parse_font_spec('family="cascadia code" variable_name=CascadiaCodeRoman wght=603')
- ff = get_font_files(opts)
- t('medium', spec='variable_name=CascadiaCodeRoman wght=455')
- t('italic', spec='variable_name= wght=405')
- t('bold', spec='variable_name=CascadiaCodeRoman wght=603')
- t('bi', spec='variable_name= wght=603')
- # Test font features
- if has('liberation mono'):
- opts = Options()
- opts.font_family = parse_font_spec('family="liberation mono"')
- ff = get_font_files(opts)
- self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': '-dlig'})
- self.ae(face_from_descriptor(ff['bold']).applied_features(), {})
- opts.font_family = parse_font_spec('family="liberation mono" features="dlig test=3"')
- ff = get_font_files(opts)
- self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
- self.ae(face_from_descriptor(ff['bold']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
- def block_helpers(s, sprites, cell_width, cell_height):
- block_size = cell_width * cell_height * 4
- def full_block():
- return b'\xff' * block_size
- def empty_block():
- return b'\0' * block_size
- def half_block(first=b'\xff', second=b'\0', swap=False):
- frac = 0.5
- height = ceil(frac * cell_height)
- rest = cell_height - height
- if swap:
- height, rest = rest, height
- first, second = second, first
- return (first * (height * cell_width * 4)) + (second * rest * cell_width * 4)
- def quarter_block():
- frac = 0.5
- height = ceil(frac * cell_height)
- width = ceil(frac * cell_width)
- ans = array.array('I', b'\0' * block_size)
- for y in range(height):
- pos = cell_width * y
- for x in range(width):
- ans[pos + x] = 0xffffffff
- return ans.tobytes()
- def upper_half_block():
- return half_block()
- def lower_half_block():
- return half_block(swap=True)
- def block_as_str(a):
- pixels = array.array('I', a)
- def row(y):
- pos = y * cell_width
- return ' '.join(f'{int(pixels[pos + x] != 0)}' for x in range(cell_width))
- return '\n'.join(row(y) for y in range(cell_height))
- def assert_blocks(a, b, msg=''):
- if a != b:
- msg = msg or 'block not equal'
- if len(a) != len(b):
- assert_blocks.__msg = msg + f' block lengths not equal: {len(a)/4} != {len(b)/4}'
- else:
- assert_blocks.__msg = msg + '\n' + block_as_str(a) + '\n\n' + block_as_str(b)
- del a, b
- raise AssertionError(assert_blocks.__msg)
- def multiline_render(text, scale=1, width=1, **kw):
- s.reset()
- draw_multicell(s, text, scale=scale, width=width, **kw)
- ans = []
- for y in range(scale):
- line = s.line(y)
- test_render_line(line)
- for x in range(width * scale):
- ans.append(sprites[sprite_idx_to_pos(line.sprite_at(x), setup_for_testing.xnum, setup_for_testing.ynum)])
- return ans
- def block_test(*expected, **kw):
- mr = multiline_render(kw.pop('text', '█'), **kw)
- try:
- z = zip(expected, mr, strict=True)
- except TypeError:
- z = zip(expected, mr)
- for i, (expected, actual) in enumerate(z):
- assert_blocks(expected(), actual, f'Block {i} is not equal')
- return full_block, empty_block, upper_half_block, lower_half_block, quarter_block, block_as_str, block_test
- class FontBaseTest(BaseTest):
- font_size = 5.0
- dpi = 72.
- font_name = 'FiraCode-Medium.otf'
- def path_for_font(self, name):
- if name not in self.font_path_cache:
- with open(os.path.join(self.tdir, name), 'wb') as f:
- self.font_path_cache[name] = f.name
- f.write(testing_font_data(name))
- return self.font_path_cache[name]
- def setUp(self):
- super().setUp()
- self.font_path_cache = {}
- self.tdir = tempfile.mkdtemp()
- self.addCleanup(self.rmtree_ignoring_errors, self.tdir)
- path = self.path_for_font(self.font_name) if self.font_name else ''
- tc = setup_for_testing(size=self.font_size, dpi=self.dpi, main_face_path=path)
- self.sprites, self.cell_width, self.cell_height = tc.__enter__()
- self.addCleanup(tc.__exit__)
- self.assertEqual([k[0] for k in self.sprites], list(range(11)))
- def tearDown(self):
- del self.sprites, self.cell_width, self.cell_height
- self.font_path_cache = {}
- super().tearDown()
- class Rendering(FontBaseTest):
- def test_sprite_map(self):
- sprite_map_set_limits(10, 3)
- sprite_map_set_layout(5, 4) # 4 because of underline_exclusion row
- self.ae(test_sprite_position_increment(), (0, 0, 0))
- self.ae(test_sprite_position_increment(), (1, 0, 0))
- self.ae(test_sprite_position_increment(), (0, 1, 0))
- self.ae(test_sprite_position_increment(), (1, 1, 0))
- self.ae(test_sprite_position_increment(), (0, 0, 1))
- self.ae(test_sprite_position_increment(), (1, 0, 1))
- self.ae(test_sprite_position_increment(), (0, 1, 1))
- self.ae(test_sprite_position_increment(), (1, 1, 1))
- self.ae(test_sprite_position_increment(), (0, 0, 2))
- self.ae(test_sprite_position_increment(), (1, 0, 2))
- def test_box_drawing(self):
- s = self.create_screen(cols=len(box_chars) + 1, lines=1, scrollback=0)
- prerendered = len(self.sprites)
- s.draw(''.join(box_chars))
- line = s.line(0)
- test_render_line(line)
- self.assertEqual(len(self.sprites) - prerendered, len(box_chars))
- def test_scaled_box_drawing(self):
- self.scaled_drawing_test()
- def test_scaled_font_drawing(self):
- set_allow_use_of_box_fonts(False)
- try:
- self.scaled_drawing_test()
- finally:
- set_allow_use_of_box_fonts(True)
- def scaled_drawing_test(self):
- s = self.create_screen(cols=8, lines=8, scrollback=0)
- full_block, empty_block, upper_half_block, lower_half_block, quarter_block, block_as_str, block_test = block_helpers(
- s, self.sprites, self.cell_width, self.cell_height)
- block_test(full_block)
- block_test(full_block, full_block, full_block, full_block, scale=2)
- block_test(full_block, empty_block, empty_block, empty_block, scale=2, subscale_n=1, subscale_d=2)
- block_test(full_block, full_block, empty_block, empty_block, scale=2, subscale_n=1, subscale_d=2, text='██')
- block_test(empty_block, empty_block, full_block, empty_block, scale=2, subscale_n=1, subscale_d=2, vertical_align=1)
- block_test(quarter_block, scale=1, subscale_n=1, subscale_d=2)
- block_test(upper_half_block, scale=1, subscale_n=1, subscale_d=2, text='██')
- block_test(lower_half_block, scale=1, subscale_n=1, subscale_d=2, text='██', vertical_align=1)
- def test_font_rendering(self):
- render_string('ab\u0347\u0305你好|\U0001F601|\U0001F64f|\U0001F63a|')
- text = 'He\u0347\u0305llo\u0341, w\u0302or\u0306l\u0354d!'
- # macOS has no fonts capable of rendering combining chars
- if is_macos:
- text = text.encode('ascii', 'ignore').decode('ascii')
- cells = render_string(text)[-1]
- self.ae(len(cells), len(text.encode('ascii', 'ignore')))
- text = '你好,世界'
- sz = sum(map(lambda x: wcwidth(ord(x)), text))
- cells = render_string(text)[-1]
- self.ae(len(cells), sz)
- @unittest.skipIf(is_macos, 'COLRv1 is only supported on Linux')
- def test_rendering_colrv1(self):
- f = create_face(self.path_for_font('twemoji_smiley-cff2_colr_1.otf'))
- f.set_size(64, 96, 96)
- for char in '😁😇😈':
- _, w, h = f.render_codepoint(ord(char))
- self.assertGreater(w, 64)
- self.assertGreater(h, 64)
- def test_shaping(self):
- def ss(text, font=None):
- path = self.path_for_font(font) if font else None
- return shape_string(text, path=path)
- def groups(text, font=None):
- return [x[:2] for x in ss(text, font)]
- for font in ('FiraCode-Medium.otf', 'CascadiaCode-Regular.otf', 'iosevka-regular.ttf'):
- g = partial(groups, font=font)
- self.ae(g('abcd'), [(1, 1) for i in range(4)])
- self.ae(g('A===B!=C'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)])
- self.ae(g('A=>>B!=C'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)])
- if 'iosevka' in font:
- self.ae(g('--->'), [(4, 4)])
- self.ae(g('-' * 12 + '>'), [(13, 13)])
- self.ae(g('<~~~'), [(4, 4)])
- self.ae(g('a<~~~b'), [(1, 1), (4, 4), (1, 1)])
- else:
- self.ae(g('----'), [(4, 4)])
- self.ae(g('F--a--'), [(1, 1), (2, 2), (1, 1), (2, 2)])
- self.ae(g('===--<>=='), [(3, 3), (2, 2), (2, 2), (2, 2)])
- self.ae(g('==!=<>==<><><>'), [(4, 4), (2, 2), (2, 2), (2, 2), (2, 2), (2, 2)])
- self.ae(g('-' * 18), [(18, 18)])
- self.ae(g('a>\u2060<b'), [(1, 1), (1, 2), (1, 1), (1, 1)])
- colon_glyph = ss('9:30', font='FiraCode-Medium.otf')[1][2]
- self.assertNotEqual(colon_glyph, ss(':', font='FiraCode-Medium.otf')[0][2])
- self.ae(colon_glyph, 1031)
- self.ae(groups('9:30', font='FiraCode-Medium.otf'), [(1, 1), (1, 1), (1, 1), (1, 1)])
- self.ae(groups('|\U0001F601|\U0001F64f|\U0001F63a|'), [(1, 1), (2, 1), (1, 1), (2, 1), (1, 1), (2, 1), (1, 1)])
- self.ae(groups('He\u0347\u0305llo\u0337,', font='LiberationMono-Regular.ttf'),
- [(1, 1), (1, 3), (1, 1), (1, 1), (1, 2), (1, 1)])
- self.ae(groups('i\u0332\u0308', font='LiberationMono-Regular.ttf'), [(1, 2)])
- self.ae(groups('u\u0332 u\u0332\u0301', font='LiberationMono-Regular.ttf'), [(1, 2), (1, 1), (1, 2)])
- def test_emoji_presentation(self):
- s = self.create_screen()
- s.draw('\u2716\u2716\ufe0f')
- self.ae((s.cursor.x, s.cursor.y), (3, 0))
- s.draw('\u2716\u2716')
- self.ae((s.cursor.x, s.cursor.y), (5, 0))
- s.draw('\ufe0f')
- self.ae((s.cursor.x, s.cursor.y), (2, 1))
- self.ae(str(s.line(0)), '\u2716\u2716\ufe0f\u2716')
- self.ae(str(s.line(1)), '\u2716\ufe0f')
- s.draw('\u2716' * 3)
- self.ae((s.cursor.x, s.cursor.y), (5, 1))
- self.ae(str(s.line(1)), '\u2716\ufe0f\u2716\u2716\u2716')
- self.ae((s.cursor.x, s.cursor.y), (5, 1))
- s.reset_mode(DECAWM)
- s.draw('\ufe0f')
- s.set_mode(DECAWM)
- self.ae((s.cursor.x, s.cursor.y), (5, 1))
- self.ae(str(s.line(1)), '\u2716\ufe0f\u2716\u2716\ufe0f')
- s.cursor.y = s.lines - 1
- s.draw('\u2716' * s.columns)
- self.ae((s.cursor.x, s.cursor.y), (5, 4))
- s.draw('\ufe0f')
- self.ae((s.cursor.x, s.cursor.y), (2, 4))
- self.ae(str(s.line(s.cursor.y)), '\u2716\ufe0f')
- @unittest.skipUnless(is_macos, 'Only macOS has a Last Resort font')
- def test_fallback_font_not_last_resort(self):
- # Ensure that the LastResort font is not reported as a fallback font on
- # macOS. See https://github.com/kovidgoyal/kitty/issues/799
- with self.assertRaises(ValueError, msg='No fallback font found'):
- get_fallback_font('\U0010FFFF', False, False)
- def test_coalesce_symbol_maps(self):
- q = {(2, 3): 'a', (4, 6): 'b', (5, 5): 'b', (7, 7): 'b', (9, 9): 'b', (1, 1): 'a'}
- self.ae(coalesce_symbol_maps(q), {(1, 3): 'a', (4, 7): 'b', (9, 9): 'b'})
- q = {(1, 4): 'a', (2, 3): 'b'}
- self.ae(coalesce_symbol_maps(q), {(1, 1): 'a', (2, 3): 'b', (4, 4): 'a'})
- q = {(2, 3): 'b', (1, 4): 'a'}
- self.ae(coalesce_symbol_maps(q), {(1, 4): 'a'})
- q = {(1, 4): 'a', (2, 5): 'b'}
- self.ae(coalesce_symbol_maps(q), {(1, 1): 'a', (2, 5): 'b'})
- q = {(2, 5): 'b', (1, 4): 'a'}
- self.ae(coalesce_symbol_maps(q), {(1, 4): 'a', (5, 5): 'b'})
- q = {(1, 4): 'a', (2, 5): 'a'}
- self.ae(coalesce_symbol_maps(q), {(1, 5): 'a'})
- q = {(1, 4): 'a', (4, 5): 'b'}
- self.ae(coalesce_symbol_maps(q), {(1, 3): 'a', (4, 5): 'b'})
- q = {(4, 5): 'b', (1, 4): 'a'}
- self.ae(coalesce_symbol_maps(q), {(1, 4): 'a', (5, 5): 'b'})
- q = {(0, 30): 'a', (10, 10): 'b', (11, 11): 'b', (2, 2): 'c', (1, 1): 'c'}
- self.ae(coalesce_symbol_maps(q), {
- (0, 0): 'a', (1, 2): 'c', (3, 9): 'a', (10, 11): 'b', (12, 30): 'a'})
- def test_chars(chars: str = '╌', sz: int = 128) -> None:
- # kitty +runpy "from kitty.fonts.box_drawing import test_chars; test_chars('XXX')"
- from kitty.fast_data_types import concat_cells, render_box_char, set_send_sprite_to_gpu
- from kitty.fonts.render import display_bitmap, setup_for_testing
- if not chars:
- import sys
- chars = sys.argv[-1]
- def as_ord(x: str) -> int:
- if x.lower().startswith('u+'):
- return int(x[2:], 16)
- return ord(x)
- if '...' in chars:
- start, end = chars.partition('...')[::2]
- chars = ''.join(map(chr, range(as_ord(start), as_ord(end)+1)))
- with setup_for_testing('monospace', sz) as (_, width, height):
- try:
- for ch in chars:
- nb = render_box_char(as_ord(ch), width, height)
- rgb_data = concat_cells(width, height, False, (nb,))
- display_bitmap(rgb_data, width, height)
- print()
- finally:
- set_send_sprite_to_gpu(None)
- def test_drawing(sz: int = 48, family: str = 'monospace', start: int = 0x2500, num_rows: int = 10, num_cols: int = 16) -> None:
- from kitty.fast_data_types import concat_cells, render_box_char, set_send_sprite_to_gpu
- from .render import display_bitmap, setup_for_testing
- with setup_for_testing(family, sz) as (_, width, height):
- space = bytearray(width * height)
- def join_cells(cells: Iterable[bytes]) -> bytes:
- cells = tuple(bytes(x) for x in cells)
- return concat_cells(width, height, False, cells)
- def render_chr(ch: str) -> bytearray:
- if ch in box_chars:
- return bytearray(render_box_char(ord(ch), width, height))
- return space
- pos = start
- rows = []
- space_row = join_cells(repeat(space, 32))
- try:
- for r in range(num_rows):
- row = []
- for i in range(num_cols):
- row.append(render_chr(chr(pos)))
- row.append(space)
- pos += 1
- rows.append(join_cells(row))
- rows.append(space_row)
- rgb_data = b''.join(rows)
- width *= 32
- height *= len(rows)
- assert len(rgb_data) == width * height * 4, f'{len(rgb_data)} != {width * height * 4}'
- display_bitmap(rgb_data, width, height)
- finally:
- set_send_sprite_to_gpu(None)
- box_chars = { # {{{
- '─',
- '━',
- '│',
- '┃',
- '┄',
- '┅',
- '┆',
- '┇',
- '┈',
- '┉',
- '┊',
- '┋',
- '┌',
- '┍',
- '┎',
- '┏',
- '┐',
- '┑',
- '┒',
- '┓',
- '└',
- '┕',
- '┖',
- '┗',
- '┘',
- '┙',
- '┚',
- '┛',
- '├',
- '┝',
- '┞',
- '┟',
- '┠',
- '┡',
- '┢',
- '┣',
- '┤',
- '┥',
- '┦',
- '┧',
- '┨',
- '┩',
- '┪',
- '┫',
- '┬',
- '┭',
- '┮',
- '┯',
- '┰',
- '┱',
- '┲',
- '┳',
- '┴',
- '┵',
- '┶',
- '┷',
- '┸',
- '┹',
- '┺',
- '┻',
- '┼',
- '┽',
- '┾',
- '┿',
- '╀',
- '╁',
- '╂',
- '╃',
- '╄',
- '╅',
- '╆',
- '╇',
- '╈',
- '╉',
- '╊',
- '╋',
- '╌',
- '╍',
- '╎',
- '╏',
- '═',
- '║',
- '╒',
- '╓',
- '╔',
- '╕',
- '╖',
- '╗',
- '╘',
- '╙',
- '╚',
- '╛',
- '╜',
- '╝',
- '╞',
- '╟',
- '╠',
- '╡',
- '╢',
- '╣',
- '╤',
- '╥',
- '╦',
- '╧',
- '╨',
- '╩',
- '╪',
- '╫',
- '╬',
- '╭',
- '╮',
- '╯',
- '╰',
- '╱',
- '╲',
- '╳',
- '╴',
- '╵',
- '╶',
- '╷',
- '╸',
- '╹',
- '╺',
- '╻',
- '╼',
- '╽',
- '╾',
- '╿',
- '▀',
- '▁',
- '▂',
- '▃',
- '▄',
- '▅',
- '▆',
- '▇',
- '█',
- '▉',
- '▊',
- '▋',
- '▌',
- '▍',
- '▎',
- '▏',
- '▐',
- '░',
- '▒',
- '▓',
- '▔',
- '▕',
- '▖',
- '▗',
- '▘',
- '▙',
- '▚',
- '▛',
- '▜',
- '▝',
- '▞',
- '▟',
- '◉',
- '○',
- '●',
- '◖',
- '◗',
- '◜',
- '◝',
- '◞',
- '◟',
- '◠',
- '◡',
- '◢',
- '◣',
- '◤',
- '◥',
- '⠀',
- '⠁',
- '⠂',
- '⠃',
- '⠄',
- '⠅',
- '⠆',
- '⠇',
- '⠈',
- '⠉',
- '⠊',
- '⠋',
- '⠌',
- '⠍',
- '⠎',
- '⠏',
- '⠐',
- '⠑',
- '⠒',
- '⠓',
- '⠔',
- '⠕',
- '⠖',
- '⠗',
- '⠘',
- '⠙',
- '⠚',
- '⠛',
- '⠜',
- '⠝',
- '⠞',
- '⠟',
- '⠠',
- '⠡',
- '⠢',
- '⠣',
- '⠤',
- '⠥',
- '⠦',
- '⠧',
- '⠨',
- '⠩',
- '⠪',
- '⠫',
- '⠬',
- '⠭',
- '⠮',
- '⠯',
- '⠰',
- '⠱',
- '⠲',
- '⠳',
- '⠴',
- '⠵',
- '⠶',
- '⠷',
- '⠸',
- '⠹',
- '⠺',
- '⠻',
- '⠼',
- '⠽',
- '⠾',
- '⠿',
- '⡀',
- '⡁',
- '⡂',
- '⡃',
- '⡄',
- '⡅',
- '⡆',
- '⡇',
- '⡈',
- '⡉',
- '⡊',
- '⡋',
- '⡌',
- '⡍',
- '⡎',
- '⡏',
- '⡐',
- '⡑',
- '⡒',
- '⡓',
- '⡔',
- '⡕',
- '⡖',
- '⡗',
- '⡘',
- '⡙',
- '⡚',
- '⡛',
- '⡜',
- '⡝',
- '⡞',
- '⡟',
- '⡠',
- '⡡',
- '⡢',
- '⡣',
- '⡤',
- '⡥',
- '⡦',
- '⡧',
- '⡨',
- '⡩',
- '⡪',
- '⡫',
- '⡬',
- '⡭',
- '⡮',
- '⡯',
- '⡰',
- '⡱',
- '⡲',
- '⡳',
- '⡴',
- '⡵',
- '⡶',
- '⡷',
- '⡸',
- '⡹',
- '⡺',
- '⡻',
- '⡼',
- '⡽',
- '⡾',
- '⡿',
- '⢀',
- '⢁',
- '⢂',
- '⢃',
- '⢄',
- '⢅',
- '⢆',
- '⢇',
- '⢈',
- '⢉',
- '⢊',
- '⢋',
- '⢌',
- '⢍',
- '⢎',
- '⢏',
- '⢐',
- '⢑',
- '⢒',
- '⢓',
- '⢔',
- '⢕',
- '⢖',
- '⢗',
- '⢘',
- '⢙',
- '⢚',
- '⢛',
- '⢜',
- '⢝',
- '⢞',
- '⢟',
- '⢠',
- '⢡',
- '⢢',
- '⢣',
- '⢤',
- '⢥',
- '⢦',
- '⢧',
- '⢨',
- '⢩',
- '⢪',
- '⢫',
- '⢬',
- '⢭',
- '⢮',
- '⢯',
- '⢰',
- '⢱',
- '⢲',
- '⢳',
- '⢴',
- '⢵',
- '⢶',
- '⢷',
- '⢸',
- '⢹',
- '⢺',
- '⢻',
- '⢼',
- '⢽',
- '⢾',
- '⢿',
- '⣀',
- '⣁',
- '⣂',
- '⣃',
- '⣄',
- '⣅',
- '⣆',
- '⣇',
- '⣈',
- '⣉',
- '⣊',
- '⣋',
- '⣌',
- '⣍',
- '⣎',
- '⣏',
- '⣐',
- '⣑',
- '⣒',
- '⣓',
- '⣔',
- '⣕',
- '⣖',
- '⣗',
- '⣘',
- '⣙',
- '⣚',
- '⣛',
- '⣜',
- '⣝',
- '⣞',
- '⣟',
- '⣠',
- '⣡',
- '⣢',
- '⣣',
- '⣤',
- '⣥',
- '⣦',
- '⣧',
- '⣨',
- '⣩',
- '⣪',
- '⣫',
- '⣬',
- '⣭',
- '⣮',
- '⣯',
- '⣰',
- '⣱',
- '⣲',
- '⣳',
- '⣴',
- '⣵',
- '⣶',
- '⣷',
- '⣸',
- '⣹',
- '⣺',
- '⣻',
- '⣼',
- '⣽',
- '⣾',
- '⣿',
- '\ue0b0',
- '\ue0b1',
- '\ue0b2',
- '\ue0b3',
- '\ue0b4',
- '\ue0b5',
- '\ue0b6',
- '\ue0b7',
- '\ue0b8',
- '\ue0b9',
- '\ue0ba',
- '\ue0bb',
- '\ue0bc',
- '\ue0bd',
- '\ue0be',
- '\ue0bf',
- '\ue0d6',
- '\ue0d7',
- '\uee00',
- '\uee01',
- '\uee02',
- '\uee03',
- '\uee04',
- '\uee05',
- '\uee06',
- '\uee07',
- '\uee08',
- '\uee09',
- '\uee0a',
- '\uee0b',
- '\uf5d0',
- '\uf5d1',
- '\uf5d2',
- '\uf5d3',
- '\uf5d4',
- '\uf5d5',
- '\uf5d6',
- '\uf5d7',
- '\uf5d8',
- '\uf5d9',
- '\uf5da',
- '\uf5db',
- '\uf5dc',
- '\uf5dd',
- '\uf5de',
- '\uf5df',
- '\uf5e0',
- '\uf5e1',
- '\uf5e2',
- '\uf5e3',
- '\uf5e4',
- '\uf5e5',
- '\uf5e6',
- '\uf5e7',
- '\uf5e8',
- '\uf5e9',
- '\uf5ea',
- '\uf5eb',
- '\uf5ec',
- '\uf5ed',
- '\uf5ee',
- '\uf5ef',
- '\uf5f0',
- '\uf5f1',
- '\uf5f2',
- '\uf5f3',
- '\uf5f4',
- '\uf5f5',
- '\uf5f6',
- '\uf5f7',
- '\uf5f8',
- '\uf5f9',
- '\uf5fa',
- '\uf5fb',
- '\uf5fc',
- '\uf5fd',
- '\uf5fe',
- '\uf5ff',
- '\uf600',
- '\uf601',
- '\uf602',
- '\uf603',
- '\uf604',
- '\uf605',
- '\uf606',
- '\uf607',
- '\uf608',
- '\uf609',
- '\uf60a',
- '\uf60b',
- '\uf60c',
- '\uf60d',
- '🬀',
- '🬁',
- '🬂',
- '🬃',
- '🬄',
- '🬅',
- '🬆',
- '🬇',
- '🬈',
- '🬉',
- '🬊',
- '🬋',
- '🬌',
- '🬍',
- '🬎',
- '🬏',
- '🬐',
- '🬑',
- '🬒',
- '🬓',
- '🬔',
- '🬕',
- '🬖',
- '🬗',
- '🬘',
- '🬙',
- '🬚',
- '🬛',
- '🬜',
- '🬝',
- '🬞',
- '🬟',
- '🬠',
- '🬡',
- '🬢',
- '🬣',
- '🬤',
- '🬥',
- '🬦',
- '🬧',
- '🬨',
- '🬩',
- '🬪',
- '🬫',
- '🬬',
- '🬭',
- '🬮',
- '🬯',
- '🬰',
- '🬱',
- '🬲',
- '🬳',
- '🬴',
- '🬵',
- '🬶',
- '🬷',
- '🬸',
- '🬹',
- '🬺',
- '🬻',
- '🬼',
- '🬽',
- '🬾',
- '🬿',
- '🭀',
- '🭁',
- '🭂',
- '🭃',
- '🭄',
- '🭅',
- '🭆',
- '🭇',
- '🭈',
- '🭉',
- '🭊',
- '🭋',
- '🭌',
- '🭍',
- '🭎',
- '🭏',
- '🭐',
- '🭑',
- '🭒',
- '🭓',
- '🭔',
- '🭕',
- '🭖',
- '🭗',
- '🭘',
- '🭙',
- '🭚',
- '🭛',
- '🭜',
- '🭝',
- '🭞',
- '🭟',
- '🭠',
- '🭡',
- '🭢',
- '🭣',
- '🭤',
- '🭥',
- '🭦',
- '🭧',
- '🭨',
- '🭩',
- '🭪',
- '🭫',
- '🭬',
- '🭭',
- '🭮',
- '🭯',
- '🭰',
- '🭱',
- '🭲',
- '🭳',
- '🭴',
- '🭵',
- '🭶',
- '🭷',
- '🭸',
- '🭹',
- '🭺',
- '🭻',
- '🭼',
- '🭽',
- '🭾',
- '🭿',
- '🮀',
- '🮁',
- '🮂',
- '🮃',
- '🮄',
- '🮅',
- '🮆',
- '🮇',
- '🮈',
- '🮉',
- '🮊',
- '🮋',
- '🮌',
- '🮍',
- '🮎',
- '🮏',
- '🮐',
- '🮑',
- '🮒',
- '\U0001fb93',
- '🮔',
- '🮕',
- '🮖',
- '🮗',
- '🮘',
- '🮙',
- '🮚',
- '🮛',
- '🮜',
- '🮝',
- '🮞',
- '🮟',
- '🮠',
- '🮡',
- '🮢',
- '🮣',
- '🮤',
- '🮥',
- '🮦',
- '🮧',
- '🮨',
- '🮩',
- '🮪',
- '🮫',
- '🮬',
- '🮭',
- '🮮',
- '\U0001fbe6', '\U0001fbe7',
- } # }}}
- for ch in range(0x1cd00, 0x1cde5+1): # octants
- box_chars.add(chr(ch))
|