Source code for functui.io.ansi

"""Functions to convert layouts to styled strings that can be rendered in a terminal."""
from ..classes import *
from typing import Callable, Iterable
from dataclasses import dataclass


from functools import cache

@cache
def default_color_to_fg_ansi(color: Color):
    if isinstance(color, int):
        if color == -1:
            return f"\033[39m"
        return f"\033[38;5;{color}m"
    else:
        return f"\033[38;2;{color.r};{color.g};{color.b}m"
@cache
def default_color_to_bg_ansi(color: Color):
    if isinstance(color, int):
        if color == -1:
            return f"\033[49m"
        return f"\033[48;5;{color}m"
    else:
        return f"\033[48;2;{color.r};{color.g};{color.b}m"

@cache
def style_to_ansi(style: StyleAttr):
    out = []
    if StyleAttr.BOLD in style:
        out.append("\033[1m")
    if StyleAttr.ITALIC in style:
        out.append("\033[3m")
    if StyleAttr.UNDERLINE in style:
        out.append("\033[4m")
    if StyleAttr.BLINK in style:
        out.append("\033[5m")
    if StyleAttr.REVERSE in style:
        out.append("\033[7m")
    if StyleAttr.STRIKE_THROUGH in style:
        out.append("\033[9m")
    if StyleAttr.DIM in style:
        out.append("\033[2m")
    return "".join(out)


ANSI_RESET_STYLES = "\033[0m"

def _render_ansi(screen: Screen) -> str:
    out = []
    lines = screen.split_by_lines()

    for line in lines:
        line.append(Pixel("", style=ComputedStyle(fg=Color4.RESET, bg=Color4.RESET)))

    curr_style = StyleAttr(0)
    curr_fg = Color4.RESET
    curr_bg = Color4.RESET
    for line in lines:
        for pixel in line:
            line_str = []
            if curr_style != pixel.style.attrs:
                style_changes = (curr_style ^ pixel.style.attrs)
                new_style =  style_changes & pixel.style.attrs
                removed_style = bool(style_changes & curr_style)
                curr_style = pixel.style.attrs
                # apparantly ANSI_RESET_STYLES also resets color, so we need to set it back.
                line_str.extend(
                    [ANSI_RESET_STYLES, style_to_ansi(pixel.style.attrs), default_color_to_fg_ansi(curr_fg), default_color_to_bg_ansi(curr_bg)]\
                    if removed_style\
                    else [style_to_ansi(new_style)]
                )
            if curr_fg != pixel.style.fg and pixel.style.fg is not None:
                curr_fg = pixel.style.fg
                line_str.append(default_color_to_fg_ansi(curr_fg))
            if curr_bg != pixel.style.bg and pixel.style.bg is not None:
                curr_bg = pixel.style.bg
                line_str.append(default_color_to_bg_ansi(curr_bg))
            if len(line_str):
                out.extend(line_str)
            out.append(pixel.char)
        out.append("\n")
    return "".join(out[:-1]) # -1 to remove the \n on the end


[docs] def result_to_str(result: Result) -> str: """Convert a result to a string with ansi escapecodes that can be displayed in a terminal.""" data = result.try_data(ResultCreatedWith) if data is None: raise AssertionError("Result has no ResultCreatedWith data. If possible please use get_result() function to get a result.") screen = Screen(data.screen_size.width, data.screen_size.height) screen.apply_draw_commands(data.measure_text_func, result.get_commands()) # 20 % return _render_ansi(screen) # 30 %
[docs] def layout_to_str(layout: Layout, dimensions: Rect) -> str: """Convert a layout to a string with ansi escapecodes that can be displayed in a terminal. This is a shorthand for ``result_to_str(layout_to_result(...)))``. """ return result_to_str(layout_to_result(dimensions=dimensions, layout=layout))