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