mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-06-29 12:21:58 +00:00
The coverage threshold (0.5 infbd4e5ad1, lowered to 0.3 ind4106ef2dto get CI green) is environment-dependent: a correctly sized emoji covers ~0.84 of its cell on one box but ~0.39 in CI, while the buggy render is ~0.28, so the margin is thin and font/cell dependent. Render the same font two ways at one size instead: via its fontconfig descriptor (which carries the size-fixup matrix) and via its file path (which does not). They must come out the same size; the bug shrinks the descriptor one. Only the matrix differs, so the check no longer depends on the environment or the emoji artwork.
1315 lines
34 KiB
Python
1315 lines
34 KiB
Python
#!/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 test_synthetic_italic_matrix(self):
|
||
# A roman-only font that find_best_match finds (e.g. Fira Code, which ships
|
||
# no italic face) must get fontconfig's synthetic-italic FC_MATRIX
|
||
# (90-synthetic.conf) attached, so its italic renders slanted rather than
|
||
# upright; real-italic faces must not. The shear value is fontconfig's, not
|
||
# ours, so assert the invariant (a non-identity matrix is present), not the
|
||
# exact tuple, for cross-config stability.
|
||
if is_macos:
|
||
self.skipTest('synthetic-italic FC_MATRIX is a fontconfig feature')
|
||
from kitty.fonts.fontconfig import FC_MONO, fc_match
|
||
names = set(all_fonts_map(True)['family_map']) | set(all_fonts_map(True)['variable_map'])
|
||
if family_name_to_key('fira code') not in names:
|
||
self.skipTest('Fira Code not installed')
|
||
# Probe fc_match directly so we can tell "environment lacks the rule" (skip)
|
||
# from "code did not attach the matrix" (fail).
|
||
if fc_match('Fira Code', False, True, FC_MONO).get('matrix') is None:
|
||
self.skipTest('fontconfig 90-synthetic.conf not active; no synthetic-italic matrix')
|
||
opts = Options()
|
||
opts.font_family = parse_font_spec('Fira Code')
|
||
ff = get_font_files(opts)
|
||
self.assertIsNone(ff['medium'].get('matrix')) # upright stays upright
|
||
mi = ff['italic'].get('matrix')
|
||
self.assertIsNotNone(mi) # roman, no italic -> sheared
|
||
self.assertNotEqual(mi[1], 0.0) # actually slanted, not identity
|
||
# Faces are built from a size-specialized descriptor at render time; the
|
||
# matrix must survive specialize_font_descriptor or the glyphs render
|
||
# upright despite the descriptor above being correct.
|
||
from kitty.fast_data_types import specialize_font_descriptor
|
||
sd = specialize_font_descriptor(dict(ff['italic']), 12.0, 96.0, 96.0)
|
||
self.ae(sd.get('matrix'), mi)
|
||
if family_name_to_key('liberation mono') in names: # real-italic control
|
||
opts.font_family = parse_font_spec('Liberation Mono')
|
||
self.assertIsNone(get_font_files(opts)['italic'].get('matrix'))
|
||
|
||
|
||
def block_helpers(s, sprites, cell_width, cell_height):
|
||
mr = {}
|
||
actual = b''
|
||
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=''):
|
||
nonlocal mr, actual, full_block, half_block, quarter_block, block_test, empty_block
|
||
if a != b:
|
||
del mr, actual, full_block, half_block, quarter_block, block_test, empty_block
|
||
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):
|
||
nonlocal mr, actual
|
||
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} expected != actual')
|
||
|
||
|
||
return full_block, empty_block, upper_half_block, lower_half_block, quarter_block, block_as_str, block_test
|
||
|
||
|
||
class FontBaseTest(BaseTest):
|
||
|
||
font_size = 16.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(empty_block, full_block, empty_block, empty_block, scale=2, subscale_n=1, subscale_d=2, horizontal_align=1)
|
||
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_color_emoji_not_shrunk(self):
|
||
# Regression test for https://github.com/kovidgoyal/kitty/issues/10144.
|
||
# fontconfig gives fixed-size color faces (e.g. Noto Color Emoji) a
|
||
# pixel-size fixup encoded as FC_MATRIX. That scale must not reach the
|
||
# cairo font matrix used for color glyphs; applying it shrinks color emoji
|
||
# to a dot (ee937bdd1b). Render the same font two ways at the same size:
|
||
# from its fontconfig descriptor, which carries the fixup matrix, and from
|
||
# its file path, which does not. A correct build renders both at the same
|
||
# size; the bug shrinks the descriptor one. Comparing the two is
|
||
# environment-independent since only the matrix differs.
|
||
if is_macos:
|
||
self.skipTest('FC_MATRIX is a fontconfig feature, not used on macOS')
|
||
from kitty.fonts.fontconfig import fc_match
|
||
desc = dict(fc_match('emoji', False, False, 0))
|
||
if not (desc.get('color') and desc.get('matrix')):
|
||
self.skipTest('no fixed-size color emoji font with a fontconfig fixup matrix')
|
||
with_matrix = face_from_descriptor(desc)
|
||
with_matrix.set_size(64, 96, 96)
|
||
without_matrix = create_face(desc['path'])
|
||
without_matrix.set_size(64, 96, 96)
|
||
_, mw, mh = with_matrix.render_codepoint(0x1F40D)
|
||
_, rw, rh = without_matrix.render_codepoint(0x1F40D)
|
||
self.assertGreater(mh, 0.5 * rh, f'color emoji shrunk by FC_MATRIX: {mh}px vs {rh}px (#10144)')
|
||
|
||
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)])
|
||
self.ae(g('->'), [(2, 2)])
|
||
self.ae(g('<-'), [(2, 2)])
|
||
self.ae(g('==>'), [(3, 3)])
|
||
self.ae(g('<=='), [(3, 3)])
|
||
self.ae(g('a->b'), [(1, 1), (2, 2), (1, 1)])
|
||
self.ae(g('a<-b'), [(1, 1), (2, 2), (1, 1)])
|
||
self.ae(g('a==>b'), [(1, 1), (3, 3), (1, 1)])
|
||
self.ae(g('a<==b'), [(1, 1), (3, 3), (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('<==>'), [(4, 4)])
|
||
self.ae(g('<!--'), [(4, 4)])
|
||
self.ae(g('a<==>b'), [(1, 1), (4, 4), (1, 1)])
|
||
self.ae(g('a<!--b'), [(1, 1), (4, 4), (1, 1)])
|
||
self.ae(g('a>\u2060<b'), [(1, 1), (1, 2), (1, 1), (1, 1)])
|
||
comfy = partial(groups, font='ComfyCode-Regular.ttf')
|
||
comfy_cases = {
|
||
'->': ('a->b', 2),
|
||
'<-': ('a<-b', 2),
|
||
'==>': ('a==>b', 3),
|
||
'<==': ('a<==b', 3),
|
||
}
|
||
for text, (wrapped, ligature_width) in comfy_cases.items():
|
||
baseline = comfy(text)
|
||
self.assertIn(baseline, ([(ligature_width, ligature_width)], [(1, 1) for i in range(ligature_width)]))
|
||
if baseline == [(ligature_width, ligature_width)]:
|
||
self.ae(comfy(wrapped), [(1, 1), (ligature_width, ligature_width), (1, 1)])
|
||
else:
|
||
self.ae(comfy(wrapped), [(1, 1) for i in range(len(wrapped))])
|
||
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('#_(', font='FiraCode-Medium.otf'), [(3, 3)])
|
||
self.ae(groups('a#_(b', font='FiraCode-Medium.otf'), [(1, 1), (3, 3), (1, 1)])
|
||
self.ae(groups('<*>>', font='FiraCode-Medium.otf'), [(3, 3), (1, 1)])
|
||
self.ae(groups('a<*>>b', font='FiraCode-Medium.otf'), [(1, 1), (3, 3), (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))
|
||
for ch in range(0x1fbce, 0x1fbf0): # blocks, diagonals, circles (legacy computing)
|
||
box_chars.add(chr(ch))
|
||
for ch in range(0x1cc1b, 0x1cc40): # box drawing variants, separated quadrants, circle arcs (supplement)
|
||
box_chars.add(chr(ch))
|
||
for ch in range(0x1ce16, 0x1ce1a): # box drawings light vertical T-junctions (supplement)
|
||
box_chars.add(chr(ch))
|
||
for ch in range(0x1ce51, 0x1ceb0): # separated block sextants, sixteenth blocks, quarter parts (supplement)
|
||
box_chars.add(chr(ch))
|