diff --git a/crates/hyfetch/src/ascii.rs b/crates/hyfetch/src/ascii.rs index 4f86a384..6252d2b5 100644 --- a/crates/hyfetch/src/ascii.rs +++ b/crates/hyfetch/src/ascii.rs @@ -4,12 +4,12 @@ use std::ops::Range; use aho_corasick::AhoCorasick; use anyhow::{Context as _, Result}; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use tracing::debug; use unicode_segmentation::UnicodeSegmentation; use crate::color_util::{ - color, ForegroundBackground, NeofetchAsciiIndexedColor, ToAnsiString as _, + color, ForegroundBackground, NeofetchAsciiIndexedColor, PresetIndexedColor, ToAnsiString as _, }; use crate::neofetch_util::{ ascii_size, ColorAlignment, NEOFETCH_COLORS_AC, NEOFETCH_COLOR_PATTERNS, @@ -68,6 +68,53 @@ impl RawAsciiArt { } } +/// Generates a random color alignment for the given ascii art. +pub fn get_random_alignment(preset: &ColorProfile, asc: &NormalizedAsciiArt) -> ColorAlignment { + let slots: IndexSet = { + let asc = asc.lines.join("\n"); + let ac = + NEOFETCH_COLORS_AC.get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap()); + ac.find_iter(&asc) + .map(|m| { + let ai_start = m.start().checked_add(3).unwrap(); + let ai_end = m.end().checked_sub(1).unwrap(); + asc[ai_start..ai_end] + .parse() + .expect("neofetch ascii color index should not be invalid") + }) + .collect() + }; + + if slots.is_empty() { + return ColorAlignment::Custom { + colors: IndexMap::new(), + }; + } + + let num_colors = preset.unique_colors().colors.len(); + if num_colors == 0 { + return ColorAlignment::Custom { + colors: IndexMap::new(), + }; + } + + let mut preset_indices: Vec = + (0..num_colors).map(|x| (x as u8).into()).collect(); + while preset_indices.len() < slots.len() { + preset_indices.extend((0..num_colors).map(|x| PresetIndexedColor::from(x as u8))); + } + + let mut rng = fastrand::Rng::new(); + rng.shuffle(&mut preset_indices); + + let colors = slots + .into_iter() + .zip(preset_indices) + .collect(); + + ColorAlignment::Custom { colors } +} + impl NormalizedAsciiArt { /// Uses a color alignment to recolor the ascii art. #[tracing::instrument(level = "debug", skip(self), fields(self.w = self.w, self.h = self.h))] @@ -91,6 +138,10 @@ impl NormalizedAsciiArt { let reset = color("&~&*", color_mode).expect("color reset should not be invalid"); let lines = match (color_align, self) { + (ColorAlignment::Random, _) => { + let resolved = get_random_alignment(color_profile, self); + return self.to_recolored(&resolved, color_profile, color_mode, theme); + }, (ColorAlignment::Horizontal, Self { fg, .. }) => { let Self { lines, .. } = self .fill_starting() @@ -394,3 +445,4 @@ impl NormalizedAsciiArt { }) } } + diff --git a/crates/hyfetch/src/bin/hyfetch.rs b/crates/hyfetch/src/bin/hyfetch.rs index 75e2380b..a8d36741 100644 --- a/crates/hyfetch/src/bin/hyfetch.rs +++ b/crates/hyfetch/src/bin/hyfetch.rs @@ -49,6 +49,8 @@ use terminal_size::{terminal_size, Height, Width}; use time::{Month, OffsetDateTime}; use tracing::debug; +pub use hyfetch::ascii::get_random_alignment; + fn main() -> Result<()> { add_pkg_path().expect("failed to add pkg path"); @@ -245,7 +247,11 @@ fn main() -> Result<()> { get_distro_ascii(distro, backend).context("failed to get distro ascii")? }; let asc = asc.to_normalized().context("failed to normalize ascii")?; - let color_align = config.color_align; + let color_align = if options.random_alignment || config.color_align == ColorAlignment::Random { + get_random_alignment(&color_profile, &asc) + } else { + config.color_align + }; let asc = asc .to_recolored(&color_align, &color_profile, color_mode, theme) .context("failed to recolor ascii")?; @@ -731,7 +737,7 @@ fn create_config( let filtered = filter_flag_indices(part, &flags); filtered.first().map(|&idx| flags[idx].0.as_ref().to_owned()) }); - + if let Some(p) = selection { resolved_presets.push(p); } else { @@ -1178,8 +1184,8 @@ fn create_config( } print_title_prompt(option_counter, "Let's choose a color arrangement!"); - println!("You can choose standard horizontal or vertical alignment, or use one of the random color schemes.\nYou can type \"roll\" to randomize again.\n"); - let mut opts: Vec> = ["horizontal", "vertical", "roll"].map(Into::into).into(); + println!("You can choose standard horizontal or vertical alignment, or use one of the random color schemes. You can type \"roll\" to randomize again. Additionally, 'truerandom' will generate a fresh random color scheme every time hyfetch runs.\n"); + let mut opts: Vec> = ["horizontal", "vertical", "roll", "truerandom"].map(Into::into).into(); opts.extend((0..random_count).map(|i| format!("random{i}").into())); let choice = literal_input("Your choice?", &opts[..], "horizontal", true, color_mode) .context("failed to ask for choice input") @@ -1193,6 +1199,7 @@ fn create_config( // Save choice color_align = if choice == "horizontal" { ColorAlignment::Horizontal } else if choice == "vertical" { ColorAlignment::Vertical } + else if choice == "truerandom" { ColorAlignment::Random } else { arrangements.into_iter() .find_map(|(k, ca)| { diff --git a/crates/hyfetch/src/cli_options.rs b/crates/hyfetch/src/cli_options.rs index 572d630e..13f1d2fa 100644 --- a/crates/hyfetch/src/cli_options.rs +++ b/crates/hyfetch/src/cli_options.rs @@ -36,6 +36,7 @@ pub struct Options { pub palette_glyph: Option, #[cfg(feature = "macchina")] pub palette_type: Option, + pub random_alignment: bool, } pub fn options() -> OptionParser { @@ -156,6 +157,7 @@ BACKEND={{{backends}}}", .help("Enables hyfetch to detect light/dark terminal background in runtime") .argument("BOOL") .optional(); + let random_alignment = long("random-alignment").help("Use a fresh random color alignment every time, independent of config").short('r').switch(); #[cfg(feature = "macchina")] let palette_glyph = long("palette-glyph") @@ -188,7 +190,8 @@ BACKEND={{{backends}}}", ask_exit, auto_detect_light_dark, palette_glyph, - palette_type + palette_type, + random_alignment }) .to_options() .header( @@ -218,7 +221,8 @@ BACKEND={{{backends}}}", // hidden test_print, ask_exit, - auto_detect_light_dark + auto_detect_light_dark, + random_alignment }) .to_options() .header( diff --git a/crates/hyfetch/src/neofetch_util.rs b/crates/hyfetch/src/neofetch_util.rs index 09ba7b0b..5af676f4 100644 --- a/crates/hyfetch/src/neofetch_util.rs +++ b/crates/hyfetch/src/neofetch_util.rs @@ -73,6 +73,7 @@ pub enum ColorAlignment { #[serde(deserialize_with = "crate::utils::index_map_serde::deserialize")] colors: IndexMap, }, + Random, } /// Asks the user to provide an input among a list of options. diff --git a/hyfetch/main.py b/hyfetch/main.py index cff94c10..20adda9d 100755 --- a/hyfetch/main.py +++ b/hyfetch/main.py @@ -22,6 +22,7 @@ from .neofetch_util import * from .presets import PRESETS, ColorProfile + def check_config(path) -> Config: """ Check if the configuration exists. Return the config object if it exists. If not, call the @@ -316,7 +317,7 @@ def create_config() -> Config: # later iterations hv_arrangements = [ ('Horizontal', ColorAlignment('horizontal', fore_back=fore_back)), - ('Vertical', ColorAlignment('vertical')) + ('Vertical', ColorAlignment('vertical')), ] arrangements = hv_arrangements.copy() @@ -348,17 +349,16 @@ def create_config() -> Config: 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.') + printc(f'You can choose standard horizontal or vertical alignment, or use one of the random color schemes. You can type "roll" to randomize again. Additionally, \'truerandom\' will generate a fresh random color scheme every time hyfetch runs.') print() - choice = literal_input(f'Your choice?', ['horizontal', 'vertical', 'roll'] + [f'random{i}' for i in range(random_count)], 'horizontal') + choice = literal_input(f'Your choice?', ['horizontal', 'vertical', 'roll', 'truerandom'] + [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} + arrangement_index = {k.lower(): ca for k, ca in hv_arrangements + arrangements + [("TrueRandom", ColorAlignment('random'))]} if choice in arrangement_index: color_alignment = arrangement_index[choice] else: @@ -466,6 +466,7 @@ def create_parser() -> argparse.ArgumentParser: 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('-r', '--random-alignment', action='store_true', help=color(f'Use a fresh random color alignment every time, independent of config')) 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') @@ -612,7 +613,8 @@ def run(): asc = Path(args.ascii_file).read_text("utf-8") else: asc = get_distro_ascii() - asc = config.color_align.recolor_ascii(asc, preset) + color_align: ColorAlignment = get_random_alignment(preset, asc) if (args.random_alignment or config.color_align.mode == "random") else config.color_align + asc = color_align.recolor_ascii(asc, preset) neofetch_util.run(asc, config.backend, config.args or '') except Exception as e: print(f'Error: {e}') diff --git a/hyfetch/neofetch_util.py b/hyfetch/neofetch_util.py index 4a9e148b..cc2548d8 100644 --- a/hyfetch/neofetch_util.py +++ b/hyfetch/neofetch_util.py @@ -116,6 +116,28 @@ def fill_starting(asc: str) -> str: return '\n'.join(new) +def get_random_alignment(preset: ColorProfile, asc: str) -> ColorAlignment: + """ + Generate a fresh random color alignment for the given ascii art. + """ + slots = sorted(set(map(int, re.findall(r'(?<=\${c)[0-9](?=})', asc)))) + if not slots: + return ColorAlignment("custom", custom_colors={}) + + colors = preset.unique_colors().colors + if not colors: + return ColorAlignment("custom", custom_colors={}) + + import random + pis = list(range(len(colors))) + while len(pis) < len(slots): + pis.extend(range(len(colors))) + + random.shuffle(pis) + choice = {slot: pis[i] for i, slot in enumerate(slots)} + return ColorAlignment("custom", custom_colors=choice) + + @dataclass class ColorAlignment: mode: ColorAlignMode @@ -145,6 +167,9 @@ class ColorAlignment: :return Colored ascii, Uncolored lines """ + if self.mode == 'random': + return get_random_alignment(preset, asc).recolor_ascii(asc, preset) + asc = fill_starting(asc) if self.fore_back and self.mode in ['horizontal', 'vertical']: diff --git a/hyfetch/types.py b/hyfetch/types.py index 2e6287db..72b2383d 100644 --- a/hyfetch/types.py +++ b/hyfetch/types.py @@ -6,5 +6,5 @@ except ImportError: AnsiMode = Literal['default', 'ansi', '8bit', 'rgb'] LightDark = Literal['light', 'dark'] BackendLiteral = Literal["neofetch", "fastfetch"] -ColorAlignMode = Literal['horizontal', 'vertical', 'custom'] +ColorAlignMode = Literal['horizontal', 'vertical', 'custom', 'random'] ColorSpacing = Literal['equal', 'weighted']