color_util.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. from __future__ import annotations
  2. import colorsys
  3. from typing import NamedTuple, Callable, Optional
  4. from typing_extensions import Literal
  5. from .constants import GLOBAL_CFG
  6. AnsiMode = Literal['default', 'ansi', '8bit', 'rgb']
  7. LightDark = Literal['light', 'dark']
  8. MINECRAFT_COLORS = ["&0/\033[0;30m", "&1/\033[0;34m", "&2/\033[0;32m", "&3/\033[0;36m", "&4/\033[0;31m",
  9. "&5/\033[0;35m", "&6/\033[0;33m", "&7/\033[0;37m", "&8/\033[1;30m", "&9/\033[1;34m",
  10. "&a/\033[1;32m", "&b/\033[1;36m", "&c/\033[1;31m", "&d/\033[1;35m", "&e/\033[1;33m",
  11. "&f/\033[1;37m",
  12. "&r/\033[0m", "&l/\033[1m", "&o/\033[3m", "&n/\033[4m", "&-/\n"]
  13. MINECRAFT_COLORS = [(r[:2], r[3:]) for r in MINECRAFT_COLORS]
  14. def color(msg: str) -> str:
  15. """
  16. Replace extended minecraft color codes in string
  17. :param msg: Message with minecraft color codes
  18. :return: Message with escape codes
  19. """
  20. for code, esc in MINECRAFT_COLORS:
  21. msg = msg.replace(code, esc)
  22. while '&gf(' in msg or '&gb(' in msg:
  23. i = msg.index('&gf(') if '&gf(' in msg else msg.index('&gb(')
  24. end = msg.index(')', i)
  25. code = msg[i + 4:end]
  26. fore = msg[i + 2] == 'f'
  27. if code.startswith('#'):
  28. rgb = tuple(int(code.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
  29. else:
  30. code = code.replace(',', ' ').replace(';', ' ').replace(' ', ' ')
  31. rgb = tuple(int(c) for c in code.split(' '))
  32. msg = msg[:i] + RGB(*rgb).to_ansi(foreground=fore) + msg[end + 1:]
  33. return msg
  34. def printc(msg: str):
  35. """
  36. Print with color
  37. :param msg: Message with minecraft color codes
  38. """
  39. print(color(msg + '&r'))
  40. def clear_screen(title: str = ''):
  41. """
  42. Clear screen using ANSI escape codes
  43. """
  44. if not GLOBAL_CFG.debug:
  45. print('\033[2J\033[H', end='')
  46. if title:
  47. print()
  48. printc(title)
  49. print()
  50. def redistribute_rgb(r: int, g: int, b: int) -> tuple[int, int, int]:
  51. """
  52. Redistribute RGB after lightening
  53. Credit: https://stackoverflow.com/a/141943/7346633
  54. """
  55. threshold = 255.999
  56. m = max(r, g, b)
  57. if m <= threshold:
  58. return int(r), int(g), int(b)
  59. total = r + g + b
  60. if total >= 3 * threshold:
  61. return int(threshold), int(threshold), int(threshold)
  62. x = (3 * threshold - total) / (3 * m - total)
  63. gray = threshold - x * m
  64. return int(gray + x * r), int(gray + x * g), int(gray + x * b)
  65. class RGB(NamedTuple):
  66. r: int
  67. g: int
  68. b: int
  69. @classmethod
  70. def from_hex(cls, hex: str) -> "RGB":
  71. """
  72. Create color from hex code
  73. >>> RGB.from_hex('#FFAAB7')
  74. RGB(r=255, g=170, b=183)
  75. :param hex: Hex color code
  76. :return: RGB object
  77. """
  78. while hex.startswith('#'):
  79. hex = hex[1:]
  80. r = int(hex[0:2], 16)
  81. g = int(hex[2:4], 16)
  82. b = int(hex[4:6], 16)
  83. return cls(r, g, b)
  84. def to_ansi_rgb(self, foreground: bool = True) -> str:
  85. """
  86. Convert RGB to ANSI TrueColor (RGB) Escape Code.
  87. This uses the 24-bit color encoding (an uint8 for each color value), and supports 16 million
  88. colors. However, not all terminal emulators support this escape code. (For example, IntelliJ
  89. debug console doesn't support it).
  90. Currently, we do not know how to detect whether a terminal environment supports ANSI RGB. If
  91. you have any thoughts, feel free to submit an issue on our Github page!
  92. :param foreground: Whether the color is for foreground text or background color
  93. :return: ANSI RGB escape code like \033[38;2;255;100;0m
  94. """
  95. c = '38' if foreground else '48'
  96. return f'\033[{c};2;{self.r};{self.g};{self.b}m'
  97. def to_ansi_8bit(self, foreground: bool = True) -> str:
  98. """
  99. Convert RGB to ANSI 8bit 256 Color Escape Code.
  100. This encoding supports 256 colors in total.
  101. :return: ANSI 256 escape code like \033[38;5;206m'
  102. """
  103. r, g, b = self.r, self.g, self.b
  104. sep = 42.5
  105. while True:
  106. if r < sep or g < sep or b < sep:
  107. gray = r < sep and g < sep and b < sep
  108. break
  109. sep += 42.5
  110. if gray:
  111. color = 232 + (r + g + b) / 33
  112. else:
  113. color = 16 + int(r / 256. * 6) * 36 + int(g / 256. * 6) * 6 + int(b / 256. * 6)
  114. c = '38' if foreground else '48'
  115. return f'\033[{c};5;{int(color)}m'
  116. def to_ansi_16(self, foreground: bool = True) -> str:
  117. """
  118. Convert RGB to ANSI 16 Color Escape Code
  119. :return: ANSI 16 escape code
  120. """
  121. raise NotImplementedError()
  122. def to_ansi(self, mode: AnsiMode | None = None, foreground: bool = True):
  123. if not mode:
  124. mode = GLOBAL_CFG.color_mode
  125. if mode == 'rgb':
  126. return self.to_ansi_rgb(foreground)
  127. if mode == '8bit':
  128. return self.to_ansi_8bit(foreground)
  129. if mode == 'ansi':
  130. return self.to_ansi_16(foreground)
  131. def lighten(self, multiplier: float) -> 'RGB':
  132. """
  133. Lighten the color by a multiplier
  134. :param multiplier: Multiplier
  135. :return: Lightened color (original isn't modified)
  136. """
  137. return RGB(*redistribute_rgb(*[v * multiplier for v in self]))
  138. def set_light(self, light: float, at_least: bool | None = None, at_most: bool | None = None) -> 'RGB':
  139. """
  140. Set HSL lightness value
  141. :param light: Lightness value (0-1)
  142. :param at_least: Set the lightness to at least this value (no change if greater)
  143. :param at_most: Set the lightness to at most this value (no change if lesser)
  144. :return: New color (original isn't modified)
  145. """
  146. # Convert to HSL
  147. h, l, s = colorsys.rgb_to_hls(*[v / 255.0 for v in self])
  148. # Modify light value
  149. if at_least is None and at_most is None:
  150. l = light
  151. else:
  152. if at_most:
  153. l = min(l, light)
  154. if at_least:
  155. l = max(l, light)
  156. # Convert back to RGB
  157. return RGB(*[round(v * 255.0) for v in colorsys.hls_to_rgb(h, l, s)])