main.py 11 KB

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