Robpol86/terminaltables

Add title_cb for truncating titles.

Opened this issue · 3 comments

The default is to hide the title if it doesn't fit. I didn't remember this until I was done writing truncating code. Saving it here for future reference.

def build_border(column_widths, horizontal, left, intersect, right, title=None):
    """Build the top/bottom/middle row. Optionally embed the table title within the border.

    Title is truncated to fit in between left/right characters.

    Example return value:
    ('<', '-----', '+', '------', '+', '-------', '>')
    ('<', 'My Table', '----', '+', '------->')

    :param iter column_widths: List of integers representing column widths.
    :param str horizontal: Character to stretch across each column.
    :param str left: Left border.
    :param str intersect: Column separator.
    :param str right: Right border.
    :param str title: Overlay the title on the border between the left and right characters.

    :return: Prepared border as a tuple of strings.
    :rtype: tuple
    """
    if not title or not column_widths or not horizontal:
        return tuple(combine((horizontal * c for c in column_widths), left, intersect, right))
    title, length = truncate(title, sum(column_widths) + len(intersect) * (len(column_widths) - 1))

    # Handle title fitting in the first column.
    if length == column_widths[0]:
        return tuple(combine([title] + [horizontal * c for c in column_widths[1:]], left, intersect, right))
    if length < column_widths[0]:
        columns = [title + horizontal * (column_widths[0] - length)] + [horizontal * c for c in column_widths[1:]]
        return tuple(combine(columns, left, intersect, right))

    # Handle wide titles/narrow columns.
    columns_and_intersects = [title]
    for width in combine(column_widths, None, bool(intersect), None):
        # If title is taken care of.
        if length < 1:
            columns_and_intersects.append(intersect if width is True else horizontal * width)
        # If title's last character overrides an intersect character.
        elif width is True and length == 1:
            length = 0
        # If this is an intersect character that is overridden by the title.
        elif width is True:
            length -= 1
        # If title's last character is within a column.
        elif width >= length:
            columns_and_intersects[0] += horizontal * (width - length)  # Append horizontal chars to title.
            length = 0
        # If remainder of title won't fit in a column.
        else:
            length -= width

    return tuple(combine(columns_and_intersects, left, None, right))
def truncate(string, max_length):
    """Truncate string to a maximum length. Handles CJK characters.

    :param str string: String to operate on.
    :param int max_length: Truncate string to this visible size. May truncate to one shorter if CJK in the middle.

    :return: Truncated string and its length (str, int).
    :rtype: tuple
    """
    truncated = list()
    length = 0
    done = False

    # Convert to unicode.
    try:
        string = string.decode('u8')
    except (AttributeError, UnicodeEncodeError):
        pass

    for item in RE_COLOR_ANSI.split(string):
        if not item:
            continue
        if RE_COLOR_ANSI.match(item):
            truncated.append(item)
            continue
        if done:
            continue
        for char in item:
            width = 2 if unicodedata.east_asian_width(char) in ('F', 'W') else 1
            if length + width > max_length:
                done = True
                break
            truncated.append(char)
            length += width

    return ''.join(truncated), length
# coding: utf-8
"""Test function in module."""

import pytest
from colorama import Fore
from colorclass import Color
from termcolor import colored

from terminaltables.width_and_alignment import truncate


@pytest.mark.parametrize('string,max_length,expected_str,expected_len', [
    ('TEST', 2, 'TE', 2),
    ('TEST', 0, '', 0),
    ('TEST', 5, 'TEST', 4),
    ('', 2, '', 0),
    ('', 0, '', 0),
    ('', 6, '', 0),
])
def test_ascii(string, max_length, expected_str, expected_len):
    """Test with ASCII characters only.

    :param str string: String to operate on.
    :param int max_length: Truncate to this size.
    :param str expected_str: Expected truncated string.
    :param int expected_len: Expected truncated string size.
    """
    actual_str, actual_len = truncate(string, max_length)
    assert actual_str == expected_str
    assert actual_len == expected_len


