Changed code to support older Python versions

This commit is contained in:
Malasaur 2025-12-01 23:27:09 +01:00
parent eb92d2d36f
commit 582458cdd0
5027 changed files with 794942 additions and 4 deletions

View file

@ -0,0 +1,225 @@
from __future__ import annotations
import re
import typing as t
from dataclasses import dataclass
from mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, TranslationTag, WebColor
from mcstatus.motd.simplifies import get_unused_elements, squash_nearby_strings
from mcstatus.motd.transformers import AnsiTransformer, HtmlTransformer, MinecraftTransformer, PlainTransformer
if t.TYPE_CHECKING:
from typing_extensions import Self
from mcstatus.responses import RawJavaResponseMotd, RawJavaResponseMotdWhenDict # circular import
else:
RawJavaResponseMotdWhenDict = dict
__all__ = ["Motd"]
MOTD_COLORS_RE = re.compile(r"([\xA7|&][0-9A-FK-OR])", re.IGNORECASE)
@dataclass(frozen=True)
class Motd:
"""Represents parsed MOTD."""
parsed: list[ParsedMotdComponent]
"""Parsed MOTD, which then will be transformed.
Bases on this attribute, you can easily write your own MOTD-to-something parser.
"""
raw: RawJavaResponseMotd
"""MOTD in raw format, just like the server gave."""
bedrock: bool = False
"""Is server Bedrock Edition? Some details may change in work of this class."""
@classmethod
def parse(
cls,
raw: RawJavaResponseMotd, # type: ignore # later, we overwrite the type
*,
bedrock: bool = False,
) -> Self:
"""Parse a raw MOTD to less raw MOTD (:attr:`.parsed` attribute).
:param raw: Raw MOTD, directly from server.
:param bedrock: Is server Bedrock Edition? Nothing changes here, just sets attribute.
:returns: New :class:`.Motd` instance.
"""
original_raw = raw.copy() if hasattr(raw, "copy") else raw # type: ignore # Cannot access "copy" for type "str"
if isinstance(raw, list):
raw: RawJavaResponseMotdWhenDict = {"extra": raw}
if isinstance(raw, str):
parsed = cls._parse_as_str(raw, bedrock=bedrock)
elif isinstance(raw, dict):
parsed = cls._parse_as_dict(raw, bedrock=bedrock)
else:
raise TypeError(f"Expected list, string or dict data, got {raw.__class__!r} ({raw!r}), report this!")
return cls(parsed, original_raw, bedrock)
@staticmethod
def _parse_as_str(raw: str, *, bedrock: bool = False) -> list[ParsedMotdComponent]:
"""Parse a MOTD when it's string.
.. note:: This method returns a lot of empty strings, use :meth:`Motd.simplify` to remove them.
:param raw: Raw MOTD, directly from server.
:param bedrock: Is server Bedrock Edition?
Ignores :attr:`MinecraftColor.MINECOIN_GOLD` if it's :obj:`False`.
:returns: :obj:`ParsedMotdComponent` list, which need to be passed to ``__init__``.
"""
parsed_motd: list[ParsedMotdComponent] = []
split_raw = MOTD_COLORS_RE.split(raw)
for element in split_raw:
clean_element = element.lstrip("").lower()
standardized_element = element.replace("&", "§").lower()
if standardized_element == "§g" and not bedrock:
parsed_motd.append(element) # minecoin_gold on java server, treat as string
continue
if standardized_element.startswith("§"):
try:
parsed_motd.append(MinecraftColor(clean_element))
except ValueError:
try:
parsed_motd.append(Formatting(clean_element))
except ValueError:
# just a text
parsed_motd.append(element)
else:
parsed_motd.append(element)
return parsed_motd
@classmethod
def _parse_as_dict(
cls,
item: RawJavaResponseMotdWhenDict,
*,
bedrock: bool = False,
auto_add: list[ParsedMotdComponent] | None = None,
) -> list[ParsedMotdComponent]:
"""Parse a MOTD when it's dict.
:param item: :class:`dict` directly from the server.
:param bedrock: Is the server Bedrock Edition?
Nothing does here, just going to :meth:`._parse_as_str` while parsing ``text`` field.
:param auto_add: Values to add on this item.
Most time, this is :class:`Formatting` from top level.
:returns: :obj:`ParsedMotdComponent` list, which need to be passed to ``__init__``.
"""
parsed_motd: list[ParsedMotdComponent] = auto_add if auto_add is not None else []
if (color := item.get("color")) is not None:
parsed_motd.append(cls._parse_color(color))
for style_key, style_val in Formatting.__members__.items():
lowered_style_key = style_key.lower()
if item.get(lowered_style_key) is False:
try:
parsed_motd.remove(style_val)
except ValueError:
# some servers set the formatting keys to false here, even without it ever being set to true before
continue
elif item.get(lowered_style_key) is not None:
parsed_motd.append(style_val)
if (text := item.get("text")) is not None:
parsed_motd.extend(cls._parse_as_str(text, bedrock=bedrock))
if (translate := item.get("translate")) is not None:
parsed_motd.append(TranslationTag(translate))
parsed_motd.append(Formatting.RESET)
if "extra" in item:
auto_add = list(filter(lambda e: type(e) is Formatting and e != Formatting.RESET, parsed_motd))
for element in item["extra"]:
parsed_motd.extend(
cls._parse_as_dict(element, auto_add=auto_add.copy())
if isinstance(element, dict)
else auto_add + cls._parse_as_str(element, bedrock=bedrock)
)
return parsed_motd
@staticmethod
def _parse_color(color: str) -> ParsedMotdComponent:
"""Parse a color string."""
try:
return MinecraftColor[color.upper()]
except KeyError:
if color == "reset":
# Minecraft servers actually can't return {"reset": True}, instead, they treat
# reset as a color and set {"color": "reset"}. However logically, reset is
# a formatting, and it resets both color and other formatting, so we use
# `Formatting.RESET` here.
#
# see `color` field in
# https://minecraft.wiki/w/Java_Edition_protocol/Chat?oldid=2763811#Shared_between_all_components
return Formatting.RESET
# Last attempt: try parsing as HTML (hex rgb) color. Some servers use these to
# achieve gradients.
try:
return WebColor.from_hex(color)
except ValueError:
raise ValueError(f"Unable to parse color: {color!r}, report this!")
def simplify(self) -> Self:
"""Create new MOTD without unused elements.
After parsing, the MOTD may contain some unused elements, like empty strings, or formattings/colors
that don't apply to anything. This method is responsible for creating a new motd with all such elements
removed, providing a much cleaner representation.
:returns: New simplified MOTD, with any unused elements removed.
"""
parsed = self.parsed.copy()
old_parsed: list[ParsedMotdComponent] | None = None
while parsed != old_parsed:
old_parsed = parsed.copy()
unused_elements = get_unused_elements(parsed)
parsed = [el for index, el in enumerate(parsed) if index not in unused_elements]
parsed = squash_nearby_strings(parsed)
return self.__class__(parsed, self.raw, bedrock=self.bedrock)
def to_plain(self) -> str:
"""Get plain text from a MOTD, without any colors/formatting.
This is just a shortcut to :class:`~mcstatus.motd.transformers.PlainTransformer`.
"""
return PlainTransformer().transform(self.parsed)
def to_minecraft(self) -> str:
"""Get Minecraft variant from a MOTD.
This is just a shortcut to :class:`~mcstatus.motd.transformers.MinecraftTransformer`.
.. note:: This will always use ``§``, even if in original MOTD used ``&``.
"""
return MinecraftTransformer().transform(self.parsed)
def to_html(self) -> str:
"""Get HTML from a MOTD.
This is just a shortcut to :class:`~mcstatus.motd.transformers.HtmlTransformer`.
"""
return HtmlTransformer(bedrock=self.bedrock).transform(self.parsed)
def to_ansi(self) -> str:
"""Get ANSI variant from a MOTD.
This is just a shortcut to :class:`~mcstatus.motd.transformers.AnsiTransformer`.
.. note:: We support only ANSI 24 bit colors, please implement your own transformer if you need other standards.
.. seealso:: https://en.wikipedia.org/wiki/ANSI_escape_code
"""
return AnsiTransformer().transform(self.parsed)

