Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/styles/style_transformation.py: 39%

125 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

1""" 

2Collection of style transformations. 

3 

4Think of it as a kind of color post processing after the rendering is done. 

5This could be used for instance to change the contrast/saturation; swap light 

6and dark colors or even change certain colors for other colors. 

7 

8When the UI is rendered, these transformations can be applied right after the 

9style strings are turned into `Attrs` objects that represent the actual 

10formatting. 

11""" 

12from __future__ import annotations 

13 

14from abc import ABCMeta, abstractmethod 

15from colorsys import hls_to_rgb, rgb_to_hls 

16from typing import Callable, Hashable, Sequence 

17 

18from prompt_toolkit.cache import memoized 

19from prompt_toolkit.filters import FilterOrBool, to_filter 

20from prompt_toolkit.utils import AnyFloat, to_float, to_str 

21 

22from .base import ANSI_COLOR_NAMES, Attrs 

23from .style import parse_color 

24 

25__all__ = [ 

26 "StyleTransformation", 

27 "SwapLightAndDarkStyleTransformation", 

28 "ReverseStyleTransformation", 

29 "SetDefaultColorStyleTransformation", 

30 "AdjustBrightnessStyleTransformation", 

31 "DummyStyleTransformation", 

32 "ConditionalStyleTransformation", 

33 "DynamicStyleTransformation", 

34 "merge_style_transformations", 

35] 

36 

37 

38class StyleTransformation(metaclass=ABCMeta): 

39 """ 

40 Base class for any style transformation. 

41 """ 

42 

43 @abstractmethod 

44 def transform_attrs(self, attrs: Attrs) -> Attrs: 

45 """ 

46 Take an `Attrs` object and return a new `Attrs` object. 

47 

48 Remember that the color formats can be either "ansi..." or a 6 digit 

49 lowercase hexadecimal color (without '#' prefix). 

50 """ 

51 

52 def invalidation_hash(self) -> Hashable: 

53 """ 

54 When this changes, the cache should be invalidated. 

55 """ 

56 return f"{self.__class__.__name__}-{id(self)}" 

57 

58 

59class SwapLightAndDarkStyleTransformation(StyleTransformation): 

60 """ 

61 Turn dark colors into light colors and the other way around. 

62 

63 This is meant to make color schemes that work on a dark background usable 

64 on a light background (and the other way around). 

65 

66 Notice that this doesn't swap foreground and background like "reverse" 

67 does. It turns light green into dark green and the other way around. 

68 Foreground and background colors are considered individually. 

69 

70 Also notice that when <reverse> is used somewhere and no colors are given 

71 in particular (like what is the default for the bottom toolbar), then this 

72 doesn't change anything. This is what makes sense, because when the 

73 'default' color is chosen, it's what works best for the terminal, and 

74 reverse works good with that. 

75 """ 

76 

77 def transform_attrs(self, attrs: Attrs) -> Attrs: 

78 """ 

79 Return the `Attrs` used when opposite luminosity should be used. 

80 """ 

81 # Reverse colors. 

82 attrs = attrs._replace(color=get_opposite_color(attrs.color)) 

83 attrs = attrs._replace(bgcolor=get_opposite_color(attrs.bgcolor)) 

84 

85 return attrs 

86 

87 

88class ReverseStyleTransformation(StyleTransformation): 

89 """ 

90 Swap the 'reverse' attribute. 

91 

92 (This is still experimental.) 

93 """ 

94 

95 def transform_attrs(self, attrs: Attrs) -> Attrs: 

96 return attrs._replace(reverse=not attrs.reverse) 

97 

98 

99class SetDefaultColorStyleTransformation(StyleTransformation): 

100 """ 

101 Set default foreground/background color for output that doesn't specify 

102 anything. This is useful for overriding the terminal default colors. 

103 

104 :param fg: Color string or callable that returns a color string for the 

105 foreground. 

106 :param bg: Like `fg`, but for the background. 

107 """ 

108 

109 def __init__( 

110 self, fg: str | Callable[[], str], bg: str | Callable[[], str] 

111 ) -> None: 

112 self.fg = fg 

113 self.bg = bg 

114 

115 def transform_attrs(self, attrs: Attrs) -> Attrs: 

116 if attrs.bgcolor in ("", "default"): 

117 attrs = attrs._replace(bgcolor=parse_color(to_str(self.bg))) 

118 

119 if attrs.color in ("", "default"): 

120 attrs = attrs._replace(color=parse_color(to_str(self.fg))) 

121 

122 return attrs 

123 

124 def invalidation_hash(self) -> Hashable: 

125 return ( 

126 "set-default-color", 

127 to_str(self.fg), 

128 to_str(self.bg), 

129 ) 

