main.py 14 KB

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