main.py 12 KB

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