Coverage for /run/media/veganeco/water/status600.com/pypi/reptilian_climates/modules_series_2/ships/fields/gardens_pip/rich/color.py: 50%

190 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-22 16:04 -0800

1import platform 

2import re 

3from colorsys import rgb_to_hls 

4from enum import IntEnum 

5from functools import lru_cache 

6from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple 

7 

8from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE 

9from .color_triplet import ColorTriplet 

10from .repr import Result, rich_repr 

11from .terminal_theme import DEFAULT_TERMINAL_THEME 

12 

13if TYPE_CHECKING: # pragma: no cover 

14 from .terminal_theme import TerminalTheme 

15 from .text import Text 

16 

17 

18WINDOWS = platform.system() == "Windows" 

19 

20 

21class ColorSystem(IntEnum): 

22 """One of the 3 color system supported by terminals.""" 

23 

24 STANDARD = 1 

25 EIGHT_BIT = 2 

26 TRUECOLOR = 3 

27 WINDOWS = 4 

28 

29 def __repr__(self) -> str: 

30 return f"ColorSystem.{self.name}" 

31 

32 def __str__(self) -> str: 

33 return repr(self) 

34 

35 

36class ColorType(IntEnum): 

37 """Type of color stored in Color class.""" 

38 

39 DEFAULT = 0 

40 STANDARD = 1 

41 EIGHT_BIT = 2 

42 TRUECOLOR = 3 

43 WINDOWS = 4 

44 

45 def __repr__(self) -> str: 

46 return f"ColorType.{self.name}" 

47 

48 

