main.py 11 KB

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