View file

@ -0,0 +1,126 @@
from __future__ import annotations
import typing as t
from dataclasses import dataclass
from enum import Enum
if t.TYPE_CHECKING:
from typing_extensions import Self, TypeAlias
class Formatting(Enum):
"""Enum for Formatting codes.
See `Minecraft wiki <https://minecraft.wiki/w/Formatting_codes#Formatting_codes>`__
for more info.
.. note::
:attr:`.STRIKETHROUGH` and :attr:`.UNDERLINED` don't work on Bedrock, which our parser
doesn't keep it in mind. See `MCPE-41729 <https://bugs.mojang.com/browse/MCPE-41729>`_.
"""
BOLD = "l"
ITALIC = "o"
UNDERLINED = "n"
STRIKETHROUGH = "m"
OBFUSCATED = "k"
RESET = "r"
class MinecraftColor(Enum):
"""Enum for Color codes.
See `Minecraft wiki <https://minecraft.wiki/w/Formatting_codes#Color_codes>`_
for more info.
"""
BLACK = "0"
DARK_BLUE = "1"
DARK_GREEN = "2"
DARK_AQUA = "3"
DARK_RED = "4"
DARK_PURPLE = "5"
GOLD = "6"
GRAY = "7"
DARK_GRAY = "8"
BLUE = "9"
GREEN = "a"
AQUA = "b"
RED = "c"
LIGHT_PURPLE = "d"
YELLOW = "e"
WHITE = "f"
# Only for bedrock
MINECOIN_GOLD = "g"
@dataclass(frozen=True)
class WebColor:
"""Raw HTML color from MOTD.
Can be found in MOTD when someone uses gradient.
.. note:: Actually supported in Minecraft 1.16+ only.
"""
hex: str
rgb: tuple[int, int, int]
@classmethod
def from_hex(cls, hex: str) -> Self:
"""Construct web color using hex color string.
:raises ValueError: Invalid hex color string.
:returns: New :class:`WebColor` instance.
"""
hex = hex.lstrip("#")
if len(hex) not in (3, 6):
raise ValueError(f"Got too long/short hex color: {'#' + hex!r}")
if len(hex) == 3:
hex = "{0}{0}{1}{1}{2}{2}".format(*hex)
try:
rgb = t.cast("tuple[int, int, int]", tuple(int(hex[i : i + 2], 16) for i in (0, 2, 4)))
except ValueError:
raise ValueError(f"Failed to parse given hex color: {'#' + hex!r}")
return cls.from_rgb(rgb)
@classmethod
def from_rgb(cls, rgb: tuple[int, int, int]) -> Self:
"""Construct web color using rgb color tuple.
:raises ValueError: When RGB color is out of its 8-bit range.
:returns: New :class:`WebColor` instance.
"""
cls._check_rgb(rgb)
hex = "#{:02x}{:02x}{:02x}".format(*rgb)
return cls(hex, rgb)
@staticmethod
def _check_rgb(rgb: tuple[int, int, int]) -> None:
index_to_color_name = {0: "red", 1: "green", 2: "blue"}
for index, value in enumerate(rgb):
if not 255 >= value >= 0:
color_name = index_to_color_name[index]
raise ValueError(f"RGB color byte out of its 8-bit range (0-255) for {color_name} ({value=})")
@dataclass(frozen=True)
class TranslationTag:
"""Represents a ``translate`` field in server's answer.
This just exists, but is completely ignored by our transformers.
You can find translation tags in :attr:`Motd.parsed <mcstatus.motd.Motd.parsed>` attribute.
.. seealso:: `Minecraft's wiki. <https://minecraft.wiki/w/Raw_JSON_text_format#Translated_Text>`__
"""
id: str
ParsedMotdComponent: TypeAlias = "Formatting | MinecraftColor | WebColor | TranslationTag | str"

