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

74 statements  

1import re 

2import sys 

3from contextlib import suppress 

4from typing import Iterable, NamedTuple, Optional 

5 

6from .color import Color 

7from .style import Style 

8from .text import Text 

9 

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 = 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:]) 

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

210 

211 return text 

212 

213 

214if sys.platform != "win32" and __name__ == "__main__": # pragma: no cover 

215 import io 

216 import os 

217 import pty 

218 import sys 

219 

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 data 

228 

229 pty.spawn(sys.argv[1:], read) 

230 

231 from .console import Console 

232 

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