49ANSI_COLOR_NAMES = { 

50 "black": 0, 

51 "red": 1, 

52 "green": 2, 

53 "yellow": 3, 

54 "blue": 4, 

55 "magenta": 5, 

56 "cyan": 6, 

57 "white": 7, 

58 "bright_black": 8, 

59 "bright_red": 9, 

60 "bright_green": 10, 

61 "bright_yellow": 11, 

62 "bright_blue": 12, 

63 "bright_magenta": 13, 

64 "bright_cyan": 14, 

65 "bright_white": 15, 

66 "grey0": 16, 

67 "gray0": 16, 

68 "navy_blue": 17, 

69 "dark_blue": 18, 

70 "blue3": 20, 

71 "blue1": 21, 

72 "dark_green": 22, 

73 "deep_sky_blue4": 25, 

74 "dodger_blue3": 26, 

75 "dodger_blue2": 27, 

76 "green4": 28, 

77 "spring_green4": 29, 

78 "turquoise4": 30, 

79 "deep_sky_blue3": 32, 

80 "dodger_blue1": 33, 

81 "green3": 40, 

82 "spring_green3": 41, 

83 "dark_cyan": 36, 

84 "light_sea_green": 37, 

85 "deep_sky_blue2": 38, 

86 "deep_sky_blue1": 39, 

87 "spring_green2": 47, 

88 "cyan3": 43, 

89 "dark_turquoise": 44, 

90 "turquoise2": 45, 

91 "green1": 46, 

92 "spring_green1": 48, 

93 "medium_spring_green": 49, 

94 "cyan2": 50, 

95 "cyan1": 51, 

96 "dark_red": 88, 

97 "deep_pink4": 125, 

98 "purple4": 55, 

99 "purple3": 56, 

100 "blue_violet": 57, 

101 "orange4": 94, 

102 "grey37": 59, 

103 "gray37": 59, 

104 "medium_purple4": 60, 

105 "slate_blue3": 62, 

106 "royal_blue1": 63, 

107 "chartreuse4": 64, 

108 "dark_sea_green4": 71, 

109 "pale_turquoise4": 66, 

110 "steel_blue": 67, 

111 "steel_blue3": 68, 

112 "cornflower_blue": 69, 

113 "chartreuse3": 76, 

114 "cadet_blue": 73, 

115 "sky_blue3": 74, 

116 "steel_blue1": 81, 

117 "pale_green3": 114, 

118 "sea_green3": 78, 

119 "aquamarine3": 79, 

120 "medium_turquoise": 80, 

121 "chartreuse2": 112, 

122 "sea_green2": 83, 

123 "sea_green1": 85, 

124 "aquamarine1": 122, 

125 "dark_slate_gray2": 87, 

126 "dark_magenta": 91, 

127 "dark_violet": 128, 

128 "purple": 129, 

129 "light_pink4": 95, 

130 "plum4": 96, 

131 "medium_purple3": 98, 

132 "slate_blue1": 99, 

133 "yellow4": 106, 

134 "wheat4": 101, 

135 "grey53": 102, 

136 "gray53": 102, 

137 "light_slate_grey": 103, 

138 "light_slate_gray": 103, 

139 "medium_purple": 104, 

140 "light_slate_blue": 105, 

141 "dark_olive_green3": 149, 

142 "dark_sea_green": 108, 

143 "light_sky_blue3": 110, 

144 "sky_blue2": 111, 

145 "dark_sea_green3": 150, 

146 "dark_slate_gray3": 116, 

147 "sky_blue1": 117, 

148 "chartreuse1": 118, 

149 "light_green": 120, 

150 "pale_green1": 156, 

151 "dark_slate_gray1": 123, 

152 "red3": 160, 

153 "medium_violet_red": 126, 

154 "magenta3": 164, 

155 "dark_orange3": 166, 

156 "indian_red": 167, 

157 "hot_pink3": 168, 

158 "medium_orchid3": 133, 

159 "medium_orchid": 134, 

160 "medium_purple2": 140, 

161 "dark_goldenrod": 136, 

162 "light_salmon3": 173, 

163 "rosy_brown": 138, 

164 "grey63": 139, 

165 "gray63": 139, 

166 "medium_purple1": 141, 

167 "gold3": 178, 

168 "dark_khaki": 143, 

169 "navajo_white3": 144, 

170 "grey69": 145, 

171 "gray69": 145, 

172 "light_steel_blue3": 146, 

173 "light_steel_blue": 147, 

174 "yellow3": 184, 

175 "dark_sea_green2": 157, 

176 "light_cyan3": 152, 

177 "light_sky_blue1": 153, 

178 "green_yellow": 154, 

179 "dark_olive_green2": 155, 

180 "dark_sea_green1": 193, 

181 "pale_turquoise1": 159, 

182 "deep_pink3": 162, 

183 "magenta2": 200, 

184 "hot_pink2": 169, 

185 "orchid": 170, 

186 "medium_orchid1": 207, 

187 "orange3": 172, 

188 "light_pink3": 174, 

189 "pink3": 175, 

190 "plum3": 176, 

191 "violet": 177, 

192 "light_goldenrod3": 179, 

193 "tan": 180, 

194 "misty_rose3": 181, 

195 "thistle3": 182, 

196 "plum2": 183, 

197 "khaki3": 185, 

198 "light_goldenrod2": 222, 

199 "light_yellow3": 187, 

200 "grey84": 188, 

201 "gray84": 188, 

202 "light_steel_blue1": 189, 

203 "yellow2": 190, 

204 "dark_olive_green1": 192, 

205 "honeydew2": 194, 

206 "light_cyan1": 195, 

207 "red1": 196, 

208 "deep_pink2": 197, 

209 "deep_pink1": 199, 

210 "magenta1": 201, 

211 "orange_red1": 202, 

212 "indian_red1": 204, 

213 "hot_pink": 206, 

214 "dark_orange": 208, 

215 "salmon1": 209, 

216 "light_coral": 210, 

217 "pale_violet_red1": 211, 

218 "orchid2": 212, 

219 "orchid1": 213, 

220 "orange1": 214, 

221 "sandy_brown": 215, 

222 "light_salmon1": 216, 

223 "light_pink1": 217, 

224 "pink1": 218, 

225 "plum1": 219, 

226 "gold1": 220, 

227 "navajo_white1": 223, 

228 "misty_rose1": 224, 

229 "thistle1": 225, 

230 "yellow1": 226, 

231 "light_goldenrod1": 227, 

232 "khaki1": 228, 

233 "wheat1": 229, 

234 "cornsilk1": 230, 

235 "grey100": 231, 

236 "gray100": 231, 

237 "grey3": 232, 

238 "gray3": 232, 

239 "grey7": 233, 

240 "gray7": 233, 

241 "grey11": 234, 

242 "gray11": 234, 

243 "grey15": 235, 

244 "gray15": 235, 

245 "grey19": 236, 

246 "gray19": 236, 

247 "grey23": 237, 

248 "gray23": 237, 

249 "grey27": 238, 

250 "gray27": 238, 

251 "grey30": 239, 

252 "gray30": 239, 

253 "grey35": 240, 

254 "gray35": 240, 

255 "grey39": 241, 

256 "gray39": 241, 

257 "grey42": 242, 

258 "gray42": 242, 

259 "grey46": 243, 

260 "gray46": 243, 

261 "grey50": 244, 

262 "gray50": 244, 

263 "grey54": 245, 

264 "gray54": 245, 

265 "grey58": 246, 

266 "gray58": 246, 

267 "grey62": 247, 

268 "gray62": 247, 

269 "grey66": 248, 

270 "gray66": 248, 

271 "grey70": 249, 

272 "gray70": 249, 

273 "grey74": 250, 

274 "gray74": 250, 

275 "grey78": 251, 

276 "gray78": 251, 

277 "grey82": 252, 

278 "gray82": 252, 

279 "grey85": 253, 

280 "gray85": 253, 

281 "grey89": 254, 

282 "gray89": 254, 

283 "grey93": 255, 

284 "gray93": 255, 

285} 

