1""" 
    2Collection of style transformations. 
    3 
    4Think of it as a kind of color post processing after the rendering is done. 
    5This could be used for instance to change the contrast/saturation; swap light 
    6and dark colors or even change certain colors for other colors. 
    7 
    8When the UI is rendered, these transformations can be applied right after the 
    9style strings are turned into `Attrs` objects that represent the actual 
    10formatting. 
    11""" 
    12 
    13from __future__ import annotations 
    14 
    15from abc import ABCMeta, abstractmethod 
    16from colorsys import hls_to_rgb, rgb_to_hls 
    17from typing import Callable, Hashable, Sequence 
    18 
    19from prompt_toolkit.cache import memoized 
    20from prompt_toolkit.filters import FilterOrBool, to_filter 
    21from prompt_toolkit.utils import AnyFloat, to_float, to_str 
    22 
    23from .base import ANSI_COLOR_NAMES, Attrs 
    24from .style import parse_color 
    25 
    26__all__ = [ 
    27    "StyleTransformation", 
    28    "SwapLightAndDarkStyleTransformation", 
    29    "ReverseStyleTransformation", 
    30    "SetDefaultColorStyleTransformation", 
    31    "AdjustBrightnessStyleTransformation", 
    32    "DummyStyleTransformation", 
    33    "ConditionalStyleTransformation", 
    34    "DynamicStyleTransformation", 
    35    "merge_style_transformations", 
    36] 
    37 
    38 
    39class StyleTransformation(metaclass=ABCMeta): 
    40    """ 
    41    Base class for any style transformation. 
    42    """ 
    43 
    44    @abstractmethod 
    45    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    46        """ 
    47        Take an `Attrs` object and return a new `Attrs` object. 
    48 
    49        Remember that the color formats can be either "ansi..." or a 6 digit 
    50        lowercase hexadecimal color (without '#' prefix). 
    51        """ 
    52 
    53    def invalidation_hash(self) -> Hashable: 
    54        """ 
    55        When this changes, the cache should be invalidated. 
    56        """ 
    57        return f"{self.__class__.__name__}-{id(self)}" 
    58 
    59 
    60class SwapLightAndDarkStyleTransformation(StyleTransformation): 
    61    """ 
    62    Turn dark colors into light colors and the other way around. 
    63 
    64    This is meant to make color schemes that work on a dark background usable 
    65    on a light background (and the other way around). 
    66 
    67    Notice that this doesn't swap foreground and background like "reverse" 
    68    does. It turns light green into dark green and the other way around. 
    69    Foreground and background colors are considered individually. 
    70 
    71    Also notice that when <reverse> is used somewhere and no colors are given 
    72    in particular (like what is the default for the bottom toolbar), then this 
    73    doesn't change anything. This is what makes sense, because when the 
    74    'default' color is chosen, it's what works best for the terminal, and 
    75    reverse works good with that. 
    76    """ 
    77 
    78    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    79        """ 
    80        Return the `Attrs` used when opposite luminosity should be used. 
    81        """ 
    82        # Reverse colors. 
    83        attrs = attrs._replace(color=get_opposite_color(attrs.color)) 
    84        attrs = attrs._replace(bgcolor=get_opposite_color(attrs.bgcolor)) 
    85 
    86        return attrs 
    87 
    88 
    89class ReverseStyleTransformation(StyleTransformation): 
    90    """ 
    91    Swap the 'reverse' attribute. 
    92 
    93    (This is still experimental.) 
    94    """ 
    95 
    96    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    97        return attrs._replace(reverse=not attrs.reverse) 
    98 
    99 
    100class SetDefaultColorStyleTransformation(StyleTransformation): 
    101    """ 
    102    Set default foreground/background color for output that doesn't specify 
    103    anything. This is useful for overriding the terminal default colors. 
    104 
    105    :param fg: Color string or callable that returns a color string for the 
    106        foreground. 
    107    :param bg: Like `fg`, but for the background. 
    108    """ 
    109 
    110    def __init__( 
    111        self, fg: str | Callable[[], str], bg: str | Callable[[], str] 
    112    ) -> None: 
    113        self.fg = fg 
    114        self.bg = bg 
    115 
    116    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    117        if attrs.bgcolor in ("", "default"): 
    118            attrs = attrs._replace(bgcolor=parse_color(to_str(self.bg))) 
    119 
    120        if attrs.color in ("", "default"): 
    121            attrs = attrs._replace(color=parse_color(to_str(self.fg))) 
    122 
    123        return attrs 
    124 
    125    def invalidation_hash(self) -> Hashable: 
    126        return ( 
    127            "set-default-color", 
    128            to_str(self.fg), 
    129            to_str(self.bg), 
    130        ) 
    131 
    132 
    133class AdjustBrightnessStyleTransformation(StyleTransformation): 
    134    """ 
    135    Adjust the brightness to improve the rendering on either dark or light 
    136    backgrounds. 
    137 
    138    For dark backgrounds, it's best to increase `min_brightness`. For light 
    139    backgrounds it's best to decrease `max_brightness`. Usually, only one 
    140    setting is adjusted. 
    141 
    142    This will only change the brightness for text that has a foreground color 
    143    defined, but no background color. It works best for 256 or true color 
    144    output. 
    145 
    146    .. note:: Notice that there is no universal way to detect whether the 
    147              application is running in a light or dark terminal. As a 
    148              developer of an command line application, you'll have to make 
    149              this configurable for the user. 
    150 
    151    :param min_brightness: Float between 0.0 and 1.0 or a callable that returns 
    152        a float. 
    153    :param max_brightness: Float between 0.0 and 1.0 or a callable that returns 
    154        a float. 
    155    """ 
    156 
    157    def __init__( 
    158        self, min_brightness: AnyFloat = 0.0, max_brightness: AnyFloat = 1.0 
    159    ) -> None: 
    160        self.min_brightness = min_brightness 
    161        self.max_brightness = max_brightness 
    162 
    163    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    164        min_brightness = to_float(self.min_brightness) 
    165        max_brightness = to_float(self.max_brightness) 
    166        assert 0 <= min_brightness <= 1 
    167        assert 0 <= max_brightness <= 1 
    168 
    169        # Don't do anything if the whole brightness range is acceptable. 
    170        # This also avoids turning ansi colors into RGB sequences. 
    171        if min_brightness == 0.0 and max_brightness == 1.0: 
    172            return attrs 
    173 
    174        # If a foreground color is given without a background color. 
    175        no_background = not attrs.bgcolor or attrs.bgcolor == "default" 
    176        has_fgcolor = attrs.color and attrs.color != "ansidefault" 
    177 
    178        if has_fgcolor and no_background: 
    179            # Calculate new RGB values. 
    180            r, g, b = self._color_to_rgb(attrs.color or "") 
    181            hue, brightness, saturation = rgb_to_hls(r, g, b) 
    182            brightness = self._interpolate_brightness( 
    183                brightness, min_brightness, max_brightness 
    184            ) 
    185            r, g, b = hls_to_rgb(hue, brightness, saturation) 
    186            new_color = f"{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}" 
    187 
    188            attrs = attrs._replace(color=new_color) 
    189 
    190        return attrs 
    191 
    192    def _color_to_rgb(self, color: str) -> tuple[float, float, float]: 
    193        """ 
    194        Parse `style.Attrs` color into RGB tuple. 
    195        """ 
    196        # Do RGB lookup for ANSI colors. 
    197        try: 
    198            from prompt_toolkit.output.vt100 import ANSI_COLORS_TO_RGB 
    199 
    200            r, g, b = ANSI_COLORS_TO_RGB[color] 
    201            return r / 255.0, g / 255.0, b / 255.0 
    202        except KeyError: 
    203            pass 
    204 
    205        # Parse RRGGBB format. 
    206        return ( 
    207            int(color[0:2], 16) / 255.0, 
    208            int(color[2:4], 16) / 255.0, 
    209            int(color[4:6], 16) / 255.0, 
    210        ) 
    211 
    212        # NOTE: we don't have to support named colors here. They are already 
    213        #       transformed into RGB values in `style.parse_color`. 
    214 
    215    def _interpolate_brightness( 
    216        self, value: float, min_brightness: float, max_brightness: float 
    217    ) -> float: 
    218        """ 
    219        Map the brightness to the (min_brightness..max_brightness) range. 
    220        """ 
    221        return min_brightness + (max_brightness - min_brightness) * value 
    222 
    223    def invalidation_hash(self) -> Hashable: 
    224        return ( 
    225            "adjust-brightness", 
    226            to_float(self.min_brightness), 
    227            to_float(self.max_brightness), 
    228        ) 
    229 
    230 
    231class DummyStyleTransformation(StyleTransformation): 
    232    """ 
    233    Don't transform anything at all. 
    234    """ 
    235 
    236    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    237        return attrs 
    238 
    239    def invalidation_hash(self) -> Hashable: 
    240        # Always return the same hash for these dummy instances. 
    241        return "dummy-style-transformation" 
    242 
    243 
    244class DynamicStyleTransformation(StyleTransformation): 
    245    """ 
    246    StyleTransformation class that can dynamically returns any 
    247    `StyleTransformation`. 
    248 
    249    :param get_style_transformation: Callable that returns a 
    250        :class:`.StyleTransformation` instance. 
    251    """ 
    252 
    253    def __init__( 
    254        self, get_style_transformation: Callable[[], StyleTransformation | None] 
    255    ) -> None: 
    256        self.get_style_transformation = get_style_transformation 
    257 
    258    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    259        style_transformation = ( 
    260            self.get_style_transformation() or DummyStyleTransformation() 
    261        ) 
    262        return style_transformation.transform_attrs(attrs) 
    263 
    264    def invalidation_hash(self) -> Hashable: 
    265        style_transformation = ( 
    266            self.get_style_transformation() or DummyStyleTransformation() 
    267        ) 
    268        return style_transformation.invalidation_hash() 
    269 
    270 
    271class ConditionalStyleTransformation(StyleTransformation): 
    272    """ 
    273    Apply the style transformation depending on a condition. 
    274    """ 
    275 
    276    def __init__( 
    277        self, style_transformation: StyleTransformation, filter: FilterOrBool 
    278    ) -> None: 
    279        self.style_transformation = style_transformation 
    280        self.filter = to_filter(filter) 
    281 
    282    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    283        if self.filter(): 
    284            return self.style_transformation.transform_attrs(attrs) 
    285        return attrs 
    286 
    287    def invalidation_hash(self) -> Hashable: 
    288        return (self.filter(), self.style_transformation.invalidation_hash()) 
    289 
    290 
    291class _MergedStyleTransformation(StyleTransformation): 
    292    def __init__(self, style_transformations: Sequence[StyleTransformation]) -> None: 
    293        self.style_transformations = style_transformations 
    294 
    295    def transform_attrs(self, attrs: Attrs) -> Attrs: 
    296        for transformation in self.style_transformations: 
    297            attrs = transformation.transform_attrs(attrs) 
    298        return attrs 
    299 
    300    def invalidation_hash(self) -> Hashable: 
    301        return tuple(t.invalidation_hash() for t in self.style_transformations) 
    302 
    303 
    304def merge_style_transformations( 
    305    style_transformations: Sequence[StyleTransformation], 
    306) -> StyleTransformation: 
    307    """ 
    308    Merge multiple transformations together. 
    309    """ 
    310    return _MergedStyleTransformation(style_transformations) 
    311 
    312 
    313# Dictionary that maps ANSI color names to their opposite. This is useful for 
    314# turning color schemes that are optimized for a black background usable for a 
    315# white background. 
    316OPPOSITE_ANSI_COLOR_NAMES = { 
    317    "ansidefault": "ansidefault", 
    318    "ansiblack": "ansiwhite", 
    319    "ansired": "ansibrightred", 
    320    "ansigreen": "ansibrightgreen", 
    321    "ansiyellow": "ansibrightyellow", 
    322    "ansiblue": "ansibrightblue", 
    323    "ansimagenta": "ansibrightmagenta", 
    324    "ansicyan": "ansibrightcyan", 
    325    "ansigray": "ansibrightblack", 
    326    "ansiwhite": "ansiblack", 
    327    "ansibrightred": "ansired", 
    328    "ansibrightgreen": "ansigreen", 
    329    "ansibrightyellow": "ansiyellow", 
    330    "ansibrightblue": "ansiblue", 
    331    "ansibrightmagenta": "ansimagenta", 
    332    "ansibrightcyan": "ansicyan", 
    333    "ansibrightblack": "ansigray", 
    334} 
    335assert set(OPPOSITE_ANSI_COLOR_NAMES.keys()) == set(ANSI_COLOR_NAMES) 
    336assert set(OPPOSITE_ANSI_COLOR_NAMES.values()) == set(ANSI_COLOR_NAMES) 
    337 
    338 
    339@memoized() 
    340def get_opposite_color(colorname: str | None) -> str | None: 
    341    """ 
    342    Take a color name in either 'ansi...' format or 6 digit RGB, return the 
    343    color of opposite luminosity (same hue/saturation). 
    344 
    345    This is used for turning color schemes that work on a light background 
    346    usable on a dark background. 
    347    """ 
    348    if colorname is None:  # Because color/bgcolor can be None in `Attrs`. 
    349        return None 
    350 
    351    # Special values. 
    352    if colorname in ("", "default"): 
    353        return colorname 
    354 
    355    # Try ANSI color names. 
    356    try: 
    357        return OPPOSITE_ANSI_COLOR_NAMES[colorname] 
    358    except KeyError: 
    359        # Try 6 digit RGB colors. 
    360        r = int(colorname[:2], 16) / 255.0 
    361        g = int(colorname[2:4], 16) / 255.0 
    362        b = int(colorname[4:6], 16) / 255.0 
    363 
    364        h, l, s = rgb_to_hls(r, g, b) 
    365 
    366        l = 1 - l 
    367 
    368        r, g, b = hls_to_rgb(h, l, s) 
    369 
    370        r = int(r * 255) 
    371        g = int(g * 255) 
    372        b = int(b * 255) 
    373 
    374        return f"{r:02x}{g:02x}{b:02x}"