main.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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 *
  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. asc = get_distro_ascii()
  67. asc_width, asc_lines = ascii_size(asc)
  68. title = 'Welcome to &b&lhy&f&lfetch&r! Let\'s set up some colors first.'
  69. clear_screen(title)
  70. ##############################
  71. # 0. Check term size
  72. try:
  73. term_len, term_lines = os.get_terminal_size().columns, os.get_terminal_size().lines
  74. if term_len < 2 * asc_width + 4 or term_lines < 30:
  75. printc(f'&cWarning: Your terminal is too small ({term_len} * {term_lines}). \n'
  76. f'Please resize it for better experience.')
  77. input('Press any key to ignore...')
  78. except:
  79. # print('Warning: We cannot detect your terminal size.')
  80. pass
  81. ##############################
  82. # 1. Select color system
  83. clear_screen(title)
  84. term_len, term_lines = term_size()
  85. try:
  86. # Demonstrate RGB with a gradient. This requires numpy
  87. from .color_scale import Scale
  88. scale2 = Scale(['#12c2e9', '#c471ed', '#f7797d'])
  89. _8bit = [scale2(i / term_len).to_ansi_8bit(False) for i in range(term_len)]
  90. _rgb = [scale2(i / term_len).to_ansi_rgb(False) for i in range(term_len)]
  91. printc('&f' + ''.join(c + t for c, t in zip(_8bit, '8bit Color Testing'.center(term_len))))
  92. printc('&f' + ''.join(c + t for c, t in zip(_rgb, 'RGB Color Testing'.center(term_len))))
  93. print()
  94. printc(f'&a1. Which &bcolor system &ado you want to use?')
  95. printc(f'(If you can\'t see colors under "RGB Color Testing", please choose 8bit)')
  96. print()
  97. color_system = literal_input('Your choice?', ['8bit', 'rgb'], 'rgb')
  98. except ModuleNotFoundError:
  99. # Numpy not found, skip gradient test, use fallback
  100. color_system = literal_input('Which &acolor &bsystem &rdo you want to use?',
  101. ['8bit', 'rgb'], 'rgb')
  102. # Override global color mode
  103. GLOBAL_CFG.color_mode = color_system
  104. title += f'\n&e1. Selected color mode: &r{color_system}'
  105. ##############################
  106. # 2. Select light/dark mode
  107. clear_screen(title)
  108. light_dark = literal_input(f'2. Is your terminal in &gf(#85e7e9)light mode&r or &gf(#c471ed)dark mode&r?',
  109. ['light', 'dark'], 'dark')
  110. is_light = light_dark == 'light'
  111. GLOBAL_CFG.is_light = is_light
  112. title += f'\n&e2. Light/Dark: &r{light_dark}'
  113. ##############################
  114. # 3. Choose preset
  115. # Create flags = [[lines]]
  116. flags = []
  117. spacing = max(max(len(k) for k in PRESETS.keys()), 20)
  118. for name, preset in PRESETS.items():
  119. flag = preset.color_text(' ' * spacing, foreground=False)
  120. flags.append([name.center(spacing), flag, flag, flag])
  121. # Calculate flags per row
  122. flags_per_row = term_size()[0] // (spacing + 2)
  123. row_per_page = max(1, (term_size()[1] - 13) // 5)
  124. num_pages = ceil(len(flags) / (flags_per_row * row_per_page))
  125. # Create pages
  126. pages = []
  127. for i in range(num_pages):
  128. page = []
  129. for j in range(row_per_page):
  130. page.append(flags[:flags_per_row])
  131. flags = flags[flags_per_row:]
  132. if not flags:
  133. break
  134. pages.append(page)
  135. def print_flag_page(page: list[list[list[str]]], page_num: int):
  136. clear_screen(title)
  137. printc('&a3. Let\'s choose a flag!')
  138. printc('Available flag presets:')
  139. print(f'Page: {page_num + 1} of {num_pages}')
  140. print()
  141. for i in page:
  142. print_flag_row(i)
  143. print()
  144. def print_flag_row(current: list[list[str]]):
  145. [printc(' '.join(line)) for line in zip(*current)]
  146. print()
  147. page = 0
  148. while True:
  149. print_flag_page(pages[page], page)
  150. tmp = PRESETS['rainbow'].set_light_dl_def(light_dark).color_text('preset')
  151. opts = list(PRESETS.keys())
  152. if page < num_pages - 1:
  153. opts.append('next')
  154. if page > 0:
  155. opts.append('prev')
  156. print("Enter 'next' to go to the next page and 'prev' to go to the previous page.")
  157. preset = literal_input(f'Which {tmp} do you want to use? ', opts, 'rainbow', show_ops=False)
  158. if preset == 'next':
  159. page += 1
  160. elif preset == 'prev':
  161. page -= 1
  162. else:
  163. _prs = PRESETS[preset]
  164. title += f'\n&e3. Selected flag: &r{_prs.color_text(preset)}'
  165. break
  166. #############################
  167. # 4. Dim/lighten colors
  168. clear_screen(title)
  169. printc(f'&a4. Let\'s adjust the color brightness!')
  170. printc(f'The colors might be a little bit too {"bright" if is_light else "dark"} for {light_dark} mode.')
  171. print()
  172. # Print cats
  173. num_cols = term_size()[0] // (TEST_ASCII_WIDTH + 2)
  174. ratios = [col / (num_cols - 1) for col in range(num_cols)]
  175. ratios = [(r * 0.4 + 0.1) if is_light else (r * 0.4 + 0.5) for r in ratios]
  176. lines = [ColorAlignment('horizontal').recolor_ascii(TEST_ASCII.replace(
  177. '{txt}', f'{r * 100:.0f}%'.center(5)), _prs.set_light_dl(r, light_dark)).split('\n') for r in ratios]
  178. [printc(' '.join(line)) for line in zip(*lines)]
  179. while True:
  180. print()
  181. printc(f'Which brightness level look the best? (Default: left blank = {GLOBAL_CFG.default_lightness(light_dark):.2f} for {light_dark} mode)')
  182. lightness = input('> ').strip().lower() or None
  183. # Parse lightness
  184. if not lightness or lightness in ['unset', 'none']:
  185. lightness = None
  186. break
  187. try:
  188. lightness = int(lightness[:-1]) / 100 if lightness.endswith('%') else float(lightness)
  189. assert 0 <= lightness <= 1
  190. break
  191. except Exception:
  192. printc('&cUnable to parse lightness value, please input it as a decimal or percentage (e.g. 0.5 or 50%)')
  193. if lightness:
  194. _prs = _prs.set_light_dl(lightness, light_dark)
  195. title += f'\n&e4. Brightness: &r{f"{lightness:.2f}" if lightness else "unset"}'
  196. #############################
  197. # 5. Color arrangement
  198. color_alignment = None
  199. fore_back = get_fore_back()
  200. # Calculate amount of row/column that can be displayed on screen
  201. ascii_per_row = term_size()[0] // (asc_width + 2)
  202. ascii_rows = max(1, (term_size()[1] - 8) // asc_lines)
  203. # Displays horizontal and vertical arrangements in the first iteration, but hide them in
  204. # later iterations
  205. hv_arrangements = [
  206. ('Horizontal', ColorAlignment('horizontal', fore_back=fore_back)),
  207. ('Vertical', ColorAlignment('vertical'))
  208. ]
  209. arrangements = hv_arrangements.copy()
  210. # Loop for random rolling
  211. while True:
  212. clear_screen(title)
  213. # Random color schemes
  214. pis = list(range(len(_prs.unique_colors().colors)))
  215. slots = len(set(re.findall('(?<=\\${c)[0-9](?=})', asc)))
  216. while len(pis) < slots:
  217. pis += pis
  218. perm = {p[:slots] for p in permutations(pis)}
  219. random_count = ascii_per_row * ascii_rows - len(arrangements)
  220. if random_count > len(perm):
  221. choices = perm
  222. else:
  223. choices = random.sample(perm, random_count)
  224. choices = [{i + 1: n for i, n in enumerate(c)} for c in choices]
  225. arrangements += [(f'random{i}', ColorAlignment('custom', r)) for i, r in enumerate(choices)]
  226. asciis = [[*ca.recolor_ascii(asc, _prs).split('\n'), k.center(asc_width)] for k, ca in arrangements]
  227. while asciis:
  228. current = asciis[:ascii_per_row]
  229. asciis = asciis[ascii_per_row:]
  230. # Print by row
  231. [printc(' '.join(line)) for line in zip(*current)]
  232. print()
  233. printc(f'&a5. Let\'s choose a color arrangement!')
  234. printc(f'You can choose standard horizontal or vertical alignment, or use one of the random color schemes.')
  235. print('You can type "roll" to randomize again.')
  236. print()
  237. choice = literal_input(f'Your choice?', ['horizontal', 'vertical', 'roll'] + [f'random{i}' for i in range(random_count)], 'horizontal')
  238. if choice == 'roll':
  239. arrangements = []
  240. continue
  241. # Save choice
  242. arrangement_index = {k.lower(): ca for k, ca in hv_arrangements + arrangements}
  243. if choice in arrangement_index:
  244. color_alignment = arrangement_index[choice]
  245. else:
  246. print('Invalid choice.')
  247. continue
  248. break
  249. title += f'\n&e5. Color Alignment: &r{color_alignment}'
  250. # Create config
  251. clear_screen(title)
  252. c = Config(preset, color_system, light_dark, lightness, color_alignment)
  253. # Save config
  254. print()
  255. save = literal_input(f'Save config?', ['y', 'n'], 'y')
  256. if save == 'y':
  257. c.save()
  258. return c
  259. def run():
  260. # Optional: Import readline
  261. try:
  262. import readline
  263. except ModuleNotFoundError:
  264. pass
  265. # Create CLI
  266. hyfetch = color('&b&lhyfetch&r')
  267. parser = argparse.ArgumentParser(description=color(f'{hyfetch} - neofetch with flags <3'))
  268. parser.add_argument('-c', '--config', action='store_true', help=color(f'Configure {hyfetch}'))
  269. parser.add_argument('-p', '--preset', help=f'Use preset', choices=PRESETS.keys())
  270. parser.add_argument('-m', '--mode', help=f'Color mode', choices=['8bit', 'rgb'])
  271. parser.add_argument('--c-scale', dest='scale', help=f'Lighten colors by a multiplier', type=float)
  272. parser.add_argument('--c-set-l', dest='light', help=f'Set lightness value of the colors', type=float)
  273. parser.add_argument('-V', '--version', dest='version', action='store_true', help=f'Check version')
  274. parser.add_argument('--debug', action='store_true', help=f'Debug mode')
  275. parser.add_argument('--test-distro', help=f'Test for a specific distro')
  276. parser.add_argument('--test-print', action='store_true', help=f'Test print distro ascii art only')
  277. args = parser.parse_args()
  278. if args.version:
  279. print(f'Version is {VERSION}')
  280. return
  281. # Test distro ascii art
  282. if args.test_distro:
  283. print(f'Setting distro to {args.test_distro}')
  284. GLOBAL_CFG.override_distro = args.test_distro
  285. if args.debug:
  286. GLOBAL_CFG.debug = True
  287. if args.test_print:
  288. print(get_distro_ascii())
  289. return
  290. # Load config
  291. config = check_config()
  292. # Reset config
  293. if args.config:
  294. config = create_config()
  295. # Param overwrite config
  296. if args.preset:
  297. config.preset = args.preset
  298. if args.mode:
  299. config.mode = args.mode
  300. # Override global color mode
  301. GLOBAL_CFG.color_mode = config.mode
  302. GLOBAL_CFG.is_light = config.light_dark == 'light'
  303. # Get preset
  304. preset = PRESETS.get(config.preset)
  305. # Lighten
  306. if args.scale:
  307. preset = preset.lighten(args.scale)
  308. if args.light:
  309. preset = preset.set_light_raw(args.light)
  310. if config.lightness:
  311. preset = preset.set_light_dl(config.lightness)
  312. # Run
  313. run_neofetch(preset, config.color_align)