286 

287 

288class ColorParseError(Exception): 

289 """The color could not be parsed.""" 

290 

291 

292RE_COLOR = re.compile( 

293 r"""^ 

294\#([0-9a-f]{6})$| 

295color\(([0-9]{1,3})\)$| 

296rgb\(([\d\s,]+)\)$ 

297""", 

298 re.VERBOSE, 

299) 

300 

301 

302@rich_repr 

303class Color(NamedTuple): 

304 """Terminal color definition.""" 

305 

306 name: str 

307 """The name of the color (typically the input to Color.parse).""" 

308 type: ColorType 

309 """The type of the color.""" 

310 number: Optional[int] = None 

311 """The color number, if a standard color, or None.""" 

312 triplet: Optional[ColorTriplet] = None 

313 """A triplet of color components, if an RGB color.""" 

314 

315 def __rich__(self) -> "Text": 

316 """Displays the actual color if Rich printed.""" 

317 from .style import Style 

318 from .text import Text 

319 

320 return Text.assemble( 

321 f"<color {self.name!r} ({self.type.name.lower()})", 

322 ("⬤", Style(color=self)), 

323 " >", 

324 ) 

325 

326 def __rich_repr__(self) -> Result: 

327 yield self.name 

328 yield self.type 

329 yield "number", self.number, None 

330 yield "triplet", self.triplet, None 

331 

332 @property 

333 def system(self) -> ColorSystem: 

334 """Get the native color system for this color.""" 

335 if self.type == ColorType.DEFAULT: 

336 return ColorSystem.STANDARD 

337 return ColorSystem(int(self.type)) 

338 

339 @property 

340 def is_system_defined(self) -> bool: 

341 """Check if the color is ultimately defined by the system.""" 

342 return self.system not in (ColorSystem.EIGHT_BIT, ColorSystem.TRUECOLOR) 

343 

344 @property 

345 def is_default(self) -> bool: 

346 """Check if the color is a default color.""" 

347 return self.type == ColorType.DEFAULT 

348 

349 def get_truecolor( 

350 self, theme: Optional["TerminalTheme"] = None, foreground: bool = True 

351 ) -> ColorTriplet: 

352 """Get an equivalent color triplet for this color. 

353 

354 Args: 

355 theme (TerminalTheme, optional): Optional terminal theme, or None to use default. Defaults to None. 

356 foreground (bool, optional): True for a foreground color, or False for background. Defaults to True. 

357 

358 Returns: 

359 ColorTriplet: A color triplet containing RGB components. 

360 """ 

361 

362 if theme is None: 

363 theme = DEFAULT_TERMINAL_THEME 

364 if self.type == ColorType.TRUECOLOR: 

365 assert self.triplet is not None 

366 return self.triplet 

367 elif self.type == ColorType.EIGHT_BIT: 

368 assert self.number is not None 

369 return EIGHT_BIT_PALETTE[self.number] 

370 elif self.type == ColorType.STANDARD: 

371 assert self.number is not None 

372 return theme.ansi_colors[self.number] 

373 elif self.type == ColorType.WINDOWS: 

374 assert self.number is not None 

375 return WINDOWS_PALETTE[self.number] 

376 else: # self.type == ColorType.DEFAULT: 

377 assert self.number is None 

378 return theme.foreground_color if foreground else theme.background_color 

379 

380 @classmethod 

381 def from_ansi(cls, number: int) -> "Color": 

382 """Create a Color number from it's 8-bit ansi number. 

383 

384 Args: 

385 number (int): A number between 0-255 inclusive. 

386 

387 Returns: 

388 Color: A new Color instance. 

389 """ 

390 return cls( 

391 name=f"color({number})", 

392 type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT), 

