From d07d14030511da078fc8af79349e7c907bf39325 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 7 Sep 2025 11:59:36 +0100 Subject: [PATCH 1/4] render ansi --- src/textual/strip.py | 51 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/textual/strip.py b/src/textual/strip.py index b608a92f78..be993596da 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -7,10 +7,12 @@ from __future__ import annotations +from functools import lru_cache from typing import Any, Iterable, Iterator, Sequence import rich.repr from rich.cells import cell_len, set_cell_size +from rich.color import ColorSystem from rich.console import Console, ConsoleOptions, RenderResult from rich.measure import Measurement from rich.segment import Segment @@ -22,6 +24,22 @@ from textual.css.types import AlignHorizontal, AlignVertical from textual.filter import LineFilter +SGR_STYLE_MAP = { + 0: "1", + 1: "2", + 2: "3", + 3: "4", + 4: "5", + 5: "6", + 6: "7", + 7: "8", + 8: "9", + 9: "21", + 10: "51", + 11: "52", + 12: "53", +} + def get_line_length(segments: Iterable[Segment]) -> int: """Get the line length (total length of all segments). @@ -645,6 +663,35 @@ def _apply_link_style(self, link_style: Style) -> Strip: ] return Strip(segments, self._cell_length) + @classmethod + @lru_cache(maxsize=16384) + def render_ansi(cls, style: Style, color_system: ColorSystem) -> str: + if attributes := style._attributes & style._set_attributes: + _style_map = SGR_STYLE_MAP + sgr = [ + _style_map[bit_offset] + for bit_offset in range(attributes.bit_length()) + if attributes & (1 << bit_offset) + ] + else: + sgr = [] + if (color := style._color) is not None: + sgr.extend(color.downgrade(color_system).get_ansi_codes()) + if (color := style._bgcolor) is not None: + sgr.extend(color.downgrade(color_system).get_ansi_codes(False)) + ansi = style._ansi = ";".join(sgr) + return ansi + + @classmethod + def render_style(cls, style: Style, text: str, color_system: ColorSystem) -> str: + ansi = style._ansi or cls.render_ansi(style, color_system) + output = f"\x1b[{ansi}m{text}\x1b[0m" if ansi else text + if style._link: + output = ( + f"\x1b]8;id={style._link_id};{style._link}\x1b\\{output}\x1b]8;;\x1b\\" + ) + return output + def render(self, console: Console) -> str: """Render the strip into terminal sequences. @@ -655,8 +702,8 @@ def render(self, console: Console) -> str: Rendered sequences. """ if self._render_cache is None: - color_system = console._color_system - render = Style.render + color_system = console._color_system or ColorSystem.TRUECOLOR + render = self.render_style self._render_cache = "".join( [ render(style, text, color_system=color_system) From b33b6aa3066881edcb87d4ebd2a5fa71d8a59eb5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 7 Sep 2025 12:06:50 +0100 Subject: [PATCH 2/4] docstrings --- src/textual/strip.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/textual/strip.py b/src/textual/strip.py index be993596da..9902ded889 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -24,21 +24,7 @@ from textual.css.types import AlignHorizontal, AlignVertical from textual.filter import LineFilter -SGR_STYLE_MAP = { - 0: "1", - 1: "2", - 2: "3", - 3: "4", - 4: "5", - 5: "6", - 6: "7", - 7: "8", - 8: "9", - 9: "21", - 10: "51", - 11: "52", - 12: "53", -} +SGR_STYLES = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "21", "51", "52", "53"] def get_line_length(segments: Iterable[Segment]) -> int: @@ -666,8 +652,17 @@ def _apply_link_style(self, link_style: Style) -> Strip: @classmethod @lru_cache(maxsize=16384) def render_ansi(cls, style: Style, color_system: ColorSystem) -> str: + """Render ANSI codes for a give style. + + Args: + style: A Rich style. + color_system: Color system enumeration. + + Returns: + A string of ANSI escape sequences to render the style. + """ if attributes := style._attributes & style._set_attributes: - _style_map = SGR_STYLE_MAP + _style_map = SGR_STYLES sgr = [ _style_map[bit_offset] for bit_offset in range(attributes.bit_length()) @@ -684,6 +679,16 @@ def render_ansi(cls, style: Style, color_system: ColorSystem) -> str: @classmethod def render_style(cls, style: Style, text: str, color_system: ColorSystem) -> str: + """Render a Rich style and text. + + Args: + style: Style to render. + text: Content string. + color_system: Color system enumeration. + + Returns: + Text with ANSI escape sequences. + """ ansi = style._ansi or cls.render_ansi(style, color_system) output = f"\x1b[{ansi}m{text}\x1b[0m" if ansi else text if style._link: From af39e382b363a0ff51e17eafa6b348d042e65270 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 7 Sep 2025 12:20:58 +0100 Subject: [PATCH 3/4] better naming --- src/textual/strip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/strip.py b/src/textual/strip.py index a4f783cc5c..b906a37396 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -672,8 +672,8 @@ def render_ansi(cls, style: Style, color_system: ColorSystem) -> str: sgr = [] if (color := style._color) is not None: sgr.extend(color.downgrade(color_system).get_ansi_codes()) - if (color := style._bgcolor) is not None: - sgr.extend(color.downgrade(color_system).get_ansi_codes(False)) + if (bgcolor := style._bgcolor) is not None: + sgr.extend(bgcolor.downgrade(color_system).get_ansi_codes(False)) ansi = style._ansi = ";".join(sgr) return ansi From cd18c0acf164319efbacd4b72123a647a9f631db Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 7 Sep 2025 14:22:52 +0100 Subject: [PATCH 4/4] typing --- src/textual/strip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/strip.py b/src/textual/strip.py index b906a37396..891e3e6b72 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -661,6 +661,7 @@ def render_ansi(cls, style: Style, color_system: ColorSystem) -> str: Returns: A string of ANSI escape sequences to render the style. """ + sgr: list[str] if attributes := style._attributes & style._set_attributes: _style_map = SGR_STYLES sgr = [