@pytest.mark.parametrize('string,max_length,expected_str,expected_len', [
    ('世界你好', 8, u'世界你好', 8),
    ('世界你好', 4, u'世界', 4),
    ('世界你好', 3, u'世', 2),
    ('a世界你好', 3, u'a世', 3),
    ('שלום', 4, u'שלום', 4),
    ('שלום', 3, u'שלו', 3),
    ('معرب', 4, u'معرب', 4),
    ('معرب', 3, u'معر', 3),
])
def test_cjk_rtl(string, max_length, expected_str, expected_len):
    """Test with CJK and RTL characters.

    :param str string: String to operate on.
    :param int max_length: Truncate to this size.
    :param str expected_str: Expected truncated string.
    :param int expected_len: Expected truncated string size.
    """
    actual_str, actual_len = truncate(string, max_length)
    assert actual_str == expected_str
    assert actual_len == expected_len


@pytest.mark.parametrize('string,max_length,expected_str,expected_len', [
    # str+ansi
    ('\x1b[34mTEST\x1b[39m', 4, '\x1b[34mTEST\x1b[39m', 4),
    ('\x1b[34mTEST\x1b[39m', 2, '\x1b[34mTE\x1b[39m', 2),
    ('\x1b[34mT\x1b[35mE\x1b[36mS\x1b[37mT\x1b[39m', 2, '\x1b[34mT\x1b[35mE\x1b[36m\x1b[37m\x1b[39m', 2),
    ('\x1b[34m世界\x1b[39m', 4, u'\x1b[34m世界\x1b[39m', 4),
    ('\x1b[34m世界\x1b[39m', 2, u'\x1b[34m世\x1b[39m', 2),
    ('\x1b[34m世\x1b[35m界\x1b[39m', 2, u'\x1b[34m世\x1b[35m\x1b[39m', 2),
    ('\x1b[34mمعرب\x1b[39m', 4, u'\x1b[34mمعرب\x1b[39m', 4),
    ('\x1b[34mمعرب\x1b[39m', 2, u'\x1b[34mمع\x1b[39m', 2),
    ('\x1b[34mשלום\x1b[39m', 4, u'\x1b[34mשלום\x1b[39m', 4),
    ('\x1b[34mשלום\x1b[39m', 2, u'\x1b[34mשל\x1b[39m', 2),

    # colorclass
    (Color('{blue}TEST{/blue}'), 4, '\x1b[34mTEST\x1b[39m', 4),
    (Color('{blue}TEST{/blue}'), 2, '\x1b[34mTE\x1b[39m', 2),
    (Color('{blue}T{magenta}E{cyan}S{white}T{/blue}'), 2, '\x1b[34mT\x1b[35mE\x1b[36m\x1b[37m\x1b[39m', 2),
    (Color(u'{blue}世界{/blue}'), 4, u'\x1b[34m世界\x1b[39m', 4),
    (Color(u'{blue}世界{/blue}'), 2, u'\x1b[34m世\x1b[39m', 2),
    (Color(u'{blue}世{magenta}界{/magenta}'), 2, u'\x1b[34m世\x1b[35m\x1b[39m', 2),
    (Color(u'{blue}معرب{/blue}'), 4, u'\x1b[34mمعرب\x1b[39m', 4),
    (Color(u'{blue}معرب{/blue}'), 2, u'\x1b[34mمع\x1b[39m', 2),
    (Color(u'{blue}שלום{/blue}'), 4, u'\x1b[34mשלום\x1b[39m', 4),
    (Color(u'{blue}שלום{/blue}'), 2, u'\x1b[34mשל\x1b[39m', 2),

    # colorama
    (Fore.BLUE + 'TEST' + Fore.RESET, 4, '\x1b[34mTEST\x1b[39m', 4),
    (Fore.BLUE + 'TEST' + Fore.RESET, 2, '\x1b[34mTE\x1b[39m', 2),
    (
        Fore.BLUE + 'T' + Fore.MAGENTA + 'E' + Fore.CYAN + 'S' + Fore.WHITE + 'T' + Fore.RESET,
        2,
        '\x1b[34mT\x1b[35mE\x1b[36m\x1b[37m\x1b[39m',
        2
    ),
    (Fore.BLUE + '世界' + Fore.RESET, 4, u'\x1b[34m世界\x1b[39m', 4),
    (Fore.BLUE + '世界' + Fore.RESET, 2, u'\x1b[34m世\x1b[39m', 2),
    (Fore.BLUE + '世' + Fore.MAGENTA + '界' + Fore.RESET, 2, u'\x1b[34m世\x1b[35m\x1b[39m', 2),
    (Fore.BLUE + 'معرب' + Fore.RESET, 4, u'\x1b[34mمعرب\x1b[39m', 4),
    (Fore.BLUE + 'معرب' + Fore.RESET, 2, u'\x1b[34mمع\x1b[39m', 2),
    (Fore.BLUE + 'שלום' + Fore.RESET, 4, u'\x1b[34mשלום\x1b[39m', 4),
    (Fore.BLUE + 'שלום' + Fore.RESET, 2, u'\x1b[34mשל\x1b[39m', 2),

    # termcolor
    (colored('TEST', 'blue'), 4, '\x1b[34mTEST\x1b[0m', 4),
    (colored('TEST', 'blue'), 2, '\x1b[34mTE\x1b[0m', 2),
    (
        colored('T', 'blue') + colored('E', 'magenta') + colored('S', 'cyan') + colored('T', 'white'),
        2,
        '\x1b[34mT\x1b[0m\x1b[35mE\x1b[0m\x1b[36m\x1b[0m\x1b[37m\x1b[0m',
        2
    ),
    (colored('世界', 'blue'), 4, u'\x1b[34m世界\x1b[0m', 4),
    (colored('世界', 'blue'), 2, u'\x1b[34m世\x1b[0m', 2),
    (colored('世', 'blue') + colored('界', 'magenta'), 2, u'\x1b[34m世\x1b[0m\x1b[35m\x1b[0m', 2),
    (colored('معرب', 'blue'), 4, u'\x1b[34mمعرب\x1b[0m', 4),
    (colored('معرب', 'blue'), 2, u'\x1b[34mمع\x1b[0m', 2),
    (colored('שלום', 'blue'), 4, u'\x1b[34mשלום\x1b[0m', 4),
    (colored('שלום', 'blue'), 2, u'\x1b[34mשל\x1b[0m', 2),
])
def test_colors(string, max_length, expected_str, expected_len):
    """Test with color characters.

    :param str string: String to operate on.
    :param int max_length: Truncate to this size.
    :param str expected_str: Expected truncated string.
    :param int expected_len: Expected truncated string size.
    """
    actual_str, actual_len = truncate(string, max_length)
    assert actual_str == expected_str
    assert actual_len == expected_len