393 number=number, 

394 ) 

395 

396 @classmethod 

397 def from_triplet(cls, triplet: "ColorTriplet") -> "Color": 

398 """Create a truecolor RGB color from a triplet of values. 

399 

400 Args: 

401 triplet (ColorTriplet): A color triplet containing red, green and blue components. 

402 

403 Returns: 

404 Color: A new color object. 

405 """ 

406 return cls(name=triplet.hex, type=ColorType.TRUECOLOR, triplet=triplet) 

407 

408 @classmethod 

409 def from_rgb(cls, red: float, green: float, blue: float) -> "Color": 

410 """Create a truecolor from three color components in the range(0->255). 

411 

412 Args: 

413 red (float): Red component in range 0-255. 

414 green (float): Green component in range 0-255. 

415 blue (float): Blue component in range 0-255. 

416 

417 Returns: 

418 Color: A new color object. 

419 """ 

420 return cls.from_triplet(ColorTriplet(int(red), int(green), int(blue))) 

421 

422 @classmethod 

423 def default(cls) -> "Color": 

424 """Get a Color instance representing the default color. 

425 

426 Returns: 

427 Color: Default color. 

428 """ 

429 return cls(name="default", type=ColorType.DEFAULT) 

430 

431 @classmethod 

432 @lru_cache(maxsize=1024) 

433 def parse(cls, color: str) -> "Color": 

434 """Parse a color definition.""" 

435 original_color = color 

436 color = color.lower().strip() 

437 

438 if color == "default": 

439 return cls(color, type=ColorType.DEFAULT) 

440 

441 color_number = ANSI_COLOR_NAMES.get(color) 

442 if color_number is not None: 

443 return cls( 

444 color, 

445 type=(ColorType.STANDARD if color_number < 16 else ColorType.EIGHT_BIT), 

446 number=color_number, 

447 ) 

448 

449 color_match = RE_COLOR.match(color) 

450 if color_match is None: 

451 raise ColorParseError(f"{original_color!r} is not a valid color") 

452 

453 color_24, color_8, color_rgb = color_match.groups() 

454 if color_24: 

455 triplet = ColorTriplet( 

456 int(color_24[0:2], 16), int(color_24[2:4], 16), int(color_24[4:6], 16) 

457 ) 

458 return cls(color, ColorType.TRUECOLOR, triplet=triplet) 

459 

460 elif color_8: 

461 number = int(color_8) 

462 if number > 255: 

463 raise ColorParseError(f"color number must be <= 255 in {color!r}") 

464 return cls( 

465 color, 

466 type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT), 

467 number=number, 

468 ) 

469 

470 else: # color_rgb: 

471 components = color_rgb.split(",") 

472 if len(components) != 3: 

473 raise ColorParseError( 

474 f"expected three components in {original_color!r}" 

475 ) 

476 red, green, blue = components 

477 triplet = ColorTriplet(int(red), int(green), int(blue)) 

478 if not all(component <= 255 for component in triplet): 

479 raise ColorParseError( 

480 f"color components must be <= 255 in {original_color!r}" 

481 ) 

482 return cls(color, ColorType.TRUECOLOR, triplet=triplet) 

483 

484 @lru_cache(maxsize=1024) 

485 def get_ansi_codes(self, foreground: bool = True) -> Tuple[str, ...]: 

486 """Get the ANSI escape codes for this color.""" 

487 _type = self.type 

488 if _type == ColorType.DEFAULT: 

489 return ("39" if foreground else "49",) 

490 

491 elif _type == ColorType.WINDOWS: 

492 number = self.number 

493 assert number is not None 

494 fore, back = (30, 40) if number < 8 else (82, 92) 

495 return (str(fore + number if foreground else back + number),) 

496 

497 elif _type == ColorType.STANDARD: 

498 number = self.number 

499 assert number is not None 

500 fore, back = (30, 40) if number < 8 else (82, 92) 

501 return (str(fore + number if foreground else back + number),) 

502 

503 elif _type == ColorType.EIGHT_BIT: 

504 assert self.number is not None 

505 return ("38" if foreground else "48", "5", str(self.number)) 

506 

507 else: # self.standard == ColorStandard.TRUECOLOR: 

508 assert self.triplet is not None 

509 red, green, blue = self.triplet 

510 return ("38" if foreground else "48", "2", str(red), str(green), str(blue)) 

511 

512 @lru_cache(maxsize=1024) 

513 def downgrade(self, system: ColorSystem) -> "Color": 

