| | 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() |
| |
|