summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/styles
diff options
context:
space:
mode:
Diffstat (limited to 'src/prompt_toolkit/styles')
-rw-r--r--src/prompt_toolkit/styles/__init__.py66
-rw-r--r--src/prompt_toolkit/styles/base.py183
-rw-r--r--src/prompt_toolkit/styles/defaults.py235
-rw-r--r--src/prompt_toolkit/styles/named_colors.py161
-rw-r--r--src/prompt_toolkit/styles/pygments.py69
-rw-r--r--src/prompt_toolkit/styles/style.py400
-rw-r--r--src/prompt_toolkit/styles/style_transformation.py373
7 files changed, 1487 insertions, 0 deletions
diff --git a/src/prompt_toolkit/styles/__init__.py b/src/prompt_toolkit/styles/__init__.py
new file mode 100644
index 0000000..23f61bb
--- /dev/null
+++ b/src/prompt_toolkit/styles/__init__.py
@@ -0,0 +1,66 @@
+"""
+Styling for prompt_toolkit applications.
+"""
+from __future__ import annotations
+
+from .base import (
+ ANSI_COLOR_NAMES,
+ DEFAULT_ATTRS,
+ Attrs,
+ BaseStyle,
+ DummyStyle,
+ DynamicStyle,
+)
+from .defaults import default_pygments_style, default_ui_style
+from .named_colors import NAMED_COLORS
+from .pygments import (
+ pygments_token_to_classname,
+ style_from_pygments_cls,
+ style_from_pygments_dict,
+)
+from .style import Priority, Style, merge_styles, parse_color
+from .style_transformation import (
+ AdjustBrightnessStyleTransformation,
+ ConditionalStyleTransformation,
+ DummyStyleTransformation,
+ DynamicStyleTransformation,
+ ReverseStyleTransformation,
+ SetDefaultColorStyleTransformation,
+ StyleTransformation,
+ SwapLightAndDarkStyleTransformation,
+ merge_style_transformations,
+)
+
+__all__ = [
+ # Base.
+ "Attrs",
+ "DEFAULT_ATTRS",
+ "ANSI_COLOR_NAMES",
+ "BaseStyle",
+ "DummyStyle",
+ "DynamicStyle",
+ # Defaults.
+ "default_ui_style",
+ "default_pygments_style",
+ # Style.
+ "Style",
+ "Priority",
+ "merge_styles",
+ "parse_color",
+ # Style transformation.
+ "StyleTransformation",
+ "SwapLightAndDarkStyleTransformation",
+ "ReverseStyleTransformation",
+ "SetDefaultColorStyleTransformation",
+ "AdjustBrightnessStyleTransformation",
+ "DummyStyleTransformation",
+ "ConditionalStyleTransformation",
+ "DynamicStyleTransformation",
+ "merge_style_transformations",
+ # Pygments.
+ "style_from_pygments_cls",
+ "style_from_pygments_dict",
+ "pygments_token_to_classname",
+ # Named colors.
+ "NAMED_COLORS",
+]
diff --git a/src/prompt_toolkit/styles/base.py b/src/prompt_toolkit/styles/base.py
new file mode 100644
index 0000000..b50f3b0
--- /dev/null
+++ b/src/prompt_toolkit/styles/base.py
@@ -0,0 +1,183 @@
+"""
+The base classes for the styling.
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod, abstractproperty
+from typing import Callable, Hashable, NamedTuple
+
+__all__ = [
+ "Attrs",
+ "DEFAULT_ATTRS",
+ "ANSI_COLOR_NAMES",
+ "ANSI_COLOR_NAMES_ALIASES",
+ "BaseStyle",
+ "DummyStyle",
+ "DynamicStyle",
+]
+
+
+#: Style attributes.
+class Attrs(NamedTuple):
+ color: str | None
+ bgcolor: str | None
+ bold: bool | None
+ underline: bool | None
+ strike: bool | None
+ italic: bool | None
+ blink: bool | None
+ reverse: bool | None
+ hidden: bool | None
+
+
+"""
+:param color: Hexadecimal string. E.g. '000000' or Ansi color name: e.g. 'ansiblue'
+:param bgcolor: Hexadecimal string. E.g. 'ffffff' or Ansi color name: e.g. 'ansired'
+:param bold: Boolean
+:param underline: Boolean
+:param strike: Boolean
+:param italic: Boolean
+:param blink: Boolean
+:param reverse: Boolean
+:param hidden: Boolean
+"""
+
+#: The default `Attrs`.
+DEFAULT_ATTRS = Attrs(
+ color="",
+ bgcolor="",
+ bold=False,
+ underline=False,
+ strike=False,
+ italic=False,
+ blink=False,
+ reverse=False,
+ hidden=False,
+)
+
+
+#: ``Attrs.bgcolor/fgcolor`` can be in either 'ffffff' format, or can be any of
+#: the following in case we want to take colors from the 8/16 color palette.
+#: Usually, in that case, the terminal application allows to configure the RGB
+#: values for these names.
+#: ISO 6429 colors
+ANSI_COLOR_NAMES = [
+ "ansidefault",
+ # Low intensity, dark. (One or two components 0x80, the other 0x00.)
+ "ansiblack",
+ "ansired",
+ "ansigreen",
+ "ansiyellow",
+ "ansiblue",
+ "ansimagenta",
+ "ansicyan",
+ "ansigray",
+ # High intensity, bright. (One or two components 0xff, the other 0x00. Not supported everywhere.)
+ "ansibrightblack",
+ "ansibrightred",
+ "ansibrightgreen",
+ "ansibrightyellow",
+ "ansibrightblue",
+ "ansibrightmagenta",
+ "ansibrightcyan",
+ "ansiwhite",
+]
+
+
+# People don't use the same ANSI color names everywhere. In prompt_toolkit 1.0
+# we used some unconventional names (which were contributed like that to
+# Pygments). This is fixed now, but we still support the old names.
+
+# The table below maps the old aliases to the current names.
+ANSI_COLOR_NAMES_ALIASES: dict[str, str] = {
+ "ansidarkgray": "ansibrightblack",
+ "ansiteal": "ansicyan",
+ "ansiturquoise": "ansibrightcyan",
+ "ansibrown": "ansiyellow",
+ "ansipurple": "ansimagenta",
+ "ansifuchsia": "ansibrightmagenta",
+ "ansilightgray": "ansigray",
+ "ansidarkred": "ansired",
+ "ansidarkgreen": "ansigreen",
+ "ansidarkblue": "ansiblue",
+}
+assert set(ANSI_COLOR_NAMES_ALIASES.values()).issubset(set(ANSI_COLOR_NAMES))
+assert not (set(ANSI_COLOR_NAMES_ALIASES.keys()) & set(ANSI_COLOR_NAMES))
+
+
+class BaseStyle(metaclass=ABCMeta):
+ """
+ Abstract base class for prompt_toolkit styles.
+ """
+
+ @abstractmethod
+ def get_attrs_for_style_str(
+ self, style_str: str, default: Attrs = DEFAULT_ATTRS
+ ) -> Attrs:
+ """
+ Return :class:`.Attrs` for the given style string.
+
+ :param style_str: The style string. This can contain inline styling as
+ well as classnames (e.g. "class:title").
+ :param default: `Attrs` to be used if no styling was defined.
+ """
+
+ @abstractproperty
+ def style_rules(self) -> list[tuple[str, str]]:
+ """
+ The list of style rules, used to create this style.
+ (Required for `DynamicStyle` and `_MergedStyle` to work.)
+ """
+ return []
+
+ @abstractmethod
+ def invalidation_hash(self) -> Hashable:
+ """
+ Invalidation hash for the style. When this changes over time, the
+ renderer knows that something in the style changed, and that everything
+ has to be redrawn.
+ """
+
+
+class DummyStyle(BaseStyle):
+ """
+ A style that doesn't style anything.
+ """
+
+ def get_attrs_for_style_str(
+ self, style_str: str, default: Attrs = DEFAULT_ATTRS
+ ) -> Attrs:
+ return default
+
+ def invalidation_hash(self) -> Hashable:
+ return 1 # Always the same value.
+
+ @property
+ def style_rules(self) -> list[tuple[str, str]]:
+ return []
+
+
+class DynamicStyle(BaseStyle):
+ """
+ Style class that can dynamically returns an other Style.
+
+ :param get_style: Callable that returns a :class:`.Style` instance.
+ """
+
+ def __init__(self, get_style: Callable[[], BaseStyle | None]):
+ self.get_style = get_style
+ self._dummy = DummyStyle()
+
+ def get_attrs_for_style_str(
+ self, style_str: str, default: Attrs = DEFAULT_ATTRS
+ ) -> Attrs:
+ style = self.get_style() or self._dummy
+
+ return style.get_attrs_for_style_str(style_str, default)
+
+ def invalidation_hash(self) -> Hashable:
+ return (self.get_style() or self._dummy).invalidation_hash()
+
+ @property
+ def style_rules(self) -> list[tuple[str, str]]:
+ return (self.get_style() or self._dummy).style_rules
diff --git a/src/prompt_toolkit/styles/defaults.py b/src/prompt_toolkit/styles/defaults.py
new file mode 100644
index 0000000..75b8dd2
--- /dev/null
+++ b/src/prompt_toolkit/styles/defaults.py
@@ -0,0 +1,235 @@
+"""
+The default styling.
+"""
+from __future__ import annotations
+
+from prompt_toolkit.cache import memoized
+
+from .base import ANSI_COLOR_NAMES, BaseStyle
+from .named_colors import NAMED_COLORS
+from .style import Style, merge_styles
+
+__all__ = [
+ "default_ui_style",
+ "default_pygments_style",
+]
+
+#: Default styling. Mapping from classnames to their style definition.
+PROMPT_TOOLKIT_STYLE = [
+ # Highlighting of search matches in document.
+ ("search", "bg:ansibrightyellow ansiblack"),
+ ("search.current", ""),
+ # Incremental search.
+ ("incsearch", ""),
+ ("incsearch.current", "reverse"),
+ # Highlighting of select text in document.
+ ("selected", "reverse"),
+ ("cursor-column", "bg:#dddddd"),
+ ("cursor-line", "underline"),
+ ("color-column", "bg:#ccaacc"),
+ # Highlighting of matching brackets.
+ ("matching-bracket", ""),
+ ("matching-bracket.other", "#000000 bg:#aacccc"),
+ ("matching-bracket.cursor", "#ff8888 bg:#880000"),
+ # Styling of other cursors, in case of block editing.
+ ("multiple-cursors", "#000000 bg:#ccccaa"),
+ # Line numbers.
+ ("line-number", "#888888"),
+ ("line-number.current", "bold"),
+ ("tilde", "#8888ff"),
+ # Default prompt.
+ ("prompt", ""),
+ ("prompt.arg", "noinherit"),
+ ("prompt.arg.text", ""),
+ ("prompt.search", "noinherit"),
+ ("prompt.search.text", ""),
+ # Search toolbar.
+ ("search-toolbar", "bold"),
+ ("search-toolbar.text", "nobold"),
+ # System toolbar
+ ("system-toolbar", "bold"),
+ ("system-toolbar.text", "nobold"),
+ # "arg" toolbar.
+ ("arg-toolbar", "bold"),
+ ("arg-toolbar.text", "nobold"),
+ # Validation toolbar.
+ ("validation-toolbar", "bg:#550000 #ffffff"),
+ ("window-too-small", "bg:#550000 #ffffff"),
+ # Completions toolbar.
+ ("completion-toolbar", "bg:#bbbbbb #000000"),
+ ("completion-toolbar.arrow", "bg:#bbbbbb #000000 bold"),
+ ("completion-toolbar.completion", "bg:#bbbbbb #000000"),
+ ("completion-toolbar.completion.current", "bg:#444444 #ffffff"),
+ # Completions menu.
+ ("completion-menu", "bg:#bbbbbb #000000"),
+ ("completion-menu.completion", ""),
+ # (Note: for the current completion, we use 'reverse' on top of fg/bg
+ # colors. This is to have proper rendering with NO_COLOR=1).
+ ("completion-menu.completion.current", "fg:#888888 bg:#ffffff reverse"),
+ ("completion-menu.meta.completion", "bg:#999999 #000000"),
+ ("completion-menu.meta.completion.current", "bg:#aaaaaa #000000"),
+ ("completion-menu.multi-column-meta", "bg:#aaaaaa #000000"),
+ # Fuzzy matches in completion menu (for FuzzyCompleter).
+ ("completion-menu.completion fuzzymatch.outside", "fg:#444444"),
+ ("completion-menu.completion fuzzymatch.inside", "bold"),
+ ("completion-menu.completion fuzzymatch.inside.character", "underline"),
+ ("completion-menu.completion.current fuzzymatch.outside", "fg:default"),
+ ("completion-menu.completion.current fuzzymatch.inside", "nobold"),
+ # Styling of readline-like completions.
+ ("readline-like-completions", ""),
+ ("readline-like-completions.completion", ""),
+ ("readline-like-completions.completion fuzzymatch.outside", "#888888"),
+ ("readline-like-completions.completion fuzzymatch.inside", ""),
+ ("readline-like-completions.completion fuzzymatch.inside.character", "underline"),
+ # Scrollbars.
+ ("scrollbar.background", "bg:#aaaaaa"),
+ ("scrollbar.button", "bg:#444444"),
+ ("scrollbar.arrow", "noinherit bold"),
+ # Start/end of scrollbars. Adding 'underline' here provides a nice little
+ # detail to the progress bar, but it doesn't look good on all terminals.
+ # ('scrollbar.start', 'underline #ffffff'),
+ # ('scrollbar.end', 'underline #000000'),
+ # Auto suggestion text.
+ ("auto-suggestion", "#666666"),
+ # Trailing whitespace and tabs.
+ ("trailing-whitespace", "#999999"),
+ ("tab", "#999999"),
+ # When Control-C/D has been pressed. Grayed.
+ ("aborting", "#888888 bg:default noreverse noitalic nounderline noblink"),
+ ("exiting", "#888888 bg:default noreverse noitalic nounderline noblink"),
+ # Entering a Vi digraph.
+ ("digraph", "#4444ff"),
+ # Control characters, like ^C, ^X.
+ ("control-character", "ansiblue"),
+ # Non-breaking space.
+ ("nbsp", "underline ansiyellow"),
+ # Default styling of HTML elements.
+ ("i", "italic"),
+ ("u", "underline"),
+ ("s", "strike"),
+ ("b", "bold"),
+ ("em", "italic"),
+ ("strong", "bold"),
+ ("del", "strike"),
+ ("hidden", "hidden"),
+ # It should be possible to use the style names in HTML.
+ # <reverse>...</reverse> or <noreverse>...</noreverse>.
+ ("italic", "italic"),
+ ("underline", "underline"),
+ ("strike", "strike"),
+ ("bold", "bold"),
+ ("reverse", "reverse"),
+ ("noitalic", "noitalic"),
+ ("nounderline", "nounderline"),
+ ("nostrike", "nostrike"),
+ ("nobold", "nobold"),
+ ("noreverse", "noreverse"),
+ # Prompt bottom toolbar
+ ("bottom-toolbar", "reverse"),
+]
+
+
+# Style that will turn for instance the class 'red' into 'red'.
+COLORS_STYLE = [(name, "fg:" + name) for name in ANSI_COLOR_NAMES] + [
+ (name.lower(), "fg:" + name) for name in NAMED_COLORS
+]
+
+
+WIDGETS_STYLE = [
+ # Dialog windows.
+ ("dialog", "bg:#4444ff"),
+ ("dialog.body", "bg:#ffffff #000000"),
+ ("dialog.body text-area", "bg:#cccccc"),
+ ("dialog.body text-area last-line", "underline"),
+ ("dialog frame.label", "#ff0000 bold"),
+ # Scrollbars in dialogs.
+ ("dialog.body scrollbar.background", ""),
+ ("dialog.body scrollbar.button", "bg:#000000"),
+ ("dialog.body scrollbar.arrow", ""),
+ ("dialog.body scrollbar.start", "nounderline"),
+ ("dialog.body scrollbar.end", "nounderline"),
+ # Buttons.
+ ("button", ""),
+ ("button.arrow", "bold"),
+ ("button.focused", "bg:#aa0000 #ffffff"),
+ # Menu bars.
+ ("menu-bar", "bg:#aaaaaa #000000"),
+ ("menu-bar.selected-item", "bg:#ffffff #000000"),
+ ("menu", "bg:#888888 #ffffff"),
+ ("menu.border", "#aaaaaa"),
+ ("menu.border shadow", "#444444"),
+ # Shadows.
+ ("dialog shadow", "bg:#000088"),
+ ("dialog.body shadow", "bg:#aaaaaa"),
+ ("progress-bar", "bg:#000088"),
+ ("progress-bar.used", "bg:#ff0000"),
+]
+
+
+# The default Pygments style, include this by default in case a Pygments lexer
+# is used.
+PYGMENTS_DEFAULT_STYLE = {
+ "pygments.whitespace": "#bbbbbb",
+ "pygments.comment": "italic #408080",
+ "pygments.comment.preproc": "noitalic #bc7a00",
+ "pygments.keyword": "bold #008000",
+ "pygments.keyword.pseudo": "nobold",
+ "pygments.keyword.type": "nobold #b00040",
+ "pygments.operator": "#666666",
+ "pygments.operator.word": "bold #aa22ff",
+ "pygments.name.builtin": "#008000",
+ "pygments.name.function": "#0000ff",
+ "pygments.name.class": "bold #0000ff",
+ "pygments.name.namespace": "bold #0000ff",
+ "pygments.name.exception": "bold #d2413a",
+ "pygments.name.variable": "#19177c",
+ "pygments.name.constant": "#880000",
+ "pygments.name.label": "#a0a000",
+ "pygments.name.entity": "bold #999999",
+ "pygments.name.attribute": "#7d9029",
+ "pygments.name.tag": "bold #008000",
+ "pygments.name.decorator": "#aa22ff",
+ # Note: In Pygments, Token.String is an alias for Token.Literal.String,
+ # and Token.Number as an alias for Token.Literal.Number.
+ "pygments.literal.string": "#ba2121",
+ "pygments.literal.string.doc": "italic",
+ "pygments.literal.string.interpol": "bold #bb6688",
+ "pygments.literal.string.escape": "bold #bb6622",
+ "pygments.literal.string.regex": "#bb6688",
+ "pygments.literal.string.symbol": "#19177c",
+ "pygments.literal.string.other": "#008000",
+ "pygments.literal.number": "#666666",
+ "pygments.generic.heading": "bold #000080",
+ "pygments.generic.subheading": "bold #800080",
+ "pygments.generic.deleted": "#a00000",
+ "pygments.generic.inserted": "#00a000",
+ "pygments.generic.error": "#ff0000",
+ "pygments.generic.emph": "italic",
+ "pygments.generic.strong": "bold",
+ "pygments.generic.prompt": "bold #000080",
+ "pygments.generic.output": "#888",
+ "pygments.generic.traceback": "#04d",
+ "pygments.error": "border:#ff0000",
+}
+
+
+@memoized()
+def default_ui_style() -> BaseStyle:
+ """
+ Create a default `Style` object.
+ """
+ return merge_styles(
+ [
+ Style(PROMPT_TOOLKIT_STYLE),
+ Style(COLORS_STYLE),
+ Style(WIDGETS_STYLE),
+ ]
+ )
+
+
+@memoized()
+def default_pygments_style() -> Style:
+ """
+ Create a `Style` object that contains the default Pygments style.
+ """
+ return Style.from_dict(PYGMENTS_DEFAULT_STYLE)
diff --git a/src/prompt_toolkit/styles/named_colors.py b/src/prompt_toolkit/styles/named_colors.py
new file mode 100644
index 0000000..0395c8b
--- /dev/null
+++ b/src/prompt_toolkit/styles/named_colors.py
@@ -0,0 +1,161 @@
+"""
+All modern web browsers support these 140 color names.
+Taken from: https://www.w3schools.com/colors/colors_names.asp
+"""
+from __future__ import annotations
+
+__all__ = [
+ "NAMED_COLORS",
+]
+
+
+NAMED_COLORS: dict[str, str] = {
+ "AliceBlue": "#f0f8ff",
+ "AntiqueWhite": "#faebd7",
+ "Aqua": "#00ffff",
+ "Aquamarine": "#7fffd4",
+ "Azure": "#f0ffff",
+ "Beige": "#f5f5dc",
+ "Bisque": "#ffe4c4",
+ "Black": "#000000",
+ "BlanchedAlmond": "#ffebcd",
+ "Blue": "#0000ff",
+ "BlueViolet": "#8a2be2",
+ "Brown": "#a52a2a",
+ "BurlyWood": "#deb887",
+ "CadetBlue": "#5f9ea0",
+ "Chartreuse": "#7fff00",
+ "Chocolate": "#d2691e",
+ "Coral": "#ff7f50",
+ "CornflowerBlue": "#6495ed",
+ "Cornsilk": "#fff8dc",
+ "Crimson": "#dc143c",
+ "Cyan": "#00ffff",
+ "DarkBlue": "#00008b",
+ "DarkCyan": "#008b8b",
+ "DarkGoldenRod": "#b8860b",
+ "DarkGray": "#a9a9a9",
+ "DarkGreen": "#006400",
+ "DarkGrey": "#a9a9a9",
+ "DarkKhaki": "#bdb76b",
+ "DarkMagenta": "#8b008b",
+ "DarkOliveGreen": "#556b2f",
+ "DarkOrange": "#ff8c00",
+ "DarkOrchid": "#9932cc",
+ "DarkRed": "#8b0000",
+ "DarkSalmon": "#e9967a",
+ "DarkSeaGreen": "#8fbc8f",
+ "DarkSlateBlue": "#483d8b",
+ "DarkSlateGray": "#2f4f4f",
+ "DarkSlateGrey": "#2f4f4f",
+ "DarkTurquoise": "#00ced1",
+ "DarkViolet": "#9400d3",
+ "DeepPink": "#ff1493",
+ "DeepSkyBlue": "#00bfff",
+ "DimGray": "#696969",
+ "DimGrey": "#696969",
+ "DodgerBlue": "#1e90ff",
+ "FireBrick": "#b22222",
+ "FloralWhite": "#fffaf0",
+ "ForestGreen": "#228b22",
+ "Fuchsia": "#ff00ff",
+ "Gainsboro": "#dcdcdc",
+ "GhostWhite": "#f8f8ff",
+ "Gold": "#ffd700",
+ "GoldenRod": "#daa520",
+ "Gray": "#808080",
+ "Green": "#008000",
+ "GreenYellow": "#adff2f",
+ "Grey": "#808080",
+ "HoneyDew": "#f0fff0",
+ "HotPink": "#ff69b4",
+ "IndianRed": "#cd5c5c",
+ "Indigo": "#4b0082",
+ "Ivory": "#fffff0",
+ "Khaki": "#f0e68c",
+ "Lavender": "#e6e6fa",
+ "LavenderBlush": "#fff0f5",
+ "LawnGreen": "#7cfc00",
+ "LemonChiffon": "#fffacd",
+ "LightBlue": "#add8e6",
+ "LightCoral": "#f08080",
+ "LightCyan": "#e0ffff",
+ "LightGoldenRodYellow": "#fafad2",
+ "LightGray": "#d3d3d3",
+ "LightGreen": "#90ee90",
+ "LightGrey": "#d3d3d3",
+ "LightPink": "#ffb6c1",
+ "LightSalmon": "#ffa07a",
+ "LightSeaGreen": "#20b2aa",
+ "LightSkyBlue": "#87cefa",
+ "LightSlateGray": "#778899",
+ "LightSlateGrey": "#778899",
+ "LightSteelBlue": "#b0c4de",
+ "LightYellow": "#ffffe0",
+ "Lime": "#00ff00",
+ "LimeGreen": "#32cd32",
+ "Linen": "#faf0e6",
+ "Magenta": "#ff00ff",
+ "Maroon": "#800000",
+ "MediumAquaMarine": "#66cdaa",
+ "MediumBlue": "#0000cd",
+ "MediumOrchid": "#ba55d3",
+ "MediumPurple": "#9370db",
+ "MediumSeaGreen": "#3cb371",
+ "MediumSlateBlue": "#7b68ee",
+ "MediumSpringGreen": "#00fa9a",
+ "MediumTurquoise": "#48d1cc",
+ "MediumVioletRed": "#c71585",
+ "MidnightBlue": "#191970",
+ "MintCream": "#f5fffa",
+ "MistyRose": "#ffe4e1",
+ "Moccasin": "#ffe4b5",
+ "NavajoWhite": "#ffdead",
+ "Navy": "#000080",
+ "OldLace": "#fdf5e6",
+ "Olive": "#808000",
+ "OliveDrab": "#6b8e23",
+ "Orange": "#ffa500",
+ "OrangeRed": "#ff4500",
+ "Orchid": "#da70d6",
+ "PaleGoldenRod": "#eee8aa",
+ "PaleGreen": "#98fb98",
+ "PaleTurquoise": "#afeeee",
+ "PaleVioletRed": "#db7093",
+ "PapayaWhip": "#ffefd5",
+ "PeachPuff": "#ffdab9",
+ "Peru": "#cd853f",
+ "Pink": "#ffc0cb",
+ "Plum": "#dda0dd",
+ "PowderBlue": "#b0e0e6",
+ "Purple": "#800080",
+ "RebeccaPurple": "#663399",
+ "Red": "#ff0000",
+ "RosyBrown": "#bc8f8f",
+ "RoyalBlue": "#4169e1",
+ "SaddleBrown": "#8b4513",
+ "Salmon": "#fa8072",
+ "SandyBrown": "#f4a460",
+ "SeaGreen": "#2e8b57",
+ "SeaShell": "#fff5ee",
+ "Sienna": "#a0522d",
+ "Silver": "#c0c0c0",
+ "SkyBlue": "#87ceeb",
+ "SlateBlue": "#6a5acd",
+ "SlateGray": "#708090",
+ "SlateGrey": "#708090",
+ "Snow": "#fffafa",
+ "SpringGreen": "#00ff7f",
+ "SteelBlue": "#4682b4",
+ "Tan": "#d2b48c",
+ "Teal": "#008080",
+ "Thistle": "#d8bfd8",
+ "Tomato": "#ff6347",
+ "Turquoise": "#40e0d0",
+ "Violet": "#ee82ee",
+ "Wheat": "#f5deb3",
+ "White": "#ffffff",
+ "WhiteSmoke": "#f5f5f5",
+ "Yellow": "#ffff00",
+ "YellowGreen": "#9acd32",
+}
diff --git a/src/prompt_toolkit/styles/pygments.py b/src/prompt_toolkit/styles/pygments.py
new file mode 100644
index 0000000..3e101f1
--- /dev/null
+++ b/src/prompt_toolkit/styles/pygments.py
@@ -0,0 +1,69 @@
+"""
+Adaptor for building prompt_toolkit styles, starting from a Pygments style.
+
+Usage::
+
+ from pygments.styles.tango import TangoStyle
+ style = style_from_pygments_cls(pygments_style_cls=TangoStyle)
+"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from .style import Style
+
+if TYPE_CHECKING:
+ from pygments.style import Style as PygmentsStyle
+ from pygments.token import Token
+
+
+__all__ = [
+ "style_from_pygments_cls",
+ "style_from_pygments_dict",
+ "pygments_token_to_classname",
+]
+
+
+def style_from_pygments_cls(pygments_style_cls: type[PygmentsStyle]) -> Style:
+ """
+ Shortcut to create a :class:`.Style` instance from a Pygments style class
+ and a style dictionary.
+
+ Example::
+
+ from prompt_toolkit.styles.from_pygments import style_from_pygments_cls
+ from pygments.styles import get_style_by_name
+ style = style_from_pygments_cls(get_style_by_name('monokai'))
+
+ :param pygments_style_cls: Pygments style class to start from.
+ """
+ # Import inline.
+ from pygments.style import Style as PygmentsStyle
+
+ assert issubclass(pygments_style_cls, PygmentsStyle)
+
+ return style_from_pygments_dict(pygments_style_cls.styles)
+
+
+def style_from_pygments_dict(pygments_dict: dict[Token, str]) -> Style:
+ """
+ Create a :class:`.Style` instance from a Pygments style dictionary.
+ (One that maps Token objects to style strings.)
+ """
+ pygments_style = []
+
+ for token, style in pygments_dict.items():
+ pygments_style.append((pygments_token_to_classname(token), style))
+
+ return Style(pygments_style)
+
+
+def pygments_token_to_classname(token: Token) -> str:
+ """
+ Turn e.g. `Token.Name.Exception` into `'pygments.name.exception'`.
+
+ (Our Pygments lexer will also turn the tokens that pygments produces in a
+ prompt_toolkit list of fragments that match these styling rules.)
+ """
+ parts = ("pygments",) + token
+ return ".".join(parts).lower()
diff --git a/src/prompt_toolkit/styles/style.py b/src/prompt_toolkit/styles/style.py
new file mode 100644
index 0000000..1abee0f
--- /dev/null
+++ b/src/prompt_toolkit/styles/style.py
@@ -0,0 +1,400 @@
+"""
+Tool for creating styles from a dictionary.
+"""
+from __future__ import annotations
+
+import itertools
+import re
+from enum import Enum
+from typing import Hashable, TypeVar
+
+from prompt_toolkit.cache import SimpleCache
+
+from .base import (
+ ANSI_COLOR_NAMES,
+ ANSI_COLOR_NAMES_ALIASES,
+ DEFAULT_ATTRS,
+ Attrs,
+ BaseStyle,
+)
+from .named_colors import NAMED_COLORS
+
+__all__ = [
+ "Style",
+ "parse_color",
+ "Priority",
+ "merge_styles",
+]
+
+_named_colors_lowercase = {k.lower(): v.lstrip("#") for k, v in NAMED_COLORS.items()}
+
+
+def parse_color(text: str) -> str:
+ """
+ Parse/validate color format.
+
+ Like in Pygments, but also support the ANSI color names.
+ (These will map to the colors of the 16 color palette.)
+ """
+ # ANSI color names.
+ if text in ANSI_COLOR_NAMES:
+ return text
+ if text in ANSI_COLOR_NAMES_ALIASES:
+ return ANSI_COLOR_NAMES_ALIASES[text]
+
+ # 140 named colors.
+ try:
+ # Replace by 'hex' value.
+ return _named_colors_lowercase[text.lower()]
+ except KeyError:
+ pass
+
+ # Hex codes.
+ if text[0:1] == "#":
+ col = text[1:]
+
+ # Keep this for backwards-compatibility (Pygments does it).
+ # I don't like the '#' prefix for named colors.
+ if col in ANSI_COLOR_NAMES:
+ return col
+ elif col in ANSI_COLOR_NAMES_ALIASES:
+ return ANSI_COLOR_NAMES_ALIASES[col]
+
+ # 6 digit hex color.
+ elif len(col) == 6:
+ return col
+
+ # 3 digit hex color.
+ elif len(col) == 3:
+ return col[0] * 2 + col[1] * 2 + col[2] * 2
+
+ # Default.
+ elif text in ("", "default"):
+ return text
+
+ raise ValueError("Wrong color format %r" % text)
+
+
+# Attributes, when they are not filled in by a style. None means that we take
+# the value from the parent.
+_EMPTY_ATTRS = Attrs(
+ color=None,
+ bgcolor=None,
+ bold=None,
+ underline=None,
+ strike=None,
+ italic=None,
+ blink=None,
+ reverse=None,
+ hidden=None,
+)
+
+
+def _expand_classname(classname: str) -> list[str]:
+ """
+ Split a single class name at the `.` operator, and build a list of classes.
+
+ E.g. 'a.b.c' becomes ['a', 'a.b', 'a.b.c']
+ """
+ result = []
+ parts = classname.split(".")
+
+ for i in range(1, len(parts) + 1):
+ result.append(".".join(parts[:i]).lower())
+
+ return result
+
+
+def _parse_style_str(style_str: str) -> Attrs:
+ """
+ Take a style string, e.g. 'bg:red #88ff00 class:title'
+ and return a `Attrs` instance.
+ """
+ # Start from default Attrs.
+ if "noinherit" in style_str:
+ attrs = DEFAULT_ATTRS
+ else:
+ attrs = _EMPTY_ATTRS
+
+ # Now update with the given attributes.
+ for part in style_str.split():
+ if part == "noinherit":
+ pass
+ elif part == "bold":
+ attrs = attrs._replace(bold=True)
+ elif part == "nobold":
+ attrs = attrs._replace(bold=False)
+ elif part == "italic":
+ attrs = attrs._replace(italic=True)
+ elif part == "noitalic":
+ attrs = attrs._replace(italic=False)
+ elif part == "underline":
+ attrs = attrs._replace(underline=True)
+ elif part == "nounderline":
+ attrs = attrs._replace(underline=False)
+ elif part == "strike":
+ attrs = attrs._replace(strike=True)
+ elif part == "nostrike":
+ attrs = attrs._replace(strike=False)
+
+ # prompt_toolkit extensions. Not in Pygments.
+ elif part == "blink":
+ attrs = attrs._replace(blink=True)
+ elif part == "noblink":
+ attrs = attrs._replace(blink=False)
+ elif part == "reverse":
+ attrs = attrs._replace(reverse=True)
+ elif part == "noreverse":
+ attrs = attrs._replace(reverse=False)
+ elif part == "hidden":
+ attrs = attrs._replace(hidden=True)
+ elif part == "nohidden":
+ attrs = attrs._replace(hidden=False)
+
+ # Pygments properties that we ignore.
+ elif part in ("roman", "sans", "mono"):
+ pass
+ elif part.startswith("border:"):
+ pass
+
+ # Ignore pieces in between square brackets. This is internal stuff.
+ # Like '[transparent]' or '[set-cursor-position]'.
+ elif part.startswith("[") and part.endswith("]"):
+ pass
+
+ # Colors.
+ elif part.startswith("bg:"):
+ attrs = attrs._replace(bgcolor=parse_color(part[3:]))
+ elif part.startswith("fg:"): # The 'fg:' prefix is optional.
+ attrs = attrs._replace(color=parse_color(part[3:]))
+ else:
+ attrs = attrs._replace(color=parse_color(part))
+
+ return attrs
+
+
+CLASS_NAMES_RE = re.compile(r"^[a-z0-9.\s_-]*$") # This one can't contain a comma!
+
+
+class Priority(Enum):
+ """
+ The priority of the rules, when a style is created from a dictionary.
+
+ In a `Style`, rules that are defined later will always override previous
+ defined rules, however in a dictionary, the key order was arbitrary before
+ Python 3.6. This means that the style could change at random between rules.
+
+ We have two options:
+
+ - `DICT_KEY_ORDER`: This means, iterate through the dictionary, and take
+ the key/value pairs in order as they come. This is a good option if you
+ have Python >3.6. Rules at the end will override rules at the beginning.
+ - `MOST_PRECISE`: keys that are defined with most precision will get higher
+ priority. (More precise means: more elements.)
+ """
+
+ DICT_KEY_ORDER = "KEY_ORDER"
+ MOST_PRECISE = "MOST_PRECISE"
+
+
+# We don't support Python versions older than 3.6 anymore, so we can always
+# depend on dictionary ordering. This is the default.
+default_priority = Priority.DICT_KEY_ORDER
+
+
+class Style(BaseStyle):
+ """
+ Create a ``Style`` instance from a list of style rules.
+
+ The `style_rules` is supposed to be a list of ('classnames', 'style') tuples.
+ The classnames are a whitespace separated string of class names and the
+ style string is just like a Pygments style definition, but with a few
+ additions: it supports 'reverse' and 'blink'.
+
+ Later rules always override previous rules.
+
+ Usage::
+
+ Style([
+ ('title', '#ff0000 bold underline'),
+ ('something-else', 'reverse'),
+ ('class1 class2', 'reverse'),
+ ])
+
+ The ``from_dict`` classmethod is similar, but takes a dictionary as input.
+ """
+
+ def __init__(self, style_rules: list[tuple[str, str]]) -> None:
+ class_names_and_attrs = []
+
+ # Loop through the rules in the order they were defined.
+ # Rules that are defined later get priority.
+ for class_names, style_str in style_rules:
+ assert CLASS_NAMES_RE.match(class_names), repr(class_names)
+
+ # The order of the class names doesn't matter.
+ # (But the order of rules does matter.)
+ class_names_set = frozenset(class_names.lower().split())
+ attrs = _parse_style_str(style_str)
+
+ class_names_and_attrs.append((class_names_set, attrs))
+
+ self._style_rules = style_rules
+ self.class_names_and_attrs = class_names_and_attrs
+
+ @property
+ def style_rules(self) -> list[tuple[str, str]]:
+ return self._style_rules
+
+ @classmethod
+ def from_dict(
+ cls, style_dict: dict[str, str], priority: Priority = default_priority
+ ) -> Style:
+ """
+ :param style_dict: Style dictionary.
+ :param priority: `Priority` value.
+ """
+ if priority == Priority.MOST_PRECISE:
+
+ def key(item: tuple[str, str]) -> int:
+ # Split on '.' and whitespace. Count elements.
+ return sum(len(i.split(".")) for i in item[0].split())
+
+ return cls(sorted(style_dict.items(), key=key))
+ else:
+ return cls(list(style_dict.items()))
+
+ def get_attrs_for_style_str(
+ self, style_str: str, default: Attrs = DEFAULT_ATTRS
+ ) -> Attrs:
+ """
+ Get `Attrs` for the given style string.
+ """
+ list_of_attrs = [default]
+ class_names: set[str] = set()
+
+ # Apply default styling.
+ for names, attr in self.class_names_and_attrs:
+ if not names:
+ list_of_attrs.append(attr)
+
+ # Go from left to right through the style string. Things on the right
+ # take precedence.
+ for part in style_str.split():
+ # This part represents a class.
+ # Do lookup of this class name in the style definition, as well
+ # as all class combinations that we have so far.
+ if part.startswith("class:"):
+ # Expand all class names (comma separated list).
+ new_class_names = []
+ for p in part[6:].lower().split(","):
+ new_class_names.extend(_expand_classname(p))
+
+ for new_name in new_class_names:
+ # Build a set of all possible class combinations to be applied.
+ combos = set()
+ combos.add(frozenset([new_name]))
+
+ for count in range(1, len(class_names) + 1):
+ for c2 in itertools.combinations(class_names, count):
+ combos.add(frozenset(c2 + (new_name,)))
+
+ # Apply the styles that match these class names.
+ for names, attr in self.class_names_and_attrs:
+ if names in combos:
+ list_of_attrs.append(attr)
+
+ class_names.add(new_name)
+
+ # Process inline style.
+ else:
+ inline_attrs = _parse_style_str(part)
+ list_of_attrs.append(inline_attrs)
+
+ return _merge_attrs(list_of_attrs)
+
+ def invalidation_hash(self) -> Hashable:
+ return id(self.class_names_and_attrs)
+
+
+_T = TypeVar("_T")
+
+
+def _merge_attrs(list_of_attrs: list[Attrs]) -> Attrs:
+ """
+ Take a list of :class:`.Attrs` instances and merge them into one.
+ Every `Attr` in the list can override the styling of the previous one. So,
+ the last one has highest priority.
+ """
+
+ def _or(*values: _T) -> _T:
+ "Take first not-None value, starting at the end."
+ for v in values[::-1]:
+ if v is not None:
+ return v
+ raise ValueError # Should not happen, there's always one non-null value.
+
+ return Attrs(
+ color=_or("", *[a.color for a in list_of_attrs]),
+ bgcolor=_or("", *[a.bgcolor for a in list_of_attrs]),
+ bold=_or(False, *[a.bold for a in list_of_attrs]),
+ underline=_or(False, *[a.underline for a in list_of_attrs]),
+ strike=_or(False, *[a.strike for a in list_of_attrs]),
+ italic=_or(False, *[a.italic for a in list_of_attrs]),
+ blink=_or(False, *[a.blink for a in list_of_attrs]),
+ reverse=_or(False, *[a.reverse for a in list_of_attrs]),
+ hidden=_or(False, *[a.hidden for a in list_of_attrs]),
+ )
+
+
+def merge_styles(styles: list[BaseStyle]) -> _MergedStyle:
+ """
+ Merge multiple `Style` objects.
+ """
+ styles = [s for s in styles if s is not None]
+ return _MergedStyle(styles)
+
+
+class _MergedStyle(BaseStyle):
+ """
+ Merge multiple `Style` objects into one.
+ This is supposed to ensure consistency: if any of the given styles changes,
+ then this style will be updated.
+ """
+
+ # NOTE: previously, we used an algorithm where we did not generate the
+ # combined style. Instead this was a proxy that called one style
+ # after the other, passing the outcome of the previous style as the
+ # default for the next one. This did not work, because that way, the
+ # priorities like described in the `Style` class don't work.
+ # 'class:aborted' was for instance never displayed in gray, because
+ # the next style specified a default color for any text. (The
+ # explicit styling of class:aborted should have taken priority,
+ # because it was more precise.)
+ def __init__(self, styles: list[BaseStyle]) -> None:
+ self.styles = styles
+ self._style: SimpleCache[Hashable, Style] = SimpleCache(maxsize=1)
+
+ @property
+ def _merged_style(self) -> Style:
+ "The `Style` object that has the other styles merged together."
+
+ def get() -> Style:
+ return Style(self.style_rules)
+
+ return self._style.get(self.invalidation_hash(), get)
+
+ @property
+ def style_rules(self) -> list[tuple[str, str]]:
+ style_rules = []
+ for s in self.styles:
+ style_rules.extend(s.style_rules)
+ return style_rules
+
+ def get_attrs_for_style_str(
+ self, style_str: str, default: Attrs = DEFAULT_ATTRS
+ ) -> Attrs:
+ return self._merged_style.get_attrs_for_style_str(style_str, default)
+
+ def invalidation_hash(self) -> Hashable:
+ return tuple(s.invalidation_hash() for s in self.styles)
diff --git a/src/prompt_toolkit/styles/style_transformation.py b/src/prompt_toolkit/styles/style_transformation.py
new file mode 100644
index 0000000..fbb5a63
--- /dev/null
+++ b/src/prompt_toolkit/styles/style_transformation.py
@@ -0,0 +1,373 @@
+"""
+Collection of style transformations.
+
+Think of it as a kind of color post processing after the rendering is done.
+This could be used for instance to change the contrast/saturation; swap light
+and dark colors or even change certain colors for other colors.
+
+When the UI is rendered, these transformations can be applied right after the
+style strings are turned into `Attrs` objects that represent the actual
+formatting.
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from colorsys import hls_to_rgb, rgb_to_hls
+from typing import Callable, Hashable, Sequence
+
+from prompt_toolkit.cache import memoized
+from prompt_toolkit.filters import FilterOrBool, to_filter
+from prompt_toolkit.utils import AnyFloat, to_float, to_str
+
+from .base import ANSI_COLOR_NAMES, Attrs
+from .style import parse_color
+
+__all__ = [
+ "StyleTransformation",
+ "SwapLightAndDarkStyleTransformation",
+ "ReverseStyleTransformation",
+ "SetDefaultColorStyleTransformation",
+ "AdjustBrightnessStyleTransformation",
+ "DummyStyleTransformation",
+ "ConditionalStyleTransformation",
+ "DynamicStyleTransformation",
+ "merge_style_transformations",
+]
+
+
+class StyleTransformation(metaclass=ABCMeta):
+ """
+ Base class for any style transformation.
+ """
+
+ @abstractmethod
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ """
+ Take an `Attrs` object and return a new `Attrs` object.
+
+ Remember that the color formats can be either "ansi..." or a 6 digit
+ lowercase hexadecimal color (without '#' prefix).
+ """
+
+ def invalidation_hash(self) -> Hashable:
+ """
+ When this changes, the cache should be invalidated.
+ """
+ return f"{self.__class__.__name__}-{id(self)}"
+
+
+class SwapLightAndDarkStyleTransformation(StyleTransformation):
+ """
+ Turn dark colors into light colors and the other way around.
+
+ This is meant to make color schemes that work on a dark background usable
+ on a light background (and the other way around).
+
+ Notice that this doesn't swap foreground and background like "reverse"
+ does. It turns light green into dark green and the other way around.
+ Foreground and background colors are considered individually.
+
+ Also notice that when <reverse> is used somewhere and no colors are given
+ in particular (like what is the default for the bottom toolbar), then this
+ doesn't change anything. This is what makes sense, because when the
+ 'default' color is chosen, it's what works best for the terminal, and
+ reverse works good with that.
+ """
+
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ """
+ Return the `Attrs` used when opposite luminosity should be used.
+ """
+ # Reverse colors.
+ attrs = attrs._replace(color=get_opposite_color(attrs.color))
+ attrs = attrs._replace(bgcolor=get_opposite_color(attrs.bgcolor))
+
+ return attrs
+
+
+class ReverseStyleTransformation(StyleTransformation):
+ """
+ Swap the 'reverse' attribute.
+
+ (This is still experimental.)
+ """
+
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ return attrs._replace(reverse=not attrs.reverse)
+
+
+class SetDefaultColorStyleTransformation(StyleTransformation):
+ """
+ Set default foreground/background color for output that doesn't specify
+ anything. This is useful for overriding the terminal default colors.
+
+ :param fg: Color string or callable that returns a color string for the
+ foreground.
+ :param bg: Like `fg`, but for the background.
+ """
+
+ def __init__(
+ self, fg: str | Callable[[], str], bg: str | Callable[[], str]
+ ) -> None:
+ self.fg = fg
+ self.bg = bg
+
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ if attrs.bgcolor in ("", "default"):
+ attrs = attrs._replace(bgcolor=parse_color(to_str(self.bg)))
+
+ if attrs.color in ("", "default"):
+ attrs = attrs._replace(color=parse_color(to_str(self.fg)))
+
+ return attrs
+
+ def invalidation_hash(self) -> Hashable:
+ return (
+ "set-default-color",
+ to_str(self.fg),
+ to_str(self.bg),
+ )
+
+
+class AdjustBrightnessStyleTransformation(StyleTransformation):
+ """
+ Adjust the brightness to improve the rendering on either dark or light
+ backgrounds.
+
+ For dark backgrounds, it's best to increase `min_brightness`. For light
+ backgrounds it's best to decrease `max_brightness`. Usually, only one
+ setting is adjusted.
+
+ This will only change the brightness for text that has a foreground color
+ defined, but no background color. It works best for 256 or true color
+ output.
+
+ .. note:: Notice that there is no universal way to detect whether the
+ application is running in a light or dark terminal. As a
+ developer of an command line application, you'll have to make
+ this configurable for the user.
+
+ :param min_brightness: Float between 0.0 and 1.0 or a callable that returns
+ a float.
+ :param max_brightness: Float between 0.0 and 1.0 or a callable that returns
+ a float.
+ """
+
+ def __init__(
+ self, min_brightness: AnyFloat = 0.0, max_brightness: AnyFloat = 1.0
+ ) -> None:
+ self.min_brightness = min_brightness
+ self.max_brightness = max_brightness
+
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ min_brightness = to_float(self.min_brightness)
+ max_brightness = to_float(self.max_brightness)
+ assert 0 <= min_brightness <= 1
+ assert 0 <= max_brightness <= 1
+
+ # Don't do anything if the whole brightness range is acceptable.
+ # This also avoids turning ansi colors into RGB sequences.
+ if min_brightness == 0.0 and max_brightness == 1.0:
+ return attrs
+
+ # If a foreground color is given without a background color.
+ no_background = not attrs.bgcolor or attrs.bgcolor == "default"
+ has_fgcolor = attrs.color and attrs.color != "ansidefault"
+
+ if has_fgcolor and no_background:
+ # Calculate new RGB values.
+ r, g, b = self._color_to_rgb(attrs.color or "")
+ hue, brightness, saturation = rgb_to_hls(r, g, b)
+ brightness = self._interpolate_brightness(
+ brightness, min_brightness, max_brightness
+ )
+ r, g, b = hls_to_rgb(hue, brightness, saturation)
+ new_color = f"{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
+
+ attrs = attrs._replace(color=new_color)
+
+ return attrs
+
+ def _color_to_rgb(self, color: str) -> tuple[float, float, float]:
+ """
+ Parse `style.Attrs` color into RGB tuple.
+ """
+ # Do RGB lookup for ANSI colors.
+ try:
+ from prompt_toolkit.output.vt100 import ANSI_COLORS_TO_RGB
+
+ r, g, b = ANSI_COLORS_TO_RGB[color]
+ return r / 255.0, g / 255.0, b / 255.0
+ except KeyError:
+ pass
+
+ # Parse RRGGBB format.
+ return (
+ int(color[0:2], 16) / 255.0,
+ int(color[2:4], 16) / 255.0,
+ int(color[4:6], 16) / 255.0,
+ )
+
+ # NOTE: we don't have to support named colors here. They are already
+ # transformed into RGB values in `style.parse_color`.
+
+ def _interpolate_brightness(
+ self, value: float, min_brightness: float, max_brightness: float
+ ) -> float:
+ """
+ Map the brightness to the (min_brightness..max_brightness) range.
+ """
+ return min_brightness + (max_brightness - min_brightness) * value
+
+ def invalidation_hash(self) -> Hashable:
+ return (
+ "adjust-brightness",
+ to_float(self.min_brightness),
+ to_float(self.max_brightness),
+ )
+
+
+class DummyStyleTransformation(StyleTransformation):
+ """
+ Don't transform anything at all.
+ """
+
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ return attrs
+
+ def invalidation_hash(self) -> Hashable:
+ # Always return the same hash for these dummy instances.
+ return "dummy-style-transformation"
+
+
+class DynamicStyleTransformation(StyleTransformation):
+ """
+ StyleTransformation class that can dynamically returns any
+ `StyleTransformation`.
+
+ :param get_style_transformation: Callable that returns a
+ :class:`.StyleTransformation` instance.
+ """
+
+ def __init__(
+ self, get_style_transformation: Callable[[], StyleTransformation | None]
+ ) -> None:
+ self.get_style_transformation = get_style_transformation
+
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ style_transformation = (
+ self.get_style_transformation() or DummyStyleTransformation()
+ )
+ return style_transformation.transform_attrs(attrs)
+
+ def invalidation_hash(self) -> Hashable:
+ style_transformation = (
+ self.get_style_transformation() or DummyStyleTransformation()
+ )
+ return style_transformation.invalidation_hash()
+
+
+class ConditionalStyleTransformation(StyleTransformation):
+ """
+ Apply the style transformation depending on a condition.
+ """
+
+ def __init__(
+ self, style_transformation: StyleTransformation, filter: FilterOrBool
+ ) -> None:
+ self.style_transformation = style_transformation
+ self.filter = to_filter(filter)
+
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ if self.filter():
+ return self.style_transformation.transform_attrs(attrs)
+ return attrs
+
+ def invalidation_hash(self) -> Hashable:
+ return (self.filter(), self.style_transformation.invalidation_hash())
+
+
+class _MergedStyleTransformation(StyleTransformation):
+ def __init__(self, style_transformations: Sequence[StyleTransformation]) -> None:
+ self.style_transformations = style_transformations
+
+ def transform_attrs(self, attrs: Attrs) -> Attrs:
+ for transformation in self.style_transformations:
+ attrs = transformation.transform_attrs(attrs)
+ return attrs
+
+ def invalidation_hash(self) -> Hashable:
+ return tuple(t.invalidation_hash() for t in self.style_transformations)
+
+
+def merge_style_transformations(
+ style_transformations: Sequence[StyleTransformation],
+) -> StyleTransformation:
+ """
+ Merge multiple transformations together.
+ """
+ return _MergedStyleTransformation(style_transformations)
+
+
+# Dictionary that maps ANSI color names to their opposite. This is useful for
+# turning color schemes that are optimized for a black background usable for a
+# white background.
+OPPOSITE_ANSI_COLOR_NAMES = {
+ "ansidefault": "ansidefault",
+ "ansiblack": "ansiwhite",
+ "ansired": "ansibrightred",
+ "ansigreen": "ansibrightgreen",
+ "ansiyellow": "ansibrightyellow",
+ "ansiblue": "ansibrightblue",
+ "ansimagenta": "ansibrightmagenta",
+ "ansicyan": "ansibrightcyan",
+ "ansigray": "ansibrightblack",
+ "ansiwhite": "ansiblack",
+ "ansibrightred": "ansired",
+ "ansibrightgreen": "ansigreen",
+ "ansibrightyellow": "ansiyellow",
+ "ansibrightblue": "ansiblue",
+ "ansibrightmagenta": "ansimagenta",
+ "ansibrightcyan": "ansicyan",
+ "ansibrightblack": "ansigray",
+}
+assert set(OPPOSITE_ANSI_COLOR_NAMES.keys()) == set(ANSI_COLOR_NAMES)
+assert set(OPPOSITE_ANSI_COLOR_NAMES.values()) == set(ANSI_COLOR_NAMES)
+
+
+@memoized()
+def get_opposite_color(colorname: str | None) -> str | None:
+ """
+ Take a color name in either 'ansi...' format or 6 digit RGB, return the
+ color of opposite luminosity (same hue/saturation).
+
+ This is used for turning color schemes that work on a light background
+ usable on a dark background.
+ """
+ if colorname is None: # Because color/bgcolor can be None in `Attrs`.
+ return None
+
+ # Special values.
+ if colorname in ("", "default"):
+ return colorname
+
+ # Try ANSI color names.
+ try:
+ return OPPOSITE_ANSI_COLOR_NAMES[colorname]
+ except KeyError:
+ # Try 6 digit RGB colors.
+ r = int(colorname[:2], 16) / 255.0
+ g = int(colorname[2:4], 16) / 255.0
+ b = int(colorname[4:6], 16) / 255.0
+
+ h, l, s = rgb_to_hls(r, g, b)
+
+ l = 1 - l
+
+ r, g, b = hls_to_rgb(h, l, s)
+
+ r = int(r * 255)
+ g = int(g * 255)
+ b = int(b * 255)
+
+ return f"{r:02x}{g:02x}{b:02x}"