#!/usr/bin/env python # License: GPL v3 Copyright: 2017, Kovid Goyal 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 = 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_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 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))