main.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  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. arrangements = [
  152. ('Horizontal', ColorAlignment('horizontal', fore_back=fore_back)),
  153. ('Vertical', ColorAlignment('vertical'))
  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. arrangements += [(f'random{i}', ColorAlignment('custom', r)) for i, r in enumerate(choices)]
  169. asciis = [[*ca.recolor_ascii(asc, _prs).split('\n'), k.center(asc_width)] for k, ca in arrangements]
  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. # Save choice
  184. arrangement_index = {k.lower(): ca for k, ca in arrangements}
  185. if choice in arrangement_index:
  186. color_alignment = arrangement_index[choice]
  187. else:
  188. print('Invalid choice.')
  189. continue
  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&lhyfetch&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('--debug-list', help=color(f'Debug recommendations'))
  213. parser.add_argument('--test-distro', help=color(f'Test for a specific distro'))
  214. parser.add_argument('--test-print', action='store_true', help=color(f'Test print distro ascii art only'))
  215. args = parser.parse_args()
  216. if args.version:
  217. print(f'Version is {VERSION}')
  218. return
  219. # Test distro ascii art
  220. if args.test_distro:
  221. print(f'Setting distro to {args.test_distro}')
  222. GLOBAL_CFG.override_distro = args.test_distro
  223. if args.debug:
  224. GLOBAL_CFG.debug = True
  225. if args.test_print:
  226. print(get_distro_ascii())
  227. return
  228. # Load config
  229. config = check_config()
  230. # Reset config
  231. if args.config:
  232. config = create_config()
  233. # Param overwrite config
  234. if args.preset:
  235. config.preset = args.preset
  236. if args.mode:
  237. config.mode = args.mode
  238. # Override global color mode
  239. GLOBAL_CFG.color_mode = config.mode
  240. GLOBAL_CFG.is_light = config.light_dark == 'light'
  241. # Get preset
  242. preset = PRESETS.get(config.preset)
  243. # Lighten
  244. if args.scale:
  245. preset = preset.lighten(args.scale)
  246. if args.light:
  247. preset = preset.set_light_raw(args.light)
  248. if config.lightness:
  249. preset = preset.set_light_dl(config.lightness)
  250. # Debug recommendations
  251. if args.debug_list:
  252. distro = args.debug_list
  253. ca = fore_back[distro]
  254. print(distro)
  255. GLOBAL_CFG.override_distro = distro
  256. asciis = [ca.recolor_ascii(get_distro_ascii(distro), p).split('\n') for p in list(PRESETS.values())[:3]]
  257. [printc(' '.join(line)) for line in zip(*asciis)]
  258. return
  259. # Run
  260. run_neofetch(preset, config.color_align)