Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exploratory Contribution: MUD Support (MXP/Pueblo) #1288

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ class ConsoleOptions:
"""Enable markup when rendering strings."""
height: Optional[int] = None
"""Height available, or None for no height limit."""
mxp: Optional[bool] = False
"""Enable MXP/MUD HTML when printing. For MUDs only."""
pueblo: Optional[bool] = False
"""Enable Pueblo/MUD HTML when printing. For MUDs only."""
links: Optional[bool] = True
"""Enable ANSI Links when printing. Turn off if MXP/Pueblo is on."""

@property
def ascii_only(self) -> bool:
Expand Down Expand Up @@ -169,6 +175,9 @@ def update(
highlight: Union[Optional[bool], NoChange] = NO_CHANGE,
markup: Union[Optional[bool], NoChange] = NO_CHANGE,
height: Union[Optional[int], NoChange] = NO_CHANGE,
mxp: Union[Optional[bool], NoChange] = NO_CHANGE,
pueblo: Union[Optional[bool], NoChange] = NO_CHANGE,
links: Union[Optional[bool], NoChange] = NO_CHANGE,
) -> "ConsoleOptions":
"""Update values, return a copy."""
options = self.copy()
Expand All @@ -190,6 +199,12 @@ def update(
options.markup = markup
if not isinstance(height, NoChange):
options.height = None if height is None else max(0, height)
if not isinstance(mxp, NoChange):
options.mxp = mxp
if not isinstance(pueblo, NoChange):
options.pueblo = pueblo
if not isinstance(links, NoChange):
options.links = links
return options

def update_width(self, width: int) -> "ConsoleOptions":
Expand Down Expand Up @@ -581,6 +596,9 @@ class Console:
markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True.
emoji (bool, optional): Enable emoji code. Defaults to True.
highlight (bool, optional): Enable automatic highlighting. Defaults to True.
mxp (bool, optional): Enable MXP mode for MUDs. Defaults to False.
pueblo (bool, optional): Enable Pueblo mode for MUDs. Defaults to False.
links (bool, optional): Enable Console Links. Defaults to True.
log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True.
log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True.
log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%X] ".
Expand Down Expand Up @@ -617,6 +635,9 @@ def __init__(
markup: bool = True,
emoji: bool = True,
highlight: bool = True,
mxp: bool = False,
pueblo: bool = False,
links: bool = True,
log_time: bool = True,
log_path: bool = True,
log_time_format: Union[str, FormatTimeCallable] = "[%X]",
Expand Down Expand Up @@ -653,6 +674,9 @@ def __init__(
self._markup = markup
self._emoji = emoji
self._highlight = highlight
self._mxp = mxp
self._pueblo = pueblo
self._links = links
self.legacy_windows: bool = (
(detect_legacy_windows() and not self.is_jupyter)
if legacy_windows is None
Expand Down Expand Up @@ -1505,6 +1529,9 @@ def print(
emoji: Optional[bool] = None,
markup: Optional[bool] = None,
highlight: Optional[bool] = None,
mxp: Optional[bool] = None,
pueblo: Optional[bool] = None,
links: Optional[bool] = None,
width: Optional[int] = None,
height: Optional[int] = None,
crop: bool = True,
Expand All @@ -1523,6 +1550,9 @@ def print(
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``.
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to ``None``.
mxp (Optional[bool], optional): Enable MXP, or ``None`` to use console default. Defaults to ``None``.
pueblo (Optional[bool], optional): Enable Pueblo, or ``None`` to use console default. Defaults to ``None``.
links (Optional[bool], optional): Enable terminal links, or ``None`` to use console default. Defaults to ``None``.
width (Optional[int], optional): Width of output, or ``None`` to auto-detect. Defaults to ``None``.
crop (Optional[bool], optional): Crop output to width of terminal. Defaults to True.
soft_wrap (bool, optional): Enable soft wrap mode which disables word wrapping and cropping of text or None for
Expand Down Expand Up @@ -1559,6 +1589,9 @@ def print(
height=height,
no_wrap=no_wrap,
markup=markup,
mxp=mxp,
pueblo=pueblo,
links=links,
)

new_segments: List[Segment] = []
Expand Down Expand Up @@ -1829,6 +1862,9 @@ def _render_buffer(self, buffer: Iterable[Segment]) -> str:
text,
color_system=color_system,
legacy_windows=legacy_windows,
mxp=self._mxp,
pueblo=self._pueblo,
links=self._links,
)
)
elif not (not_terminal and control):
Expand Down
108 changes: 93 additions & 15 deletions rich/style.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
import html
from functools import lru_cache
from random import randint
from time import time
Expand Down Expand Up @@ -53,13 +54,6 @@ class Style:

"""

_color: Optional[Color]
_bgcolor: Optional[Color]
_attributes: int
_set_attributes: int
_hash: int
_null: bool

__slots__ = [
"_color",
"_bgcolor",
Expand All @@ -71,6 +65,9 @@ class Style:
"_style_definition",
"_hash",
"_null",
"_tag",
"_xml_attr",
"_xml_attr_data",
]

# maps bits on to SGR parameter
Expand Down Expand Up @@ -109,10 +106,23 @@ def __init__(
encircle: Optional[bool] = None,
overline: Optional[bool] = None,
link: Optional[str] = None,
tag: Optional[str] = None,
xml_attr: Optional[Dict] = None,
):
self._ansi: Optional[str] = None
self._style_definition: Optional[str] = None

self._tag = tag
self._xml_attr = xml_attr
if self._xml_attr:
self._xml_attr_data = (
" ".join(f'{k}="{html.escape(v)}"' for k, v in xml_attr.items())
if xml_attr
else ""
)
else:
self._xml_attr_data = ""

def _make_color(color: Union[Color, str]) -> Color:
return color if isinstance(color, Color) else Color.parse(color)

Expand Down Expand Up @@ -165,10 +175,12 @@ def _make_color(color: Union[Color, str]) -> Color:
self._bgcolor,
self._attributes,
self._set_attributes,
link,
self._tag,
self._xml_attr_data,
self._link,
)
)
self._null = not (self._set_attributes or color or bgcolor or link)
self._null = not (self._set_attributes or color or bgcolor or link or tag)

@classmethod
def null(cls) -> "Style":
Expand Down Expand Up @@ -605,6 +617,9 @@ def render(
*,
color_system: Optional[ColorSystem] = ColorSystem.TRUECOLOR,
legacy_windows: bool = False,
mxp: bool = False,
pueblo: bool = False,
links: bool = True,
) -> str:
"""Render the ANSI codes for the style.

Expand All @@ -615,14 +630,31 @@ def render(
Returns:
str: A string containing ANSI style codes.
"""
if not text or color_system is None:
return text
attrs = self._make_ansi_codes(color_system)
rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text
if self._link and not legacy_windows:
out_text = html.escape(text) if mxp or pueblo else text
if not out_text:
return out_text
if color_system is not None:
attrs = self._make_ansi_codes(color_system)
rendered = f"\x1b[{attrs}m{out_text}\x1b[0m" if attrs else out_text
else:
rendered = out_text
if links and self._link and not legacy_windows:
rendered = (
f"\x1b]8;id={self._link_id};{self._link}\x1b\\{rendered}\x1b]8;;\x1b\\"
)
if (pueblo or mxp) and self._tag:
if mxp:
if self._xml_attr:
rendered = f"\x1b[4z<{self._tag} {self._xml_attr_data}>{rendered}\x1b[4z</{self._tag}>"
else:
rendered = f"\x1b[4z<{self._tag}>{rendered}\x1b[4z</{self._tag}>"
else:
if self._xml_attr:
rendered = (
f"{self._tag} {self._xml_attr_data}>{rendered}</{self._tag}>"
)
else:
rendered = f"<{self._tag}>{rendered}</{self._tag}>"
return rendered

def test(self, text: Optional[str] = None) -> None:
Expand All @@ -637,7 +669,9 @@ def test(self, text: Optional[str] = None) -> None:
text = text or str(self)
sys.stdout.write(f"{self.render(text)}\n")

def __add__(self, style: Optional["Style"]) -> "Style":
def __add__(self, style: Union["Style", str]) -> "Style":
if isinstance(style, str):
style = self.__class__.parse(style)
if not (isinstance(style, Style) or style is None):
return NotImplemented
if style is None or style._null:
Expand All @@ -655,10 +689,54 @@ def __add__(self, style: Optional["Style"]) -> "Style":
new_style._set_attributes = self._set_attributes | style._set_attributes
new_style._link = style._link or self._link
new_style._link_id = style._link_id or self._link_id
new_style._tag = style._tag or self._tag
new_style._xml_attr = style._xml_attr or self._xml_attr
new_style._xml_attr_data = style._xml_attr_data or self._xml_attr_data
new_style._hash = style._hash
new_style._null = self._null or style._null

return new_style

def __radd__(self, other):
if isinstance(other, str):
other = self.__class__.parse(other)
return other + self
return NotImplemented

def serialize(self) -> dict:
attrs = (
"color",
"bgcolor",
"bold",
"dim",
"italic",
"underline",
"blink",
"blink2",
"reverse",
"conceal",
"strike",
"underline2",
"frame",
"encircle",
"overline",
"link",
"tag",
"xml_attr",
)

out = dict()

for attr in attrs:
found = getattr(self, attr, None)
if found is not None:
if isinstance(found, Color):
out[attr] = found.name
else:
out[attr] = found

return out


NULL_STYLE = Style()

Expand Down
Loading