main.py 11 KB

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