import re import sys from colorsys import rgb_to_hls from enum import IntEnum from functools import lru_cache from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple
from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE from .color_triplet import ColorTriplet from .repr import Result, rich_repr from .terminal_theme import DEFAULT_TERMINAL_THEME
if TYPE_CHECKING: # pragma: no cover from .terminal_theme import TerminalTheme from .text import Text
WINDOWS = sys.platform == "win32"
class ColorSystem(IntEnum): """One of the 3 color system supported by terminals."""
STANDARD = 1
EIGHT_BIT = 2
TRUECOLOR = 3
WINDOWS = 4
@rich_repr class Color(NamedTuple): """Terminal color definition."""
name: str """The name of the color (typically the input to Color.parse)."""
type: ColorType """The type of the color."""
number: Optional[int] = None """The color number, if a standard color, or None."""
triplet: Optional[ColorTriplet] = None """A triplet of color components, if an RGB color."""
def __rich__(self) -> "Text": """Displays the actual color if Rich printed.""" from .style import Style from .text import Text
@property def system(self) -> ColorSystem: """Get the native color system for this color.""" if self.type == ColorType.DEFAULT: return ColorSystem.STANDARD return ColorSystem(int(self.type))
@property def is_system_defined(self) -> bool: """Check if the color is ultimately defined by the system.""" return self.system notin (ColorSystem.EIGHT_BIT, ColorSystem.TRUECOLOR)
@property def is_default(self) -> bool: """Check if the color is a default color.""" return self.type == ColorType.DEFAULT
def get_truecolor(
self, theme: Optional["TerminalTheme"] = None, foreground: bool = True
) -> ColorTriplet: """Get an equivalent color triplet for this color.
Args:
theme (TerminalTheme, optional): Optional terminal theme, orNone to use default. Defaults to None.
foreground (bool, optional): Truefor a foreground color, orFalsefor background. Defaults to True.
Returns:
ColorTriplet: A color triplet containing RGB components. """
@classmethod def from_ansi(cls, number: int) -> "Color": """Create a Color number from it's 8-bit ansi number.
Args:
number (int): A number between 0-255 inclusive.
Returns:
Color: A new Color instance. """ return cls(
name=f"color({number})",
type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT),
number=number,
)
@classmethod def from_triplet(cls, triplet: "ColorTriplet") -> "Color": """Create a truecolor RGB color from a triplet of values.
Args:
triplet (ColorTriplet): A color triplet containing red, green and blue components.
Returns:
Color: A new color object. """ return cls(name=triplet.hex, type=ColorType.TRUECOLOR, triplet=triplet)
@classmethod def from_rgb(cls, red: float, green: float, blue: float) -> "Color": """Create a truecolor from three color components in the range(0->255).
Args:
red (float): Red component in range 0-255.
green (float): Green component in range 0-255.
blue (float): Blue component in range 0-255.
Returns:
Color: A new color object. """ return cls.from_triplet(ColorTriplet(int(red), int(green), int(blue)))
@classmethod def default(cls) -> "Color": """Get a Color instance representing the default color.
@classmethod
@lru_cache(maxsize=1024) def parse(cls, color: str) -> "Color": """Parse a color definition."""
original_color = color
color = color.lower().strip()
if color == "default": return cls(color, type=ColorType.DEFAULT)
color_number = ANSI_COLOR_NAMES.get(color) if color_number isnotNone: return cls(
color,
type=(ColorType.STANDARD if color_number < 16 else ColorType.EIGHT_BIT),
number=color_number,
)
color_match = RE_COLOR.match(color) if color_match isNone: raise ColorParseError(f"{original_color!r} is not a valid color")
elif color_8:
number = int(color_8) if number > 255: raise ColorParseError(f"color number must be <= 255 in {color!r}") return cls(
color,
type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT),
number=number,
)
else: # color_rgb:
components = color_rgb.split(",") if len(components) != 3: raise ColorParseError(
f"expected three components in {original_color!r}"
)
red, green, blue = components
triplet = ColorTriplet(int(red), int(green), int(blue)) ifnot all(component <= 255 for component in triplet): raise ColorParseError(
f"color components must be <= 255 in {original_color!r}"
) return cls(color, ColorType.TRUECOLOR, triplet=triplet)
@lru_cache(maxsize=1024) def get_ansi_codes(self, foreground: bool = True) -> Tuple[str, ...]: """Get the ANSI escape codes for this color."""
_type = self.type if _type == ColorType.DEFAULT: return ("39"if foreground else"49",)
elif _type == ColorType.WINDOWS:
number = self.number assert number isnotNone
fore, back = (30, 40) if number < 8 else (82, 92) return (str(fore + number if foreground else back + number),)
elif _type == ColorType.STANDARD:
number = self.number assert number isnotNone
fore, back = (30, 40) if number < 8 else (82, 92) return (str(fore + number if foreground else back + number),)
@lru_cache(maxsize=1024) def downgrade(self, system: ColorSystem) -> "Color": """Downgrade a color system to a system with fewer colors."""
if self.type in (ColorType.DEFAULT, system): return self # Convert to 8-bit color from truecolor color if system == ColorSystem.EIGHT_BIT and self.system == ColorSystem.TRUECOLOR: assert self.triplet isnotNone
_h, l, s = rgb_to_hls(*self.triplet.normalized) # If saturation is under 15% assume it is grayscale if s < 0.15:
gray = round(l * 25.0) if gray == 0:
color_number = 16 elif gray == 25:
color_number = 231 else:
color_number = 231 + gray return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
red, green, blue = self.triplet
six_red = red / 95 if red < 95 else 1 + (red - 95) / 40
six_green = green / 95 if green < 95 else 1 + (green - 95) / 40
six_blue = blue / 95 if blue < 95 else 1 + (blue - 95) / 40
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.