hyfetch/hyfetch/main.py
2023-03-20 23:31:14 -04:00

397 lines
14 KiB
Python
Executable file

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import random
import traceback
from itertools import permutations
from math import ceil
from . import termenv, neofetch_util
from .color_scale import Scale
from .color_util import clear_screen
from .constants import *
from .models import Config
from .neofetch_util import *
from .presets import PRESETS
def check_config(path) -> Config:
"""
Check if the configuration exists. Return the config object if it exists. If not, call the
config creator
:return: Config object
"""
if path.is_file():
try:
return Config.from_dict(json.loads(CONFIG_PATH.read_text('utf-8')))
except KeyError:
return create_config()
return create_config()
def create_config() -> Config:
"""
Create config interactively
:return: Config object (automatically stored)
"""
# Detect terminal environment (doesn't work on Windows)
det_bg = termenv.get_background_color()
det_ansi = termenv.detect_ansi_mode()
asc = get_distro_ascii()
asc_width, asc_lines = ascii_size(asc)
logo = color("&l&bhyfetch&~&L" if det_bg is None or det_bg.is_light() else "&l&bhy&ffetch&~&L")
title = f'Welcome to {logo} Let\'s set up some colors first.'
clear_screen(title)
option_counter = 1
def update_title(k: str, v: str):
nonlocal title, option_counter
if not k.endswith(":"):
k += ':'
title += f"\n&e{option_counter}. {k.ljust(30)} &~{v}"
option_counter += 1
def print_title_prompt(prompt: str):
printc(f'&a{option_counter}. {prompt}')
##############################
# 0. Check term size
try:
term_len, term_lines = os.get_terminal_size().columns, os.get_terminal_size().lines
if term_len < 2 * asc_width + 4 or term_lines < 30:
printc(f'&cWarning: Your terminal is too small ({term_len} * {term_lines}). \n'
f'Please resize it for better experience.')
input('Press any key to ignore...')
except:
# print('Warning: We cannot detect your terminal size.')
pass
##############################
# 1. Select color system
def select_color_system():
if det_ansi == 'rgb':
return 'rgb', 'Detected color mode'
clear_screen(title)
term_len, term_lines = term_size()
scale2 = Scale(['#12c2e9', '#c471ed', '#f7797d'])
_8bit = [scale2(i / term_len).to_ansi_8bit(False) for i in range(term_len)]
_rgb = [scale2(i / term_len).to_ansi_rgb(False) for i in range(term_len)]
printc('&f' + ''.join(c + t for c, t in zip(_8bit, '8bit Color Testing'.center(term_len))))
printc('&f' + ''.join(c + t for c, t in zip(_rgb, 'RGB Color Testing'.center(term_len))))
print()
print_title_prompt('Which &bcolor system &ado you want to use?')
printc(f'(If you can\'t see colors under "RGB Color Testing", please choose 8bit)')
print()
return literal_input('Your choice?', ['8bit', 'rgb'], 'rgb'), 'Selected color mode'
# Override global color mode
color_system, ttl = select_color_system()
GLOBAL_CFG.color_mode = color_system
update_title(ttl, color_system)
##############################
# 2. Select light/dark mode
def select_light_dark():
if det_bg is not None:
return det_bg.is_light(), 'Detected background color'
clear_screen(title)
inp = literal_input(f'2. Is your terminal in &blight mode&~ or &4dark mode&~?',
['light', 'dark'], 'dark')
return inp == 'light', 'Selected background color'
is_light, ttl = select_light_dark()
light_dark = 'light' if is_light else 'dark'
GLOBAL_CFG.is_light = is_light
update_title(ttl, light_dark)
##############################
# 3. Choose preset
# Create flags = [[lines]]
flags = []
spacing = max(max(len(k) for k in PRESETS.keys()), 20)
for name, preset in PRESETS.items():
flag = preset.color_text(' ' * spacing, foreground=False)
flags.append([name.center(spacing), flag, flag, flag])
# Calculate flags per row
flags_per_row = term_size()[0] // (spacing + 2)
row_per_page = max(1, (term_size()[1] - 13) // 5)
num_pages = ceil(len(flags) / (flags_per_row * row_per_page))
# Create pages
pages = []
for i in range(num_pages):
page = []
for j in range(row_per_page):
page.append(flags[:flags_per_row])
flags = flags[flags_per_row:]
if not flags:
break
pages.append(page)
def print_flag_page(page: list[list[list[str]]], page_num: int):
clear_screen(title)
print_title_prompt("Let's choose a flag!")
printc('Available flag presets:')
print(f'Page: {page_num + 1} of {num_pages}')
print()
for i in page:
print_flag_row(i)
print()
def print_flag_row(current: list[list[str]]):
[printc(' '.join(line)) for line in zip(*current)]
print()
page = 0
while True:
print_flag_page(pages[page], page)
tmp = PRESETS['rainbow'].set_light_dl_def(light_dark).color_text('preset')
opts = list(PRESETS.keys())
if page < num_pages - 1:
opts.append('next')
if page > 0:
opts.append('prev')
print("Enter 'next' to go to the next page and 'prev' to go to the previous page.")
preset = literal_input(f'Which {tmp} do you want to use? ', opts, 'rainbow', show_ops=False)
if preset == 'next':
page += 1
elif preset == 'prev':
page -= 1
else:
_prs = PRESETS[preset]
update_title('Selected flag', _prs.set_light_dl_def(light_dark).color_text(preset))
break
#############################
# 4. Dim/lighten colors
def select_lightness():
clear_screen(title)
print_title_prompt("Let's adjust the color brightness!")
printc(f'The colors might be a little bit too {"bright" if is_light else "dark"} for {light_dark} mode.')
print()
# Print cats
num_cols = (term_size()[0] // (TEST_ASCII_WIDTH + 2)) or 1
mn, mx = 0.15, 0.85
ratios = [col / num_cols for col in range(num_cols)]
ratios = [(r * (mx - mn) / 2 + mn) if is_light else ((r * (mx - mn) + (mx + mn)) / 2) for r in ratios]
lines = [ColorAlignment('horizontal').recolor_ascii(TEST_ASCII.replace(
'{txt}', f'{r * 100:.0f}%'.center(5)), _prs.set_light_dl(r, light_dark)).split('\n') for r in ratios]
[printc(' '.join(line)) for line in zip(*lines)]
def_lightness = GLOBAL_CFG.default_lightness(light_dark)
while True:
print()
printc(f'Which brightness level looks the best? (Default: {def_lightness * 100:.0f}% for {light_dark} mode)')
lightness = input('> ').strip().lower() or None
# Parse lightness
if not lightness or lightness in ['unset', 'none']:
return def_lightness
try:
lightness = int(lightness[:-1]) / 100 if lightness.endswith('%') else float(lightness)
assert 0 <= lightness <= 1
return lightness
except Exception:
printc('&cUnable to parse lightness value, please input it as a decimal or percentage (e.g. 0.5 or 50%)')
lightness = select_lightness()
_prs = _prs.set_light_dl(lightness, light_dark)
update_title('Selected Brightness', f"{lightness:.2f}")
#############################
# 5. Color arrangement
color_alignment = None
fore_back = get_fore_back()
# Calculate amount of row/column that can be displayed on screen
ascii_per_row = max(1, term_size()[0] // (asc_width + 2))
ascii_rows = max(1, (term_size()[1] - 8) // asc_lines)
# Displays horizontal and vertical arrangements in the first iteration, but hide them in
# later iterations
hv_arrangements = [
('Horizontal', ColorAlignment('horizontal', fore_back=fore_back)),
('Vertical', ColorAlignment('vertical'))
]
arrangements = hv_arrangements.copy()
# Loop for random rolling
while True:
clear_screen(title)
# Random color schemes
pis = list(range(len(_prs.unique_colors().colors)))
slots = list(set(re.findall('(?<=\\${c)[0-9](?=})', asc)))
while len(pis) < len(slots):
pis += pis
perm = {p[:len(slots)] for p in permutations(pis)}
random_count = max(0, ascii_per_row * ascii_rows - len(arrangements))
if random_count > len(perm):
choices = perm
else:
choices = random.sample(sorted(perm), random_count)
choices = [{slots[i]: n for i, n in enumerate(c)} for c in choices]
arrangements += [(f'random{i}', ColorAlignment('custom', r)) for i, r in enumerate(choices)]
asciis = [[*ca.recolor_ascii(asc, _prs).split('\n'), k.center(asc_width)] for k, ca in arrangements]
while asciis:
current = asciis[:ascii_per_row]
asciis = asciis[ascii_per_row:]
# Print by row
[printc(' '.join(line)) for line in zip(*current)]
print()
print_title_prompt("Let's choose a color arrangement!")
printc(f'You can choose standard horizontal or vertical alignment, or use one of the random color schemes.')
print('You can type "roll" to randomize again.')
print()
choice = literal_input(f'Your choice?', ['horizontal', 'vertical', 'roll'] + [f'random{i}' for i in range(random_count)], 'horizontal')
if choice == 'roll':
arrangements = []
continue
# Save choice
arrangement_index = {k.lower(): ca for k, ca in hv_arrangements + arrangements}
if choice in arrangement_index:
color_alignment = arrangement_index[choice]
else:
print('Invalid choice.')
continue
break
update_title('Color alignment', color_alignment)
# Create config
clear_screen(title)
c = Config(preset, color_system, light_dark, lightness, color_alignment)
# Save config
print()
save = literal_input(f'Save config?', ['y', 'n'], 'y')
if save == 'y':
c.save()
return c
def run():
# Optional: Import readline
try:
import readline
except ModuleNotFoundError:
pass
# On Windows: Try to fix color rendering if not in git bash
if IS_WINDOWS:
import colorama
colorama.just_fix_windows_console()
# Create CLI
hyfetch = color('&l&bhyfetch&~&L')
parser = argparse.ArgumentParser(description=color(f'{hyfetch} - neofetch with flags <3'))
parser.add_argument('-c', '--config', action='store_true', help=color(f'Configure {hyfetch}'))
parser.add_argument('-C', '--config-file', dest='config_file', default=CONFIG_PATH, help=f'Use another config file')
parser.add_argument('-p', '--preset', help=f'Use preset', choices=PRESETS.keys())
parser.add_argument('-m', '--mode', help=f'Color mode', choices=['8bit', 'rgb'])
parser.add_argument('-b', '--backend', help=f'Choose a *fetch backend', choices=['neofetch', 'fastfetch', 'fastfetch-old'])
parser.add_argument('--c-scale', dest='scale', help=f'Lighten colors by a multiplier', type=float)
parser.add_argument('--c-set-l', dest='light', help=f'Set lightness value of the colors', type=float)
parser.add_argument('-V', '--version', dest='version', action='store_true', help=f'Check version')
parser.add_argument('--debug', action='store_true', help=f'Debug mode')
parser.add_argument('--distro', '--test-distro', dest='distro', help=f'Test for a specific distro')
parser.add_argument('--ascii-file', help='Use a specific file for the ascii art')
# Hidden debug arguments
# --test-print: Print the ascii distro and exit
parser.add_argument('--test-print', action='store_true', help=argparse.SUPPRESS)
# --ask-exit: Ask for input before exiting
parser.add_argument('--ask-exit', action='store_true', help=argparse.SUPPRESS)
args = parser.parse_args()
if args.version:
print(f'Version is {VERSION}')
return
# Ensure git bash for windows
ensure_git_bash()
check_windows_cmd()
if args.debug:
GLOBAL_CFG.debug = True
if args.test_print:
print(get_distro_ascii())
return
# Check if user provided alternative config path
if not args.config_file == CONFIG_PATH:
args.config_file = Path(os.path.abspath(args.config_file))
# If provided file does not exist use default config
if not args.config_file.is_file():
args.config_file = CONFIG_PATH
# Load config or create config
config = create_config() if args.config else check_config(args.config_file)
# Use a custom distro
GLOBAL_CFG.override_distro = args.distro or config.distro
# Param overwrite config
if args.preset:
config.preset = args.preset
if args.mode:
config.mode = args.mode
if args.backend:
config.backend = args.backend
# Override global color mode
GLOBAL_CFG.color_mode = config.mode
GLOBAL_CFG.is_light = config.light_dark == 'light'
# Get preset
preset = PRESETS.get(config.preset)
# Lighten (args > config)
if args.scale:
preset = preset.lighten(args.scale)
elif args.light:
preset = preset.set_light_raw(args.light)
else:
preset = preset.set_light_dl(config.lightness or GLOBAL_CFG.default_lightness())
# Run
try:
asc = get_distro_ascii() if not args.ascii_file else Path(args.ascii_file).read_text("utf-8")
asc = config.color_align.recolor_ascii(asc, preset)
neofetch_util.run(asc, config.backend)
except Exception as e:
print(f'Error: {e}')
traceback.print_exc()
if args.ask_exit:
input('Press any key to exit...')