first comit
This commit is contained in:
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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}"
|
||||
Reference in New Issue
Block a user