main.py 12 KB

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