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

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

126 statements  

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

12 

13from __future__ import annotations 

14 

15from abc import ABCMeta, abstractmethod 

16from colorsys import hls_to_rgb, rgb_to_hls 

17from typing import Callable, Hashable, Sequence 

18 

19from prompt_toolkit.cache import memoized 

20from prompt_toolkit.filters import FilterOrBool, to_filter 

21from prompt_toolkit.utils import AnyFloat, to_float, to_str 

22 

23from .base import ANSI_COLOR_NAMES, Attrs 

24from .style import parse_color 

25 

26__all__ = [ 

27 "StyleTransformation", 

28 "SwapLightAndDarkStyleTransformation", 

29 "ReverseStyleTransformation", 

30 "SetDefaultColorStyleTransformation", 

31 "AdjustBrightnessStyleTransformation", 

32 "DummyStyleTransformation", 

33 "ConditionalStyleTransformation", 

34 "DynamicStyleTransformation", 

35 "merge_style_transformations", 

36] 

37 

38 

39class StyleTransformation(metaclass=ABCMeta): 

40 """ 

41 Base class for any style transformation. 

42 """ 

43 

44 @abstractmethod 

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

46 """ 

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

48 

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

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

51 """ 

52 

53 def invalidation_hash(self) -> Hashable: 

54 """ 

55 When this changes, the cache should be invalidated. 

56 """ 

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

58 

59 

60class SwapLightAndDarkStyleTransformation(StyleTransformation): 

61 """ 

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

63 

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

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

66 

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

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

69 Foreground and background colors are considered individually. 

70 

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

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

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

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

75 reverse works good with that. 

76 """ 

77 

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

79 """ 

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

81 """ 

82 # Reverse colors. 

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

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

85 

86 return attrs 

87 

88 

89class ReverseStyleTransformation(StyleTransformation): 

90 """ 

91 Swap the 'reverse' attribute. 

92 

93 (This is still experimental.) 

94 """ 

95 

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

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

98 

99 

100class SetDefaultColorStyleTransformation(StyleTransformation): 

101 """ 

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

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

104 

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

106 foreground. 

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

108 """ 

109 

110 def __init__( 

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

112 ) -> None: 

113 self.fg = fg 

114 self.bg = bg 

115 

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

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

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

119 

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

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

122 

123 return attrs 

124 

125 def invalidation_hash(self) -> Hashable: 

126 return ( 

127 "set-default-color", 

128 to_str(self.fg), 

129 to_str(self.bg), 

130 ) 

131 

132 

133class AdjustBrightnessStyleTransformation(StyleTransformation): 

134 """ 

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

136 backgrounds. 

137 

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

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

140 setting is adjusted. 

141 

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

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

144 output. 

145 

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

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

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

149 this configurable for the user. 

150 

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

152 a float. 

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

154 a float. 

155 """ 

156 

157 def __init__( 

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

159 ) -> None: 

160 self.min_brightness = min_brightness 

161 self.max_brightness = max_brightness 

162 

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

164 min_brightness = to_float(self.min_brightness) 

165 max_brightness = to_float(self.max_brightness) 

166 assert 0 <= min_brightness <= 1 

167 assert 0 <= max_brightness <= 1 

168 

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

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

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

172 return attrs 

173 

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

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

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

177 

178 if has_fgcolor and no_background: 

179 # Calculate new RGB values. 

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

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

182 brightness = self._interpolate_brightness( 

183 brightness, min_brightness, max_brightness 

184 ) 

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

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

187 

188 attrs = attrs._replace(color=new_color) 

189 

190 return attrs 

191 

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

193 """ 

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

195 """ 

196 # Do RGB lookup for ANSI colors. 

197 try: 

198 from prompt_toolkit.output.vt100 import ANSI_COLORS_TO_RGB 

199 

200 r, g, b = ANSI_COLORS_TO_RGB[color] 

201 return r / 255.0, g / 255.0, b / 255.0 

202 except KeyError: 

203 pass 

204 

205 # Parse RRGGBB format. 

206 return ( 

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

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

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

210 ) 

211 

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

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

214 

215 def _interpolate_brightness( 

216 self, value: float, min_brightness: float, max_brightness: float 

217 ) -> float: 

