main.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/env python3
  2. from __future__ import annotations
  3. import argparse
  4. import importlib
  5. import json
  6. import os
  7. from dataclasses import dataclass
  8. from pathlib import Path
  9. from typing import Iterable
  10. from typing_extensions import Literal
  11. from . import constants
  12. from .color_util import AnsiMode, printc, color, clear_screen, RGB
  13. from .constants import CONFIG_PATH, VERSION, TERM_LEN, TEST_ASCII_WIDTH
  14. from .neofetch_util import run_neofetch, replace_colors, get_custom_distro_ascii
  15. from .presets import PRESETS, ColorProfile
  16. from .serializer import json_stringify
  17. @dataclass
  18. class Config:
  19. preset: str
  20. mode: AnsiMode
  21. light_dark: Literal['light', 'dark'] = 'dark'
  22. def save(self):
  23. CONFIG_PATH.parent.mkdir(exist_ok=True, parents=True)
  24. CONFIG_PATH.write_text(json_stringify(self), 'utf-8')
  25. def check_config() -> Config:
  26. """
  27. Check if the configuration exists. Return the config object if it exists. If not, call the
  28. config creator
  29. TODO: Config path param
  30. :return: Config object
  31. """
  32. if CONFIG_PATH.is_file():
  33. return Config(**json.loads(CONFIG_PATH.read_text('utf-8')))
  34. return create_config()
  35. def literal_input(prompt: str, options: Iterable[str], default: str, show_ops: bool = True) -> str:
  36. """
  37. Ask the user to provide an input among a list of options
  38. :param prompt: Input prompt
  39. :param options: Options
  40. :param default: Default option
  41. :param show_ops: Show options
  42. :return: Selection
  43. """
  44. options = list(options)
  45. lows = [o.lower() for o in options]
  46. if show_ops:
  47. op_text = '|'.join([f'&l&n{o}&r' if o == default else o for o in options])
  48. printc(f'{prompt} ({op_text})')
  49. else:
  50. printc(f'{prompt} (default: {default})')
  51. selection = input('> ') or default
  52. while not selection.lower() in lows:
  53. print(f'Invalid selection! {selection} is not one of {"|".join(options)}')
  54. selection = input('> ') or default
  55. print()
  56. return options[lows.index(selection)]
  57. def create_config() -> Config:
  58. """
  59. Create config interactively
  60. :return: Config object (automatically stored)
  61. """
  62. title = 'Welcome to &b&lhy&f&lfetch&r! Let\'s set up some colors first.'
  63. clear_screen(title)
  64. ##############################
  65. # 1. Select color system
  66. try:
  67. # Demonstrate RGB with a gradient. This requires numpy
  68. from .color_scale import Scale
  69. scale2 = Scale(['#12c2e9', '#c471ed', '#f7797d'])
  70. _8bit = [scale2(i / TERM_LEN).to_ansi_8bit(False) for i in range(TERM_LEN)]
  71. _rgb = [scale2(i / TERM_LEN).to_ansi_rgb(False) for i in range(TERM_LEN)]
  72. printc('&f' + ''.join(c + t for c, t in zip(_8bit, '8bit Color Testing'.center(TERM_LEN))))
  73. printc('&f' + ''.join(c + t for c, t in zip(_rgb, 'RGB Color Testing'.center(TERM_LEN))))
  74. print()
  75. printc(f'&a1. Which &bcolor system &ado you want to use?')
  76. printc(f'(If you can\'t see colors under "RGB Color Testing", please choose 8bit)')
  77. print()
  78. color_system = literal_input('Your choice?', ['8bit', 'rgb'], 'rgb')
  79. except ModuleNotFoundError:
  80. # Numpy not found, skip gradient test, use fallback
  81. color_system = literal_input('Which &acolor &bsystem &rdo you want to use?',
  82. ['8bit', 'rgb'], 'rgb')
  83. # Override global color mode
  84. constants.COLOR_MODE = color_system
  85. title += f'\n&e1. Selected color mode: &r{color_system}'
  86. ##############################
  87. # 2. Choose preset
  88. clear_screen(title)
  89. printc('&a2. Let\'s choose a flag!')
  90. printc('Available flag presets:')
  91. print()
  92. # Create flags = [[lines]]
  93. flags = []
  94. spacing = max(max(len(k) for k in PRESETS.keys()), 20)
  95. for name, preset in PRESETS.items():
  96. flag = preset.color_text(' ' * spacing, foreground=False)
  97. flags.append([name.center(spacing), flag, flag, flag])
  98. # Calculate flags per row
  99. flags_per_row = TERM_LEN // (spacing + 2)
  100. while flags:
  101. current = flags[:flags_per_row]
  102. flags = flags[flags_per_row:]
  103. # Print by row
  104. for line in zip(*current):
  105. printc(' '.join(line))
  106. print()
  107. print()
  108. tmp = PRESETS['rainbow'].set_light(.7).color_text('preset')
  109. preset = literal_input(f'Which {tmp} do you want to use?', PRESETS.keys(), 'rainbow', show_ops=False)
  110. title += f'\n&e2. Selected flag: &r{PRESETS[preset].color_text(preset)}'
  111. ##############################
  112. # 3. Select light/dark mode
  113. clear_screen(title)
  114. light_dark = literal_input(f'3. Is your terminal in &gf(#85e7e9)light mode&r or &gf(#c471ed)dark mode&r?',
  115. ['light', 'dark'], 'dark')
  116. is_light = light_dark == 'light'
  117. title += f'\n&e3. Light/Dark: &r{light_dark}'
  118. # Create config
  119. c = Config(preset, color_system, light_dark)
  120. # Save config
  121. save = literal_input(f'Save config?', ['y', 'n'], 'y')
  122. if save == 'y':
  123. c.save()
  124. return c
  125. def run():
  126. # Create CLI
  127. hyfetch = color('&b&lhy&f&lfetch&r')
  128. parser = argparse.ArgumentParser(description=color(f'{hyfetch} - neofetch with flags <3'))
  129. parser.add_argument('-c', '--config', action='store_true', help=color(f'Configure {hyfetch}'))
  130. parser.add_argument('-p', '--preset', help=f'Use preset', choices=PRESETS.keys())
  131. parser.add_argument('-m', '--mode', help=f'Color mode', choices=['8bit', 'rgb'])
  132. parser.add_argument('--c-scale', dest='scale', help=f'Lighten colors by a multiplier', type=float)
  133. parser.add_argument('--c-set-l', dest='light', help=f'Set lightness value of the colors', type=float)
  134. parser.add_argument('-V', '--version', dest='version', action='store_true', help=f'Check version')
  135. parser.add_argument('--debug', action='store_true', help=color(f'Debug mode'))
  136. parser.add_argument('--test-distro', help=color(f'Test print a specific distro\'s ascii art'))
  137. args = parser.parse_args()
  138. if args.version:
  139. print(f'Version is {VERSION}')
  140. return
  141. # Load config
  142. config = check_config()
  143. # Reset config
  144. if args.config:
  145. config = create_config()
  146. # Param overwrite config
  147. if args.preset:
  148. config.preset = args.preset
  149. if args.mode:
  150. config.mode = args.mode
  151. # Override global color mode
  152. constants.COLOR_MODE = config.mode
  153. # Get preset
  154. preset = PRESETS.get(config.preset)
  155. # Lighten
  156. if args.scale:
  157. preset = preset.lighten(args.scale)
  158. if args.light:
  159. preset = preset.set_light(args.light)
  160. # Test distro ascii art
  161. if args.test_distro:
  162. asc = get_custom_distro_ascii(args.test_distro)
  163. print(asc)
  164. print(replace_colors(asc, preset, config.mode)[0])
  165. return
  166. # Run
  167. run_neofetch(preset, config.mode)