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

1 import platform

2 import re

3 from colorsys import rgb_to_hls

4 from enum import IntEnum

5 from functools import lru_cache

6 from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple

7

8 from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE

9 from .color_triplet import ColorTriplet

10 from .repr import Result, rich_repr

11 from .terminal_theme import DEFAULT_TERMINAL_THEME

12

13 if TYPE_CHECKING: # pragma: no cover

14 from .terminal_theme import TerminalTheme

15 from .text import Text

16

17

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

19

20

21 class 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

36 class 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

49 ANSI_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

288 class ColorParseError(Exception):

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

290

291

292 RE_COLOR = re.compile(

293 r"""^

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

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

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

297""",

298 re.VERBOSE,

299)

300

301

302@rich_repr

303 class 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

571 def 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

580 def 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

594 if __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)