main.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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('--test-distro', help=color(f'Test for a specific distro'))
  224. parser.add_argument('--test-print', action='store_true', help=color(f'Test print distro ascii art only'))
  225. args = parser.parse_args()
  226. if args.version:
  227. print(f'Version is {VERSION}')
  228. return
  229. # Test distro ascii art
  230. if args.test_distro:
  231. print(f'Setting distro to {args.test_distro}')
  232. GLOBAL_CFG.override_distro = args.test_distro
  233. if args.debug:
  234. GLOBAL_CFG.debug = True
  235. if args.test_print:
  236. print(get_distro_ascii())
  237. return
  238. # Load config
  239. config = check_config()
  240. # Reset config
  241. if args.config:
  242. config = create_config()
  243. # Param overwrite config
  244. if args.preset:
  245. config.preset = args.preset
  246. if args.mode:
  247. config.mode = args.mode
  248. # Override global color mode
  249. GLOBAL_CFG.color_mode = config.mode
  250. GLOBAL_CFG.is_light = config.light_dark == 'light'
  251. # Get preset
  252. preset = PRESETS.get(config.preset)
  253. # Lighten
  254. if args.scale:
  255. preset = preset.lighten(args.scale)
  256. if args.light:
  257. preset = preset.set_light_raw(args.light)
  258. if config.lightness:
  259. preset = preset.set_light_dl(config.lightness)
  260. # Run
  261. run_neofetch(preset, config.color_align)