View file

@ -0,0 +1,194 @@
from __future__ import annotations
import typing as t
from collections.abc import Sequence
from mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, WebColor
_PARSED_MOTD_COMPONENTS_TYPEVAR = t.TypeVar("_PARSED_MOTD_COMPONENTS_TYPEVAR", bound="list[ParsedMotdComponent]")
def get_unused_elements(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
"""Get indices of all items which are unused and can be safely removed from the MOTD.
This is a wrapper method around several unused item collection methods.
"""
to_remove: set[int] = set()
for simplifier in [
get_double_items,
get_double_colors,
get_formatting_before_color,
get_meaningless_resets_and_colors,
get_empty_text,
get_end_non_text,
]:
to_remove.update(simplifier(parsed))
return to_remove
def squash_nearby_strings(parsed: _PARSED_MOTD_COMPONENTS_TYPEVAR) -> _PARSED_MOTD_COMPONENTS_TYPEVAR:
"""Squash duplicate strings together.
Note that this function doesn't create a copy of passed array, it modifies it.
This is what those typevars are for in the function signature.
"""
# in order to not break indexes, we need to fill values and then remove them after the loop
fillers: set[int] = set()
for index, item in enumerate(parsed):
if not isinstance(item, str):
continue
try:
next_item = parsed[index + 1]
except IndexError: # Last item (without any next item)
break
if isinstance(next_item, str):
parsed[index + 1] = item + next_item
fillers.add(index)
for already_removed, index_to_remove in enumerate(fillers):
parsed.pop(index_to_remove - already_removed)
return parsed
def get_double_items(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
"""Get indices of all doubled items that can be removed.
Removes any items that are followed by an item of the same kind (compared using ``__eq__``).
"""
to_remove: set[int] = set()
for index, item in enumerate(parsed):
try:
next_item = parsed[index + 1]
except IndexError: # Last item (without any next item)
break
if isinstance(item, (Formatting, MinecraftColor, WebColor)) and item == next_item:
to_remove.add(index)
return to_remove
def get_double_colors(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
"""Get indices of all doubled color items.
As colors (obviously) override each other, we only ever care about the last one, ignore
the previous ones. (for example: specifying red color, then orange, then yellow, then some text
will just result in yellow text)
"""
to_remove: set[int] = set()
prev_color: int | None = None
for index, item in enumerate(parsed):
if isinstance(item, (MinecraftColor, WebColor)):
# If we found a color after another, remove the previous color
if prev_color is not None:
to_remove.add(prev_color)
prev_color = index
# If we find a string, that's what our color we found previously applies to,
# set prev_color to None, marking this color as used
if isinstance(item, str):
prev_color = None
return to_remove
def get_formatting_before_color(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
"""Obtain indices of all unused formatting items before colors.
Colors override any formatting before them, meaning we only ever care about the color, and can
ignore all formatting before it. (For example: specifying bold formatting, then italic, then yellow,
will just result in yellow text.)
"""
to_remove: set[int] = set()
collected_formattings = []
for index, item in enumerate(parsed):
# Collect the indices of formatting items
if isinstance(item, Formatting):
collected_formattings.append(index)
# Only run checks if we have some collected formatting items
if len(collected_formattings) == 0:
continue
# If there's a string after some formattings, the formattings apply to it.
# This means they're not unused, remove them.
if isinstance(item, str) and not item.isspace():
collected_formattings = []
continue
# If there's a color after some formattings, these formattings will be overridden
# as colors reset everything. This makes these formattings pointless, mark them
# for removal.
if isinstance(item, (MinecraftColor, WebColor)):
to_remove.update(collected_formattings)
collected_formattings = []
return to_remove
def get_empty_text(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
"""Get indices of all empty text items.
Empty strings in motd serve no purpose and can be marked for removal.
"""
to_remove: set[int] = set()
for index, item in enumerate(parsed):
if isinstance(item, str) and len(item) == 0:
to_remove.add(index)
return to_remove
def get_end_non_text(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
"""Get indices of all trailing items, found after the last text component.
Any color/formatting items only make sense when they apply to some text.
If there are some at the end, after the last text, they're pointless and
can be removed.
"""
to_remove: set[int] = set()
for rev_index, item in enumerate(reversed(parsed)):
# The moment we find our last string, stop the loop
if isinstance(item, str):
break
# Remove any color/formatting that doesn't apply to text
if isinstance(item, (MinecraftColor, WebColor, Formatting)):
index = len(parsed) - 1 - rev_index
to_remove.add(index)
return to_remove
def get_meaningless_resets_and_colors(parsed: Sequence[ParsedMotdComponent]) -> set[int]:
to_remove: set[int] = set()
active_color: MinecraftColor | WebColor | None = None
active_formatting: Formatting | None = None
for index, item in enumerate(parsed):
if isinstance(item, (MinecraftColor, WebColor)):
if active_color == item:
to_remove.add(index)
active_color = item
continue
if isinstance(item, Formatting):
if item == Formatting.RESET:
if active_color is None and active_formatting is None:
to_remove.add(index)
continue
active_color, active_formatting = None, None
continue
if active_formatting == item:
to_remove.add(index)
active_formatting = item
return to_remove

View file

@ -0,0 +1,223 @@
from __future__ import annotations
import abc
import typing as t
from collections.abc import Callable, Sequence
from mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, TranslationTag, WebColor
_HOOK_RETURN_TYPE = t.TypeVar("_HOOK_RETURN_TYPE")
_END_RESULT_TYPE = t.TypeVar("_END_RESULT_TYPE")
class BaseTransformer(abc.ABC, t.Generic[_HOOK_RETURN_TYPE, _END_RESULT_TYPE]):
"""Base motd transformer class.
Motd transformer is responsible for providing a way to generate an alternative representation
of motd, such as one that is able to be printed in the terminal.
"""
def transform(self, motd_components: Sequence[ParsedMotdComponent]) -> _END_RESULT_TYPE:
return self._format_output([handled for component in motd_components for handled in self._handle_component(component)])
@abc.abstractmethod
def _format_output(self, results: list[_HOOK_RETURN_TYPE]) -> _END_RESULT_TYPE: ...
def _handle_component(
self, component: ParsedMotdComponent
) -> tuple[_HOOK_RETURN_TYPE, _HOOK_RETURN_TYPE] | tuple[_HOOK_RETURN_TYPE]:
handler: Callable[[ParsedMotdComponent], _HOOK_RETURN_TYPE] = {
MinecraftColor: self._handle_minecraft_color,
WebColor: self._handle_web_color,
Formatting: self._handle_formatting,
TranslationTag: self._handle_translation_tag,
str: self._handle_str,
}[type(component)]
additional = None
if isinstance(component, MinecraftColor):
additional = self._handle_formatting(Formatting.RESET)
return (additional, handler(component)) if additional is not None else (handler(component),)
@abc.abstractmethod
def _handle_str(self, element: str, /) -> _HOOK_RETURN_TYPE: ...
@abc.abstractmethod
def _handle_translation_tag(self, _: TranslationTag, /) -> _HOOK_RETURN_TYPE: ...
@abc.abstractmethod
def _handle_web_color(self, element: WebColor, /) -> _HOOK_RETURN_TYPE: ...
@abc.abstractmethod
def _handle_formatting(self, element: Formatting, /) -> _HOOK_RETURN_TYPE: ...
@abc.abstractmethod
def _handle_minecraft_color(self, element: MinecraftColor, /) -> _HOOK_RETURN_TYPE: ...
class NothingTransformer(BaseTransformer[str, str]):
"""Transformer that transforms all elements into empty strings.
This transformer acts as a base for other transformers with string result type.
"""
def _format_output(self, results: list[str]) -> str:
return "".join(results)
def _handle_str(self, element: str, /) -> str:
return ""
def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:
return ""
def _handle_web_color(self, element: WebColor, /) -> str:
return ""
def _handle_formatting(self, element: Formatting, /) -> str:
return ""
def _handle_translation_tag(self, element: TranslationTag, /) -> str:
return ""
class PlainTransformer(NothingTransformer):
def _handle_str(self, element: str, /) -> str:
return element
class MinecraftTransformer(PlainTransformer):
def _handle_component(self, component: ParsedMotdComponent) -> tuple[str, str] | tuple[str]:
result = super()._handle_component(component)
if len(result) == 2:
return (result[1],)
return result
def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:
return "§" + element.value
def _handle_formatting(self, element: Formatting, /) -> str:
return "§" + element.value
class HtmlTransformer(PlainTransformer):
"""Formatter for HTML variant of a MOTD.
.. warning::
You should implement obfuscated CSS class yourself (name - ``obfuscated``).
See `this answer <https://stackoverflow.com/a/30313558>`_ as example.
"""
FORMATTING_TO_HTML_TAGS = {
Formatting.BOLD: "b",
Formatting.STRIKETHROUGH: "s",
Formatting.ITALIC: "i",
Formatting.UNDERLINED: "u",
}
MINECRAFT_COLOR_TO_RGB_BEDROCK = {
MinecraftColor.BLACK: ((0, 0, 0), (0, 0, 0)),
MinecraftColor.DARK_BLUE: ((0, 0, 170), (0, 0, 42)),
MinecraftColor.DARK_GREEN: ((0, 170, 0), (0, 42, 0)),
MinecraftColor.DARK_AQUA: ((0, 170, 170), (0, 42, 42)),
MinecraftColor.DARK_RED: ((170, 0, 0), (42, 0, 0)),
MinecraftColor.DARK_PURPLE: ((170, 0, 170), (42, 0, 42)),
MinecraftColor.GOLD: ((255, 170, 0), (64, 42, 0)),
MinecraftColor.GRAY: ((170, 170, 170), (42, 42, 42)),
MinecraftColor.DARK_GRAY: ((85, 85, 85), (21, 21, 21)),
MinecraftColor.BLUE: ((85, 85, 255), (21, 21, 63)),
MinecraftColor.GREEN: ((85, 255, 85), (21, 63, 21)),
MinecraftColor.AQUA: ((85, 255, 255), (21, 63, 63)),
MinecraftColor.RED: ((255, 85, 85), (63, 21, 21)),
MinecraftColor.LIGHT_PURPLE: ((255, 85, 255), (63, 21, 63)),
MinecraftColor.YELLOW: ((255, 255, 85), (63, 63, 21)),
MinecraftColor.WHITE: ((255, 255, 255), (63, 63, 63)),
MinecraftColor.MINECOIN_GOLD: ((221, 214, 5), (55, 53, 1)),
}
MINECRAFT_COLOR_TO_RGB_JAVA = MINECRAFT_COLOR_TO_RGB_BEDROCK.copy()
MINECRAFT_COLOR_TO_RGB_JAVA[MinecraftColor.GOLD] = ((255, 170, 0), (42, 42, 0))
def __init__(self, *, bedrock: bool = False) -> None:
self.bedrock = bedrock
self.on_reset: list[str] = []
def transform(self, motd_components: Sequence[ParsedMotdComponent]) -> str:
self.on_reset = []
return super().transform(motd_components)
def _format_output(self, results: list[str]) -> str:
return "<p>" + super()._format_output(results) + "".join(self.on_reset) + "</p>"
def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:
color_map = self.MINECRAFT_COLOR_TO_RGB_BEDROCK if self.bedrock else self.MINECRAFT_COLOR_TO_RGB_JAVA
fg_color, bg_color = color_map[element]
self.on_reset.append("</span>")
return f"<span style='color:rgb{fg_color};text-shadow:0 0 1px rgb{bg_color}'>"
def _handle_web_color(self, element: WebColor, /) -> str:
self.on_reset.append("</span>")
return f"<span style='color:rgb{element.rgb}'>"
def _handle_formatting(self, element: Formatting, /) -> str:
if element is Formatting.RESET:
to_return = "".join(self.on_reset)
self.on_reset = []
return to_return
if element is Formatting.OBFUSCATED:
self.on_reset.append("</span>")
return "<span class=obfuscated>"
tag_name = self.FORMATTING_TO_HTML_TAGS[element]
self.on_reset.append(f"</{tag_name}>")
return f"<{tag_name}>"
class AnsiTransformer(PlainTransformer):
FORMATTING_TO_ANSI_TAGS = {
Formatting.BOLD: "1",
Formatting.STRIKETHROUGH: "9",
Formatting.ITALIC: "3",
Formatting.UNDERLINED: "4",
Formatting.OBFUSCATED: "5",
}
MINECRAFT_COLOR_TO_RGB = {
MinecraftColor.BLACK: (0, 0, 0),
MinecraftColor.DARK_BLUE: (0, 0, 170),
MinecraftColor.DARK_GREEN: (0, 170, 0),
MinecraftColor.DARK_AQUA: (0, 170, 170),
MinecraftColor.DARK_RED: (170, 0, 0),
MinecraftColor.DARK_PURPLE: (170, 0, 170),
MinecraftColor.GOLD: (255, 170, 0),
MinecraftColor.GRAY: (170, 170, 170),
MinecraftColor.DARK_GRAY: (85, 85, 85),
MinecraftColor.BLUE: (85, 85, 255),
MinecraftColor.GREEN: (85, 255, 85),
MinecraftColor.AQUA: (85, 255, 255),
MinecraftColor.RED: (255, 85, 85),
MinecraftColor.LIGHT_PURPLE: (255, 85, 255),
MinecraftColor.YELLOW: (255, 255, 85),
MinecraftColor.WHITE: (255, 255, 255),
MinecraftColor.MINECOIN_GOLD: (221, 214, 5),
}
def ansi_color(self, color: tuple[int, int, int] | MinecraftColor) -> str:
"""Transform RGB color to ANSI color code."""
if isinstance(color, MinecraftColor):
color = self.MINECRAFT_COLOR_TO_RGB[color]
return "\033[38;2;{0};{1};{2}m".format(*color)
def _format_output(self, results: list[str]) -> str:
return "\033[0m" + super()._format_output(results) + "\033[0m"
def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:
return self.ansi_color(element)
def _handle_web_color(self, element: WebColor, /) -> str:
return self.ansi_color(element.rgb)
def _handle_formatting(self, element: Formatting, /) -> str:
if element is Formatting.RESET:
return "\033[0m"
return "\033[" + self.FORMATTING_TO_ANSI_TAGS[element] + "m"