main.py 11 KB

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