[+] Add option to generate random alignment every execution (#550)
Some checks failed
Shellcheck / check (push) Has been cancelled

* ft: random alignment python

* ft: random alignment rust

* fx: printlns

* [O] Optimize random selection & wording

---------

Co-authored-by: djairoh <djairoh@mykolab.com>
Co-authored-by: Azalea <noreply@aza.moe>
This commit is contained in:
Djai
2026-10-11 19:58:35 +00:00
committed by GitHub
parent 66f2d121f6
commit 9287823968
7 changed files with 106 additions and 15 deletions

View File

@@ -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<NeofetchAsciiIndexedColor> = {
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<PresetIndexedColor> =
(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 {
})
}
}

View File

@@ -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<Cow<str>> = ["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<Cow<str>> = ["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)| {

View File

@@ -36,6 +36,7 @@ pub struct Options {
pub palette_glyph: Option<String>,
#[cfg(feature = "macchina")]
pub palette_type: Option<String>,
pub random_alignment: bool,
}
pub fn options() -> OptionParser<Options> {
@@ -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(

View File

@@ -73,6 +73,7 @@ pub enum ColorAlignment {
#[serde(deserialize_with = "crate::utils::index_map_serde::deserialize")]
colors: IndexMap<NeofetchAsciiIndexedColor, PresetIndexedColor>,
},
Random,
}
/// Asks the user to provide an input among a list of options.

View File

@@ -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}')

View File

@@ -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']:

View File

@@ -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']