| from __future__ import annotations |
|
|
| from string import Formatter |
| from typing import Generator |
|
|
| from prompt_toolkit.output.vt100 import BG_ANSI_COLORS, FG_ANSI_COLORS |
| from prompt_toolkit.output.vt100 import _256_colors as _256_colors_table |
|
|
| from .base import StyleAndTextTuples |
|
|
| __all__ = [ |
| "ANSI", |
| "ansi_escape", |
| ] |
|
|
|
|
| class ANSI: |
| """ |
| ANSI formatted text. |
| Take something ANSI escaped text, for use as a formatted string. E.g. |
| |
| :: |
| |
| ANSI('\\x1b[31mhello \\x1b[32mworld') |
| |
| Characters between ``\\001`` and ``\\002`` are supposed to have a zero width |
| when printed, but these are literally sent to the terminal output. This can |
| be used for instance, for inserting Final Term prompt commands. They will |
| be translated into a prompt_toolkit '[ZeroWidthEscape]' fragment. |
| """ |
|
|
| def __init__(self, value: str) -> None: |
| self.value = value |
| self._formatted_text: StyleAndTextTuples = [] |
|
|
| |
| self._color: str | None = None |
| self._bgcolor: str | None = None |
| self._bold = False |
| self._underline = False |
| self._strike = False |
| self._italic = False |
| self._blink = False |
| self._reverse = False |
| self._hidden = False |
|
|
| |
| parser = self._parse_corot() |
| parser.send(None) |
| for c in value: |
| parser.send(c) |
|
|
| def _parse_corot(self) -> Generator[None, str, None]: |
| """ |
| Coroutine that parses the ANSI escape sequences. |
| """ |
| style = "" |
| formatted_text = self._formatted_text |
|
|
| while True: |
| |
| |
| |
| csi = False |
|
|
| c = yield |
|
|
| |
| if c == "\001": |
| escaped_text = "" |
| while c != "\002": |
| c = yield |
| if c == "\002": |
| formatted_text.append(("[ZeroWidthEscape]", escaped_text)) |
| c = yield |
| break |
| else: |
| escaped_text += c |
|
|
| |
| if c == "\x1b": |
| |
| square_bracket = yield |
| if square_bracket == "[": |
| csi = True |
| else: |
| continue |
| elif c == "\x9b": |
| csi = True |
|
|
| if csi: |
| |
| current = "" |
| params = [] |
|
|
| while True: |
| char = yield |
|
|
| |
| if char.isdigit(): |
| current += char |
|
|
| |
| else: |
| |
| params.append(min(int(current or 0), 9999)) |
|
|
| |
| if char == ";": |
| current = "" |
|
|
| |
| elif char == "m": |
| |
| self._select_graphic_rendition(params) |
| style = self._create_style_string() |
| break |
|
|
| |
| elif char == "C": |
| for i in range(params[0]): |
| |
| formatted_text.append((style, " ")) |
| break |
|
|
| else: |
| |
| break |
| else: |
| |
| |
| |
| |
| |
| |
| formatted_text.append((style, c)) |
|
|
| def _select_graphic_rendition(self, attrs: list[int]) -> None: |
| """ |
| Taken a list of graphics attributes and apply changes. |
| """ |
| if not attrs: |
| attrs = [0] |
| else: |
| attrs = list(attrs[::-1]) |
|
|
| while attrs: |
| attr = attrs.pop() |
|
|
| if attr in _fg_colors: |
| self._color = _fg_colors[attr] |
| elif attr in _bg_colors: |
| self._bgcolor = _bg_colors[attr] |
| elif attr == 1: |
| self._bold = True |
| |
| |
| elif attr == 3: |
| self._italic = True |
| elif attr == 4: |
| self._underline = True |
| elif attr == 5: |
| self._blink = True |
| elif attr == 6: |
| self._blink = True |
| elif attr == 7: |
| self._reverse = True |
| elif attr == 8: |
| self._hidden = True |
| elif attr == 9: |
| self._strike = True |
| elif attr == 22: |
| self._bold = False |
| elif attr == 23: |
| self._italic = False |
| elif attr == 24: |
| self._underline = False |
| elif attr == 25: |
| self._blink = False |
| elif attr == 27: |
| self._reverse = False |
| elif attr == 28: |
| self._hidden = False |
| elif attr == 29: |
| self._strike = False |
| elif not attr: |
| |
| self._color = None |
| self._bgcolor = None |
| self._bold = False |
| self._underline = False |
| self._strike = False |
| self._italic = False |
| self._blink = False |
| self._reverse = False |
| self._hidden = False |
|
|
| elif attr in (38, 48) and len(attrs) > 1: |
| n = attrs.pop() |
|
|
| |
| if n == 5 and len(attrs) >= 1: |
| if attr == 38: |
| m = attrs.pop() |
| self._color = _256_colors.get(m) |
| elif attr == 48: |
| m = attrs.pop() |
| self._bgcolor = _256_colors.get(m) |
|
|
| |
| if n == 2 and len(attrs) >= 3: |
| try: |
| color_str = ( |
| f"#{attrs.pop():02x}{attrs.pop():02x}{attrs.pop():02x}" |
| ) |
| except IndexError: |
| pass |
| else: |
| if attr == 38: |
| self._color = color_str |
| elif attr == 48: |
| self._bgcolor = color_str |
|
|
| def _create_style_string(self) -> str: |
| """ |
| Turn current style flags into a string for usage in a formatted text. |
| """ |
| result = [] |
| if self._color: |
| result.append(self._color) |
| if self._bgcolor: |
| result.append("bg:" + self._bgcolor) |
| if self._bold: |
| result.append("bold") |
| if self._underline: |
| result.append("underline") |
| if self._strike: |
| result.append("strike") |
| if self._italic: |
| result.append("italic") |
| if self._blink: |
| result.append("blink") |
| if self._reverse: |
| result.append("reverse") |
| if self._hidden: |
| result.append("hidden") |
|
|
| return " ".join(result) |
|
|
| def __repr__(self) -> str: |
| return f"ANSI({self.value!r})" |
|
|
| def __pt_formatted_text__(self) -> StyleAndTextTuples: |
| return self._formatted_text |
|
|
| def format(self, *args: str, **kwargs: str) -> ANSI: |
| """ |
| Like `str.format`, but make sure that the arguments are properly |
| escaped. (No ANSI escapes can be injected.) |
| """ |
| return ANSI(FORMATTER.vformat(self.value, args, kwargs)) |
|
|
| def __mod__(self, value: object) -> ANSI: |
| """ |
| ANSI('<b>%s</b>') % value |
| """ |
| if not isinstance(value, tuple): |
| value = (value,) |
|
|
| value = tuple(ansi_escape(i) for i in value) |
| return ANSI(self.value % value) |
|
|
|
|
| |
| _fg_colors = {v: k for k, v in FG_ANSI_COLORS.items()} |
| _bg_colors = {v: k for k, v in BG_ANSI_COLORS.items()} |
|
|
| |
| _256_colors = {} |
|
|
| for i, (r, g, b) in enumerate(_256_colors_table.colors): |
| _256_colors[i] = f"#{r:02x}{g:02x}{b:02x}" |
|
|
|
|
| def ansi_escape(text: object) -> str: |
| """ |
| Replace characters with a special meaning. |
| """ |
| return str(text).replace("\x1b", "?").replace("\b", "?") |
|
|
|
|
| class ANSIFormatter(Formatter): |
| def format_field(self, value: object, format_spec: str) -> str: |
| return ansi_escape(format(value, format_spec)) |
|
|
|
|
| FORMATTER = ANSIFormatter() |
|
|