Compare commits

...

45 Commits

Author SHA1 Message Date
Azalea (on HyDEV-Daisy) eb76b2f474 [U] Release v1.1.0 2022-07-25 20:41:09 -04:00
Azalea (on HyDEV-Daisy) c9ed8e2088 [F] Put labels below ascii 2022-07-25 20:40:43 -04:00
Azalea (on HyDEV-Daisy) b68e0ccf52 [+] Add reference for default brightness 2022-07-03 13:02:39 -04:00
Azalea (on HyDEV-Daisy) 143bfd2db7 [O] Add more characters for ascii cat 2022-07-03 12:54:24 -04:00
Azalea (on HyDEV-Daisy) 2e20d4e529 [F] Fix numbering 2022-07-03 12:47:45 -04:00
Azalea (on HyDEV-Daisy) 66ff8d06d9 [F] Fix lightness config reading 2022-07-03 12:46:37 -04:00
Azalea (on HyDEV-Daisy) 887f766464 [+] Dynamically set lightness based on light_dark 2022-07-03 12:42:19 -04:00
Azalea (on HyDEV-Daisy) 85eaad14f2 [F] AttributeError: 'dict' object has no attribute 'recolor_ascii' 2022-07-02 23:37:18 -04:00
Azalea (on HyDEV-Daisy) d11796ef02 [+] Recommended color alignments 2022-06-19 22:37:27 -04:00
Azalea (on HyDEV-Daisy) 59f5fd5651 [O] Refactor global cfg 2022-06-19 21:22:36 -04:00
Azalea (on HyDEV-Daisy) a1d687d3bd [+] Random custom coloring 2022-06-19 20:50:09 -04:00
Azalea (on HyDEV-Daisy) 7bd96e422f [+] Lightness config 2022-06-19 18:34:08 -04:00
Azalea (on HyDEV-Daisy) d1f601048b [+] Add test ascii 2022-06-19 18:03:28 -04:00
Azalea (on HyDEV-Daisy) 3b2fb773d3 [O] Optimize print flag 2022-06-19 18:03:19 -04:00
Azalea (on HyDEV-Daisy) fed771d930 [O] Track selected options 2022-06-19 18:03:05 -04:00
Azalea (on HyDEV-Daisy) 8930f921b4 [O] Change prompt order 2022-06-19 17:42:19 -04:00
Azalea (on HyDEV-Daisy) 5de1bf65e4 [F] set_light should be float 2022-06-19 17:39:39 -04:00
Azalea (on HyDEV-Daisy) b46d0e96f4 [+] Encapsulate set_light and lighten 2022-06-19 17:37:04 -04:00
Azalea (on HyDEV-Daisy) 1c67333b45 [+] light/dark mode config 2022-06-19 17:25:02 -04:00
Azalea (on HyDEV-Daisy) 5e26a44c19 [+] Add global color mode 2022-06-19 17:20:41 -04:00
Azalea (on HyDEV-Daisy) 865dea5dc3 [+] Add beiyang flag 2022-06-19 17:06:40 -04:00
Azalea (on HyDEV-Daisy) d5eea9a8ef [O] Refactor flag printing 2022-06-19 17:06:13 -04:00
Azalea (on HyDEV-Daisy) 365aedafe6 [-] Remove center_text function, use str.center 2022-06-19 16:47:03 -04:00
Azalea (on HyDEV-Daisy) 399fcd3c64 [O] Reformat code 2022-06-19 16:37:10 -04:00
Azalea (on HyDEV-Daisy) 299adb8e4d [O] Clear screen with title 2022-06-19 16:33:17 -04:00
Azalea (on HyDEV-Daisy) f86792356e [F] Remove new line after clear screen 2022-06-19 16:31:00 -04:00
Azalea (on HyDEV-Daisy) 892ed4e141 [+] Clear screen feature 2022-06-19 16:29:52 -04:00
Azalea (on HyDEV-Daisy) 5364e22f39 [+] Gradient RGB demo 2022-06-19 16:29:45 -04:00
Azalea (on HyDEV-Daisy) 6d77ebc7a7 [U] Test color scale 2022-06-19 15:33:16 -04:00
Azalea (on HyDEV-Daisy) 1eab4bb157 [F] Actually, make numpy a dependency 2022-06-19 15:33:02 -04:00
Azalea (on HyDEV-Daisy) d59e4a64d3 [+] Add numpy optional dependency 2022-06-19 15:30:20 -04:00
Azalea (on HyDEV-Daisy) 0dd67146d7 [+] Gradient color scale 2022-06-19 15:28:25 -04:00
Azalea (on HyDEV-Daisy) 46b7771f2c [+] Test distro command 2022-06-19 15:20:07 -04:00
Azalea (on HyDEV-Daisy) 002697b49a [+] Get custom distro ascii 2022-06-19 15:18:34 -04:00
Hykilpikonna 9a77c39170 [F] Remove unexpected quotation mark 2022-06-19 13:23:51 -04:00
Hykilpikonna b744be3255 [+] Count choco packages and winget 2022-05-18 16:23:35 -04:00
Hykilpikonna e97c64e7c2 [+] Windows support (git bash) 2022-05-18 16:22:26 -04:00
Hykilpikonna b572e5d35f [U] nixos: Update readme usage 2022-04-29 07:58:32 -04:00
Hykilpikonna 86af5598c6 [-] nixos: remove old script 2022-04-29 07:52:28 -04:00
Hykilpikonna d7273909f6 [F] nixos: Fix something (?) 2022-04-29 07:52:10 -04:00
Hykilpikonna 7b4f209ebe [F] nixos: Fix unexpected argument 2022-04-29 07:39:50 -04:00
Hykilpikonna 46d00666e2 [+] Create defualt.nix 2022-04-29 07:37:33 -04:00
Hykilpikonna 1cffe3556a [+] nixos package 2022-04-29 07:35:51 -04:00
Hykilpikonna 07d94eec8b [+] nixos instructions 2022-04-26 17:39:10 -04:00
Hykilpikonna 62469e6f52 [+] Add nixos package 2022-04-25 15:56:56 -04:00
15 changed files with 725 additions and 100 deletions
+3
View File
@@ -19,8 +19,11 @@ pip install hyfetch
Currently, these distributions have existing packages for HyFetch:
* ArchLinux: `yay -S hyfetch` (Thanks to @ Aleksana)
* NixOS: `nix-env -i hyfetch` ([In Progress](https://github.com/NixOS/nixpkgs/pull/170309))
* Guix: [In progress](https://issues.guix.gnu.org/54847#8-lineno27)
Currently, if you're using NixOS, you can use HyFetch with `nix-env -if https://github.com/hykilpikonna/hyfetch/tarball/master -A hyfetch`
## Usage
When you run `hyfetch` for the first time, it will prompt you to choose a color system and a preset. Just follow the prompt, and everything should work (hopefully). If something doesn't work, feel free to submit an issue!
+36
View File
@@ -0,0 +1,36 @@
with import <nixpkgs> {};
rec {
hyfetch = python3Packages.buildPythonPackage rec {
pname = "HyFetch";
version = "1.0.7";
src = pythonPackages.fetchPypi {
inherit pname version;
hash = "sha256-3/6/3EtTqHXTMuRIo2nclIxYSzOFvQegR29OJsKMQU4=";
};
propagatedBuildInputs = with python3Packages; [
typing-extensions
setuptools
];
doCheck = false;
meta = with lib; {
description = "neofetch with pride flags <3";
longDescription = ''
HyFetch is a command-line system information tool fork of neofetch.
HyFetch displays information about your system next to your OS logo
in ASCII representation. The ASCII representation is then colored in
the pattern of the pride flag of your choice. The main purpose of
HyFetch is to be used in screenshots to show other users what
operating system or distribution you are running, what theme or
icon set you are using, etc.
'';
homepage = "https://github.com/hykilpikonna/HyFetch";
license = licenses.mit;
mainProgram = "hyfetch";
};
};
}
-1
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
from . import main
__version__ = main.VERSION
+71
View File
@@ -0,0 +1,71 @@
from __future__ import annotations
import numpy as np
from numpy import ndarray
from .color_util import RGB
def create_gradient_hex(colors: list[str], resolution: int = 300) -> ndarray:
"""
Create gradient array from hex
"""
colors = np.array([RGB.from_hex(s) for s in colors])
return create_gradient(colors, resolution)
def create_gradient(colors: ndarray, resolution: int) -> ndarray:
"""
Create gradient 2d array.
Usage: arr[ratio / len(arr), :] = Scaled gradient color at that point
"""
result = np.zeros((resolution * (len(colors) - 1), 3), dtype='uint8')
# Create gradient mapping
for i in range(len(colors) - 1):
c1 = colors[i, :]
c2 = colors[i + 1, :]
bi = i * resolution
for r in range(resolution):
ratio = r / resolution
result[bi + r, :] = c2 * ratio + c1 * (1 - ratio)
return result
def get_raw(gradient: ndarray, ratio: float) -> ndarray:
"""
:param gradient: Gradient array (2d)
:param ratio: Between 0-1
:return: RGB subarray (1d, has 3 values)
"""
if ratio == 1:
return gradient[-1, :]
i = int(ratio * len(gradient))
return gradient[i, :]
class Scale:
colors: ndarray
rgb: ndarray
def __init__(self, scale: list[str], resolution: int = 300):
self.colors = np.array([RGB.from_hex(s) for s in scale])
self.rgb = create_gradient(self.colors, resolution)
def __call__(self, ratio: float) -> RGB:
"""
:param ratio: Between 0-1
"""
return RGB(*get_raw(self.rgb, ratio))
def test_color_scale():
scale = Scale(['#232323', '#4F1879', '#B43A78', '#F98766', '#FCFAC0'])
colors = 100
for i in range(colors + 1):
print(scale(i / colors).to_ansi_rgb(False), end=' ')
+39 -6
View File
@@ -1,10 +1,14 @@
from __future__ import annotations
import colorsys
from typing import NamedTuple
from typing import NamedTuple, Callable, Optional
from typing_extensions import Literal
from .constants import GLOBAL_CFG
AnsiMode = Literal['default', 'ansi', '8bit', 'rgb']
LightDark = Literal['light', 'dark']
MINECRAFT_COLORS = ["&0/\033[0;30m", "&1/\033[0;34m", "&2/\033[0;32m", "&3/\033[0;36m", "&4/\033[0;31m",
@@ -36,7 +40,7 @@ def color(msg: str) -> str:
code = code.replace(',', ' ').replace(';', ' ').replace(' ', ' ')
rgb = tuple(int(c) for c in code.split(' '))
msg = msg[:i] + RGB(*rgb).to_ansi_rgb(foreground=fore) + msg[end + 1:]
msg = msg[:i] + RGB(*rgb).to_ansi(foreground=fore) + msg[end + 1:]
return msg
@@ -49,6 +53,19 @@ def printc(msg: str):
print(color(msg + '&r'))
def clear_screen(title: str = ''):
"""
Clear screen using ANSI escape codes
"""
if not GLOBAL_CFG.debug:
print('\033[2J\033[H', end='')
if title:
print()
printc(title)
print()
def redistribute_rgb(r: int, g: int, b: int) -> tuple[int, int, int]:
"""
Redistribute RGB after lightening
@@ -141,7 +158,9 @@ class RGB(NamedTuple):
"""
raise NotImplementedError()
def to_ansi(self, mode: AnsiMode, foreground: bool = True):
def to_ansi(self, mode: AnsiMode | None = None, foreground: bool = True):
if not mode:
mode = GLOBAL_CFG.color_mode
if mode == 'rgb':
return self.to_ansi_rgb(foreground)
if mode == '8bit':
@@ -158,12 +177,26 @@ class RGB(NamedTuple):
"""
return RGB(*redistribute_rgb(*[v * multiplier for v in self]))
def set_light(self, light: int) -> 'RGB':
def set_light(self, light: float, at_least: bool | None = None, at_most: bool | None = None) -> 'RGB':
"""
Set HSL lightness value
:param light: 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
h, l, s = colorsys.rgb_to_hls(*[v / 255.0 for v in self])
return RGB(*[round(v * 255.0) for v in colorsys.hls_to_rgb(h, light, s)])
# Modify light value
if at_least is None and at_most is None:
l = light
else:
if at_most:
l = min(l, light)
if at_least:
l = max(l, light)
# Convert back to RGB
return RGB(*[round(v * 255.0) for v in colorsys.hls_to_rgb(h, l, s)])
+52
View File
@@ -0,0 +1,52 @@
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from typing_extensions import Literal
CONFIG_PATH = Path.home() / '.config/hyfetch.json'
VERSION = '1.1.0'
# Obtain terminal size
try:
TERM_LEN = os.get_terminal_size().columns
except Exception:
TERM_LEN = 40
TEST_ASCII = r"""
### |\___/| ###
### ) ( ###
## =\ /= ##
#### )===( ####
### / \ ###
### | | ###
## / {txt} \ ##
## \ / ##
_/\_\_ _/_/\_
|##| ( ( |##|
|##| ) ) |##|
|##| (_( |##|""".strip('\n')
TEST_ASCII_WIDTH = max(len(line) for line in TEST_ASCII.split('\n'))
DEFAULT_DARK_L = 0.
@dataclass
class GlobalConfig:
# Global color mode default to 8-bit for compatibility
color_mode: str
override_distro: str | None
debug: bool
is_light: bool
def light_dark(self) -> Literal['light', 'dark']:
return 'light' if self.is_light else 'dark'
def default_lightness(self, term: Literal['light', 'dark'] | None = None) -> float:
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)
+211 -66
View File
@@ -3,27 +3,18 @@ from __future__ import annotations
import argparse
import json
from dataclasses import dataclass
from pathlib import Path
import random
import re
from itertools import permutations
from typing import Iterable
from .color_util import AnsiMode, printc, color
from .neofetch_util import run_neofetch
from .presets import PRESETS, ColorProfile
from .serializer import json_stringify
from hyfetch import presets
CONFIG_PATH = Path.home() / '.config/hyfetch.json'
VERSION = '1.0.7'
@dataclass
class Config:
preset: str
mode: AnsiMode
def save(self):
CONFIG_PATH.parent.mkdir(exist_ok=True, parents=True)
CONFIG_PATH.write_text(json_stringify(self), 'utf-8')
from .color_util import printc, color, clear_screen, LightDark
from .constants import CONFIG_PATH, VERSION, TERM_LEN, TEST_ASCII_WIDTH, TEST_ASCII, GLOBAL_CFG
from .models import Config
from .neofetch_util import run_neofetch, get_distro_ascii, ColorAlignment, ascii_size, color_alignments
from .presets import PRESETS
def check_config() -> Config:
@@ -36,25 +27,30 @@ def check_config() -> Config:
:return: Config object
"""
if CONFIG_PATH.is_file():
return Config(**json.loads(CONFIG_PATH.read_text('utf-8')))
return Config.from_dict(json.loads(CONFIG_PATH.read_text('utf-8')))
return create_config()
def literal_input(prompt: str, options: Iterable[str], default: str) -> str:
def literal_input(prompt: str, options: Iterable[str], default: str, show_ops: bool = True) -> str:
"""
Ask the user to provide an input among a list of options
:param prompt: Input prompt
:param options: Options
:param default: Default option
:param show_ops: Show options
:return: Selection
"""
options = list(options)
lows = [o.lower() for o in options]
op_text = '|'.join([f'&l&n{o}&r' if o == default else o for o in options])
printc(f'{prompt} ({op_text})')
if show_ops:
op_text = '|'.join([f'&l&n{o}&r' if o == default else o for o in options])
printc(f'{prompt} ({op_text})')
else:
printc(f'{prompt} (default: {default})')
selection = input('> ') or default
while not selection.lower() in lows:
print(f'Invalid selection! {selection} is not one of {"|".join(options)}')
@@ -64,65 +60,180 @@ def literal_input(prompt: str, options: Iterable[str], default: str) -> str:
return options[lows.index(selection)]
def center_text(txt: str, spaces: int) -> str:
"""
Put the text in the center in a defined space
>>> center_text('meow', 9)
' meow '
:param txt: Text
:param spaces: Total space of the text
:return: Text with length spaces
"""
spaces -= len(txt)
if spaces % 2 == 1:
spaces -= 1
txt += ' '
while spaces > 0:
spaces -= 2
txt = f' {txt} '
return txt
def create_config() -> Config:
"""
Create config interactively
:return: Config object (automatically stored)
"""
# Select color system
# TODO: Demo of each color system
color_system = literal_input('Which &acolor &bsystem &rdo you want to use?',
['8bit', 'rgb'], 'rgb')
title = 'Welcome to &b&lhy&f&lfetch&r! Let\'s set up some colors first.'
clear_screen(title)
# Print preset
print('Available presets:\n')
spacing = max(max(len(k) for k in PRESETS.keys()), 30)
##############################
# 1. Select color system
try:
# Demonstrate RGB with a gradient. This requires numpy
from .color_scale import Scale
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()
printc(f'&a1. Which &bcolor system &ado you want to use?')
printc(f'(If you can\'t see colors under "RGB Color Testing", please choose 8bit)')
print()
color_system = literal_input('Your choice?', ['8bit', 'rgb'], 'rgb')
except ModuleNotFoundError:
# Numpy not found, skip gradient test, use fallback
color_system = literal_input('Which &acolor &bsystem &rdo you want to use?',
['8bit', 'rgb'], 'rgb')
# Override global color mode
GLOBAL_CFG.color_mode = color_system
title += f'\n&e1. Selected color mode: &r{color_system}'
##############################
# 2. Select light/dark mode
clear_screen(title)
light_dark = literal_input(f'2. Is your terminal in &gf(#85e7e9)light mode&r or &gf(#c471ed)dark mode&r?',
['light', 'dark'], 'dark')
is_light = light_dark == 'light'
GLOBAL_CFG.is_light = is_light
title += f'\n&e2. Light/Dark: &r{light_dark}'
##############################
# 3. Choose preset
clear_screen(title)
printc('&a3. Let\'s choose a flag!')
printc('Available flag presets:')
print()
# Create flags = [[lines]]
flags = []
spacing = max(max(len(k) for k in PRESETS.keys()), 20)
for name, preset in PRESETS.items():
flags.append([preset.color_text(' ' * spacing, foreground=False),
'&0' + preset.color_text(center_text(name, spacing), foreground=False),
preset.color_text(' ' * spacing, foreground=False)])
flags_per_row = 3
flag = preset.color_text(' ' * spacing, foreground=False)
flags.append([name.center(spacing), flag, flag, flag])
# Calculate flags per row
flags_per_row = TERM_LEN // (spacing + 2)
while flags:
current = flags[:flags_per_row]
flags = flags[flags_per_row:]
for line in range(len(current[0])):
printc(' '.join(flag[line] for flag in current))
# Print by row
[printc(' '.join(line)) for line in zip(*current)]
print()
print()
tmp = PRESETS['rainbow'].color_text('preset')
preset = literal_input(f'Which {tmp} do you want to use?', PRESETS.keys(), 'rainbow')
tmp = PRESETS['rainbow'].set_light_dl_def(light_dark).color_text('preset')
preset = literal_input(f'Which {tmp} do you want to use?', PRESETS.keys(), 'rainbow', show_ops=False)
_prs = PRESETS[preset]
title += f'\n&e3. Selected flag: &r{_prs.color_text(preset)}'
#############################
# 4. Dim/lighten colors
clear_screen(title)
printc(f'&a4. 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_LEN // (TEST_ASCII_WIDTH + 2)
ratios = [col / (num_cols - 1) for col in range(num_cols)]
ratios = [(r * 0.4 + 0.1) if is_light else (r * 0.4 + 0.5) 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)]
while True:
print()
printc(f'Which brightness level look the best? (Default: left blank = {GLOBAL_CFG.default_lightness(light_dark):.2f} for {light_dark} mode)')
lightness = input('> ').strip().lower() or None
# Parse lightness
if not lightness or lightness in ['unset', 'none']:
lightness = None
break
try:
lightness = int(lightness[:-1]) / 100 if lightness.endswith('%') else float(lightness)
assert 0 <= lightness <= 1
break
except Exception:
printc('&cUnable to parse lightness value, please input it as a decimal or percentage (e.g. 0.5 or 50%)')
if lightness:
_prs = _prs.set_light_dl(lightness, light_dark)
title += f'\n&e4. Brightness: &r{f"{lightness:.2f}" if lightness else "unset"}'
#############################
# 5. Color arrangement
color_alignment = None
while True:
clear_screen(title)
printc(f'&a5. Let\'s choose a color arrangement!')
printc(f'You can choose standard horizontal or vertical alignment, or use one of the random color schemes, or assign colors yourself (TODO).')
print()
asc = get_distro_ascii()
asc_width = ascii_size(asc)[0]
asciis = [
[*ColorAlignment('horizontal').recolor_ascii(asc, _prs).split('\n'), 'Horizontal'.center(asc_width)],
[*ColorAlignment('vertical').recolor_ascii(asc, _prs).split('\n'), 'Vertical'.center(asc_width)],
]
ascii_per_row = TERM_LEN // (asc_width + 2)
# Random color schemes
# ascii_indices =
pis = list(range(len(_prs.unique_colors().colors)))
while len(pis) < len(set(re.findall('(?<=\\${c)[0-9](?=})', asc))):
pis += pis
perm = list(permutations(pis))
random_count = ascii_per_row * 2 - 2
choices = random.sample(perm, random_count)
choices = [{i: n for i, n in enumerate(c)} for c in choices]
asciis += [[*ColorAlignment('custom', r).recolor_ascii(asc, _prs).split('\n'), f'random{i}'.center(asc_width)]
for i, r in enumerate(choices)]
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('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':
continue
if choice in ['horizontal', 'vertical']:
color_alignment = ColorAlignment(choice)
elif choice.startswith('random'):
color_alignment = ColorAlignment('custom', choices[int(choice[6])])
else:
raise NotImplementedError()
break
title += f'\n&e5. Color Alignment: &r{color_alignment}'
# Create config
c = Config(preset, color_system)
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()
@@ -132,7 +243,7 @@ def create_config() -> Config:
def run():
# Create CLI
hyfetch = color('&b&lhy&f&lfetch&r')
hyfetch = color('&b&lhyfetch&r')
parser = argparse.ArgumentParser(description=color(f'{hyfetch} - neofetch with flags <3'))
parser.add_argument('-c', '--config', action='store_true', help=color(f'Configure {hyfetch}'))
@@ -141,6 +252,10 @@ def run():
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=color(f'Debug mode'))
parser.add_argument('--debug-list', help=color(f'Debug recommendations'))
parser.add_argument('--test-distro', help=color(f'Test for a specific distro'))
parser.add_argument('--test-print', action='store_true', help=color(f'Test print distro ascii art only'))
args = parser.parse_args()
@@ -148,6 +263,18 @@ def run():
print(f'Version is {VERSION}')
return
# Test distro ascii art
if args.test_distro:
print(f'Setting distro to {args.test_distro}')
GLOBAL_CFG.override_distro = args.test_distro
if args.debug:
GLOBAL_CFG.debug = True
if args.test_print:
print(get_distro_ascii())
return
# Load config
config = check_config()
@@ -161,13 +288,31 @@ def run():
if args.mode:
config.mode = args.mode
# 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
if args.scale:
preset = ColorProfile([c.lighten(args.scale) for c in preset.colors])
preset = preset.lighten(args.scale)
if args.light:
preset = ColorProfile([c.set_light(args.light) for c in preset.colors])
preset = preset.set_light_raw(args.light)
if config.lightness:
preset = preset.set_light_dl(config.lightness)
# Debug recommendations
if args.debug_list:
distro = args.debug_list
ca = color_alignments[distro]
print(distro)
GLOBAL_CFG.override_distro = distro
asciis = [ca.recolor_ascii(get_distro_ascii(distro), p).split('\n') for p in list(PRESETS.values())[:3]]
[printc(' '.join(line)) for line in zip(*asciis)]
return
# Run
run_neofetch(preset, config.mode)
run_neofetch(preset, config.color_align)
+28
View File
@@ -0,0 +1,28 @@
from __future__ import annotations
from dataclasses import dataclass
from typing_extensions import Literal
from .color_util import AnsiMode, LightDark
from .constants import CONFIG_PATH
from .neofetch_util import ColorAlignment
from .serializer import json_stringify, from_dict
@dataclass
class Config:
preset: str
mode: AnsiMode
light_dark: LightDark = 'dark'
lightness: float | None = None
color_align: ColorAlignment = ColorAlignment('horizontal')
@classmethod
def from_dict(cls, d: dict):
d['color_align'] = ColorAlignment.from_dict(d['color_align'])
return from_dict(cls, d)
def save(self):
CONFIG_PATH.parent.mkdir(exist_ok=True, parents=True)
CONFIG_PATH.write_text(json_stringify(self), 'utf-8')
+153 -17
View File
@@ -1,15 +1,124 @@
from __future__ import annotations
import inspect
import os
import platform
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
from subprocess import check_output
from tempfile import TemporaryDirectory
import pkg_resources
from typing_extensions import Literal
from .color_util import AnsiMode
from hyfetch.color_util import color
from .constants import GLOBAL_CFG
from .presets import ColorProfile
from .serializer import from_dict
RE_NEOFETCH_COLOR = re.compile('\\${c[0-9]}')
def ascii_size(asc: str) -> tuple[int, int]:
"""
Get distro ascii width, height ignoring color code
:param asc: Distro ascii
:return: Width, Height
"""
return max(len(line) for line in re.sub(RE_NEOFETCH_COLOR, '', asc).split('\n')), len(asc.split('\n'))
def normalize_ascii(asc: str) -> str:
"""
Make sure every line are the same width
"""
w = ascii_size(asc)[0]
return '\n'.join(line + ' ' * (w - ascii_size(line)[0]) for line in asc.split('\n'))
def fill_starting(asc: str) -> str:
"""
Fill the missing starting placeholders.
E.g. "${c1}...\n..." -> "${c1}...\n${c1}..."
"""
new = []
last = ''
for line in asc.split('\n'):
new.append(last + line)
# Line has color placeholders
matches = RE_NEOFETCH_COLOR.findall(line)
if len(matches) > 0:
# Get the last placeholder for the next line
last = matches[-1]
return '\n'.join(new)
@dataclass
class ColorAlignment:
mode: Literal['horizontal', 'vertical', 'custom']
# 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('&r') 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('&r') for i, l in enumerate(lines)])
else:
asc = '\n'.join(preset.color_text(line) + color('&r') for line in lines)
else:
preset = preset.unique_colors()
# 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)
return asc
def get_command_path() -> str:
@@ -21,26 +130,30 @@ def get_command_path() -> str:
return pkg_resources.resource_filename(__name__, 'scripts/neofetch_mod.sh')
def get_distro_ascii() -> str:
def get_distro_ascii(distro: str | None = None) -> str:
"""
Get the distro ascii
Get the distro ascii of the current distro. Or if distro is specified, get the specific distro's
ascii art instead.
:return: Distro ascii
"""
return check_output([get_command_path(), "print_ascii"]).decode().strip()
if not distro and GLOBAL_CFG.override_distro:
distro = GLOBAL_CFG.override_distro
if GLOBAL_CFG.debug:
print(distro)
print(GLOBAL_CFG)
cmd = 'print_ascii'
if distro:
os.environ['CUSTOM_DISTRO'] = distro
cmd = 'print_custom_ascii'
return normalize_ascii(check_output([get_command_path(), cmd]).decode().strip())
def run_neofetch(preset: ColorProfile, mode: AnsiMode):
# Get existing ascii
def run_neofetch(preset: ColorProfile, alignment: ColorAlignment):
asc = get_distro_ascii()
# Remove existing colors
asc = re.sub('\\${.*?}', '', asc)
# Add new colors
lines = asc.split('\n')
colors = preset.with_length(len(lines))
asc = '\n'.join([colors[i].to_ansi(mode) + l for i, l in enumerate(lines)])
w, h = ascii_size(asc)
asc = alignment.recolor_ascii(asc, preset)
# Write temp file
with TemporaryDirectory() as tmp_dir:
@@ -49,6 +162,29 @@ def run_neofetch(preset: ColorProfile, mode: AnsiMode):
path.write_text(asc)
# Call neofetch with the temp file
os.environ['ascii_len'] = str(max(len(l) for l in lines))
os.environ['ascii_lines'] = str(len(lines))
os.system(get_command_path() + f' --ascii --source {path.absolute()} --ascii-colors')
os.environ['ascii_len'] = str(w)
os.environ['ascii_lines'] = str(h)
if platform.system() != 'Windows':
os.system(f'{get_command_path()} --ascii --source {path.absolute()} --ascii-colors')
if platform.system() == 'Windows':
cmd = get_command_path().replace("\\", "/").replace("C:/", "/c/")
path_str = str(path.absolute()).replace('\\', '/').replace('C:/', '/c/')
cmd = f'ascii_len={w} ascii_lines={h} {cmd} --ascii --source {path_str} --ascii-colors'
full_cmd = ['C:\\Program Files\\Git\\bin\\bash.exe', '-c', cmd]
# print(full_cmd)
subprocess.run(full_cmd)
# Color alignment recommendations
color_alignments = {
'fedora': ColorAlignment('horizontal', fore_back=(2, 1)),
'ubuntu': ColorAlignment('horizontal', fore_back=(2, 1)),
'NixOS.*': ColorAlignment('custom', {1: 1, 2: 0}),
# 'arch': ColorAlignment('horizontal'),
# 'centos': ColorAlignment('horizontal'),
}
+91 -7
View File
@@ -1,8 +1,20 @@
from __future__ import annotations
from typing import Iterable
from typing_extensions import Literal
from .color_util import RGB
from .color_util import RGB, LightDark
from .constants import GLOBAL_CFG
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:
@@ -17,7 +29,6 @@ class ColorProfile:
else:
self.colors = colors
def with_weights(self, weights: list[int]) -> list[RGB]:
"""
Map colors based on weights
@@ -76,11 +87,59 @@ class ColorProfile:
result += '\033[0m'
result += t
else:
result += colors[i].to_ansi_rgb(foreground) + t
result += colors[i].to_ansi(foreground=foreground) + t
result += '\033[0m'
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 = GLOBAL_CFG.light_dark()):
"""
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)
"""
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 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',
@@ -98,12 +157,14 @@ PRESETS: dict[str, ColorProfile] = {
'#F6AAB7',
'#55CDFD'
]),
'nonbinary': ColorProfile([
'#FCF431',
'#FCFCFC',
'#9D59D2',
'#282828'
]),
'agender': ColorProfile([
'#000000',
'#BABABA',
@@ -113,11 +174,13 @@ PRESETS: dict[str, ColorProfile] = {
'#BABABA',
'#000000'
]),
'queer': ColorProfile([
'#B57FDD',
'#FFFFFF',
'#49821E'
]),
'genderfluid': ColorProfile([
'#FE76A2',
'#FFFFFF',
@@ -131,11 +194,13 @@ PRESETS: dict[str, ColorProfile] = {
'#9B4F96',
'#0038A8'
]),
'pansexual': ColorProfile([
'#FF1C8D',
'#FFD700',
'#1AB3FF'
]),
'lesbian': ColorProfile([
'#D62800',
'#FF9B56',
@@ -143,12 +208,14 @@ PRESETS: dict[str, ColorProfile] = {
'#D462A6',
'#A40062'
]),
'asexual': ColorProfile([
'#000000',
'#A4A4A4',
'#FFFFFF',
'#810081'
]),
'aromantic': ColorProfile([
'#3BA740',
'#A8D47A',
@@ -156,6 +223,7 @@ PRESETS: dict[str, ColorProfile] = {
'#ABABAB',
'#000000'
]),
# below sourced from https://www.flagcolorcodes.com/flags/pride
# goto f"https://www.flagcolorcodes.com/{preset}" for info
# todo: sane sorting
@@ -163,6 +231,7 @@ PRESETS: dict[str, ColorProfile] = {
'#99D9EA',
'#7F7F7F'
]),
'intergender': ColorProfile([
# todo: use weighted spacing
'#900DC2',
@@ -171,6 +240,7 @@ PRESETS: dict[str, ColorProfile] = {
'#900DC2',
'#900DC2',
]),
'greygender': ColorProfile([
'#B3B3B3',
'#B3B3B3',
@@ -181,6 +251,7 @@ PRESETS: dict[str, ColorProfile] = {
'#535353',
'#535353',
]),
'akiosexual': ColorProfile([
'#F9485E',
'#FEA06A',
@@ -188,6 +259,7 @@ PRESETS: dict[str, ColorProfile] = {
'#FFFFFF',
'#000000',
]),
'transmasculine': ColorProfile([
'#FF8ABD',
'#CDF5FE',
@@ -197,6 +269,7 @@ PRESETS: dict[str, ColorProfile] = {
'#CDF5FE',
'#FF8ABD',
]),
'demifaun': ColorProfile([
'#7F7F7F',
'#7F7F7F',
@@ -212,25 +285,29 @@ PRESETS: dict[str, ColorProfile] = {
'#7F7F7F',
'#7F7F7F',
]),
'neutrois': ColorProfile([
'#FFFFFF',
'#1F9F00',
'#000000'
]),
'biromantic alt 2': ColorProfile([
'biromantic1': ColorProfile([
'#8869A5',
'#D8A7D8',
'#FFFFFF',
'#FDB18D',
'#151638',
]),
'biromantic alt 2': ColorProfile([
'biromantic2': ColorProfile([
'#740194',
'#AEB1AA',
'#FFFFFF',
'#AEB1AA',
'#740194',
]),
'autoromantic': ColorProfile([ # symbol interpreted
'#99D9EA',
'#99D9EA',
@@ -247,8 +324,9 @@ PRESETS: dict[str, ColorProfile] = {
'#7F7F7F',
'#7F7F7F',
]),
# i didn't expect this one to work. cool!
'boyflux alt 2': ColorProfile([
'boyflux2': ColorProfile([
'#E48AE4',
'#9A81B4',
'#55BFAB',
@@ -281,5 +359,11 @@ PRESETS: dict[str, ColorProfile] = {
'#5276D4',
]),
'beiyang': ColorProfile([
'#DF1B12',
'#FFC600',
'#01639D',
'#FFFFFF',
'#000000',
]),
}
+5
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import dataclasses
import inspect
import json
from datetime import datetime, date
@@ -41,3 +42,7 @@ def json_stringify(obj: object, indent: int | None = None) -> str:
:return: Json strings
"""
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})
+31 -2
View File
@@ -1677,8 +1677,12 @@ get_packages() {
has scoop && pkgs_h=1 dir ~/scoop/apps/* && ((packages-=1))
# Count chocolatey packages.
[[ -d /cygdrive/c/ProgramData/chocolatey/lib ]] && \
dir /cygdrive/c/ProgramData/chocolatey/lib/*
# [[ -d /c/ProgramData/chocolatey/lib ]] && \
# dir /c/ProgramData/chocolatey/lib/*
has choco && tot choco list --localonly
# Count winget
has winget && tot winget list
;;
Haiku)
@@ -11594,6 +11598,11 @@ main() {
return 0
}
get_ascii_distro_name() {
get_distro
echo "$ascii_distro"
}
get_print_ascii() {
cache_uname
get_os
@@ -11603,4 +11612,24 @@ get_print_ascii() {
echo "$ascii_data"
}
get_print_custom_ascii() {
distro="$CUSTOM_DISTRO"
ascii_distro=$distro
get_distro_ascii
echo "$ascii_data"
}
get_test_distro_ascii() {
# Load default config.
eval "$config"
distro="$CUSTOM_DISTRO"
ascii_distro=$distro
get_bold
get_distro_ascii
image_backend
dynamic_prompt
}
main "$@"
+2
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env python3
import hyfetch
if __name__ == '__main__':
+1 -1
View File
@@ -32,7 +32,7 @@ setup(
packages=['hyfetch'],
package_data={'hyfetch': ['hyfetch/*']},
include_package_data=True,
install_requires=['setuptools', 'typing_extensions'],
install_requires=['setuptools', 'typing_extensions', 'numpy'],
entry_points={
"console_scripts": [
"hyfetch=hyfetch.main:run",
+2
View File
@@ -1,3 +1,4 @@
from hyfetch.color_scale import test_color_scale
from hyfetch.color_util import RGB, printc
from hyfetch.neofetch_util import get_command_path, run_neofetch
from hyfetch.presets import PRESETS
@@ -35,3 +36,4 @@ def test_rgb_8bit_conversion():
if __name__ == '__main__':
test_rgb_8bit_conversion()
test_color_scale()