130 

131 

132class AdjustBrightnessStyleTransformation(StyleTransformation): 

133 """ 

134 Adjust the brightness to improve the rendering on either dark or light 

135 backgrounds. 

136 

137 For dark backgrounds, it's best to increase `min_brightness`. For light 

138 backgrounds it's best to decrease `max_brightness`. Usually, only one 

139 setting is adjusted. 

140 

141 This will only change the brightness for text that has a foreground color 

142 defined, but no background color. It works best for 256 or true color 

143 output. 

144 

145 .. note:: Notice that there is no universal way to detect whether the 

146 application is running in a light or dark terminal. As a 

147 developer of an command line application, you'll have to make 

148 this configurable for the user. 

149 

150 :param min_brightness: Float between 0.0 and 1.0 or a callable that returns 

151 a float. 

152 :param max_brightness: Float between 0.0 and 1.0 or a callable that returns 

153 a float. 

154 """ 

155 

156 def __init__( 

157 self, min_brightness: AnyFloat = 0.0, max_brightness: AnyFloat = 1.0 

158 ) -> None: 

159 self.min_brightness = min_brightness 

160 self.max_brightness = max_brightness 

161 

162 def transform_attrs(self, attrs: Attrs) -> Attrs: 

163 min_brightness = to_float(self.min_brightness) 

164 max_brightness = to_float(self.max_brightness) 

165 assert 0 <= min_brightness <= 1 

166 assert 0 <= max_brightness <= 1 

167 

168 # Don't do anything if the whole brightness range is acceptable. 

169 # This also avoids turning ansi colors into RGB sequences. 

170 if min_brightness == 0.0 and max_brightness == 1.0: 

171 return attrs 

172 

173 # If a foreground color is given without a background color. 

174 no_background = not attrs.bgcolor or attrs.bgcolor == "default" 

175 has_fgcolor = attrs.color and attrs.color != "ansidefault" 

176 

177 if has_fgcolor and no_background: 

178 # Calculate new RGB values. 

179 r, g, b = self._color_to_rgb(attrs.color or "") 

180 hue, brightness, saturation = rgb_to_hls(r, g, b) 

181 brightness = self._interpolate_brightness( 

182 brightness, min_brightness, max_brightness 

183 ) 

184 r, g, b = hls_to_rgb(hue, brightness, saturation) 

185 new_color = f"{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}" 

186 

187 attrs = attrs._replace(color=new_color) 

188 

189 return attrs 

190 

191 def _color_to_rgb(self, color: str) -> tuple[float, float, float]: 

192 """ 

193 Parse `style.Attrs` color into RGB tuple. 

194 """ 

195 # Do RGB lookup for ANSI colors. 

196 try: 

197 from prompt_toolkit.output.vt100 import ANSI_COLORS_TO_RGB 

198 

199 r, g, b = ANSI_COLORS_TO_RGB[color] 

200 return r / 255.0, g / 255.0, b / 255.0 

201 except KeyError: 

202 pass 

203 

204 # Parse RRGGBB format. 

205 return ( 

206 int(color[0:2], 16) / 255.0, 

207 int(color[2:4], 16) / 255.0, 

208 int(color[4:6], 16) / 255.0, 

209 ) 

210 

211 # NOTE: we don't have to support named colors here. They are already 

212 # transformed into RGB values in `style.parse_color`. 

213 

214 def _interpolate_brightness( 

215 self, value: float, min_brightness: float, max_brightness: float 

216 ) -> float: 

217 """ 

218 Map the brightness to the (min_brightness..max_brightness) range. 

219 """ 

220 return min_brightness + (max_brightness - min_brightness) * value 

221 

222 def invalidation_hash(self) -> Hashable: 

223 return ( 

224 "adjust-brightness", 

225 to_float(self.min_brightness), 

226 to_float(self.max_brightness), 

227 ) 

228 

229 

230class DummyStyleTransformation(StyleTransformation): 

231 """ 

232 Don't transform anything at all. 

233 """ 

234 

235 def transform_attrs(self, attrs: Attrs) -> Attrs: 

236 return attrs 

237 

238 def invalidation_hash(self) -> Hashable: 

239 # Always return the same hash for these dummy instances. 

240 return "dummy-style-transformation" 

241 

242 

243class DynamicStyleTransformation(StyleTransformation): 

244 """ 

245 StyleTransformation class that can dynamically returns any 

246 `StyleTransformation`. 

247 

248 :param get_style_transformation: Callable that returns a 

249 :class:`.StyleTransformation` instance. 

250 """ 

251 

252 def __init__( 

253 self, get_style_transformation: Callable[[], StyleTransformation | None] 

254 ) -> None: 

