Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/ansi.py: 47%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import re
2import sys
3from contextlib import suppress
4from typing import Iterable, NamedTuple, Optional
6from .color import Color
7from .style import Style
8from .text import Text
10re_ansi = re.compile(
11 r"""
12(?:\x1b[0-?])|
13(?:\x1b\](.*?)\x1b\\)|
14(?:\x1b([(@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))
15""",
16 re.VERBOSE,
17)
20class _AnsiToken(NamedTuple):
21 """Result of ansi tokenized string."""
23 plain: str = ""
24 sgr: Optional[str] = ""
25 osc: Optional[str] = ""
28def _ansi_tokenize(ansi_text: str) -> Iterable[_AnsiToken]:
29 """Tokenize a string in to plain text and ANSI codes.
31 Args:
32 ansi_text (str): A String containing ANSI codes.
34 Yields:
35 AnsiToken: A named tuple of (plain, sgr, osc)
36 """
38 position = 0
39 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 + 1
49 continue
50 if sgr.endswith("m"):
51 yield _AnsiToken("", sgr[1:-1], osc)
52 else:
53 yield _AnsiToken("", sgr, osc)
54 position = end
55 if position < len(ansi_text):
56 yield _AnsiToken(ansi_text[position:])
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}
120class AnsiDecoder:
121 """Translate ANSI code in to styled Text."""
123 def __init__(self) -> None:
124 self.style = Style.null()
126 def decode(self, terminal_text: str) -> Iterable[Text]:
127 """Decode ANSI codes in an iterable of lines.
129 Args:
130 lines (Iterable[str]): An iterable of lines of terminal output.
132 Yields:
133 Text: Marked up Text.
134 """
135 for line in terminal_text.splitlines():
136 yield self.decode_line(line)
138 def decode_line(self, line: str) -> Text:
139 """Decode a line containing ansi codes.
141 Args:
142 line (str): A line of terminal output.
144 Returns:
145 Text: A Text instance marked up according to ansi codes.
146 """
147 from_ansi = Color.from_ansi
148 from_rgb = Color.from_rgb
149 _Style = Style
150 text = Text()
151 append = text.append
152 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 codes
163 # Ignore invalid codes, because we want to be lenient
164 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 # reset
173 self.style = _Style.null()
174 elif code in SGR_STYLE_MAP:
175 # styles
176 self.style += _Style.parse(SGR_STYLE_MAP[code])
177 elif code == 38:
178 # Foreground
179 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 # Background
195 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 )
211 return text
214if sys.platform != "win32" and __name__ == "__main__": # pragma: no cover
215 import io
216 import os
217 import pty
218 import sys
220 decoder = AnsiDecoder()
222 stdout = io.BytesIO()
224 def read(fd: int) -> bytes:
225 data = os.read(fd, 1024)
226 stdout.write(data)
227 return data
229 pty.spawn(sys.argv[1:], read)
231 from .console import Console
233 console = Console(record=True)
235 stdout_result = stdout.getvalue().decode("utf-8")
236 print(stdout_result)
238 for line in decoder.decode(stdout_result):
239 console.print(line)
241 console.save_html("stdout.html")