# coding: utf-8
"""Test function in module."""

import pytest
from colorama import Fore, Style
from colorclass import Color
from termcolor import colored

from terminaltables.build import build_border


@pytest.mark.parametrize('column_widths,horizontal,left,intersect,right,expected', [
    ([5, 6, 7], '-', '<', '+', '>', '<-----+------+------->'),
    ([1, 1, 1], '-', '', '', '', '---'),
    ([1, 1, 1], '', '', '', '', ''),
    ([1], '-', '<', '+', '>', '<->'),
    ([], '-', '<', '+', '>', '<>'),
])
def test_no_title(column_widths, horizontal, left, intersect, right, expected):
    """Test without title.

    :param iter column_widths: List of integers representing column widths.
    :param str horizontal: Character to stretch across each column.
    :param str left: Left border.
    :param str intersect: Column separator.
    :param str right: Right border.
    :param str expected: Expected output.
    """
    actual = build_border(column_widths, horizontal, left, intersect, right)
    assert ''.join(actual) == expected


@pytest.mark.parametrize('column_widths,intersect,expected', [
    ([20], '+', 'Applications--------'),
    ([20], '', 'Applications--------'),

    ([15, 5], '+', 'Applications---+-----'),
    ([15, 5], '', 'Applications--------'),

    ([12], '+', 'Applications'),
    ([12], '', 'Applications'),

    ([12, 1], '+', 'Applications+-'),
    ([12, 1], '', 'Applications-'),

    ([12, 0], '+', 'Applications+'),
    ([12, 0], '', 'Applications'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_first_column_fit(column_widths, left, intersect, right, expected):
    """Test with title that fits in the first column.

    :param iter column_widths: List of integers representing column widths.
    :param str left: Left border.
    :param str intersect: Column separator.
    :param str right: Right border.
    :param str expected: Expected output.
    """
    if left and right:
        expected = left + expected + right
    actual = build_border(column_widths, '-', left, intersect, right, title='Applications')
    assert ''.join(actual) == expected


@pytest.mark.parametrize('column_widths,expected', [
    ([20], 'Applications--------'),
    ([10, 10], 'Applications--------'),
    ([5, 5, 5, 5], 'Applications--------'),
    ([3, 2, 3, 2, 3, 2, 3, 2], 'Applications--------'),
    ([1] * 20, 'Applications--------'),
    ([10, 5], 'Applications---'),
    ([9, 5], 'Applications--'),
    ([8, 5], 'Applications-'),
    ([7, 5], 'Applications'),
    ([6, 5], 'Application'),
    ([5, 5], 'Applicatio'),
    ([5, 4], 'Applicati'),
    ([4, 4], 'Applicat'),
    ([4, 3], 'Applica'),
    ([3, 3], 'Applic'),
    ([3, 2], 'Appli'),
    ([2, 2], 'Appl'),
    ([2, 1], 'App'),
    ([1, 1], 'Ap'),
    ([1, 0], 'A'),
    ([0, 0], ''),
    ([1], 'A'),
    ([0], ''),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_no_intersect(column_widths, left, right, expected):
    """Test with no column dividers.

    :param iter column_widths: List of integers representing column widths.
    :param str left: Left border.
    :param str right: Right border.
    :param str expected: Expected output.
    """
    if left and right:
        expected = left + expected + right
    actual = build_border(column_widths, '-', left, '', right, title='Applications')
    assert ''.join(actual) == expected


@pytest.mark.parametrize('column_widths,expected', [
    ([20], 'Applications--------'),
    ([0, 20], 'Applications---------'),
    ([20, 0], 'Applications--------+'),
    ([0, 0, 20], 'Applications----------'),
    ([20, 0, 0], 'Applications--------++'),

    ([10, 10], 'Applications---------'),
    ([11, 9], 'Applications---------'),
    ([12, 8], 'Applications+--------'),
    ([13, 7], 'Applications-+-------'),

    ([5, 5, 5, 5], 'Applications-----+-----'),
    ([4, 4, 6, 6], 'Applications----+------'),
    ([3, 3, 7, 7], 'Applications---+-------'),
    ([2, 2, 7, 9], 'Applications-+---------'),
    ([1, 1, 9, 9], 'Applications-+---------'),

    ([2, 2, 2, 2, 2, 2, 2], 'Applications--+--+--'),
    ([1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'Applications-+-+-+-'),
    ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'Applications++++++++'),

    ([2, 2, 2, 2], 'Application'),
    ([1, 1, 1, 1, 1], 'Applicati'),
    ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'Applicati'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_intersect(column_widths, left, right, expected):
    """Test with column dividers.

    :param iter column_widths: List of integers representing column widths.
    :param str left: Left border.
    :param str right: Right border.
    :param str expected: Expected output.
    """
    if left and right:
        expected = left + expected + right
    actual = build_border(column_widths, '-', left, '+', right, title='Applications')
    assert ''.join(actual) == expected


@pytest.mark.parametrize('column_widths,intersect,expected', [
    ([12], '+', u'蓝色--------'),
    ([12], '', u'蓝色--------'),
    ([7, 5], '+', u'蓝色---+-----'),
    ([7, 5], '', u'蓝色--------'),
    ([4], '+', u'蓝色'),
    ([4], '', u'蓝色'),
    ([4, 1], '+', u'蓝色+-'),
    ([4, 1], '', u'蓝色-'),
    ([4, 0], '+', u'蓝色+'),
    ([4, 0], '', u'蓝色'),
    ([12], '', u'蓝色--------'),
    ([6, 6], '', u'蓝色--------'),
    ([3, 3, 3, 3], '', u'蓝色--------'),
    ([2, 1, 2, 1, 2, 1, 2, 1], '', u'蓝色--------'),
    ([1] * 12, '', u'蓝色--------'),
    ([2, 4], '', u'蓝色--'),
    ([1, 4], '', u'蓝色-'),
    ([1, 3], '', u'蓝色'),
    ([1, 2], '', u'蓝-'),
    ([1, 1], '', u'蓝'),
    ([1, 0], '', '-'),
    ([0, 0], '', ''),
    ([2], '', u'蓝'),
    ([1], '', '-'),
    ([0], '', ''),
    ([12], '+', u'蓝色--------'),
    ([0, 12], '+', u'蓝色---------'),
    ([12, 0], '+', u'蓝色--------+'),
    ([0, 0, 12], '+', u'蓝色----------'),
    ([12, 0, 0], '+', u'蓝色--------++'),
    ([3, 3], '+', u'蓝色---'),
    ([4, 2], '+', u'蓝色+--'),
    ([5, 1], '+', u'蓝色-+-'),
    ([3, 3, 3, 3], '+', u'蓝色---+---+---'),
    ([2, 2, 4, 4], '+', u'蓝色-+----+----'),
    ([1, 1, 5, 5], '+', u'蓝色-----+-----'),
    ([2, 2, 2, 2], '+', u'蓝色-+--+--'),
    ([1, 1, 1, 1, 1], '+', u'蓝色-+-+-'),
    ([0, 0, 0, 0, 0, 0, 0], '+', u'蓝色++'),
    ([1, 1], '+', u'蓝-'),
    ([1, 0], '+', u'蓝'),
    ([0, 0], '+', '+'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_cjk(column_widths, left, intersect, right, expected):
    """Test with CJK characters in title.

    :param iter column_widths: List of integers representing column widths.
    :param str left: Left border.
    :param str intersect: Column separator.
    :param str right: Right border.
    :param str expected: Expected output.
    """
    if left and right:
        expected = left + expected + right
    actual = build_border(column_widths, '-', left, intersect, right, title='蓝色')
    assert ''.join(actual) == expected


@pytest.mark.parametrize('column_widths,intersect,expected', [
    ([12], '+', u'معرب--------'),
    ([12], '', u'معرب--------'),
    ([7, 5], '+', u'معرب---+-----'),
    ([7, 5], '', u'معرب--------'),
    ([4], '+', u'معرب'),
    ([4], '', u'معرب'),
    ([4, 1], '+', u'معرب+-'),
    ([4, 1], '', u'معرب-'),
    ([4, 0], '+', u'معرب+'),
    ([4, 0], '', u'معرب'),
    ([12], '', u'معرب--------'),
    ([6, 6], '', u'معرب--------'),
    ([3, 3, 3, 3], '', u'معرب--------'),
    ([2, 1, 2, 1, 2, 1, 2, 1], '', u'معرب--------'),
    ([1] * 12, '', u'معرب--------'),
    ([2, 4], '', u'معرب--'),
    ([1, 4], '', u'معرب-'),
    ([1, 3], '', u'معرب'),
    ([1, 2], '', u'معر'),
    ([1, 1], '', u'مع'),
    ([1, 0], '', u'م'),
    ([0, 0], '', ''),
    ([2], '', u'مع'),
    ([1], '', u'م'),
    ([0], '', ''),
    ([12], '+', u'معرب--------'),
    ([0, 12], '+', u'معرب---------'),
    ([12, 0], '+', u'معرب--------+'),
    ([0, 0, 12], '+', u'معرب----------'),
    ([12, 0, 0], '+', u'معرب--------++'),
    ([3, 3], '+', u'معرب---'),
    ([4, 2], '+', u'معرب+--'),
    ([5, 1], '+', u'معرب-+-'),
    ([3, 3, 3, 3], '+', u'معرب---+---+---'),
    ([2, 2, 4, 4], '+', u'معرب-+----+----'),
    ([1, 1, 5, 5], '+', u'معرب-----+-----'),
    ([2, 2, 2, 2], '+', u'معرب-+--+--'),
    ([1, 1, 1, 1, 1], '+', u'معرب-+-+-'),
    ([0, 0, 0, 0, 0, 0, 0], '+', u'معرب++'),
    ([1, 1], '+', u'معر'),
    ([1, 0], '+', u'مع'),
    ([0, 0], '+', u'م'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_rtl(column_widths, left, intersect, right, expected):
    """Test with RTL characters in title.

    :param iter column_widths: List of integers representing column widths.
    :param str left: Left border.
    :param str intersect: Column separator.
    :param str right: Right border.
    :param str expected: Expected output.
    """
    if left and right:
        expected = left + expected + right
    actual = build_border(column_widths, '-', left, intersect, right, title='معرب')
    assert ''.join(actual) == expected


@pytest.mark.parametrize('column_widths,intersect,expected', [
    ([12], '+', '\x1b[34mTEST\x1b[0m--------'),
    ([12], '', '\x1b[34mTEST\x1b[0m--------'),
    ([7, 5], '+', '\x1b[34mTEST\x1b[0m---+-----'),
    ([7, 5], '', '\x1b[34mTEST\x1b[0m--------'),
    ([4], '+', '\x1b[34mTEST\x1b[0m'),
    ([4], '', '\x1b[34mTEST\x1b[0m'),
    ([4, 1], '+', '\x1b[34mTEST\x1b[0m+-'),
    ([4, 1], '', '\x1b[34mTEST\x1b[0m-'),
    ([4, 0], '+', '\x1b[34mTEST\x1b[0m+'),
    ([4, 0], '', '\x1b[34mTEST\x1b[0m'),
    ([12], '', '\x1b[34mTEST\x1b[0m--------'),
    ([6, 6], '', '\x1b[34mTEST\x1b[0m--------'),
    ([3, 3, 3, 3], '', '\x1b[34mTEST\x1b[0m--------'),
    ([2, 1, 2, 1, 2, 1, 2, 1], '', '\x1b[34mTEST\x1b[0m--------'),
    ([1] * 12, '', '\x1b[34mTEST\x1b[0m--------'),
    ([2, 4], '', '\x1b[34mTEST\x1b[0m--'),
    ([1, 4], '', '\x1b[34mTEST\x1b[0m-'),
    ([1, 3], '', '\x1b[34mTEST\x1b[0m'),
    ([1, 2], '', '\x1b[34mTES\x1b[0m'),
    ([1, 1], '', '\x1b[34mTE\x1b[0m'),
    ([1, 0], '', '\x1b[34mT\x1b[0m'),
    ([0, 0], '', '\x1b[34m\x1b[0m'),
    ([2], '', '\x1b[34mTE\x1b[0m'),
    ([1], '', '\x1b[34mT\x1b[0m'),
    ([0], '', '\x1b[34m\x1b[0m'),
    ([12], '+', '\x1b[34mTEST\x1b[0m--------'),
    ([0, 12], '+', '\x1b[34mTEST\x1b[0m---------'),
    ([12, 0], '+', '\x1b[34mTEST\x1b[0m--------+'),
    ([0, 0, 12], '+', '\x1b[34mTEST\x1b[0m----------'),
    ([12, 0, 0], '+', '\x1b[34mTEST\x1b[0m--------++'),
    ([3, 3], '+', '\x1b[34mTEST\x1b[0m---'),
    ([4, 2], '+', '\x1b[34mTEST\x1b[0m+--'),
    ([5, 1], '+', '\x1b[34mTEST\x1b[0m-+-'),
    ([3, 3, 3, 3], '+', '\x1b[34mTEST\x1b[0m---+---+---'),
    ([2, 2, 4, 4], '+', '\x1b[34mTEST\x1b[0m-+----+----'),
    ([1, 1, 5, 5], '+', '\x1b[34mTEST\x1b[0m-----+-----'),
    ([2, 2, 2, 2], '+', '\x1b[34mTEST\x1b[0m-+--+--'),
    ([1, 1, 1, 1, 1], '+', '\x1b[34mTEST\x1b[0m-+-+-'),
    ([0, 0, 0, 0, 0, 0, 0], '+', '\x1b[34mTEST\x1b[0m++'),
    ([1, 1], '+', '\x1b[34mTES\x1b[0m'),
    ([1, 0], '+', '\x1b[34mTE\x1b[0m'),
    ([0, 0], '+', '\x1b[34mT\x1b[0m'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
@pytest.mark.parametrize('title', [
    '\x1b[34mTEST\x1b[0m',
    Color('{blue}TEST{/all}'),
    Fore.BLUE + 'TEST' + Style.RESET_ALL,
    colored('TEST', 'blue'),
])
def test_colors(column_widths, left, intersect, right, title, expected):
    """Test with color title characters.

    :param iter column_widths: List of integers representing column widths.
    :param str left: Left border.
    :param str intersect: Column separator.
    :param str right: Right border.
    :param title: Title in border with color codes.
    :param str expected: Expected output.
    """
    if left and right:
        expected = left + expected + right
    actual = build_border(column_widths, '-', left, intersect, right, title=title)
    assert ''.join(actual) == expected

The truncate() function seems almost done.
@Robpol86 Is there any chance we can help with this issue? 👻