Skip to content

exporters

This module provides various methods and utilities to turn TIM into HTML & SVG.

prettify_xml(xml)

Prettifies some XML.

Source code in pytermgui/exporters.py
106
107
108
109
110
111
112
def prettify_xml(xml: str) -> str:
    """Prettifies some XML."""

    dom = md.parseString(xml)
    pretty_xml = dom.toprettyxml()

    return "\n".join([s for s in pretty_xml.splitlines()[1:] if s.strip() != ""])

to_html(obj, prefix=None, inline_styles=False, include_background=True, vertical_offset=0.0, horizontal_offset=0.0, formatter=HTML_FORMAT, joiner='\n')

Creates a static HTML representation of the given object.

Note that the output HTML will not be very attractive or easy to read. This is because these files probably aren't meant to be read by a human anyways, so file sizes are more important.

If you do care about the visual style of the output, you can run it through some prettifiers to get the result you are looking for.

Parameters:

Name Type Description Default
obj Widget | StyledText | str

The object to represent. Takes either a Widget or some markup text.

required
prefix str | None

The prefix included in the generated classes, e.g. instead of ptg-0, you would get ptg-my-prefix-0.

None
inline_styles bool

If set, styles will be set for each span using the inline style argument, otherwise a full style section is constructed.

False
include_background bool

Whether to include the terminal's background color in the output.

True
Source code in pytermgui/exporters.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def to_html(  # pylint: disable=too-many-arguments, too-many-locals
    obj: Widget | StyledText | str,
    prefix: str | None = None,
    inline_styles: bool = False,
    include_background: bool = True,
    vertical_offset: float = 0.0,
    horizontal_offset: float = 0.0,
    formatter: str = HTML_FORMAT,
    joiner: str = "\n",
) -> str:
    """Creates a static HTML representation of the given object.

    Note that the output HTML will not be very attractive or easy to read. This is
    because these files probably aren't meant to be read by a human anyways, so file
    sizes are more important.

    If you do care about the visual style of the output, you can run it through some
    prettifiers to get the result you are looking for.

    Args:
        obj: The object to represent. Takes either a Widget or some markup text.
        prefix: The prefix included in the generated classes, e.g. instead of `ptg-0`,
            you would get `ptg-my-prefix-0`.
        inline_styles: If set, styles will be set for each span using the inline `style`
            argument, otherwise a full style section is constructed.
        include_background: Whether to include the terminal's background color in the
            output.
    """

    document_styles: list[list[str]] = []

    if isinstance(obj, Widget):
        data = obj.get_lines()

    elif isinstance(obj, str):
        data = obj.splitlines()

    else:
        data = str(obj).splitlines()

    lines = []
    for dataline in data:
        line = ""

        for span, styles in _get_spans(
            dataline, vertical_offset, horizontal_offset, include_background
        ):
            index = _generate_index_in(document_styles, styles)
            if index == len(document_styles):
                document_styles.append(styles)

            if inline_styles:
                stylesheet = ";".join(styles)
                line += span.format(f" styles='{stylesheet}'")

            else:
                line += span.format(" class='" + _get_cls(prefix or "", index) + "'")

        # Close any previously not closed divs
        line += "</div>" * (line.count("<div") - line.count("</div"))
        lines.append(line)

    stylesheet = ""
    if not inline_styles:
        stylesheet = _generate_stylesheet(document_styles, prefix)

    document = formatter.format(
        foreground=Color.get_default_foreground().hex,
        background=Color.get_default_background().hex if include_background else "",
        content=joiner.join(lines),
        styles=stylesheet,
        font_size=FONT_SIZE,
    )

    return document

to_svg(obj, prefix=None, chrome=True, inline_styles=False, title='PyTermGUI', formatter=SVG_FORMAT)

Creates an SVG screenshot of the given object.

This screenshot tries to mimick what the Kitty terminal looks like on MacOS, complete with the menu buttons and drop shadow. The title argument will be displayed in the window's top bar.

