main.py 11 KB

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