main.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. #!/usr/bin/env python3
  2. from __future__ import annotations
  3. import argparse
  4. import json
  5. import random
  6. from dataclasses import dataclass
  7. from itertools import permutations
  8. from typing import Iterable
  9. from typing_extensions import Literal
  10. from . import constants
  11. from .color_util import AnsiMode, printc, color, clear_screen
  12. from .constants import CONFIG_PATH, VERSION, TERM_LEN, TEST_ASCII_WIDTH, TEST_ASCII
  13. from .neofetch_util import run_neofetch, get_distro_ascii, ColorAlignment, ascii_size
  14. from .presets import PRESETS
  15. from .serializer import json_stringify
  16. @dataclass
  17. class Config:
  18. preset: str
  19. mode: AnsiMode
  20. light_dark: Literal['light', 'dark'] = 'dark'
  21. lightness: float | None = None
  22. color_align: ColorAlignment = ColorAlignment('horizontal')
  23. def save(self):
  24. CONFIG_PATH.parent.mkdir(exist_ok=True, parents=True)
  25. CONFIG_PATH.write_text(json_stringify(self), 'utf-8')
  26. def check_config() -> Config:
  27. """
  28. Check if the configuration exists. Return the config object if it exists. If not, call the
  29. config creator
  30. TODO: Config path param
  31. :return: Config object
  32. """
  33. if CONFIG_PATH.is_file():
  34. return Config(**json.loads(CONFIG_PATH.read_text('utf-8')))
  35. return create_config()
  36. def literal_input(prompt: str, options: Iterable[str], default: str, show_ops: bool = True) -> str:
  37. """
  38. Ask the user to provide an input among a list of options
  39. :param prompt: Input prompt
  40. :param options: Options
  41. :param default: Default option
  42. :param show_ops: Show options
  43. :return: Selection
  44. """
  45. options = list(options)
  46. lows = [o.lower() for o in options]
  47. if show_ops:
  48. op_text = '|'.join([f'&l&n{o}&r' if o == default else o for o in options])
  49. printc(f'{prompt} ({op_text})')
  50. else:
  51. printc(f'{prompt} (default: {default})')
  52. selection = input('> ') or default
  53. while not selection.lower() in lows:
  54. print(f'Invalid selection! {selection} is not one of {"|".join(options)}')
  55. selection = input('> ') or default
  56. print()
  57. return options[lows.index(selection)]
  58. def create_config() -> Config:
  59. """
  60. Create config interactively
  61. :return: Config object (automatically stored)
  62. """
  63. title = 'Welcome to &b&lhy&f&lfetch&r! Let\'s set up some colors first.'
  64. clear_screen(title)
  65. ##############################
  66. # 1. Select color system
  67. try:
  68. # Demonstrate RGB with a gradient. This requires numpy
  69. from .color_scale import Scale
  70. scale2 = Scale(['#12c2e9', '#c471ed', '#f7797d'])
  71. _8bit = [scale2(i / TERM_LEN).to_ansi_8bit(False) for i in range(TERM_LEN)]
  72. _rgb = [scale2(i / TERM_LEN).to_ansi_rgb(False) for i in range(TERM_LEN)]
  73. printc('&f' + ''.join(c + t for c, t in zip(_8bit, '8bit Color Testing'.center(TERM_LEN))))
  74. printc('&f' + ''.join(c + t for c, t in zip(_rgb, 'RGB Color Testing'.center(TERM_LEN))))
  75. print()
  76. printc(f'&a1. Which &bcolor system &ado you want to use?')
  77. printc(f'(If you can\'t see colors under "RGB Color Testing", please choose 8bit)')
  78. print()
  79. color_system = literal_input('Your choice?', ['8bit', 'rgb'], 'rgb')
  80. except ModuleNotFoundError:
  81. # Numpy not found, skip gradient test, use fallback
  82. color_system = literal_input('Which &acolor &bsystem &rdo you want to use?',
  83. ['8bit', 'rgb'], 'rgb')
  84. # Override global color mode
  85. constants.COLOR_MODE = color_system
  86. title += f'\n&e1. Selected color mode: &r{color_system}'
  87. ##############################
  88. # 2. Choose preset
  89. clear_screen(title)
  90. printc('&a2. Let\'s choose a flag!')
  91. printc('Available flag presets:')
  92. print()
  93. # Create flags = [[lines]]
  94. flags = []
  95. spacing = max(max(len(k) for k in PRESETS.keys()), 20)
  96. for name, preset in PRESETS.items():
  97. flag = preset.color_text(' ' * spacing, foreground=False)
  98. flags.append([name.center(spacing), flag, flag, flag])
  99. # Calculate flags per row
  100. flags_per_row = TERM_LEN // (spacing + 2)
  101. while flags:
  102. current = flags[:flags_per_row]
  103. flags = flags[flags_per_row:]
  104. # Print by row
  105. [printc(' '.join(line)) for line in zip(*current)]
  106. print()
  107. print()
  108. tmp = PRESETS['rainbow'].set_light(.7).color_text('preset')
  109. preset = literal_input(f'Which {tmp} do you want to use?', PRESETS.keys(), 'rainbow', show_ops=False)
  110. _prs = PRESETS[preset]
  111. title += f'\n&e2. Selected flag: &r{_prs.color_text(preset)}'
  112. ##############################
  113. # 3. Select light/dark mode
  114. clear_screen(title)
  115. light_dark = literal_input(f'3. Is your terminal in &gf(#85e7e9)light mode&r or &gf(#c471ed)dark mode&r?',
  116. ['light', 'dark'], 'dark')
  117. is_light = light_dark == 'light'
  118. title += f'\n&e3. Light/Dark: &r{light_dark}'
  119. #############################
  120. # 4. Dim/lighten colors
  121. clear_screen(title)
  122. printc(f'&a4. Let\'s adjust the color brightness!')
  123. printc(f'The colors might be a little bit too {"bright" if is_light else "dark"} for {light_dark} mode.')
  124. print()
  125. # Print cats
  126. num_cols = TERM_LEN // (TEST_ASCII_WIDTH + 2)
  127. ratios = [col / (num_cols - 1) for col in range(num_cols)]
  128. ratios = [r * 0.6 + 0.2 for r in ratios]
  129. lines = [ColorAlignment('horizontal').recolor_ascii(TEST_ASCII.replace(
  130. '{txt}', f'{r * 100:.0f}%'.center(5)), _prs.set_light(r)).split('\n') for r in ratios]
  131. [printc(' '.join(line)) for line in zip(*lines)]
  132. while True:
  133. print()
  134. printc('Which brightness level look the best? (Default: unset)')
  135. lightness = input('> ').strip().lower() or None
  136. # Parse lightness
  137. if not lightness or lightness in ['unset', 'none']:
  138. lightness = None
  139. break
  140. try:
  141. lightness = int(lightness[:-1]) / 100 if lightness.endswith('%') else float(lightness)
  142. assert 0 <= lightness <= 1
  143. break
  144. except Exception:
  145. printc('&cUnable to parse lightness value, please input it as a decimal or percentage (e.g. 0.5 or 50%)')
  146. if lightness:
  147. _prs = _prs.set_light(lightness)
  148. title += f'\n&e4. Brightness: &r{f"{lightness:.2f}" if lightness else "unset"}'
  149. #############################
  150. # 5. Color arrangement
  151. while True:
  152. clear_screen(title)
  153. printc(f'&a5. Let\'s choose a color arrangement!')
  154. printc(f'You can choose standard horizontal or vertical alignment, or use one of the random color schemes, or assign colors yourself (TODO).')
  155. print()
  156. asc = get_distro_ascii()
  157. asc_width = ascii_size(asc)[0]
  158. asciis = [
  159. ['Horizontal'.center(asc_width), *ColorAlignment('horizontal').recolor_ascii(asc, _prs).split('\n')],
  160. ['Vertical'.center(asc_width), *ColorAlignment('vertical').recolor_ascii(asc, _prs).split('\n')],
  161. ]
  162. # Random color schemes
  163. # ascii_indices =
  164. pis = list(range(len(_prs.unique_colors().colors)))
  165. while len(pis) < 6:
  166. pis += pis
  167. perm = list(permutations(pis))
  168. choices = random.sample(perm, 4)
  169. choices = [{i: n for i, n in enumerate(c)} for c in choices]
  170. asciis += [[f'Random {i}'.center(asc_width), *ColorAlignment('custom', r).recolor_ascii(asc, _prs).split('\n')]
  171. for i, r in enumerate(choices)]
  172. ascii_per_row = TERM_LEN // (asc_width + 2)
  173. while asciis:
  174. current = asciis[:ascii_per_row]
  175. asciis = asciis[ascii_per_row:]
  176. # Print by row
  177. [printc(' '.join(line)) for line in zip(*current)]
  178. print()
  179. print('You can type "roll" to randomize again.')
  180. print()
  181. choice = literal_input(f'Your choice?', ['horizontal', 'vertical', 'roll', 'random1', 'random2', 'random3', 'random4'], 'horizontal')
  182. if choice == 'roll':
  183. continue
  184. if choice in ['horizontal', 'vertical']:
  185. color_alignment = ColorAlignment(choice)
  186. elif choice.startswith('random'):
  187. color_alignment = ColorAlignment('custom', choices[int(choice[6]) - 1])
  188. else:
  189. raise NotImplementedError()
  190. break
  191. title += f'\n&e5. Color Alignment: &r{color_alignment}'
  192. # Create config
  193. clear_screen(title)
  194. c = Config(preset, color_system, light_dark, lightness, color_alignment)
  195. # Save config
  196. print()
  197. save = literal_input(f'Save config?', ['y', 'n'], 'y')
  198. if save == 'y':
  199. c.save()
  200. return c
  201. def run():
  202. # Create CLI
  203. hyfetch = color('&b&lhy&f&lfetch&r')
  204. parser = argparse.ArgumentParser(description=color(f'{hyfetch} - neofetch with flags <3'))
  205. parser.add_argument('-c', '--config', action='store_true', help=color(f'Configure {hyfetch}'))
  206. parser.add_argument('-p', '--preset', help=f'Use preset', choices=PRESETS.keys())
  207. parser.add_argument('-m', '--mode', help=f'Color mode', choices=['8bit', 'rgb'])
  208. parser.add_argument('--c-scale', dest='scale', help=f'Lighten colors by a multiplier', type=float)
  209. parser.add_argument('--c-set-l', dest='light', help=f'Set lightness value of the colors', type=float)
  210. parser.add_argument('-V', '--version', dest='version', action='store_true', help=f'Check version')
  211. parser.add_argument('--debug', action='store_true', help=color(f'Debug mode'))
  212. parser.add_argument('--test-distro', help=color(f'Test print a specific distro\'s ascii art'))
  213. args = parser.parse_args()
  214. if args.version:
  215. print(f'Version is {VERSION}')
  216. return
  217. # Load config
  218. config = check_config()
  219. # Reset config
  220. if args.config:
  221. config = create_config()
  222. # Param overwrite config
  223. if args.preset:
  224. config.preset = args.preset
  225. if args.mode:
  226. config.mode = args.mode
  227. # Override global color mode
  228. constants.COLOR_MODE = config.mode
  229. # Get preset
  230. preset = PRESETS.get(config.preset)
  231. # Lighten
  232. if args.scale:
  233. preset = preset.lighten(args.scale)
  234. if args.light:
  235. preset = preset.set_light(args.light)
  236. if config.lightness:
  237. preset = preset.set_light(config.lightness)
  238. # Test distro ascii art
  239. if args.test_distro:
  240. asc = get_distro_ascii(args.test_distro)
  241. print(asc)
  242. print(ColorAlignment('horizontal').recolor_ascii(asc, preset))
  243. return
  244. # Run
  245. run_neofetch(preset, config.color_align)