Parameters:

Name Type Description Default
obj Widget | StyledText | str

The object to represent. Takes either a Widget or some markup text.

required
prefix str | None

The prefix included in the generated classes, e.g. instead of ptg-0, you would get ptg-my-prefix-0.

None
chrome bool

Sets the visibility of the window "chrome", e.g. the part of the SVG that mimicks the outside border of a terminal.

True
inline_styles bool

If set, styles will be set for each span using the inline style argument, otherwise a full style section is constructed.

False
title str

A string to display in the top bar of the fake terminal.

'PyTermGUI'
formatter str

The formatting string to use. Inspect pytermgui.exporters.SVG_FORMAT to see all of its arguments.

SVG_FORMAT
Source code in pytermgui/exporters.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
def to_svg(  # pylint: disable=too-many-locals, too-many-arguments, too-many-statements
    obj: Widget | StyledText | str,
    prefix: str | None = None,
    chrome: bool = True,
    inline_styles: bool = False,
    title: str = "PyTermGUI",
    formatter: str = SVG_FORMAT,
) -> str:
    """Creates an SVG screenshot of the given object.

    This screenshot tries to mimick what the Kitty terminal looks like on MacOS,
    complete with the menu buttons and drop shadow. The `title` argument will be
    displayed in the window's top bar.

    Args:
        obj: The object to represent. Takes either a Widget or some markup text.
        prefix: The prefix included in the generated classes, e.g. instead of `ptg-0`,
            you would get `ptg-my-prefix-0`.
        chrome: Sets the visibility of the window "chrome", e.g. the part of the SVG
            that mimicks the outside border of a terminal.
        inline_styles: If set, styles will be set for each span using the inline `style`
            argument, otherwise a full style section is constructed.
        title: A string to display in the top bar of the fake terminal.
        formatter: The formatting string to use. Inspect `pytermgui.exporters.SVG_FORMAT`
            to see all of its arguments.
    """

    def _is_block(text: str) -> bool:
        """Determines whether the given text only contains block characters.

        These characters reside in the unicode range of 9600-9631, which is what we test
        against.
        """

        return all(9600 <= ord(char) <= 9631 for char in text)

    prefix = prefix if prefix is not None else "ptg"

    terminal = get_terminal()
    default_fore = Color.get_default_foreground().hex
    default_back = Color.get_default_background().hex

    text = ""

    lines = 1
    cursor_x = cursor_y = 0.0
    document_styles: list[list[str]] = []

    # We manually set all text to have an alignment-baseline of
    # text-after-edge to avoid block characters rendering in the
    # wrong place (not at the top of their "box"), but with that
    # our background rects will be rendered in the wrong place too,
    # so this is used to offset that.
    baseline_offset = 0.17 * FONT_HEIGHT

    if isinstance(obj, Widget):
        obj = "\n".join(obj.get_lines())

    elif isinstance(obj, StyledText):
        obj = str(obj)

    for plain in tim.group_styles(obj):
        if "\x1b" in plain.plain:
            continue

        should_newline = False

        pos, back, styles = _handle_tokens_svg(plain, default_fore, default_back)

        index = _generate_index_in(document_styles, styles)

        if index == len(document_styles):
            document_styles.append(styles)

        style_attr = (
            f"class='{prefix}' style='{';'.join(styles)}'"
            if inline_styles
            else f"class='{prefix} {_get_cls(prefix, index)}'"
        )

        # Manual positioning
        if pos is not None:
            cursor_x = pos[0] * FONT_WIDTH - 10
            cursor_y = pos[1] * FONT_HEIGHT - 15

        for line in plain.plain.splitlines():
            text_len = len(line) * FONT_WIDTH

            if should_newline:
                cursor_y += FONT_HEIGHT
                cursor_x = 0

                lines += 1
                if lines > terminal.height:
                    break

            text += _make_tag(
                "rect",
                x=cursor_x,
                y=cursor_y - (baseline_offset if not _is_block(line) else 0),
                fill=back or default_back,
                width=round(text_len * 1.02, 4),
                height=round(FONT_HEIGHT * 1.08, 4),
            )

            text += _make_tag(
                "text",
                _escape_text(line),
                dy="-0.25em",
                x=cursor_x,
                y=cursor_y + FONT_SIZE,
                textLength=text_len,
                raw=style_attr,
            )

            cursor_x += text_len
            should_newline = True

        if lines > terminal.height:
            break

        if plain.plain.endswith("\n"):
            cursor_y += FONT_HEIGHT
            cursor_x = 0

            lines += 1

    stylesheet = "" if inline_styles else _generate_stylesheet(document_styles, prefix)

    terminal_width = round(terminal.width * FONT_WIDTH + 2 * TEXT_MARGIN_LEFT, 4)
    terminal_height = round(
        terminal.height * FONT_HEIGHT + (2 if chrome else 1) * TEXT_MARGIN_TOP, 4
    )

    total_width = terminal_width + (2 * SVG_MARGIN_LEFT if chrome else 0)
    total_height = terminal_height + (2 * SVG_MARGIN_TOP if chrome else 0)

    if chrome:
        transform = (
            f"translate({TEXT_MARGIN_LEFT + SVG_MARGIN_LEFT}, "
            + f"{TEXT_MARGIN_TOP + SVG_MARGIN_TOP})"
        )

        chrome_part = f"""<g>
            <rect x="{SVG_MARGIN_LEFT}" y="{SVG_MARGIN_TOP}"
                rx="9px" ry="9px" stroke-width="1px" stroke-linejoin="round"
                width="{terminal_width}" height="{terminal_height}" fill="{default_back}" />
            <circle cx="{SVG_MARGIN_LEFT+15}" cy="{SVG_MARGIN_TOP + 15}" r="6" fill="#ff6159"/>
            <circle cx="{SVG_MARGIN_LEFT+35}" cy="{SVG_MARGIN_TOP + 15}" r="6" fill="#ffbd2e"/>
            <circle cx="{SVG_MARGIN_LEFT+55}" cy="{SVG_MARGIN_TOP + 15}" r="6" fill="#28c941"/>
            <text x="{terminal_width // 2}" y="{SVG_MARGIN_TOP + FONT_HEIGHT}" text-anchor="middle"
                class="{prefix}-title">{title}</text>
        </g>
        """

    else:
        transform = "translate(16, 16)"

        chrome_part = f"""<rect width="{total_width}" height="{total_height}"
            fill="{default_back}" />"""

    output = _make_tag("g", text, transform=transform) + "\n"

    return prettify_xml(
        formatter.format(
            # Dimensions
            total_width=round(total_width, 4),
            total_height=round(total_height, 4),
            terminal_width=terminal_width * 1.02,
            terminal_height=terminal_height - 15,
            # Styles
            background=default_back,
            stylesheet=stylesheet,
            # Code
            code=output,
            prefix=prefix,
            chrome=chrome_part,
        )
    )

token_to_css(token, invert=False)

Finds the CSS representation of a token.

Parameters:

Name Type Description Default
token Token

The token to represent.

required
invert bool

If set, the role of background & foreground colors are flipped.

False
Source code in pytermgui/exporters.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def token_to_css(token: Token, invert: bool = False) -> str:
    """Finds the CSS representation of a token.

    Args:
        token: The token to represent.
        invert: If set, the role of background & foreground colors
            are flipped.
    """

    if Token.is_color(token):
        color = token.color

        style = "color:" + color.hex

        if invert:
            color.background = not color.background

        if color.background:
            style = "background-" + style

        return style

    if token.is_style() and token.value in _STYLE_TO_CSS:
        return _STYLE_TO_CSS[token.value]

    return ""