514 """Downgrade a color system to a system with fewer colors.""" 

515 

516 if self.type in (ColorType.DEFAULT, system): 

517 return self 

518 # Convert to 8-bit color from truecolor color 

519 if system == ColorSystem.EIGHT_BIT and self.system == ColorSystem.TRUECOLOR: 

520 assert self.triplet is not None 

521 _h, l, s = rgb_to_hls(*self.triplet.normalized) 

522 # If saturation is under 15% assume it is grayscale 

523 if s < 0.15: 

524 gray = round(l * 25.0) 

525 if gray == 0: 

526 color_number = 16 

527 elif gray == 25: 

528 color_number = 231 

529 else: 

530 color_number = 231 + gray 

531 return Color(self.name, ColorType.EIGHT_BIT, number=color_number) 

532 

533 red, green, blue = self.triplet 

534 six_red = red / 95 if red < 95 else 1 + (red - 95) / 40 

535 six_green = green / 95 if green < 95 else 1 + (green - 95) / 40 

536 six_blue = blue / 95 if blue < 95 else 1 + (blue - 95) / 40 

537 

538 color_number = ( 

539 16 + 36 * round(six_red) + 6 * round(six_green) + round(six_blue) 

540 ) 

541 return Color(self.name, ColorType.EIGHT_BIT, number=color_number) 

542 

543 # Convert to standard from truecolor or 8-bit 

544 elif system == ColorSystem.STANDARD: 

545 if self.system == ColorSystem.TRUECOLOR: 

546 assert self.triplet is not None 

547 triplet = self.triplet 

548 else: # self.system == ColorSystem.EIGHT_BIT 

549 assert self.number is not None 

550 triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number]) 

551 

552 color_number = STANDARD_PALETTE.match(triplet) 

553 return Color(self.name, ColorType.STANDARD, number=color_number) 

554 

555 elif system == ColorSystem.WINDOWS: 

556 if self.system == ColorSystem.TRUECOLOR: 

557 assert self.triplet is not None 

558 triplet = self.triplet 

559 else: # self.system == ColorSystem.EIGHT_BIT 

560 assert self.number is not None 

561 if self.number < 16: 

562 return Color(self.name, ColorType.WINDOWS, number=self.number) 

563 triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number]) 

564 

565 color_number = WINDOWS_PALETTE.match(triplet) 

566 return Color(self.name, ColorType.WINDOWS, number=color_number) 

567 

568 return self 

569 

570 

571def parse_rgb_hex(hex_color: str) -> ColorTriplet: 

572 """Parse six hex characters in to RGB triplet.""" 

573 assert len(hex_color) == 6, "must be 6 characters" 

574 color = ColorTriplet( 

575 int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16) 

576 ) 

577 return color 

578 

579 

580def blend_rgb( 

581 color1: ColorTriplet, color2: ColorTriplet, cross_fade: float = 0.5 

582) -> ColorTriplet: 

583 """Blend one RGB color in to another.""" 

584 r1, g1, b1 = color1 

585 r2, g2, b2 = color2 

586 new_color = ColorTriplet( 

587 int(r1 + (r2 - r1) * cross_fade), 

588 int(g1 + (g2 - g1) * cross_fade), 

589 int(b1 + (b2 - b1) * cross_fade), 

590 ) 

591 return new_color 

592 

593 

594if __name__ == "__main__": # pragma: no cover 

595 from .console import Console 

596 from .table import Table 

597 from .text import Text 

598 

599 console = Console() 

600 

601 table = Table(show_footer=False, show_edge=True) 

602 table.add_column("Color", width=10, overflow="ellipsis") 

603 table.add_column("Number", justify="right", style="yellow") 

604 table.add_column("Name", style="green") 

605 table.add_column("Hex", style="blue") 

606 table.add_column("RGB", style="magenta") 

607 

608 colors = sorted((v, k) for k, v in ANSI_COLOR_NAMES.items()) 

609 for color_number, name in colors: 

610 if "grey" in name: 

611 continue 

612 color_cell = Text(" " * 10, style=f"on {name}") 

613 if color_number < 16: 

614 table.add_row(color_cell, f"{color_number}", Text(f'"{name}"')) 

615 else: 

616 color = EIGHT_BIT_PALETTE[color_number] # type: ignore[has-type] 

617 table.add_row( 

618 color_cell, str(color_number), Text(f'"{name}"'), color.hex, color.rgb 

619 ) 

620 

621 console.print(table)