218 """ 

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

220 """ 

221 return min_brightness + (max_brightness - min_brightness) * value 

222 

223 def invalidation_hash(self) -> Hashable: 

224 return ( 

225 "adjust-brightness", 

226 to_float(self.min_brightness), 

227 to_float(self.max_brightness), 

228 ) 

229 

230 

231class DummyStyleTransformation(StyleTransformation): 

232 """ 

233 Don't transform anything at all. 

234 """ 

235 

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

237 return attrs 

238 

239 def invalidation_hash(self) -> Hashable: 

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

241 return "dummy-style-transformation" 

242 

243 

244class DynamicStyleTransformation(StyleTransformation): 

245 """ 

246 StyleTransformation class that can dynamically returns any 

247 `StyleTransformation`. 

248 

249 :param get_style_transformation: Callable that returns a 

250 :class:`.StyleTransformation` instance. 

251 """ 

252 

253 def __init__( 

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

255 ) -> None: 

256 self.get_style_transformation = get_style_transformation 

257 

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

259 style_transformation = ( 

260 self.get_style_transformation() or DummyStyleTransformation() 

261 ) 

262 return style_transformation.transform_attrs(attrs) 

263 

264 def invalidation_hash(self) -> Hashable: 

265 style_transformation = ( 

266 self.get_style_transformation() or DummyStyleTransformation() 

267 ) 

268 return style_transformation.invalidation_hash() 

269 

270 

271class ConditionalStyleTransformation(StyleTransformation): 

272 """ 

273 Apply the style transformation depending on a condition. 

274 """ 

275 

276 def __init__( 

277 self, style_transformation: StyleTransformation, filter: FilterOrBool 

278 ) -> None: 

279 self.style_transformation = style_transformation 

280 self.filter = to_filter(filter) 

281 

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

283 if self.filter(): 

284 return self.style_transformation.transform_attrs(attrs) 

285 return attrs 

286 

287 def invalidation_hash(self) -> Hashable: 

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

289 

290 

291class _MergedStyleTransformation(StyleTransformation): 

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

293 self.style_transformations = style_transformations 

294 

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

296 for transformation in self.style_transformations: 

297 attrs = transformation.transform_attrs(attrs) 

298 return attrs 

299 

300 def invalidation_hash(self) -> Hashable: 

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

302 

303 

304def merge_style_transformations( 

305 style_transformations: Sequence[StyleTransformation], 

306) -> StyleTransformation: 

307 """ 

308 Merge multiple transformations together. 

309 """ 

310 return _MergedStyleTransformation(style_transformations) 

311 

312 

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

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

315# white background. 

316OPPOSITE_ANSI_COLOR_NAMES = { 

317 "ansidefault": "ansidefault", 

318 "ansiblack": "ansiwhite", 

319 "ansired": "ansibrightred", 

320 "ansigreen": "ansibrightgreen", 

321 "ansiyellow": "ansibrightyellow", 

322 "ansiblue": "ansibrightblue", 

323 "ansimagenta": "ansibrightmagenta", 

324 "ansicyan": "ansibrightcyan", 

325 "ansigray": "ansibrightblack", 

326 "ansiwhite": "ansiblack", 

327 "ansibrightred": "ansired", 

328 "ansibrightgreen": "ansigreen", 

329 "ansibrightyellow": "ansiyellow", 

330 "ansibrightblue": "ansiblue", 

331 "ansibrightmagenta": "ansimagenta", 

332 "ansibrightcyan": "ansicyan", 

333 "ansibrightblack": "ansigray", 

334} 

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

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

337 

338 

339@memoized() 

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

341 """ 

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

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

344 

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

346 usable on a dark background. 

347 """ 

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

349 return None 

350 

351 # Special values. 

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

353 return colorname 

354 

355 # Try ANSI color names. 

356 try: 

357 return OPPOSITE_ANSI_COLOR_NAMES[colorname] 

358 except KeyError: 

359 # Try 6 digit RGB colors. 

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

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

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

363 

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

365 

366 l = 1 - l 

367 

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

369 

370 r = int(r * 255) 

371 g = int(g * 255) 

372 b = int(b * 255) 

373 

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