main.py 11 KB

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