255 self.get_style_transformation = get_style_transformation 

256 

257 def transform_attrs(self, attrs: Attrs) -> Attrs: 

258 style_transformation = ( 

259 self.get_style_transformation() or DummyStyleTransformation() 

260 ) 

261 return style_transformation.transform_attrs(attrs) 

262 

263 def invalidation_hash(self) -> Hashable: 

264 style_transformation = ( 

265 self.get_style_transformation() or DummyStyleTransformation() 

266 ) 

267 return style_transformation.invalidation_hash() 

268 

269 

270class ConditionalStyleTransformation(StyleTransformation): 

271 """ 

272 Apply the style transformation depending on a condition. 

273 """ 

274 

275 def __init__( 

276 self, style_transformation: StyleTransformation, filter: FilterOrBool 

277 ) -> None: 

278 self.style_transformation = style_transformation 

279 self.filter = to_filter(filter) 

280 

281 def transform_attrs(self, attrs: Attrs) -> Attrs: 

282 if self.filter(): 

283 return self.style_transformation.transform_attrs(attrs) 

284 return attrs 

285 

286 def invalidation_hash(self) -> Hashable: 

287 return (self.filter(), self.style_transformation.invalidation_hash()) 

288 

289 

290class _MergedStyleTransformation(StyleTransformation): 

291 def __init__(self, style_transformations: Sequence[StyleTransformation]) -> None: 

292 self.style_transformations = style_transformations 

293 

294 def transform_attrs(self, attrs: Attrs) -> Attrs: 

295 for transformation in self.style_transformations: 

296 attrs = transformation.transform_attrs(attrs) 

297 return attrs 

298 

299 def invalidation_hash(self) -> Hashable: 

300 return tuple(t.invalidation_hash() for t in self.style_transformations) 

301 

302 

303def merge_style_transformations( 

304 style_transformations: Sequence[StyleTransformation], 

305) -> StyleTransformation: 

306 """ 

307 Merge multiple transformations together. 

308 """ 

309 return _MergedStyleTransformation(style_transformations) 

310 

311 

312# Dictionary that maps ANSI color names to their opposite. This is useful for 

313# turning color schemes that are optimized for a black background usable for a 

314# white background. 

315OPPOSITE_ANSI_COLOR_NAMES = { 

316 "ansidefault": "ansidefault", 

317 "ansiblack": "ansiwhite", 

318 "ansired": "ansibrightred", 

319 "ansigreen": "ansibrightgreen", 

320 "ansiyellow": "ansibrightyellow", 

321 "ansiblue": "ansibrightblue", 

322 "ansimagenta": "ansibrightmagenta", 

323 "ansicyan": "ansibrightcyan", 

324 "ansigray": "ansibrightblack", 

325 "ansiwhite": "ansiblack", 

326 "ansibrightred": "ansired", 

327 "ansibrightgreen": "ansigreen", 

328 "ansibrightyellow": "ansiyellow", 

329 "ansibrightblue": "ansiblue", 

330 "ansibrightmagenta": "ansimagenta", 

331 "ansibrightcyan": "ansicyan", 

332 "ansibrightblack": "ansigray", 

333} 

334assert set(OPPOSITE_ANSI_COLOR_NAMES.keys()) == set(ANSI_COLOR_NAMES) 

335assert set(OPPOSITE_ANSI_COLOR_NAMES.values()) == set(ANSI_COLOR_NAMES) 

336 

337 

338@memoized() 

339def get_opposite_color(colorname: str | None) -> str | None: 

340 """ 

341 Take a color name in either 'ansi...' format or 6 digit RGB, return the 

342 color of opposite luminosity (same hue/saturation). 

343 

344 This is used for turning color schemes that work on a light background 

345 usable on a dark background. 

346 """ 

347 if colorname is None: # Because color/bgcolor can be None in `Attrs`. 

348 return None 

349 

350 # Special values. 

351 if colorname in ("", "default"): 

352 return colorname 

353 

354 # Try ANSI color names. 

355 try: 

356 return OPPOSITE_ANSI_COLOR_NAMES[colorname] 

357 except KeyError: 

358 # Try 6 digit RGB colors. 

359 r = int(colorname[:2], 16) / 255.0 

360 g = int(colorname[2:4], 16) / 255.0 

361 b = int(colorname[4:6], 16) / 255.0 

362 

363 h, l, s = rgb_to_hls(r, g, b) 

364 

365 l = 1 - l 

366 

367 r, g, b = hls_to_rgb(h, l, s) 

368 

369 r = int(r * 255) 

370 g = int(g * 255) 

371 b = int(b * 255) 

372 

373 return f"{r:02x}{g:02x}{b:02x}"