main.py 13 KB

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