main.py 14 KB

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