diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9c0191e..c655aa85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,12 +43,12 @@ request on the repo and our Travis.ci hook will run ShellCheck for you. - Don’t use GNU conventions in commands. - Use POSIX arguments and flags. - Don’t use `cut`. - - Use `bash`'s built-in [parameter expansion](http://wiki.bash-hackers.org/syntax/pe). + - Use `bash`'s built-in [parameter expansion](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html). - Don’t use `echo`. - Use `printf "%s\n"` - Don’t use `bc`. - Don’t use `sed`. - - Use `bash`'s built-in [parameter expansion](http://wiki.bash-hackers.org/syntax/pe). + - Use `bash`'s built-in [parameter expansion](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html). - Don’t use `cat`. - Use `bash`'s built-in syntax (`file="$(< /path/to/file.txt)")`). - Don’t use `grep "pattern" | awk '{ printf }'`. diff --git a/hyfetch/__init__.py b/hyfetch/__init__.py index 52a313a9..a2a60324 100644 --- a/hyfetch/__init__.py +++ b/hyfetch/__init__.py @@ -1,9 +1,9 @@ from __future__ import annotations -from . import main, constants - -__version__ = constants.VERSION +from .main import run +from .__version__ import VERSION +__version__ = VERSION if __name__ == '__main__': - main.run() + run() diff --git a/hyfetch/__main__.py b/hyfetch/__main__.py index f50c20c2..46abd46a 100644 --- a/hyfetch/__main__.py +++ b/hyfetch/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from . import main from .color_util import printc @@ -6,4 +8,4 @@ main.run() except KeyboardInterrupt: printc('&cThe program is interrupted by ^C, exiting...') - exit(0) \ No newline at end of file + exit(0) diff --git a/hyfetch/color_scale.py b/hyfetch/color_scale.py index 05347a49..6b541062 100644 --- a/hyfetch/color_scale.py +++ b/hyfetch/color_scale.py @@ -1,8 +1,9 @@ """ This version of color_scale is a special version made without numpy dependency. The numpy version -would be faster, but numpy is 11 MB large. In comparison, hyfetch 1.0.7 is only 105 kB, so it's not +would be faster, but numpy is 11 MB. In comparison, hyfetch 1.0.7 is only 105 kB, so it's not a good idea to depend on numpy. """ + from __future__ import annotations from .color_util import RGB @@ -26,15 +27,14 @@ def create_gradient(colors: list[RGB], resolution: int) -> list[RGB]: # Create gradient mapping for i in range(len(colors) - 1): - c1 = colors[i] - c2 = colors[i + 1] - bi = i * resolution - - for ri in range(resolution): - ratio = ri / resolution - r = int(c2.r * ratio + c1.r * (1 - ratio)) - g = int(c2.g * ratio + c1.g * (1 - ratio)) - b = int(c2.b * ratio + c1.b * (1 - ratio)) + color_1 = colors[i] + color_2 = colors[i + 1] + + for point in range(resolution): + ratio = point / resolution + r = int(color_2.r * ratio + color_1.r * (1 - ratio)) + g = int(color_2.g * ratio + color_1.g * (1 - ratio)) + b = int(color_2.b * ratio + color_1.b * (1 - ratio)) result.append(RGB(r, g, b)) return result @@ -54,12 +54,16 @@ def get_raw(gradient: list[RGB], ratio: float) -> RGB: class Scale: + """ + A simple color scale using linear interpolation + """ colors: list[RGB] rgb: list[RGB] def __init__(self, scale: list[str], resolution: int = 300): + self.resolution = resolution self.colors = [RGB.from_hex(s) for s in scale] - self.rgb = create_gradient(self.colors, resolution) + self.rgb = create_gradient(self.colors, self.resolution) def __call__(self, ratio: float) -> RGB: """ @@ -67,8 +71,19 @@ def __call__(self, ratio: float) -> RGB: """ return get_raw(self.rgb, ratio) + def __len__(self): + return self.resolution + def test_color_scale(): + """ + Display a gradient + + Returns + ------- + None. + + """ scale = Scale(['#232323', '#4F1879', '#B43A78', '#F98766', '#FCFAC0']) colors = 100 diff --git a/hyfetch/color_util.py b/hyfetch/color_util.py index d277526e..f79ba359 100644 --- a/hyfetch/color_util.py +++ b/hyfetch/color_util.py @@ -2,9 +2,8 @@ import colorsys from dataclasses import dataclass, astuple - from .constants import GLOBAL_CFG -from .types import * +from .types import AnsiMode MINECRAFT_COLORS = [ @@ -83,35 +82,30 @@ def clear_screen(title: str = ''): print() -def redistribute_rgb(r: int, g: int, b: int) -> tuple[int, int, int]: +def redistribute_rgb(rgb: list[int]) -> tuple[int, int, int]: """ Redistribute RGB after lightening Credit: https://stackoverflow.com/a/141943/7346633 """ - threshold = 255.999 - m = max(r, g, b) - if m <= threshold: - return int(r), int(g), int(b) - total = r + g + b - if total >= 3 * threshold: - return int(threshold), int(threshold), int(threshold) - x = (3 * threshold - total) / (3 * m - total) - gray = threshold - x * m - return int(gray + x * r), int(gray + x * g), int(gray + x * b) + threshold = 256 + rgb_max = max(rgb) + if rgb_max < threshold: + return tuple(int(c) for c in rgb) + total = sum(rgb) + if total > 3 * threshold: + return 255, 255, 255 + x = (3 * threshold - total) / (3 * rgb_max - total) + grey = threshold - x * rgb_max + return tuple(grey + x * c for c in rgb) -@dataclass(unsafe_hash=True) -class HSL: - h: float - s: float - l: float +def rgb_to_hls(rgb) -> list: + return [*colorsys.rgb_to_hls(*[v / 255.0 for v in rgb])] - def __iter__(self): - return iter(astuple(self)) - def rgb(self) -> RGB: - return RGB(*[round(v * 255.0) for v in colorsys.hls_to_rgb(self.h, self.l, self.s)]) +def hls_to_rgb(hls: list): + return RGB(*[round(v * 255.0) for v in colorsys.hls_to_rgb(*hls)]) @dataclass(unsafe_hash=True) @@ -124,20 +118,20 @@ def __iter__(self): return iter(astuple(self)) @classmethod - def from_hex(cls, hex: str) -> "RGB": + def from_hex(cls, hex_val: str) -> "RGB": """ Create color from hex code >>> RGB.from_hex('#FFAAB7') RGB(r=255, g=170, b=183) - :param hex: Hex color code + :param hex_val: Hex color code :return: RGB object """ - hex = hex.lstrip("#") - r = int(hex[0:2], 16) - g = int(hex[2:4], 16) - b = int(hex[4:6], 16) + hex_val = hex_val.lstrip("#") + r = int(hex_val[0:2], 16) + g = int(hex_val[2:4], 16) + b = int(hex_val[4:6], 16) return cls(r, g, b) def to_ansi_rgb(self, foreground: bool = True) -> str: @@ -175,12 +169,13 @@ def to_ansi_8bit(self, foreground: bool = True) -> str: sep += 42.5 if gray: - color = 232 + (r + g + b) / 33 + rgb_color = 232 + (r + g + b) / 33 else: - color = 16 + int(r / 256. * 6) * 36 + int(g / 256. * 6) * 6 + int(b / 256. * 6) + rgb_color = 16 + int(r / 256. * 6) * 36 + \ + int(g / 256. * 6) * 6 + int(b / 256. * 6) - c = '38' if foreground else '48' - return f'\033[{c};5;{int(color)}m' + code = '38' if foreground else '48' + return f'\033[{code};5;{int(rgb_color)}m' def to_ansi_16(self, foreground: bool = True) -> str: """ @@ -191,7 +186,7 @@ def to_ansi_16(self, foreground: bool = True) -> str: raise NotImplementedError() def to_ansi(self, mode: AnsiMode | None = None, foreground: bool = True): - if not mode: + if mode is None: mode = GLOBAL_CFG.color_mode if mode == 'rgb': return self.to_ansi_rgb(foreground) @@ -207,45 +202,43 @@ def lighten(self, multiplier: float) -> 'RGB': :param multiplier: Multiplier :return: Lightened color (original isn't modified) """ - return RGB(*redistribute_rgb(*[v * multiplier for v in self])) - - def hsl(self) -> HSL: - h, l, s = colorsys.rgb_to_hls(*[v / 255.0 for v in self]) - return HSL(h, s, l) + return RGB(*redistribute_rgb([v * multiplier for v in self])) - def set_light(self, light: float, at_least: bool | None = None, at_most: bool | None = None) -> 'RGB': + def set_light(self, light: float, at_least: bool | None = None, + at_most: bool | None = None) -> 'RGB': """ - Set HSL lightness value + Set HLS lightness value :param light: Lightness value (0-1) :param at_least: Set the lightness to at least this value (no change if greater) :param at_most: Set the lightness to at most this value (no change if lesser) :return: New color (original isn't modified) """ - # Convert to HSL - hsl = self.hsl() + # Convert to HLS + hls = rgb_to_hls(self) # Modify light value if at_least is None and at_most is None: - hsl.l = light + hls[1] = light else: if at_most: - hsl.l = min(hsl.l, light) + hls[1] = min(hls[1], light) if at_least: - hsl.l = max(hsl.l, light) + hls[1] = max(hls[1], light) # Convert back to RGB - return hsl.rgb() + rgb = hls_to_rgb(hls) + return rgb def is_light(self): - return self.hsl().l > 0.5 + return rgb_to_hls(self)[1] > 0.5 - def overlay(self, color: 'RGB', alpha: float) -> 'RGB': + def overlay(self, rgb_color: 'RGB', alpha: float) -> 'RGB': """ Overlay a color on top of this color - :param color: Overlay color + :param rgb_color: Overlay color :param alpha: Overlay alpha :return: New color (original isn't modified) """ - return RGB(*[round((1 - alpha) * v1 + alpha * v2) for v1, v2 in zip(self, color)]) + return RGB(*[round((1 - alpha) * v1 + alpha * v2) for v1, v2 in zip(self, rgb_color)]) diff --git a/hyfetch/constants.py b/hyfetch/constants.py index 81536d40..feb0f3e6 100644 --- a/hyfetch/constants.py +++ b/hyfetch/constants.py @@ -1,12 +1,18 @@ from __future__ import annotations import os +import shutil import platform from dataclasses import dataclass from pathlib import Path - from .types import LightDark -from .__version__ import VERSION + + +TERM_WIDTH, TERM_HEIGHT = shutil.get_terminal_size(fallback=(100, 20)) + +CONFIGURE_FLAG_WIDTH = 18 +CONFIGURE_FLAG_HEIGHT = 3 + CONFIG_PATH = Path.home() / '.config/hyfetch.json' @@ -29,10 +35,15 @@ DEFAULT_DARK_L = 0. IS_WINDOWS = platform.system() == 'Windows' -CACHE_PATH = Path(os.getenv("LOCALAPPDATA") or os.getenv("XDG_CACHE_HOME") or Path.home() / '.cache') / 'hyfetch' +CACHE_PATH = Path(os.getenv("LOCALAPPDATA") or os.getenv( + "XDG_CACHE_HOME") or Path.home() / '.cache') / 'hyfetch' + @dataclass class GlobalConfig: + """ + Global configuration + """ # Global color mode default to 8-bit for compatibility color_mode: str override_distro: str | None @@ -41,14 +52,38 @@ class GlobalConfig: use_overlay: bool def light_dark(self) -> LightDark: + """ + Get color mode + + Returns + ------- + LightDark + string 'light' or string 'dark'. + + """ return 'light' if self.is_light else 'dark' def default_lightness(self, term: LightDark | None = None) -> float: + """ + Get default lightness for a color mode + + Parameters + ---------- + term : LightDark | None, optional + string 'light' or string 'dark'. The default is None. + + Returns + ------- + float + default lightness. + + """ if term is None: term = self.light_dark() return 0.65 if term.lower() == 'dark' else 0.4 -GLOBAL_CFG = GlobalConfig(color_mode='8bit', override_distro=None, debug=False, is_light=False, use_overlay=False) +GLOBAL_CFG = GlobalConfig(color_mode='8bit', override_distro=None, + debug=False, is_light=False, use_overlay=False) MINGIT_URL = 'https://github.com/git-for-windows/git/releases/download/v2.37.2.windows.2/MinGit-2.37.2.2-busybox-32-bit.zip' diff --git a/hyfetch/create_config.py b/hyfetch/create_config.py new file mode 100644 index 00000000..962e6135 --- /dev/null +++ b/hyfetch/create_config.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +Create configuration file interactively +""" + +from __future__ import annotations + +from math import ceil +from .color_scale import Scale +from .color_util import clear_screen +from . import constants, termenv +from .models import Config +from .neofetch_util import get_distro_ascii, ascii_size, color, printc, literal_input, recolor_ascii +from .flag_utils import get_flags + + +class Configure: + """ + Return a configuration file created interactively. + Usage: config = Configure().create() + """ + + def __init__(self): + self.det_bg = termenv.get_background_color() + self.det_ansi = termenv.detect_ansi_mode() + asc = get_distro_ascii() + asc_width = ascii_size(asc)[0] + if self.det_bg is None or self.det_bg.is_light(): + self.logo = color("&l&bhyfetch&~&L") + else: + self.logo = color("&l&bhy&ffetch&~&L") + + self.term_len_min = 2 * asc_width + 4 + self.term_lines_min = 30 + + self.title = f'Welcome to {self.logo}! Let\'s set up some colors first.' + self.option_counter = 1 + + def update_title(self, k: str, v: str): + if not k.endswith(":"): + k += ':' + self.title += f"\n&e{self.option_counter}. {k.ljust(30)} &~{v}" + self.option_counter += 1 + + def print_title_prompt(self, prompt: str): + printc(f'&a{self.option_counter}. {prompt}') + + def select_color_system(self): + if self.det_ansi == 'rgb': + return 'rgb', 'Detected color mode' + + clear_screen(self.title) + + scale2 = Scale(['#12c2e9', '#c471ed', '#f7797d']) + _8bit = [scale2(i / constants.TERM_WIDTH).to_ansi_8bit(False) + for i in range(constants.TERM_WIDTH)] + _rgb = [scale2(i / constants.TERM_WIDTH).to_ansi_rgb(False) + for i in range(constants.TERM_WIDTH)] + + printc('&f' + ''.join(c + t for c, t in zip(_8bit, + '8bit Color Testing'.center(constants.TERM_WIDTH)))) + printc('&f' + ''.join(c + t for c, t in zip(_rgb, + 'RGB Color Testing'.center(constants.TERM_WIDTH)))) + + print() + self.print_title_prompt( + 'Which &bcolor system &ado you want to use?') + printc('(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' + + def select_light_dark(self): + if self.det_bg is not None: + return self.det_bg.is_light(), 'Detected background color' + + clear_screen(self.title) + inp = literal_input('2. Is your terminal in &blight mode&~ or &4dark mode&~?', + ['light', 'dark'], 'dark') + return inp == 'light', 'Selected background color' + + def print_flag_row(self, current: list[list[str]]): + for line in zip(*current): + printc(' '.join(line)) + print() + + def print_flag_page(self, page: list[list[list[str]]], num_pages: int, page_num: int): + clear_screen(self.title) + self.print_title_prompt("Let's choose a flag!") + printc('Available flags:') + print(f'Page: {page_num + 1} of {num_pages}') + print() + for i in page: + self.print_flag_row(i) + print() + + def select_lightness(self, light_dark, preset): + clear_screen(self.title) + self.print_title_prompt("Let's adjust the color brightness!") + adj = "bright" if constants.GLOBAL_CFG.is_light else "dark" + printc( + f'The colors might be a little bit too {adj} for {light_dark} mode.') + print() + + # Print cats + num_cols = (constants.TERM_WIDTH // + (constants.TEST_ASCII_WIDTH + 2)) or 1 + min_l, max_l = 0.15, 0.85 + ratios = [col / num_cols for col in range(num_cols)] + ratios = [(r * (max_l - min_l) / 2 + min_l) if constants.GLOBAL_CFG.is_light else ( + (r * (max_l - min_l) + (max_l + min_l)) / 2) for r in ratios] + lines = [recolor_ascii(constants.TEST_ASCII.replace( + '{txt}', f'{r * 100:.0f}%'.center(5)), preset, r).split('\n') for r in ratios] + + for line in zip(*lines): + printc(' '.join(line)) + + def_lightness = constants.GLOBAL_CFG.default_lightness(light_dark) + + while True: + print() + def_val = int(100 * def_lightness) + printc( + f'Which brightness level looks the best? (Default: {def_val}% for {light_dark} mode)') + lightness = input('> ').strip().lower() or None + + # Parse lightness + if not lightness or lightness in ['unset', 'none']: + return def_lightness + + light_val_msg = """&cUnable to parse lightness value, please input it as a decimal or percentage (e.g. 0.5 or 50%)""" + + try: + lightness = int( + lightness[:-1]) / 100 if lightness.endswith('%') else float(lightness) + assert 0 <= lightness <= 1 + return lightness + + except ValueError: + printc(light_val_msg) + + except AssertionError: + printc(light_val_msg) + + def choose_flag(self): + flags = [] + spacing = max([len(k) for k in get_flags()] + + [constants.CONFIGURE_FLAG_WIDTH]) + for name in get_flags(): + flag_lines = '\n'.join( + constants.CONFIGURE_FLAG_HEIGHT * [' ' * spacing]) + flag_lines = recolor_ascii( + flag_lines, name, rotation=270, lightness_mode=None, foreground=False).split('\n') + flags.append([name.center(spacing)] + flag_lines) + + # Calculate flags per row + flags_per_row = constants.TERM_WIDTH // (spacing + 2) + row_per_page = max( + 1, (constants.TERM_HEIGHT - 13) // (constants.CONFIGURE_FLAG_HEIGHT + 2)) + num_pages = ceil(len(flags) / (flags_per_row * row_per_page)) + + pages = [] + for _ in range(num_pages): + page = [] + for _ in range(row_per_page): + page.append(flags[:flags_per_row]) + flags = flags[flags_per_row:] + if not flags: + break + pages.append(page) + page = 0 + while True: + self.print_flag_page( + pages[page], num_pages, page) + + tmp = recolor_ascii('preset', 'rainbow', rotation=90) + opts = get_flags() + 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: + self.update_title( + 'Selected flag', recolor_ascii(preset, preset, rotation=90)) + return preset + + def run(self) -> Config: + """ + Create config interactively + + Returns + ------- + Config + Config object (automatically stored). + + """ + clear_screen(self.title) + + ############################## + # 0. Check term size + + if constants.TERM_WIDTH < self.term_len_min or constants.TERM_HEIGHT < self.term_lines_min: + printc(f'&cWarning: Your terminal is too small ({constants.TERM_WIDTH} * {constants.TERM_HEIGHT}). \n' + f'Please resize it to at least ({self.term_len_min} * {self.term_lines_min}) for a better experience.') + input('Press enter to ignore...') + + ############################## + # 1. Select color system + + # Override global color mode + color_mode, ttl = self.select_color_system() + constants.GLOBAL_CFG.color_mode = color_mode + self.update_title(ttl, color_mode) + + ############################## + # 2. Select light/dark mode + + constants.GLOBAL_CFG.is_light, ttl = self.select_light_dark() + light_dark = 'light' if constants.GLOBAL_CFG.is_light else 'dark' + self.update_title(ttl, light_dark) + + ############################## + # 3. Choose preset + # Create flags = [[lines]] + + preset = self.choose_flag() + + ############################# + # 4. Dim/lighten colors + + lightness = self.select_lightness(light_dark, preset) + self.update_title('Selected Brightness', f"{lightness:.2f}") + + # Create config + clear_screen(self.title) + config = Config(preset, constants.GLOBAL_CFG.color_mode, + light_dark, lightness) + + # Save config + print() + save = literal_input('Save config?', ['y', 'n'], 'y') + if save == 'y': + config_path = config.save() + print('Configuration file saved at ' + str(config_path)+'\n') + + return config diff --git a/hyfetch/flag_utils.py b/hyfetch/flag_utils.py new file mode 100644 index 00000000..db4aaba1 --- /dev/null +++ b/hyfetch/flag_utils.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import os +from pathlib import Path +from PIL import Image + + +def get_flags() -> list: + """ + Gets installed flags + + Returns + ------- + list + Alphabetical list of installed flags. + + """ + files = os.listdir(Path(__file__).parent / 'flags') + files_no_ext = [f.split('.')[0] for f in files] + files_no_ext.sort() + return files_no_ext + + +def get_flag(flag: str, x_len: int, y_len: int, rotation: int = 0) -> Image.Image: + """ + Opens a flag file, then rotates and resizes it + + Parameters + ---------- + flag : str + flag name. + x_len : int + width of flag. + y_len : int + height of flag. + rotation : int, optional + Counterclockwise rotation of the image in degrees. The default is 0. + + Raises + ------ + error + Flag is not installed. + FileNotFoundError + Flag is not installed. + + Returns + ------- + img : Image.Image + RGB image of flag. + + """ + # Get files in the flag directory + files = os.listdir(Path(__file__).parent / 'flags') + # Remove file extensions + files_no_ext = [f.split('.')[0] for f in files] + # Get index of file in list of files without extensions, raise error if it doesn't exist + try: + index = files_no_ext.index(flag.lower()) + except ValueError as ex: + raise FileNotFoundError(f'{flag.lower()} flag does not exist') from ex + + filename = files[index] + img = Image.open('hyfetch/flags/' + filename).convert('RGB') + img = img.rotate(rotation, expand=True) + img = img.resize((x_len, y_len), resample=Image.Resampling.NEAREST) + return img diff --git a/hyfetch/flags/abrosexual.png b/hyfetch/flags/abrosexual.png new file mode 100644 index 00000000..de1b81f9 Binary files /dev/null and b/hyfetch/flags/abrosexual.png differ diff --git a/hyfetch/flags/agender.png b/hyfetch/flags/agender.png new file mode 100644 index 00000000..52c93b37 Binary files /dev/null and b/hyfetch/flags/agender.png differ diff --git a/hyfetch/flags/akiosexual.png b/hyfetch/flags/akiosexual.png new file mode 100644 index 00000000..5f13fd0f Binary files /dev/null and b/hyfetch/flags/akiosexual.png differ diff --git a/hyfetch/flags/aroace 1.png b/hyfetch/flags/aroace 1.png new file mode 100644 index 00000000..e6dd91ce Binary files /dev/null and b/hyfetch/flags/aroace 1.png differ diff --git a/hyfetch/flags/aroace 2.png b/hyfetch/flags/aroace 2.png new file mode 100644 index 00000000..ebda8be1 Binary files /dev/null and b/hyfetch/flags/aroace 2.png differ diff --git a/hyfetch/flags/aroace 3.png b/hyfetch/flags/aroace 3.png new file mode 100644 index 00000000..47eb3c02 Binary files /dev/null and b/hyfetch/flags/aroace 3.png differ diff --git a/hyfetch/flags/aromantic.png b/hyfetch/flags/aromantic.png new file mode 100644 index 00000000..b34e0945 Binary files /dev/null and b/hyfetch/flags/aromantic.png differ diff --git a/hyfetch/flags/asexual.png b/hyfetch/flags/asexual.png new file mode 100644 index 00000000..34dcce2d Binary files /dev/null and b/hyfetch/flags/asexual.png differ diff --git a/hyfetch/flags/autoromantic.png b/hyfetch/flags/autoromantic.png new file mode 100644 index 00000000..58aab1a7 Binary files /dev/null and b/hyfetch/flags/autoromantic.png differ diff --git a/hyfetch/flags/autosexual.png b/hyfetch/flags/autosexual.png new file mode 100644 index 00000000..6cf42644 Binary files /dev/null and b/hyfetch/flags/autosexual.png differ diff --git a/hyfetch/flags/beiyang.png b/hyfetch/flags/beiyang.png new file mode 100644 index 00000000..07568b0d Binary files /dev/null and b/hyfetch/flags/beiyang.png differ diff --git a/hyfetch/flags/bigender.png b/hyfetch/flags/bigender.png new file mode 100644 index 00000000..8179b0c0 Binary files /dev/null and b/hyfetch/flags/bigender.png differ diff --git a/hyfetch/flags/biromantic 1.png b/hyfetch/flags/biromantic 1.png new file mode 100644 index 00000000..b763bedd Binary files /dev/null and b/hyfetch/flags/biromantic 1.png differ diff --git a/hyfetch/flags/bisexual.png b/hyfetch/flags/bisexual.png new file mode 100644 index 00000000..5c747ecc Binary files /dev/null and b/hyfetch/flags/bisexual.png differ diff --git a/hyfetch/flags/boyflux.png b/hyfetch/flags/boyflux.png new file mode 100644 index 00000000..1cd59bd7 Binary files /dev/null and b/hyfetch/flags/boyflux.png differ diff --git a/hyfetch/flags/burger.png b/hyfetch/flags/burger.png new file mode 100644 index 00000000..f3b4ebc4 Binary files /dev/null and b/hyfetch/flags/burger.png differ diff --git a/hyfetch/flags/demiboy.png b/hyfetch/flags/demiboy.png new file mode 100644 index 00000000..447cdb36 Binary files /dev/null and b/hyfetch/flags/demiboy.png differ diff --git a/hyfetch/flags/demifae.png b/hyfetch/flags/demifae.png new file mode 100644 index 00000000..217c7553 Binary files /dev/null and b/hyfetch/flags/demifae.png differ diff --git a/hyfetch/flags/demifaun.png b/hyfetch/flags/demifaun.png new file mode 100644 index 00000000..db4dc605 Binary files /dev/null and b/hyfetch/flags/demifaun.png differ diff --git a/hyfetch/flags/demigender.png b/hyfetch/flags/demigender.png new file mode 100644 index 00000000..77c9eb0c Binary files /dev/null and b/hyfetch/flags/demigender.png differ diff --git a/hyfetch/flags/demigirl.png b/hyfetch/flags/demigirl.png new file mode 100644 index 00000000..5691c1d9 Binary files /dev/null and b/hyfetch/flags/demigirl.png differ diff --git a/hyfetch/flags/demisexual.png b/hyfetch/flags/demisexual.png new file mode 100644 index 00000000..ae591479 Binary files /dev/null and b/hyfetch/flags/demisexual.png differ diff --git a/hyfetch/flags/femboy.png b/hyfetch/flags/femboy.png new file mode 100644 index 00000000..385931b6 Binary files /dev/null and b/hyfetch/flags/femboy.png differ diff --git a/hyfetch/flags/finsexual.png b/hyfetch/flags/finsexual.png new file mode 100644 index 00000000..73bc61b3 Binary files /dev/null and b/hyfetch/flags/finsexual.png differ diff --git a/hyfetch/flags/gay men.png b/hyfetch/flags/gay men.png new file mode 100644 index 00000000..c16d2238 Binary files /dev/null and b/hyfetch/flags/gay men.png differ diff --git a/hyfetch/flags/genderfae.png b/hyfetch/flags/genderfae.png new file mode 100644 index 00000000..b1488d75 Binary files /dev/null and b/hyfetch/flags/genderfae.png differ diff --git a/hyfetch/flags/genderfaun.png b/hyfetch/flags/genderfaun.png new file mode 100644 index 00000000..5ba8cb6a Binary files /dev/null and b/hyfetch/flags/genderfaun.png differ diff --git a/hyfetch/flags/genderfluid.png b/hyfetch/flags/genderfluid.png new file mode 100644 index 00000000..454bc0ad Binary files /dev/null and b/hyfetch/flags/genderfluid.png differ diff --git a/hyfetch/flags/gendervoid.png b/hyfetch/flags/gendervoid.png new file mode 100644 index 00000000..fa673e07 Binary files /dev/null and b/hyfetch/flags/gendervoid.png differ diff --git a/hyfetch/flags/gnc 1.png b/hyfetch/flags/gnc 1.png new file mode 100644 index 00000000..87720fe5 Binary files /dev/null and b/hyfetch/flags/gnc 1.png differ diff --git a/hyfetch/flags/gnc 2.png b/hyfetch/flags/gnc 2.png new file mode 100644 index 00000000..54e6d1ee Binary files /dev/null and b/hyfetch/flags/gnc 2.png differ diff --git a/hyfetch/flags/grey ace.png b/hyfetch/flags/grey ace.png new file mode 100644 index 00000000..cd3f06e7 Binary files /dev/null and b/hyfetch/flags/grey ace.png differ diff --git a/hyfetch/flags/greygender.png b/hyfetch/flags/greygender.png new file mode 100644 index 00000000..747fe8e7 Binary files /dev/null and b/hyfetch/flags/greygender.png differ diff --git a/hyfetch/flags/intergender.png b/hyfetch/flags/intergender.png new file mode 100644 index 00000000..37a72d79 Binary files /dev/null and b/hyfetch/flags/intergender.png differ diff --git a/hyfetch/flags/lesbian.png b/hyfetch/flags/lesbian.png new file mode 100644 index 00000000..1d6b0e93 Binary files /dev/null and b/hyfetch/flags/lesbian.png differ diff --git a/hyfetch/flags/neurodivergent.png b/hyfetch/flags/neurodivergent.png new file mode 100644 index 00000000..98cdcdf0 Binary files /dev/null and b/hyfetch/flags/neurodivergent.png differ diff --git a/hyfetch/flags/neutrois.png b/hyfetch/flags/neutrois.png new file mode 100644 index 00000000..351a8a87 Binary files /dev/null and b/hyfetch/flags/neutrois.png differ diff --git a/hyfetch/flags/nonbinary.png b/hyfetch/flags/nonbinary.png new file mode 100644 index 00000000..abfbff31 Binary files /dev/null and b/hyfetch/flags/nonbinary.png differ diff --git a/hyfetch/flags/omniromantic.png b/hyfetch/flags/omniromantic.png new file mode 100644 index 00000000..9d3e268f Binary files /dev/null and b/hyfetch/flags/omniromantic.png differ diff --git a/hyfetch/flags/omnisexual.png b/hyfetch/flags/omnisexual.png new file mode 100644 index 00000000..a47e3fc1 Binary files /dev/null and b/hyfetch/flags/omnisexual.png differ diff --git a/hyfetch/flags/pangender.png b/hyfetch/flags/pangender.png new file mode 100644 index 00000000..4d69ada1 Binary files /dev/null and b/hyfetch/flags/pangender.png differ diff --git a/hyfetch/flags/pansexual.png b/hyfetch/flags/pansexual.png new file mode 100644 index 00000000..db290510 Binary files /dev/null and b/hyfetch/flags/pansexual.png differ diff --git a/hyfetch/flags/polysexual.png b/hyfetch/flags/polysexual.png new file mode 100644 index 00000000..81662dad Binary files /dev/null and b/hyfetch/flags/polysexual.png differ diff --git a/hyfetch/flags/progressive.png b/hyfetch/flags/progressive.png new file mode 100644 index 00000000..2e712a64 Binary files /dev/null and b/hyfetch/flags/progressive.png differ diff --git a/hyfetch/flags/queer.png b/hyfetch/flags/queer.png new file mode 100644 index 00000000..772c2812 Binary files /dev/null and b/hyfetch/flags/queer.png differ diff --git a/hyfetch/flags/rainbow.png b/hyfetch/flags/rainbow.png new file mode 100644 index 00000000..b713f176 Binary files /dev/null and b/hyfetch/flags/rainbow.png differ diff --git a/hyfetch/flags/tomboy.png b/hyfetch/flags/tomboy.png new file mode 100644 index 00000000..79f2075b Binary files /dev/null and b/hyfetch/flags/tomboy.png differ diff --git a/hyfetch/flags/transfem.png b/hyfetch/flags/transfem.png new file mode 100644 index 00000000..c27c351e Binary files /dev/null and b/hyfetch/flags/transfem.png differ diff --git a/hyfetch/flags/transgender.png b/hyfetch/flags/transgender.png new file mode 100644 index 00000000..e5966e71 Binary files /dev/null and b/hyfetch/flags/transgender.png differ diff --git a/hyfetch/flags/transmasc.png b/hyfetch/flags/transmasc.png new file mode 100644 index 00000000..80324bff Binary files /dev/null and b/hyfetch/flags/transmasc.png differ diff --git a/hyfetch/flags/voidboy.png b/hyfetch/flags/voidboy.png new file mode 100644 index 00000000..4aaa4b84 Binary files /dev/null and b/hyfetch/flags/voidboy.png differ diff --git a/hyfetch/flags/voidgirl.png b/hyfetch/flags/voidgirl.png new file mode 100644 index 00000000..d46f2c06 Binary files /dev/null and b/hyfetch/flags/voidgirl.png differ diff --git a/hyfetch/main.py b/hyfetch/main.py index 0c8f4c4d..5bf6b0fb 100755 --- a/hyfetch/main.py +++ b/hyfetch/main.py @@ -1,21 +1,20 @@ #!/usr/bin/env python3 + from __future__ import annotations +import os +import sys +import json import argparse import datetime -import json -import random import traceback -from itertools import permutations -from math import ceil - -from . import termenv, neofetch_util, pride_month -from .color_scale import Scale -from .color_util import clear_screen -from .constants import * +from pathlib import Path +from .__version__ import VERSION +from . import neofetch_util, pride_month, constants from .models import Config -from .neofetch_util import * -from .presets import PRESETS +from .neofetch_util import get_distro_ascii, color, recolor_ascii, ensure_git_bash, check_windows_cmd +from .flag_utils import get_flags +from .create_config import Configure def check_config(path) -> Config: @@ -29,302 +28,53 @@ def check_config(path) -> Config: try: return Config.from_dict(json.loads(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 - term_len_min = 2 * asc_width + 4 - term_lines_min = 30 - if term_len < term_len_min or term_lines < term_lines_min: - printc(f'&cWarning: Your terminal is too small ({term_len} * {term_lines}). \n' - f'Please resize it to at least ({term_len_min} * {term_lines_min}) for better experience.') - input('Press enter 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() + return Configure().run() - 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 + return Configure().run() def create_parser() -> argparse.ArgumentParser: # Create CLI hyfetch = color('&l&bhyfetch&~&L') - parser = argparse.ArgumentParser(description=color(f'{hyfetch} - neofetch with flags <3'), prog="hyfetch") - - 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=list(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=['qwqfetch', 'neofetch', 'fastfetch', 'fastfetch-old']) - parser.add_argument('--args', help=f'Additional arguments pass-through to backend') - 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('--c-overlay', action='store_true', dest='overlay', help=f'Use experimental overlay color adjusting instead of HSL lightness') - parser.add_argument('-V', '--version', dest='version', action='store_true', help=f'Check version') - parser.add_argument('--june', action='store_true', help=f'Show pride month easter egg') - 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') + parser = argparse.ArgumentParser(description=color( + f'{hyfetch} - neofetch with flags <3'), prog="hyfetch") + + parser.add_argument('-c', '--config', action='store_true', + help=color('Configure hyfetch')) + parser.add_argument('-C', '--config-file', dest='config_file', + default=constants.CONFIG_PATH, help='Use another config file') + parser.add_argument('-p', '--preset', help='Use preset', + choices=get_flags()) + parser.add_argument('-m', '--mode', help='Color mode', + choices=['8bit', 'rgb']) + parser.add_argument('-b', '--backend', help='Choose a *fetch backend', + choices=['qwqfetch', 'neofetch', 'fastfetch', 'fastfetch-old']) + parser.add_argument( + '--args', help='Additional arguments pass-through to backend') + parser.add_argument('--c-scale', dest='scale', + help='Lighten colors by a multiplier', type=float) + parser.add_argument('--c-set-l', dest='light', + help='Set lightness value of the colors', type=float) + parser.add_argument('--c-overlay', action='store_true', dest='overlay', + help='Use experimental overlay color adjusting instead of HSL lightness') + parser.add_argument('-V', '--version', dest='version', + action='store_true', help='Check version') + parser.add_argument('--june', action='store_true', + help='Show pride month easter egg') + parser.add_argument('--debug', action='store_true', help='Debug mode') + + parser.add_argument('-d', '--distro', '--test-distro', + dest='distro', help='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) + 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) + parser.add_argument('--ask-exit', action='store_true', + help=argparse.SUPPRESS) return parser @@ -337,7 +87,7 @@ def run(): pass # On Windows: Try to fix color rendering if not in git bash - if IS_WINDOWS: + if constants.IS_WINDOWS: import colorama colorama.just_fix_windows_console() @@ -345,8 +95,8 @@ def run(): args = parser.parse_args() # Use a custom distro - GLOBAL_CFG.override_distro = args.distro - GLOBAL_CFG.use_overlay = args.overlay + constants.GLOBAL_CFG.override_distro = args.distro + constants.GLOBAL_CFG.use_overlay = args.overlay if args.version: print(f'Version is {VERSION}') @@ -357,31 +107,31 @@ def run(): check_windows_cmd() if args.debug: - GLOBAL_CFG.debug = True + constants.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: + if not args.config_file == constants.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 + args.config_file = constants.CONFIG_PATH # Load config or create config - config = create_config() if args.config else check_config(args.config_file) + config = Configure().run() if args.config else check_config(args.config_file) # Check if it's June (pride month) now = datetime.datetime.now() - june_path = CACHE_PATH / f'animation-displayed-{now.year}' + june_path = constants.CACHE_PATH / f'animation-displayed-{now.year}' if now.month == 6 and now.year not in config.pride_month_shown and not june_path.is_file() and os.isatty(sys.stdout.fileno()): args.june = True if args.june and not config.pride_month_disable: - pride_month.start_animation() + pride_month.play_animation() print() print("Happy pride month!") print("(You can always view the animation again with `hyfetch --june`)") @@ -392,7 +142,7 @@ def run(): june_path.touch() # Use a custom distro - GLOBAL_CFG.override_distro = args.distro or config.distro + constants.GLOBAL_CFG.override_distro = args.distro or config.distro # Param overwrite config if args.preset: @@ -405,27 +155,33 @@ def run(): config.args = args.args # Override global color mode - GLOBAL_CFG.color_mode = config.mode - GLOBAL_CFG.is_light = config.light_dark == 'light' + constants.GLOBAL_CFG.color_mode = config.mode + constants.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()) + flag = config.preset # 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) + asc = get_distro_ascii() if not args.ascii_file else Path( + args.ascii_file).read_text("utf-8") + + if args.scale: + if args.scale > 0: + mode = ("scale", args.scale) + else: + raise ValueError("Color scale must be greater than 0") + elif args.light: + mode = ("set_raw", args.light) + else: + mode = ( + "set_dl", config.lightness or constants.GLOBAL_CFG.default_lightness()) + + asc = recolor_ascii( + asc, flag, lightness=mode[1], lightness_mode=mode[0]) neofetch_util.run(asc, config.backend, config.args or '') - except Exception as e: - print(f'Error: {e}') + except Exception as ex: + print(f'Error: {ex}') traceback.print_exc() if args.ask_exit: diff --git a/hyfetch/models.py b/hyfetch/models.py index 9f3a747d..9fd3075c 100644 --- a/hyfetch/models.py +++ b/hyfetch/models.py @@ -1,31 +1,43 @@ from __future__ import annotations from dataclasses import dataclass, field - +from pathlib import Path from .constants import CONFIG_PATH -from .neofetch_util import ColorAlignment from .serializer import json_stringify, from_dict from .types import AnsiMode, LightDark, BackendLiteral @dataclass class Config: + """ + Configuration object + """ preset: str mode: AnsiMode light_dark: LightDark = 'dark' lightness: float | None = None - color_align: ColorAlignment = field(default_factory=lambda: ColorAlignment('horizontal')) backend: BackendLiteral = "neofetch" args: str | None = None distro: str | None = None - pride_month_shown: list[int] = field(default_factory=list) # This is deprecated, see issue #136 + + # This is deprecated, see issue #136 + pride_month_shown: list[int] = field(default_factory=list) pride_month_disable: bool = False @classmethod - def from_dict(cls, d: dict): - d['color_align'] = ColorAlignment.from_dict(d['color_align']) - return from_dict(cls, d) + def from_dict(cls, _dict: dict): + return from_dict(cls, _dict) + + def save(self) -> Path: + """ + Save to path + + Returns + ------- + Path + Path of config file. - def save(self): + """ CONFIG_PATH.parent.mkdir(exist_ok=True, parents=True) CONFIG_PATH.write_text(json_stringify(self, indent=4), 'utf-8') + return CONFIG_PATH diff --git a/hyfetch/neofetch_util.py b/hyfetch/neofetch_util.py index 17829ac9..ba79c440 100644 --- a/hyfetch/neofetch_util.py +++ b/hyfetch/neofetch_util.py @@ -8,7 +8,6 @@ import subprocess import sys import zipfile -from dataclasses import dataclass from pathlib import Path from subprocess import check_output from tempfile import TemporaryDirectory @@ -16,12 +15,12 @@ import pkg_resources -from .color_util import color, printc +from .color_util import RGB, color, printc from .constants import GLOBAL_CFG, MINGIT_URL, IS_WINDOWS from .distros import distro_detector -from .presets import ColorProfile -from .serializer import from_dict -from .types import BackendLiteral, ColorAlignMode +from .types import BackendLiteral + +from .flag_utils import get_flag RE_NEOFETCH_COLOR = re.compile('\\${c[0-9]}') @@ -70,17 +69,6 @@ def find_selection(sel: str): return find_selection(selection) -def term_size() -> tuple[int, int]: - """ - Get terminal size - :return: - """ - try: - return os.get_terminal_size().columns, os.get_terminal_size().lines - except Exception: - return 100, 20 - - def ascii_size(asc: str) -> tuple[int, int]: """ Get distro ascii width, height ignoring color code @@ -93,10 +81,10 @@ def ascii_size(asc: str) -> tuple[int, int]: def normalize_ascii(asc: str) -> str: """ - Make sure every line are the same width + Make sure every line is the same width """ w = ascii_size(asc)[0] - return '\n'.join(line + ' ' * (w - ascii_size(line)[0]) for line in asc.split('\n')) + return '\n'.join(line.ljust(w) for line in asc.split('\n')) def fill_starting(asc: str) -> str: @@ -119,66 +107,70 @@ def fill_starting(asc: str) -> str: return '\n'.join(new) -@dataclass -class ColorAlignment: - mode: ColorAlignMode - - # custom_colors[ascii color index] = unique color index in preset - custom_colors: dict[int, int] = () - - # Foreground/background ascii color index - fore_back: tuple[int, int] = () - - @classmethod - def from_dict(cls, d: dict): - return from_dict(cls, d) - - def recolor_ascii(self, asc: str, preset: ColorProfile) -> str: - """ - Use the color alignment to recolor an ascii art - - :return Colored ascii, Uncolored lines - """ - asc = fill_starting(asc) - - if self.fore_back and self.mode in ['horizontal', 'vertical']: - fore, back = self.fore_back - - # Replace foreground colors - asc = asc.replace(f'${{c{fore}}}', color('&0' if GLOBAL_CFG.is_light else '&f')) - lines = asc.split('\n') - - # Add new colors - if self.mode == 'horizontal': - colors = preset.with_length(len(lines)) - asc = '\n'.join([l.replace(f'${{c{back}}}', colors[i].to_ansi()) + color('&~&*') for i, l in enumerate(lines)]) - else: - raise NotImplementedError() - - # Remove existing colors - asc = re.sub(RE_NEOFETCH_COLOR, '', asc) - - elif self.mode in ['horizontal', 'vertical']: - # Remove existing colors - asc = re.sub(RE_NEOFETCH_COLOR, '', asc) - lines = asc.split('\n') - - # Add new colors - if self.mode == 'horizontal': - colors = preset.with_length(len(lines)) - asc = '\n'.join([colors[i].to_ansi() + l + color('&~&*') for i, l in enumerate(lines)]) - else: - asc = '\n'.join(preset.color_text(line) + color('&~&*') for line in lines) - +def recolor_ascii(asc: str, flag: str, lightness: float = 0.5, lightness_mode: str = "set_dl", + foreground: bool = True, rotation: int = 0, + term: str = 'dark') -> str: + """ + Use the color alignment to recolor an ascii art + """ + asc = fill_starting(asc) + # Remove existing colors + asc = re.sub(RE_NEOFETCH_COLOR, '', asc) + + # Get image array + lines = asc.split('\n') + x_len = max(len(line) for line in lines) + y_len = len(lines) + asc = normalize_ascii(asc) + img = get_flag(flag, x_len, y_len, rotation=rotation) + # Bold + new_asc = color("&l\033[1m") + current_color = '' + x = 0 + y = 0 + # Color + if lightness_mode == "set_dl": + if term.lower() == 'dark': + at_least, at_most = (True, None) + else: + at_least, at_most = (None, True) + + for char in asc: + if char == '\n': + # Clear + new_asc += color('&~&*') + new_asc += '\n' + new_asc += color("&l\033[1m") + current_color = '' + continue + if x == x_len: + x = 0 + y += 1 + + rgb_color = RGB(*img.getpixel((x, y))) + if lightness_mode == "set_dl": + rgb_color = rgb_color.set_light(lightness, at_least, at_most) + elif lightness_mode == "set_raw": + rgb_color = rgb_color.set_light(lightness, None, None) + elif lightness_mode == "scale": + rgb_color = rgb_color.lighten(lightness) + elif lightness_mode is None: + pass else: - preset = preset.unique_colors() + raise NotImplementedError( + "lightness_mode must be either set_dl, set_raw, scale, or None") + color_str = rgb_color.to_ansi(foreground=foreground) + if color_str != current_color and (char != ' ' or not foreground): + new_asc += color_str + current_color = color_str + new_asc += char - # Apply colors - color_map = {ai: preset.colors[pi].to_ansi() for ai, pi in self.custom_colors.items()} - for ascii_i, c in color_map.items(): - asc = asc.replace(f'${{c{ascii_i}}}', c) + x += 1 - return asc + # Unbold + clear + new_asc += color("&l\033[22m") + new_asc += color('&~&*') + return new_asc def if_file(f: str | Path) -> Path | None: @@ -210,7 +202,7 @@ def get_command_path() -> str: if not pth: printc("&cError: Neofetch script cannot be found") - exit(127) + sys.exit(127) return str(pth) @@ -235,7 +227,7 @@ def ensure_git_bash() -> Path: pth = Path(git_exe).parent if (pth / r'bash.exe').is_file(): return pth / r'bash.exe' - elif (pth / r'bin\bash.exe').is_file(): + if (pth / r'bin\bash.exe').is_file(): return pth / r'bin\bash.exe' # Find installation in PATH (C:\Program Files\Git\cmd should be in path) @@ -261,8 +253,7 @@ def ensure_git_bash() -> Path: zip_ref.extractall(path) print('Done!') return path / r'bin\bash.exe' - else: - sys.exit() + sys.exit() def check_windows_cmd(): @@ -296,8 +287,7 @@ def run_neofetch_cmd(args: str, pipe: bool = False) -> str | None: if pipe: return check_output(full_cmd).decode().strip() - else: - subprocess.run(full_cmd) + subprocess.run(full_cmd) def get_distro_ascii(distro: str | None = None) -> str: @@ -441,4 +431,3 @@ def get_fore_back(distro: str | None = None) -> tuple[int, int] | None: 'ubuntu-studio': (2, 1), 'ubuntu-sway': (2, 1), } - diff --git a/hyfetch/presets.py b/hyfetch/presets.py deleted file mode 100644 index 6c0b9e79..00000000 --- a/hyfetch/presets.py +++ /dev/null @@ -1,660 +0,0 @@ -from __future__ import annotations - -from typing import Iterable - -from .color_util import RGB -from .constants import GLOBAL_CFG -from .types import LightDark, ColorSpacing - - -def remove_duplicates(seq: Iterable) -> list: - """ - Remove duplicate items from a sequence while preserving the order - """ - seen = set() - seen_add = seen.add - return [x for x in seq if not (x in seen or seen_add(x))] - - -class ColorProfile: - raw: list[str] - colors: list[RGB] - spacing: ColorSpacing = 'equal' - - def __init__(self, colors: list[str] | list[RGB]): - if isinstance(colors[0], str): - self.raw = colors - self.colors = [RGB.from_hex(c) for c in colors] - else: - self.colors = colors - - def with_weights(self, weights: list[int]) -> list[RGB]: - """ - Map colors based on weights - - :param weights: Weights of each color (weights[i] = how many times color[i] appears) - :return: - """ - return [c for i, w in enumerate(weights) for c in [self.colors[i]] * w] - - def with_length(self, length: int) -> list[RGB]: - """ - Spread to a specific length of text - - :param length: Length of text - :return: List of RGBs of the length - """ - preset_len = len(self.colors) - center_i = preset_len // 2 - - # How many copies of each color should be displayed at least? - repeats = length // preset_len - weights = [repeats] * preset_len - - # How many extra space left? - extras = length % preset_len - - # If there is an even space left, extend the center by one space - if extras % 2 == 1: - extras -= 1 - weights[center_i] += 1 - - # Add weight to border until there's no space left (extras must be even at this point) - border_i = 0 - while extras > 0: - extras -= 2 - weights[border_i] += 1 - weights[-(border_i + 1)] += 1 - border_i += 1 - - return self.with_weights(weights) - - def color_text(self, txt: str, foreground: bool = True, space_only: bool = False) -> str: - """ - Color a text - - :param txt: Text - :param foreground: Whether the foreground text show the color or the background block - :param space_only: Whether to only color spaces - :return: Colored text - """ - colors = self.with_length(len(txt)) - result = '' - for i, t in enumerate(txt): - if space_only and t != ' ': - if i > 0 and txt[i - 1] == ' ': - result += '\033[39;49m' - result += t - else: - result += colors[i].to_ansi(foreground=foreground) + t - - result += '\033[39;49m' - return result - - def lighten(self, multiplier: float) -> ColorProfile: - """ - Lighten the color profile by a multiplier - - :param multiplier: Multiplier - :return: Lightened color profile (original isn't modified) - """ - return ColorProfile([c.lighten(multiplier) for c in self.colors]) - - def set_light_raw(self, light: float, at_least: bool | None = None, at_most: bool | None = None) -> 'ColorProfile': - """ - Set HSL lightness value - - :param light: Lightness value (0-1) - :param at_least: Set the lightness to at least this value (no change if greater) - :param at_most: Set the lightness to at most this value (no change if lesser) - :return: New color profile (original isn't modified) - """ - return ColorProfile([c.set_light(light, at_least, at_most) for c in self.colors]) - - def set_light_dl(self, light: float, term: LightDark | None = None): - """ - Set HSL lightness value with respect to dark/light terminals - - :param light: Lightness value (0-1) - :param term: Terminal color (can be "dark" or "light") - :return: New color profile (original isn't modified) - """ - if GLOBAL_CFG.use_overlay: - return self.overlay_dl(light, term) - - term = term or GLOBAL_CFG.light_dark() - assert term.lower() in ['light', 'dark'] - at_least, at_most = (True, None) if term.lower() == 'dark' else (None, True) - return self.set_light_raw(light, at_least, at_most) - - def overlay_raw(self, color: RGB, alpha: float) -> 'ColorProfile': - """ - Overlay a color on top of the color profile - - :param color: Color to overlay - :param alpha: Alpha value (0-1) - :return: New color profile (original isn't modified) - """ - return ColorProfile([c.overlay(color, alpha) for c in self.colors]) - - def overlay_dl(self, light: float, term: LightDark | None = None): - """ - Same as set_light_dl except that this function uses RGB overlaying instead of HSL lightness change - """ - term = term or GLOBAL_CFG.light_dark() - assert term.lower() in ['light', 'dark'] - - # If it's light bg, overlay black, else overlay white - overlay_color = RGB.from_hex('#000000' if term.lower() == 'light' else '#FFFFFF') - return self.overlay_raw(overlay_color, abs(light - 0.5) * 2) - - def set_light_dl_def(self, term: LightDark | None = None): - """ - Set default lightness with respect to dark/light terminals - - :param term: Terminal color (can be "dark" or "light") - :return: New color profile (original isn't modified) - """ - return self.set_light_dl(GLOBAL_CFG.default_lightness(term), term) - - def unique_colors(self) -> ColorProfile: - """ - Create another color profile with only the unique colors - """ - return ColorProfile(remove_duplicates(self.colors)) - - -PRESETS: dict[str, ColorProfile] = { - 'rainbow': ColorProfile([ - '#E50000', - '#FF8D00', - '#FFEE00', - '#028121', - '#004CFF', - '#770088' - ]), - - 'transgender': ColorProfile([ - '#55CDFD', - '#F6AAB7', - '#FFFFFF', - '#F6AAB7', - '#55CDFD' - ]), - - 'nonbinary': ColorProfile([ - '#FCF431', - '#FCFCFC', - '#9D59D2', - '#282828' - ]), - - 'agender': ColorProfile([ - '#000000', - '#BABABA', - '#FFFFFF', - '#BAF484', - '#FFFFFF', - '#BABABA', - '#000000' - ]), - - 'queer': ColorProfile([ - '#B57FDD', - '#FFFFFF', - '#49821E' - ]), - - 'genderfluid': ColorProfile([ - '#FE76A2', - '#FFFFFF', - '#BF12D7', - '#000000', - '#303CBE' - ]), - - 'bisexual': ColorProfile([ - '#D60270', - '#9B4F96', - '#0038A8' - ]), - - 'pansexual': ColorProfile([ - '#FF1C8D', - '#FFD700', - '#1AB3FF' - ]), - - 'polysexual': ColorProfile([ - '#F714BA', - '#01D66A', - '#1594F6', - ]), - - # omnisexual sorced from https://www.flagcolorcodes.com/omnisexual - 'omnisexual': ColorProfile([ - '#FE9ACE', - '#FF53BF', - '#200044', - '#6760FE', - '#8EA6FF', - ]), - - 'omniromantic': ColorProfile([ - '#FEC8E4', - '#FDA1DB', - '#89739A', - '#ABA7FE', - '#BFCEFF', - ]), - - # gay men sourced from https://www.flagcolorcodes.com/gay-men - 'gay-men': ColorProfile([ - '#078D70', - '#98E8C1', - '#FFFFFF', - '#7BADE2', - '#3D1A78' - ]), - - 'lesbian': ColorProfile([ - '#D62800', - '#FF9B56', - '#FFFFFF', - '#D462A6', - '#A40062' - ]), - - # abrosexual used colorpicker to source from - # https://fyeahaltpride.tumblr.com/post/151704251345/could-you-guys-possibly-make-an-abrosexual-pride - 'abrosexual': ColorProfile([ - '#46D294', - '#A3E9CA', - '#FFFFFF', - '#F78BB3', - '#EE1766', - ]), - - 'asexual': ColorProfile([ - '#000000', - '#A4A4A4', - '#FFFFFF', - '#810081' - ]), - - 'aromantic': ColorProfile([ - '#3BA740', - '#A8D47A', - '#FFFFFF', - '#ABABAB', - '#000000' - ]), - - # aroace1 sourced from https://flag.library.lgbt/flags/aroace/ - 'aroace1': ColorProfile([ - '#E28C00', - '#ECCD00', - '#FFFFFF', - '#62AEDC', - '#203856' - ]), - - 'aroace2': ColorProfile([ - '#000000', - '#810081', - '#A4A4A4', - '#FFFFFF', - '#A8D47A', - '#3BA740' - ]), - - 'aroace3': ColorProfile([ - '#3BA740', - '#A8D47A', - '#FFFFFF', - '#ABABAB', - '#000000', - '#A4A4A4', - '#FFFFFF', - '#810081' - ]), - - # below sourced from https://www.flagcolorcodes.com/flags/pride - # goto f"https://www.flagcolorcodes.com/{preset}" for info - # todo: sane sorting - 'autosexual': ColorProfile([ - '#99D9EA', - '#7F7F7F' - ]), - - 'intergender': ColorProfile([ - # todo: use weighted spacing - '#900DC2', - '#900DC2', - '#FFE54F', - '#900DC2', - '#900DC2', - ]), - - 'greygender': ColorProfile([ - '#B3B3B3', - '#B3B3B3', - '#FFFFFF', - '#062383', - '#062383', - '#FFFFFF', - '#535353', - '#535353', - ]), - - 'akiosexual': ColorProfile([ - '#F9485E', - '#FEA06A', - '#FEF44C', - '#FFFFFF', - '#000000', - ]), - - # bigender sourced from https://www.flagcolorcodes.com/bigender - 'bigender': ColorProfile([ - '#C479A2', - '#EDA5CD', - '#D6C7E8', - '#FFFFFF', - '#D6C7E8', - '#9AC7E8', - '#6D82D1', - ]), - - # demigender yellow sourced from https://lgbtqia.fandom.com/f/p/4400000000000041031 - # other colors sourced from demiboy and demigirl flags - 'demigender': ColorProfile([ - '#7F7F7F', - '#C4C4C4', - '#FBFF75', - '#FFFFFF', - '#FBFF75', - '#C4C4C4', - '#7F7F7F', - ]), - - # demiboy sourced from https://www.flagcolorcodes.com/demiboy - 'demiboy': ColorProfile([ - '#7F7F7F', - '#C4C4C4', - '#9DD7EA', - '#FFFFFF', - '#9DD7EA', - '#C4C4C4', - '#7F7F7F', - ]), - - # demigirl sourced from https://www.flagcolorcodes.com/demigirl - 'demigirl': ColorProfile([ - '#7F7F7F', - '#C4C4C4', - '#FDADC8', - '#FFFFFF', - '#FDADC8', - '#C4C4C4', - '#7F7F7F', - ]), - - 'transmasculine': ColorProfile([ - '#FF8ABD', - '#CDF5FE', - '#9AEBFF', - '#74DFFF', - '#9AEBFF', - '#CDF5FE', - '#FF8ABD', - ]), - - # transfeminine used colorpicker to source from https://www.deviantart.com/pride-flags/art/Trans-Woman-Transfeminine-1-543925985 - # linked from https://gender.fandom.com/wiki/Transfeminine - 'transfeminine': ColorProfile([ - '#73DEFF', - '#FFE2EE', - '#FFB5D6', - '#FF8DC0', - '#FFB5D6', - '#FFE2EE', - '#73DEFF', - ]), - - # genderfaun sourced from https://www.flagcolorcodes.com/genderfaun - 'genderfaun': ColorProfile([ - '#FCD689', - '#FFF09B', - '#FAF9CD', - '#FFFFFF', - '#8EDED9', - '#8CACDE', - '#9782EC', - ]), - - 'demifaun': ColorProfile([ - '#7F7F7F', - '#7F7F7F', - '#C6C6C6', - '#C6C6C6', - '#FCC688', - '#FFF19C', - '#FFFFFF', - '#8DE0D5', - '#9682EC', - '#C6C6C6', - '#C6C6C6', - '#7F7F7F', - '#7F7F7F', - ]), - - # genderfae sourced from https://www.flagcolorcodes.com/genderfae - 'genderfae': ColorProfile([ - '#97C3A5', - '#C3DEAE', - '#F9FACD', - '#FFFFFF', - '#FCA2C4', - '#DB8AE4', - '#A97EDD', - ]), - - # demifae used colorpicker to source form https://www.deviantart.com/pride-flags/art/Demifae-870194777 - 'demifae': ColorProfile([ - '#7F7F7F', - '#7F7F7F', - '#C5C5C5', - '#C5C5C5', - '#97C3A4', - '#C4DEAE', - '#FFFFFF', - '#FCA2C5', - '#AB7EDF', - '#C5C5C5', - '#C5C5C5', - '#7F7F7F', - '#7F7F7F', - ]), - - 'neutrois': ColorProfile([ - '#FFFFFF', - '#1F9F00', - '#000000' - ]), - - 'biromantic1': ColorProfile([ - '#8869A5', - '#D8A7D8', - '#FFFFFF', - '#FDB18D', - '#151638', - ]), - - 'biromantic2': ColorProfile([ - '#740194', - '#AEB1AA', - '#FFFFFF', - '#AEB1AA', - '#740194', - ]), - - 'autoromantic': ColorProfile([ # symbol interpreted - '#99D9EA', - '#99D9EA', - '#3DA542', - '#7F7F7F', - '#7F7F7F', - ]), - - # i didn't expect this one to work. cool! - 'boyflux2': ColorProfile(ColorProfile([ - '#E48AE4', - '#9A81B4', - '#55BFAB', - '#FFFFFF', - '#A8A8A8', - '#81D5EF', - '#69ABE5', - '#5276D4', - ]).with_weights([1, 1, 1, 1, 1, 5, 5, 5])), - - "finsexual": ColorProfile([ - "#B18EDF", - "#D7B1E2", - "#F7CDE9", - "#F39FCE", - "#EA7BB3", - ]), - - 'unlabeled1': ColorProfile([ - '#EAF8E4', - '#FDFDFB', - '#E1EFF7', - '#F4E2C4' - ]), - - 'unlabeled2': ColorProfile([ - '#250548', - '#FFFFFF', - '#F7DCDA', - '#EC9BEE', - '#9541FA', - '#7D2557' - ]), - - 'pangender': ColorProfile([ - '#FFF798', - '#FEDDCD', - '#FFEBFB', - '#FFFFFF', - '#FFEBFB', - '#FEDDCD', - '#FFF798', - ]), - - 'gendernonconforming1': ColorProfile( - ColorProfile([ - '#50284d', - '#96467b', - '#5c96f7', - '#ffe6f7', - '#5c96f7', - '#96467b', - '#50284d' - ]).with_weights([ - 4,1,1,1,1,1,4 - ]) - ), - - 'gendernonconforming2': ColorProfile([ - '#50284d', - '#96467b', - '#5c96f7', - '#ffe6f7', - '#5c96f7', - '#96467b', - '#50284d' - ]), - - 'femboy': ColorProfile([ - "#d260a5", - "#e4afcd", - "#fefefe", - "#57cef8", - "#fefefe", - "#e4afcd", - "#d260a5" - ]), - - 'tomboy': ColorProfile([ - "#2f3fb9", - "#613a03", - "#fefefe", - "#f1a9b7", - "#fefefe", - "#613a03", - "#2f3fb9" - ]), - - 'gynesexual': ColorProfile([ - "#F4A9B7", - "#903F2B", - "#5B953B", - ]), - - 'androsexual': ColorProfile([ - "#01CCFF", - "#603524", - "#B799DE", - ]), - - # gendervoid and related flags sourced from: https://gender.fandom.com/wiki/Gendervoid - 'gendervoid' : ColorProfile([ - "#081149", - "#4B484B", - "#000000", - "#4B484B", - "#081149" - ]), - - 'voidgirl' : ColorProfile([ - "#180827", - "#7A5A8B", - "#E09BED", - "#7A5A8B", - "#180827" - ]), - - 'voidboy' : ColorProfile([ - "#0B130C", - "#547655", - "#66B969", - "#547655", - "#0B130C" - ]), - - # used https://twitter.com/foxbrained/status/1667621855518236674/photo/1 as source and colorpicked - 'nonhuman-unity' : ColorProfile([ - "#177B49", - "#FFFFFF", - "#593C90" - ]), - - # Meme flags - 'beiyang': ColorProfile([ - '#DF1B12', - '#FFC600', - '#01639D', - '#FFFFFF', - '#000000', - ]), - - 'burger': ColorProfile([ - '#F3A26A', - '#498701', - '#FD1C13', - '#7D3829', - '#F3A26A', - ]), -} diff --git a/hyfetch/pride_month.py b/hyfetch/pride_month.py index 8242057a..2401a30b 100644 --- a/hyfetch/pride_month.py +++ b/hyfetch/pride_month.py @@ -1,115 +1,135 @@ -import math +from __future__ import annotations + +import sys +import random +import select from time import sleep +from PIL import Image -from hyfetch import presets from hyfetch.color_util import RGB, color, printc -from hyfetch.constants import IS_WINDOWS -from hyfetch.neofetch_util import term_size -from hyfetch.presets import PRESETS - - -def key_pressed(): - if IS_WINDOWS: - import msvcrt - return msvcrt.kbhit() # Non-blocking check for key press - else: - import select - import sys - return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []) +from . import constants +from .flag_utils import get_flag, get_flags +if constants.IS_WINDOWS: + import msvcrt -def start_animation(): - text = r""" +TEXT = r""" .======================================================. | . . .__ . . . , . | | | |__| _.._ ._ . [__)._.* _| _ |\/| _ ._ -+-|_ | | | | |(_][_)[_)\_| | [ |(_](/, | |(_)[ ) | [ ) * | | | | ._| | '======================================================'""".strip("\n") - text_lines = text.split("\n") - text_height = len(text_lines) - text_width = len(text_lines[0]) +NOTICE = "Press enter to continue" +FRAME_DELAY = 0.01 - notice = "Press enter to continue" +text_lines = TEXT.split("\n") +text_height = len(text_lines) +text_width = len(text_lines[0]) - speed = 2 - frame_delay = 1 / 25 +text_start_y = constants.TERM_HEIGHT // 2 - text_height // 2 +text_end_y = text_start_y + text_height +text_start_x = constants.TERM_WIDTH // 2 - text_width // 2 +text_end_x = text_start_x + text_width - colors: list[RGB] = [] - frame = 0 +notice_start_x = constants.TERM_WIDTH - len(NOTICE) - 1 +notice_end_x = constants.TERM_WIDTH - 1 +notice_y = constants.TERM_HEIGHT - 1 - w, h = term_size() - blocks = 9 - block_width = w // blocks +FLAG_WIDTH = constants.TERM_WIDTH +FLAG_HEIGHT = constants.TERM_HEIGHT - text_start_y = h // 2 - text_height // 2 - text_end_y = text_start_y + text_height - text_start_x = w // 2 - text_width // 2 - text_end_x = text_start_x + text_width +flag_list = get_flags() +random.shuffle(flag_list) +total_flag_height = len(flag_list)*FLAG_HEIGHT +flag_im = Image.new('RGB', (FLAG_WIDTH, total_flag_height)) - notice_start_x = w - len(notice) - 1 - notice_end_x = w - 1 - notice_y = h - 1 +for i, flag in enumerate(flag_list): + tmp_im = get_flag(flag, FLAG_WIDTH, FLAG_HEIGHT) + flag_im.paste(tmp_im, (0, i*FLAG_HEIGHT)) - # Add everything in PRESETS to colors - colors = [c for preset in PRESETS.values() for c in preset.colors] - black = RGB(0, 0, 0) - fg = RGB.from_hex("#FFE09B") +def key_pressed(): + """ + Check for key press - def draw_frame(): - buf = "" + Returns + ------- + bool + Key press status. - # Loop over the height - for y in range(h): - # Print the starting color - buf += colors[((frame + y) // block_width) % len(colors)].to_ansi_rgb(foreground=False) - buf += fg.to_ansi_rgb(foreground=True) + """ + if constants.IS_WINDOWS: + return msvcrt.kbhit() # Non-blocking check for key press + return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []) - # Loop over the width - x = 0 - while x < w: - idx = frame + x + y + int(math.sin(y + 0.5 * frame) * 2) - y_text = text_start_y <= y < text_end_y - border = 1 + int(not (y == text_start_y or y == text_end_y - 1)) +def play_animation(): + """ + Play the pride month animation + + Returns + ------- + None. + """ + frame = 0 + + def draw_frame(frame): + buf = "" + overlay = False + # Loop over the height + for y_pos in range(constants.TERM_HEIGHT): + current_color = '' + # Loop over the width + y_text = text_start_y <= y_pos < text_end_y + border = int(not y_pos in (text_start_y, text_end_y - 1)) + 1 + x_switch_pts = (text_start_x - border, text_end_x + + border, notice_start_x - 1, notice_end_x + 1) + for x_pos in range(constants.TERM_WIDTH): # If it's a switching point - if idx % block_width == 0 or x == text_start_x - border or x == text_end_x + border \ - or x == notice_start_x - 1 or x == notice_end_x + 1: + if x_pos in x_switch_pts: # Print the color at the current frame - c = colors[(idx // block_width) % len(colors)] - if (y_text and text_start_x - border <= x < text_end_x + border) \ - or (y == notice_y and notice_start_x - 1 <= x < notice_end_x + 1): - # buf += c.set_light(0.3).to_ansi_rgb(foreground=False) - buf += c.overlay(black, 0.5).to_ansi_rgb(foreground=False) - else: - buf += c.to_ansi_rgb(foreground=False) + overlay = (y_text and text_start_x - border <= x_pos < text_end_x + border) or ( + y_pos == notice_y and notice_start_x - 1 <= x_pos < notice_end_x + 1) + + # Add flag + diff = int(frame + 0.1 * x_pos) + + rgb_color = RGB( + *flag_im.getpixel((x_pos % FLAG_WIDTH, (y_pos + diff) % total_flag_height))) + + if overlay: + rgb_color = rgb_color.overlay(RGB(0, 0, 0), 0.5) + + if rgb_color != current_color: + buf += rgb_color.to_ansi(foreground=False) + current_color = rgb_color # If text should be printed, print text - if y_text and text_start_x <= x < text_end_x: + if y_text and text_start_x <= x_pos < text_end_x: # Add white background - buf += text_lines[y - text_start_y][x - text_start_x] - elif y == notice_y and notice_start_x <= x < notice_end_x: - buf += notice[x - notice_start_x] + buf += text_lines[y_pos - + text_start_y][x_pos - text_start_x] + elif y_pos == notice_y and notice_start_x <= x_pos < notice_end_x: + buf += NOTICE[x_pos - notice_start_x] else: buf += ' ' - x += 1 - + x_pos += 1 # New line if it isn't the last line - if y != h - 1: + if y_pos != constants.TERM_HEIGHT - 1: buf += color('&r\n') print(buf, end='', flush=True) try: - while 1: + while True: # Clear the screen print("\033[2J\033[H", end="") - draw_frame() - frame += speed - sleep(frame_delay) + draw_frame(frame) + frame += 1 + sleep(FRAME_DELAY) if key_pressed(): break @@ -122,6 +142,4 @@ def draw_frame(): if __name__ == '__main__': - start_animation() - - + play_animation() diff --git a/hyfetch/serializer.py b/hyfetch/serializer.py index a392707c..0c722432 100644 --- a/hyfetch/serializer.py +++ b/hyfetch/serializer.py @@ -44,5 +44,19 @@ def json_stringify(obj: object, indent: int | None = None) -> str: return json.dumps(obj, indent=indent, cls=EnhancedJSONEncoder, ensure_ascii=False) -def from_dict(cls, d: dict): - return cls(**{k: v for k, v in d.items() if k in inspect.signature(cls).parameters}) +def from_dict(cls, _dict: dict): + """ + Load class from dict + + Parameters + ---------- + _dict : dict + Dictionary to load from. + + Returns + ------- + cls + Class with attributes as defined in the dictionary. + + """ + return cls(**{k: v for k, v in _dict.items() if k in inspect.signature(cls).parameters}) diff --git a/hyfetch/termenv.py b/hyfetch/termenv.py index b91bfcc4..0c982123 100644 --- a/hyfetch/termenv.py +++ b/hyfetch/termenv.py @@ -1,9 +1,8 @@ from __future__ import annotations import os -import platform import sys - +import platform from .color_util import RGB, AnsiMode @@ -24,17 +23,17 @@ def unix_detect_ansi_mode() -> AnsiMode | None: term = os.environ.get('TERM') color_term = os.environ.get('COLORTERM') - if color_term == 'truecolor' or color_term == '24bit': + if color_term in ('truecolor', '24bit'): if term.startswith('screen') and os.environ.get('TERM_PROGRAM') != 'tmux': return '8bit' return 'rgb' - elif color_term == 'true' or color_term == 'yes': + if color_term in ('true', 'yes'): return '8bit' if term == 'xterm-kitty': return 'rgb' - elif term == 'linux': + if term == 'linux': return 'ansi' if '256color' in term: @@ -87,38 +86,38 @@ def detect_ansi_mode() -> AnsiMode | None: def unix_read_osc(seq: int) -> str: - import termios import tty import signal + import termios from select import select - # screen/tmux can't support OSC, because they can be connected to multiple # terminals concurrently. term = os.environ.get('TERM') if term.startswith("screen") or term.startswith("tmux"): raise OSCException("Screen/tmux not supported") - t = sys.stdout - if not t.isatty(): + term = sys.stdout + if not term.isatty(): raise OSCException("Not a tty") - fd = sys.stdin.fileno() + file_desc = sys.stdin.fileno() # Set raw mode - settings = termios.tcgetattr(fd) + settings = termios.tcgetattr(file_desc) tty.setraw(sys.stdin.fileno()) # first, send OSC query, which is ignored by terminal which do not support it - t.write(f"\x1b]{seq};?\x1b\\") - t.flush() + term.write(f"\x1b]{seq};?\x1b\\") + term.flush() # stdin response timeout should be higher for ssh sessions - timeout = 0.05 if (os.environ.get('SSH_TTY') or os.environ.get('SSH_SESSION')) is None else 0.5 + timeout = 0.05 if (os.environ.get('SSH_TTY') + or os.environ.get('SSH_SESSION')) is None else 0.5 # Wait for input to appear if not select([sys.stdin], [], [], timeout)[0]: # Reset terminal back to normal mode (previously set to raw mode) - termios.tcsetattr(fd, termios.TCSADRAIN, settings) + termios.tcsetattr(file_desc, termios.TCSADRAIN, settings) raise OSCException("No response received") # Read until termination, or if it doesn't terminate, read until 1 second passes @@ -139,7 +138,7 @@ def handler(signum, frame): pass # Reset terminal back to normal mode (previously set to raw mode) - termios.tcsetattr(fd, termios.TCSADRAIN, settings) + termios.tcsetattr(file_desc, termios.TCSADRAIN, settings) # Validate output if not code: @@ -157,13 +156,13 @@ def handler(signum, frame): def get_background_color() -> RGB | None: system = platform.system().lower() + if system.startswith("windows"): + return None if system.startswith("linux") or system.startswith("darwin"): try: osc = unix_read_osc(11).lstrip("rgb:") - return RGB.from_hex(''.join([v[:2] for v in osc.split('/')])) - except Exception: + except OSCException: return None - if system.startswith("windows"): - return None - - + background_color = RGB.from_hex( + ''.join([v[:2] for v in osc.split('/')])) + return background_color diff --git a/hyfetch/types.py b/hyfetch/types.py index 283bd8fd..d88536ec 100644 --- a/hyfetch/types.py +++ b/hyfetch/types.py @@ -1,7 +1,7 @@ +from __future__ import annotations + from typing_extensions import Literal AnsiMode = Literal['default', 'ansi', '8bit', 'rgb'] LightDark = Literal['light', 'dark'] BackendLiteral = Literal["neofetch", "fastfetch"] -ColorAlignMode = Literal['horizontal', 'vertical', 'custom'] -ColorSpacing = Literal['equal', 'weighted'] diff --git a/setup.py b/setup.py index 0ec804b6..a25db39f 100755 --- a/setup.py +++ b/setup.py @@ -36,8 +36,8 @@ include_package_data=True, install_requires=[ # Universal dependencies - 'setuptools', 'typing_extensions', - + 'setuptools', 'typing_extensions', 'Pillow', + # Windows dependencies 'psutil ; platform_system=="Windows"', 'colorama>=0.4.6 ; platform_system=="Windows"', diff --git a/test.py b/test.py new file mode 100644 index 00000000..76c14fc0 --- /dev/null +++ b/test.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from urllib.request import urlretrieve + +from hyfetch.color_util import printc, color +import hyfetch.constants + + +@dataclass +class Theme: + done_char: str + todo_char: str + prefix: str = '' + suffix: str = '' + done_len: int = 1 + todo_len: int = 1 + + +CLASSIC_THEME = Theme('█', '.', '[', ']') +NEW_THEME = Theme('&a━', '&c━') +EMOJI_THEME = Theme('✅', '🕑', done_len=2, todo_len=2) +EGG_THEME = Theme('🐣', '🥚', done_len=2, todo_len=2) +FLOWER_THEME = Theme('🌸', '🥀', done_len=2, todo_len=2) + + +def print_progressbar(total: int, i: int, length: int | None = None, theme: Theme = EMOJI_THEME, unit=''): + if not length: + length = hyfetch.constants.TERM_WIDTH + i += 1 + + completed = f'{i * 100 / total:.0f}%' + placeholder = 'PLACEHOLDER_BAR' + template = f'{theme.prefix}{placeholder}{theme.suffix}&r {completed} {i}/{total}{unit}' + + length -= len(template) - len(placeholder) + 2 + + progress = int(i / total * length) + bar = f'{theme.done_char * (progress // theme.todo_len)}{theme.todo_char * ((length - progress) // theme.done_len)}' + print(color(template.replace(placeholder, bar)), end='\r', flush=True) + + +def download_pbar(url: str, path: Path): + def hook(b: int, bsize: int, tsize: int): + print_progressbar(tsize // 1024 // 1024, b * + bsize // 1024 // 1024, unit=' MB') + + if path.is_dir(): + filename = url.split('/')[-1] + path = path / filename + path.parent.mkdir(exist_ok=True, parents=True) + + urlretrieve(url, filename=path, reporthook=hook) + print() + + +if __name__ == '__main__': + # theme = {'emoji': EMOJI_THEME, 'flower': FLOWER_THEME, 'egg': EGG_THEME, 'classic': CLASSIC_THEME, 'new': NEW_THEME} + # + # for name, t in theme.items(): + # print(f'\n{name} theme:') + # for i in range(100): + # print_progressbar(100, i, theme=t) + # time.sleep(0.015) + # print() + download_pbar( + 'https://github.com/git-for-windows/git/releases/download/v2.37.2.windows.2/MinGit-2.37.2.2-busybox-64-bit.zip', Path('Downloads'))