presets.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. from __future__ import annotations
  2. from typing import Iterable
  3. from typing_extensions import Literal
  4. from .color_util import RGB, LightDark
  5. from .constants import GLOBAL_CFG
  6. def remove_duplicates(seq: Iterable) -> list:
  7. """
  8. Remove duplicate items from a sequence while preserving the order
  9. """
  10. seen = set()
  11. seen_add = seen.add
  12. return [x for x in seq if not (x in seen or seen_add(x))]
  13. class ColorProfile:
  14. raw: list[str]
  15. colors: list[RGB]
  16. spacing: Literal['equal', 'weighted'] = 'equal'
  17. def __init__(self, colors: list[str] | list[RGB]):
  18. if isinstance(colors[0], str):
  19. self.raw = colors
  20. self.colors = [RGB.from_hex(c) for c in colors]
  21. else:
  22. self.colors = colors
  23. def with_weights(self, weights: list[int]) -> list[RGB]:
  24. """
  25. Map colors based on weights
  26. :param weights: Weights of each color (weights[i] = how many times color[i] appears)
  27. :return:
  28. """
  29. return [c for i, w in enumerate(weights) for c in [self.colors[i]] * w]
  30. def with_length(self, length: int) -> list[RGB]:
  31. """
  32. Spread to a specific length of text
  33. :param length: Length of text
  34. :return: List of RGBs of the length
  35. """
  36. preset_len = len(self.colors)
  37. center_i = preset_len // 2
  38. # How many copies of each color should be displayed at least?
  39. repeats = length // preset_len
  40. weights = [repeats] * preset_len
  41. # How many extra space left?
  42. extras = length % preset_len
  43. # If there is an even space left, extend the center by one space
  44. if extras % 2 == 1:
  45. extras -= 1
  46. weights[center_i] += 1
  47. # Add weight to border until there's no space left (extras must be even at this point)
  48. border_i = 0
  49. while extras > 0:
  50. extras -= 2
  51. weights[border_i] += 1
  52. weights[-(border_i + 1)] += 1
  53. border_i += 1
  54. return self.with_weights(weights)
  55. def color_text(self, txt: str, foreground: bool = True, space_only: bool = False) -> str:
  56. """
  57. Color a text
  58. :param txt: Text
  59. :param foreground: Whether the foreground text show the color or the background block
  60. :param space_only: Whether to only color spaces
  61. :return: Colored text
  62. """
  63. colors = self.with_length(len(txt))
  64. result = ''
  65. for i, t in enumerate(txt):
  66. if space_only and t != ' ':
  67. if i > 0 and txt[i - 1] == ' ':
  68. result += '\033[0m'
  69. result += t
  70. else:
  71. result += colors[i].to_ansi(foreground=foreground) + t
  72. result += '\033[0m'
  73. return result
  74. def lighten(self, multiplier: float) -> ColorProfile:
  75. """
  76. Lighten the color profile by a multiplier
  77. :param multiplier: Multiplier
  78. :return: Lightened color profile (original isn't modified)
  79. """
  80. return ColorProfile([c.lighten(multiplier) for c in self.colors])
  81. def set_light_raw(self, light: float, at_least: bool | None = None, at_most: bool | None = None) -> 'ColorProfile':
  82. """
  83. Set HSL lightness value
  84. :param light: Lightness value (0-1)
  85. :param at_least: Set the lightness to at least this value (no change if greater)
  86. :param at_most: Set the lightness to at most this value (no change if lesser)
  87. :return: New color profile (original isn't modified)
  88. """
  89. return ColorProfile([c.set_light(light, at_least, at_most) for c in self.colors])
  90. def set_light_dl(self, light: float, term: LightDark = GLOBAL_CFG.light_dark()):
  91. """
  92. Set HSL lightness value with respect to dark/light terminals
  93. :param light: Lightness value (0-1)
  94. :param term: Terminal color (can be "dark" or "light")
  95. :return: New color profile (original isn't modified)
  96. """
  97. assert term.lower() in ['light', 'dark']
  98. at_least, at_most = (True, None) if term.lower() == 'dark' else (None, True)
  99. return self.set_light_raw(light, at_least, at_most)
  100. def set_light_dl_def(self, term: LightDark | None = None):
  101. """
  102. Set default lightness with respect to dark/light terminals
  103. :param term: Terminal color (can be "dark" or "light")
  104. :return: New color profile (original isn't modified)
  105. """
  106. return self.set_light_dl(GLOBAL_CFG.default_lightness(term), term)
  107. def unique_colors(self) -> ColorProfile:
  108. """
  109. Create another color profile with only the unique colors
  110. """
  111. return ColorProfile(remove_duplicates(self.colors))
  112. PRESETS: dict[str, ColorProfile] = {
  113. 'rainbow': ColorProfile([
  114. '#E50000',
  115. '#FF8D00',
  116. '#FFEE00',
  117. '#028121',
  118. '#004CFF',
  119. '#770088'
  120. ]),
  121. 'transgender': ColorProfile([
  122. '#55CDFD',
  123. '#F6AAB7',
  124. '#FFFFFF',
  125. '#F6AAB7',
  126. '#55CDFD'
  127. ]),
  128. 'nonbinary': ColorProfile([
  129. '#FCF431',
  130. '#FCFCFC',
  131. '#9D59D2',
  132. '#282828'
  133. ]),
  134. 'agender': ColorProfile([
  135. '#000000',
  136. '#BABABA',
  137. '#FFFFFF',
  138. '#BAF484',
  139. '#FFFFFF',
  140. '#BABABA',
  141. '#000000'
  142. ]),
  143. 'queer': ColorProfile([
  144. '#B57FDD',
  145. '#FFFFFF',
  146. '#49821E'
  147. ]),
  148. 'genderfluid': ColorProfile([
  149. '#FE76A2',
  150. '#FFFFFF',
  151. '#BF12D7',
  152. '#000000',
  153. '#303CBE'
  154. ]),
  155. 'bisexual': ColorProfile([
  156. '#D60270',
  157. '#9B4F96',
  158. '#0038A8'
  159. ]),
  160. 'pansexual': ColorProfile([
  161. '#FF1C8D',
  162. '#FFD700',
  163. '#1AB3FF'
  164. ]),
  165. 'lesbian': ColorProfile([
  166. '#D62800',
  167. '#FF9B56',
  168. '#FFFFFF',
  169. '#D462A6',
  170. '#A40062'
  171. ]),
  172. 'asexual': ColorProfile([
  173. '#000000',
  174. '#A4A4A4',
  175. '#FFFFFF',
  176. '#810081'
  177. ]),
  178. 'aromantic': ColorProfile([
  179. '#3BA740',
  180. '#A8D47A',
  181. '#FFFFFF',
  182. '#ABABAB',
  183. '#000000'
  184. ]),
  185. # below sourced from https://www.flagcolorcodes.com/flags/pride
  186. # goto f"https://www.flagcolorcodes.com/{preset}" for info
  187. # todo: sane sorting
  188. 'autosexual': ColorProfile([
  189. '#99D9EA',
  190. '#7F7F7F'
  191. ]),
  192. 'intergender': ColorProfile([
  193. # todo: use weighted spacing
  194. '#900DC2',
  195. '#900DC2',
  196. '#FFE54F',
  197. '#900DC2',
  198. '#900DC2',
  199. ]),
  200. 'greygender': ColorProfile([
  201. '#B3B3B3',
  202. '#B3B3B3',
  203. '#FFFFFF',
  204. '#062383',
  205. '#062383',
  206. '#FFFFFF',
  207. '#535353',
  208. '#535353',
  209. ]),
  210. 'akiosexual': ColorProfile([
  211. '#F9485E',
  212. '#FEA06A',
  213. '#FEF44C',
  214. '#FFFFFF',
  215. '#000000',
  216. ]),
  217. 'transmasculine': ColorProfile([
  218. '#FF8ABD',
  219. '#CDF5FE',
  220. '#9AEBFF',
  221. '#74DFFF',
  222. '#9AEBFF',
  223. '#CDF5FE',
  224. '#FF8ABD',
  225. ]),
  226. 'demifaun': ColorProfile([
  227. '#7F7F7F',
  228. '#7F7F7F',
  229. '#C6C6C6',
  230. '#C6C6C6',
  231. '#FCC688',
  232. '#FFF19C',
  233. '#FFFFFF',
  234. '#8DE0D5',
  235. '#9682EC',
  236. '#C6C6C6',
  237. '#C6C6C6',
  238. '#7F7F7F',
  239. '#7F7F7F',
  240. ]),
  241. 'neutrois': ColorProfile([
  242. '#FFFFFF',
  243. '#1F9F00',
  244. '#000000'
  245. ]),
  246. 'biromantic1': ColorProfile([
  247. '#8869A5',
  248. '#D8A7D8',
  249. '#FFFFFF',
  250. '#FDB18D',
  251. '#151638',
  252. ]),
  253. 'biromantic2': ColorProfile([
  254. '#740194',
  255. '#AEB1AA',
  256. '#FFFFFF',
  257. '#AEB1AA',
  258. '#740194',
  259. ]),
  260. 'autoromantic': ColorProfile([ # symbol interpreted
  261. '#99D9EA',
  262. '#99D9EA',
  263. '#99D9EA',
  264. '#99D9EA',
  265. '#99D9EA',
  266. '#000000',
  267. '#3DA542',
  268. '#3DA542',
  269. '#000000',
  270. '#7F7F7F',
  271. '#7F7F7F',
  272. '#7F7F7F',
  273. '#7F7F7F',
  274. '#7F7F7F',
  275. ]),
  276. # i didn't expect this one to work. cool!
  277. 'boyflux2': ColorProfile([
  278. '#E48AE4',
  279. '#9A81B4',
  280. '#55BFAB',
  281. '#FFFFFF',
  282. '#A8A8A8',
  283. '#81D5EF',
  284. '#81D5EF',
  285. '#81D5EF',
  286. '#81D5EF',
  287. '#81D5EF',
  288. '#69ABE5',
  289. '#69ABE5',
  290. '#69ABE5',
  291. '#69ABE5',
  292. '#69ABE5',
  293. '#69ABE5',
  294. '#69ABE5',
  295. '#69ABE5',
  296. '#69ABE5',
  297. '#69ABE5',
  298. '#5276D4',
  299. '#5276D4',
  300. '#5276D4',
  301. '#5276D4',
  302. '#5276D4',
  303. '#5276D4',
  304. '#5276D4',
  305. '#5276D4',
  306. '#5276D4',
  307. '#5276D4',
  308. ]),
  309. 'beiyang': ColorProfile([
  310. '#DF1B12',
  311. '#FFC600',
  312. '#01639D',
  313. '#FFFFFF',
  314. '#000000',
  315. ]),
  316. }