CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
ansi.py242 linesDownload Raw Back to rich
1import re2import sys3from contextlib import suppress4from typing import Iterable, NamedTuple, Optional5 6from .color import Color7from .style import Style8from .text import Text9 10re_ansi = re.compile(11    r"""12(?:\x1b[0-?])|13(?:\x1b\](.*?)\x1b\\)|14(?:\x1b([(@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))15""",16    re.VERBOSE,17)18 19 20class _AnsiToken(NamedTuple):21    """Result of ansi tokenized string."""22 23    plain: str = ""24    sgr: Optional[str] = ""25    osc: Optional[str] = ""26 27 28def _ansi_tokenize(ansi_text: str) -> Iterable[_AnsiToken]:29    """Tokenize a string in to plain text and ANSI codes.30 31    Args:32        ansi_text (str): A String containing ANSI codes.33 34    Yields:35        AnsiToken: A named tuple of (plain, sgr, osc)36    """37 38    position = 039    sgr: Optional[str]40    osc: Optional[str]41    for match in re_ansi.finditer(ansi_text):42        start, end = match.span(0)43        osc, sgr = match.groups()44        if start > position:45            yield _AnsiToken(ansi_text[position:start])46        if sgr:47            if sgr == "(":48                position = end + 149                continue50            if sgr.endswith("m"):51                yield _AnsiToken("", sgr[1:-1], osc)52        else:53            yield _AnsiToken("", sgr, osc)54        position = end55    if position < len(ansi_text):56        yield _AnsiToken(ansi_text[position:])57 58 59SGR_STYLE_MAP = {60    1: "bold",61    2: "dim",62    3: "italic",63    4: "underline",64    5: "blink",65    6: "blink2",66    7: "reverse",67    8: "conceal",68    9: "strike",69    21: "underline2",70    22: "not dim not bold",71    23: "not italic",72    24: "not underline",73    25: "not blink",74    26: "not blink2",75    27: "not reverse",76    28: "not conceal",77    29: "not strike",78    30: "color(0)",79    31: "color(1)",80    32: "color(2)",81    33: "color(3)",82    34: "color(4)",83    35: "color(5)",84    36: "color(6)",85    37: "color(7)",86    39: "default",87    40: "on color(0)",88    41: "on color(1)",89    42: "on color(2)",90    43: "on color(3)",91    44: "on color(4)",92    45: "on color(5)",93    46: "on color(6)",94    47: "on color(7)",95    49: "on default",96    51: "frame",97    52: "encircle",98    53: "overline",99    54: "not frame not encircle",100    55: "not overline",101    90: "color(8)",102    91: "color(9)",103    92: "color(10)",104    93: "color(11)",105    94: "color(12)",106    95: "color(13)",107    96: "color(14)",108    97: "color(15)",109    100: "on color(8)",110    101: "on color(9)",111    102: "on color(10)",112    103: "on color(11)",113    104: "on color(12)",114    105: "on color(13)",115    106: "on color(14)",116    107: "on color(15)",117}118 119 120class AnsiDecoder:121    """Translate ANSI code in to styled Text."""122 123    def __init__(self) -> None:124        self.style = Style.null()125 126    def decode(self, terminal_text: str) -> Iterable[Text]:127        """Decode ANSI codes in an iterable of lines.128 129        Args:130            lines (Iterable[str]): An iterable of lines of terminal output.131 132        Yields:133            Text: Marked up Text.134        """135        for line in terminal_text.splitlines():136            yield self.decode_line(line)137 138    def decode_line(self, line: str) -> Text:139        """Decode a line containing ansi codes.140 141        Args:142            line (str): A line of terminal output.143 144        Returns:145            Text: A Text instance marked up according to ansi codes.146        """147        from_ansi = Color.from_ansi148        from_rgb = Color.from_rgb149        _Style = Style150        text = Text()151        append = text.append152        line = line.rsplit("\r", 1)[-1]153        for plain_text, sgr, osc in _ansi_tokenize(line):154            if plain_text:155                append(plain_text, self.style or None)156            elif osc is not None:157                if osc.startswith("8;"):158                    _params, semicolon, link = osc[2:].partition(";")159                    if semicolon:160                        self.style = self.style.update_link(link or None)161            elif sgr is not None:162                # Translate in to semi-colon separated codes163                # Ignore invalid codes, because we want to be lenient164                codes = [165                    min(255, int(_code) if _code else 0)166                    for _code in sgr.split(";")167                    if _code.isdigit() or _code == ""168                ]169                iter_codes = iter(codes)170                for code in iter_codes:171                    if code == 0:172                        # reset173                        self.style = _Style.null()174                    elif code in SGR_STYLE_MAP:175                        # styles176                        self.style += _Style.parse(SGR_STYLE_MAP[code])177                    elif code == 38:178                        #  Foreground179                        with suppress(StopIteration):180                            color_type = next(iter_codes)181                            if color_type == 5:182                                self.style += _Style.from_color(183                                    from_ansi(next(iter_codes))184                                )185                            elif color_type == 2:186                                self.style += _Style.from_color(187                                    from_rgb(188                                        next(iter_codes),189                                        next(iter_codes),190                                        next(iter_codes),191                                    )192                                )193                    elif code == 48:194                        # Background195                        with suppress(StopIteration):196                            color_type = next(iter_codes)197                            if color_type == 5:198                                self.style += _Style.from_color(199                                    None, from_ansi(next(iter_codes))200                                )201                            elif color_type == 2:202                                self.style += _Style.from_color(203                                    None,204                                    from_rgb(205                                        next(iter_codes),206                                        next(iter_codes),207                                        next(iter_codes),208                                    ),209                                )210 211        return text212 213 214if sys.platform != "win32" and __name__ == "__main__":  # pragma: no cover215    import io216    import os217    import pty218    import sys219 220    decoder = AnsiDecoder()221 222    stdout = io.BytesIO()223 224    def read(fd: int) -> bytes:225        data = os.read(fd, 1024)226        stdout.write(data)227        return data228 229    pty.spawn(sys.argv[1:], read)230 231    from .console import Console232 233    console = Console(record=True)234 235    stdout_result = stdout.getvalue().decode("utf-8")236    print(stdout_result)237 238    for line in decoder.decode(stdout_result):239        console.print(line)240 241    console.save_html("stdout.html")242 
Aluode/PerceptionLabPortable · CoolFace