Track only fg color indices in ascii art; the rest are bg colors

* Fix vertical fore-back coloring when there are non-ASCII chars
* Allow 0-width / 0-height ascii art (again)

Co-authored-by: Luna <contact@luna.computer>
This commit is contained in:
Teoh Han Hui 2024-07-28 02:35:32 +08:00
parent 52844b55ad
commit bcdc720d8a
No known key found for this signature in database
GPG key ID: D43E2BABAF97DCAE
4 changed files with 261 additions and 296 deletions

View file

@ -1,11 +1,12 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt::Write as _; use std::fmt::Write as _;
use std::num::NonZeroU8; use std::ops::Range;
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use itertools::Itertools as _; use indexmap::IndexMap;
use tracing::debug; use tracing::debug;
use unicode_segmentation::UnicodeSegmentation;
use crate::color_util::{ use crate::color_util::{
color, ForegroundBackground, NeofetchAsciiIndexedColor, ToAnsiString as _, color, ForegroundBackground, NeofetchAsciiIndexedColor, ToAnsiString as _,
@ -21,25 +22,23 @@ use crate::types::{AnsiMode, TerminalTheme};
pub struct RawAsciiArt { pub struct RawAsciiArt {
pub asc: String, pub asc: String,
pub fg: Vec<NeofetchAsciiIndexedColor>, pub fg: Vec<NeofetchAsciiIndexedColor>,
pub bg: Vec<NeofetchAsciiIndexedColor>,
} }
/// Normalized ascii art where every line has the same width. /// Normalized ascii art where every line has the same width.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct NormalizedAsciiArt { pub struct NormalizedAsciiArt {
pub lines: Vec<String>, pub lines: Vec<String>,
pub w: NonZeroU8, pub w: u8,
pub h: NonZeroU8, pub h: u8,
pub fg: Vec<NeofetchAsciiIndexedColor>, pub fg: Vec<NeofetchAsciiIndexedColor>,
pub bg: Vec<NeofetchAsciiIndexedColor>,
} }
/// Recolored ascii art with all color codes replaced. /// Recolored ascii art with all color codes replaced.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RecoloredAsciiArt { pub struct RecoloredAsciiArt {
pub lines: Vec<String>, pub lines: Vec<String>,
pub w: NonZeroU8, pub w: u8,
pub h: NonZeroU8, pub h: u8,
} }
impl RawAsciiArt { impl RawAsciiArt {
@ -55,7 +54,7 @@ impl RawAsciiArt {
.lines() .lines()
.map(|line| { .map(|line| {
let (line_w, _) = ascii_size(line).unwrap(); let (line_w, _) = ascii_size(line).unwrap();
let pad = " ".repeat(usize::from(w.get().checked_sub(line_w.get()).unwrap())); let pad = " ".repeat(usize::from(w.checked_sub(line_w).unwrap()));
format!("{line}{pad}") format!("{line}{pad}")
}) })
.collect(); .collect();
@ -65,7 +64,6 @@ impl RawAsciiArt {
w, w,
h, h,
fg: self.fg.clone(), fg: self.fg.clone(),
bg: self.bg.clone(),
}) })
} }
} }
@ -82,12 +80,18 @@ impl NormalizedAsciiArt {
) -> Result<RecoloredAsciiArt> { ) -> Result<RecoloredAsciiArt> {
debug!("recolor ascii"); debug!("recolor ascii");
if self.lines.is_empty() {
return Ok(RecoloredAsciiArt {
lines: self.lines.clone(),
w: 0,
h: 0,
});
}
let reset = color("&~&*", color_mode).expect("color reset should not be invalid"); let reset = color("&~&*", color_mode).expect("color reset should not be invalid");
let lines = match (color_align, self) { let lines = match (color_align, self) {
(ColorAlignment::Horizontal, Self { fg, bg, .. }) (ColorAlignment::Horizontal, Self { fg, .. }) => {
if !fg.is_empty() || !bg.is_empty() =>
{
let Self { lines, .. } = self let Self { lines, .. } = self
.fill_starting() .fill_starting()
.context("failed to fill in starting neofetch color codes")?; .context("failed to fill in starting neofetch color codes")?;
@ -117,156 +121,160 @@ impl NormalizedAsciiArt {
// Add new colors // Add new colors
let lines = { let lines = {
let ColorProfile { colors } = let ColorProfile { colors } = color_profile
color_profile.with_length(self.h).with_context(|| { .with_length(self.h.try_into().expect("`h` should not be 0"))
.with_context(|| {
format!("failed to spread color profile to length {h}", h = self.h) format!("failed to spread color profile to length {h}", h = self.h)
})?; })?;
lines.enumerate().map(move |(i, line)| { lines.enumerate().map(move |(i, line)| {
let mut replacements = NEOFETCH_COLOR_PATTERNS; let bg_color =
let bg_color = colors[i].to_ansi_string(color_mode, { colors[i].to_ansi_string(color_mode, ForegroundBackground::Foreground);
// This is "background" in the ascii art, but foreground text in const N: usize = NEOFETCH_COLOR_PATTERNS.len();
// terminal let replacements = [&bg_color; N];
ForegroundBackground::Foreground
});
for &back in bg {
replacements[usize::from(u8::from(back)).checked_sub(1).unwrap()] =
&bg_color;
}
ac.replace_all(line, &replacements) ac.replace_all(line, &replacements)
}) })
}; };
// Remove existing colors
let asc = {
let mut lines = lines;
let asc = lines.join("\n");
const N: usize = NEOFETCH_COLOR_PATTERNS.len();
let replacements: [&str; N] = [&reset; N];
ac.replace_all(&asc, &replacements)
};
let lines = asc.lines();
// Reset colors at end of each line to prevent color bleeding // Reset colors at end of each line to prevent color bleeding
let lines = lines.map(|line| format!("{line}{reset}")); lines.map(|line| format!("{line}{reset}")).collect()
lines.collect()
}, },
(ColorAlignment::Vertical, Self { fg, bg, .. }) if !fg.is_empty() || !bg.is_empty() => { (ColorAlignment::Vertical, Self { fg, .. }) if !fg.is_empty() => {
if self.w == 0 {
return Ok(RecoloredAsciiArt {
lines: self.lines.clone(),
w: 0,
h: self.h,
});
}
let Self { lines, .. } = self let Self { lines, .. } = self
.fill_starting() .fill_starting()
.context("failed to fill in starting neofetch color codes")?; .context("failed to fill in starting neofetch color codes")?;
let color_profile = color_profile.with_length(self.w).with_context(|| { let color_profile = color_profile
format!("failed to spread color profile to length {w}", w = self.w) .with_length(self.w.try_into().expect("`w` should not be 0"))
})?; .with_context(|| {
format!("failed to spread color profile to length {w}", w = self.w)
})?;
// Apply colors // Apply colors
let lines: Vec<_> = { let ac = NEOFETCH_COLORS_AC
let ac = NEOFETCH_COLORS_AC .get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap());
.get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap()); lines
lines .into_iter()
.into_iter() .map(|line| {
.map(|line| { let line: &str = line.as_ref();
let line: &str = line.as_ref();
let mut matches = ac.find_iter(line).peekable(); // `AhoCorasick` operates on bytes; we need to map that back to grapheme
let mut dst = String::new(); // clusters (i.e. a character as seen on the terminal)
let mut offset: u8 = 0; // See https://github.com/BurntSushi/aho-corasick/issues/72#issuecomment-821128859
loop { let byte_idx_to_grapheme_idx: IndexMap<usize, usize> = {
let current = matches.next(); let mut m: IndexMap<_, _> = line
let next = matches.peek(); .grapheme_indices(true)
let (neofetch_color_idx, span, done) = match (current, next) { .enumerate()
(Some(m), Some(m_next)) => { .map(|(gr_idx, (byte_idx, _))| (byte_idx, gr_idx))
let ai_start = m.start().checked_add(3).unwrap(); .collect();
let ai_end = m.end().checked_sub(1).unwrap(); // Add an extra entry at the end, to support lookup using exclusive
let neofetch_color_idx: NeofetchAsciiIndexedColor = line // range end
[ai_start..ai_end] m.insert(line.len(), m.len());
.parse() m
.expect("neofetch color index should be valid"); };
offset = offset
.checked_add(u8::try_from(m.len()).unwrap())
.unwrap();
let mut span = m.span();
span.start = m.end();
span.end = m_next.start();
(neofetch_color_idx, span, false)
},
(Some(m), None) => {
// Last color code
let ai_start = m.start().checked_add(3).unwrap();
let ai_end = m.end().checked_sub(1).unwrap();
let neofetch_color_idx: NeofetchAsciiIndexedColor = line
[ai_start..ai_end]
.parse()
.expect("neofetch color index should be valid");
offset = offset
.checked_add(u8::try_from(m.len()).unwrap())
.unwrap();
let mut span = m.span();
span.start = m.end();
span.end = line.len();
(neofetch_color_idx, span, true)
},
(None, _) => {
// No color code in the entire line
unreachable!(
"`fill_starting` ensured each line of ascii art \
starts with neofetch color code"
);
},
};
let txt = &line[span];
if fg.contains(&neofetch_color_idx) { let mut matches = ac.find_iter(line).peekable();
let fore = color( let mut dst = String::new();
match theme { let mut offset: u8 = 0;
TerminalTheme::Light => "&0", loop {
TerminalTheme::Dark => "&f", let current = matches.next();
}, let next = matches.peek();
color_mode, let (neofetch_color_idx, span, done) = match (current, next) {
) (Some(m), Some(m_next)) => {
.expect("foreground color should not be invalid"); let ai_start = m.start().checked_add(3).unwrap();
write!(dst, "{fore}{txt}{reset}").unwrap(); let ai_end = m.end().checked_sub(1).unwrap();
} else if bg.contains(&neofetch_color_idx) { let neofetch_color_idx: NeofetchAsciiIndexedColor = line
let adjusted_start = [ai_start..ai_end]
span.start.checked_sub(usize::from(offset)).unwrap(); .parse()
let adjusted_end = .expect("neofetch color index should be valid");
span.end.checked_sub(usize::from(offset)).unwrap(); if offset == 0 && m.start() > 0 {
dst.push_str( dst.push_str(&line[..m.start()]);
&ColorProfile::new(Vec::from( }
&color_profile.colors[adjusted_start..adjusted_end], offset =
)) offset.checked_add(u8::try_from(m.len()).unwrap()).unwrap();
let mut span = m.span();
span.start = m.end();
span.end = m_next.start();
(neofetch_color_idx, span, false)
},
(Some(m), None) => {
// Last color code
let ai_start = m.start().checked_add(3).unwrap();
let ai_end = m.end().checked_sub(1).unwrap();
let neofetch_color_idx: NeofetchAsciiIndexedColor = line
[ai_start..ai_end]
.parse()
.expect("neofetch color index should be valid");
if offset == 0 && m.start() > 0 {
dst.push_str(&line[..m.start()]);
}
offset =
offset.checked_add(u8::try_from(m.len()).unwrap()).unwrap();
let mut span = m.span();
span.start = m.end();
span.end = line.len();
(neofetch_color_idx, span, true)
},
(None, _) => {
// No color code in the entire line
unreachable!(
"`fill_starting` ensured each line of ascii art starts \
with neofetch color code"
);
},
};
let txt = &line[span];
if fg.contains(&neofetch_color_idx) {
let fore = color(
match theme {
TerminalTheme::Light => "&0",
TerminalTheme::Dark => "&f",
},
color_mode,
)
.expect("foreground color should not be invalid");
write!(dst, "{fore}{txt}{reset}").unwrap();
} else {
let mut c_range: Range<usize> = span.into();
c_range.start = byte_idx_to_grapheme_idx
.get(&c_range.start)
.unwrap()
.checked_sub(usize::from(offset))
.unwrap();
c_range.end = byte_idx_to_grapheme_idx
.get(&c_range.end)
.unwrap()
.checked_sub(usize::from(offset))
.unwrap();
dst.push_str(
&ColorProfile::new(Vec::from(&color_profile.colors[c_range]))
.color_text( .color_text(
txt, txt,
color_mode, color_mode,
{ ForegroundBackground::Foreground,
// This is "background" in the ascii art, but
// foreground text in terminal
ForegroundBackground::Foreground
},
false, false,
) )
.context("failed to color text using color profile")?, .context("failed to color text using color profile")?,
); );
} else {
dst.push_str(txt);
}
if done {
break;
}
} }
Ok(dst)
})
.collect::<Result<_>>()?
};
lines if done {
break;
}
}
Ok(dst)
})
.collect::<Result<_>>()?
}, },
(ColorAlignment::Horizontal, Self { fg, bg, .. }) (ColorAlignment::Vertical, Self { fg, .. }) if fg.is_empty() => {
| (ColorAlignment::Vertical, Self { fg, bg, .. })
if fg.is_empty() && bg.is_empty() =>
{
// Remove existing colors // Remove existing colors
let asc = { let asc = {
let asc = self.lines.join("\n"); let asc = self.lines.join("\n");
@ -279,38 +287,14 @@ impl NormalizedAsciiArt {
let lines = asc.lines(); let lines = asc.lines();
// Add new colors // Add new colors
match color_align { lines
ColorAlignment::Horizontal => { .map(|line| {
let ColorProfile { colors } = let line = color_profile
color_profile.with_length(self.h).with_context(|| { .color_text(line, color_mode, ForegroundBackground::Foreground, false)
format!("failed to spread color profile to length {h}", h = self.h) .context("failed to color text using color profile")?;
})?; Ok(line)
lines })
.enumerate() .collect::<Result<_>>()?
.map(|(i, line)| {
let fore = colors[i]
.to_ansi_string(color_mode, ForegroundBackground::Foreground);
format!("{fore}{line}{reset}")
})
.collect()
},
ColorAlignment::Vertical => lines
.map(|line| {
let line = color_profile
.color_text(
line,
color_mode,
ForegroundBackground::Foreground,
false,
)
.context("failed to color text using color profile")?;
Ok(line)
})
.collect::<Result<_>>()?,
_ => {
unreachable!();
},
}
}, },
( (
ColorAlignment::Custom { ColorAlignment::Custom {
@ -344,9 +328,7 @@ impl NormalizedAsciiArt {
let lines = asc.lines(); let lines = asc.lines();
// Reset colors at end of each line to prevent color bleeding // Reset colors at end of each line to prevent color bleeding
let lines = lines.map(|line| format!("{line}{reset}")); lines.map(|line| format!("{line}{reset}")).collect()
lines.collect()
}, },
_ => { _ => {
unreachable!() unreachable!()
@ -382,19 +364,24 @@ impl NormalizedAsciiArt {
if m.start() == 0 if m.start() == 0
|| line[0..m.start()].trim_end_matches(' ').is_empty() => || line[0..m.start()].trim_end_matches(' ').is_empty() =>
{ {
// line starts with neofetch color code, do nothing // Line starts with neofetch color code
last = Some(&line[m.span()]);
}, },
_ => { Some(_) => {
new.push_str(last.context( new.push_str(last.context(
"failed to find neofetch color code from a previous line", "failed to find neofetch color code from a previous line",
)?); )?);
}, },
None => {
new.push_str(last.unwrap_or(NEOFETCH_COLOR_PATTERNS[0]));
},
} }
new.push_str(line); new.push_str(line);
// Get the last placeholder for the next line // Get the last placeholder for the next line
if let Some(m) = matches.last() { if let Some(m) = matches.last() {
last = Some(&line[m.span()]) last.context("non-space character seen before first color code")?;
last = Some(&line[m.span()]);
} }
Ok(new) Ok(new)
@ -404,7 +391,6 @@ impl NormalizedAsciiArt {
Ok(Self { Ok(Self {
lines, lines,
fg: self.fg.clone(), fg: self.fg.clone(),
bg: self.bg.clone(),
..*self ..*self
}) })
} }

View file

@ -4,7 +4,7 @@ use std::fmt::Write as _;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{self, IsTerminal as _, Read as _, Write as _}; use std::io::{self, IsTerminal as _, Read as _, Write as _};
use std::iter::zip; use std::iter::zip;
use std::num::{NonZeroU16, NonZeroU8, NonZeroUsize}; use std::num::NonZeroU8;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
@ -22,8 +22,8 @@ use hyfetch::models::Config;
#[cfg(feature = "macchina")] #[cfg(feature = "macchina")]
use hyfetch::neofetch_util::macchina_path; use hyfetch::neofetch_util::macchina_path;
use hyfetch::neofetch_util::{ use hyfetch::neofetch_util::{
self, ascii_size, fastfetch_path, get_distro_ascii, literal_input, ColorAlignment, self, fastfetch_path, get_distro_ascii, literal_input, ColorAlignment, NEOFETCH_COLORS_AC,
NEOFETCH_COLORS_AC, NEOFETCH_COLOR_PATTERNS, TEST_ASCII, NEOFETCH_COLOR_PATTERNS, TEST_ASCII,
}; };
use hyfetch::presets::{AssignLightness, Preset}; use hyfetch::presets::{AssignLightness, Preset};
use hyfetch::pride_month; use hyfetch::pride_month;
@ -132,7 +132,6 @@ fn main() -> Result<()> {
asc: fs::read_to_string(&path) asc: fs::read_to_string(&path)
.with_context(|| format!("failed to read ascii from {path:?}"))?, .with_context(|| format!("failed to read ascii from {path:?}"))?,
fg: Vec::new(), fg: Vec::new(),
bg: Vec::new(),
} }
} else { } else {
get_distro_ascii(distro, backend).context("failed to get distro ascii")? get_distro_ascii(distro, backend).context("failed to get distro ascii")?
@ -267,14 +266,14 @@ fn create_config(
let (Width(term_w), Height(term_h)) = let (Width(term_w), Height(term_h)) =
terminal_size().context("failed to get terminal size")?; terminal_size().context("failed to get terminal size")?;
let (term_w_min, term_h_min) = ( let (term_w_min, term_h_min) = (
NonZeroU16::from(asc.w) u16::from(asc.w)
.checked_mul(NonZeroU16::new(2).unwrap()) .checked_mul(2)
.unwrap() .unwrap()
.checked_add(4) .checked_add(4)
.unwrap(), .unwrap(),
NonZeroU16::new(30).unwrap(), 30,
); );
if term_w < term_w_min.get() || term_h < term_h_min.get() { if term_w < term_w_min || term_h < term_h_min {
printc( printc(
format!( format!(
"&cWarning: Your terminal is too small ({term_w} * {term_h}).\nPlease resize \ "&cWarning: Your terminal is too small ({term_w} * {term_h}).\nPlease resize \
@ -612,9 +611,15 @@ fn create_config(
////////////////////////////// //////////////////////////////
// 4. Dim/lighten colors // 4. Dim/lighten colors
let test_ascii = &TEST_ASCII[1..TEST_ASCII.len().checked_sub(1).unwrap()]; let test_ascii = {
let (test_ascii_width, test_ascii_height) = let asc = &TEST_ASCII[1..TEST_ASCII.len().checked_sub(1).unwrap()];
ascii_size(test_ascii).expect("test ascii should have valid width and height"); let asc = RawAsciiArt {
asc: asc.to_owned(),
fg: Vec::new(),
};
asc.to_normalized()
.expect("normalizing test ascii should not fail")
};
let select_lightness = || -> Result<Lightness> { let select_lightness = || -> Result<Lightness> {
clear_screen(Some(&title), color_mode, debug_mode).context("failed to clear screen")?; clear_screen(Some(&title), color_mode, debug_mode).context("failed to clear screen")?;
@ -642,12 +647,7 @@ fn create_config(
let (Width(term_w), _) = terminal_size().context("failed to get terminal size")?; let (Width(term_w), _) = terminal_size().context("failed to get terminal size")?;
let num_cols = cmp::max( let num_cols = cmp::max(
1, 1,
term_w.div_euclid( term_w.div_euclid(u16::from(test_ascii.w).checked_add(2).unwrap()),
NonZeroU16::from(test_ascii_width)
.checked_add(2)
.unwrap()
.get(),
),
); );
let num_cols: u8 = num_cols.try_into().expect("`num_cols` should fit in `u8`"); let num_cols: u8 = num_cols.try_into().expect("`num_cols` should fit in `u8`");
const MIN: f32 = 0.15; const MIN: f32 = 0.15;
@ -661,20 +661,20 @@ fn create_config(
}); });
let row: Vec<Vec<String>> = ratios let row: Vec<Vec<String>> = ratios
.map(|r| { .map(|r| {
let asc = RawAsciiArt { let mut asc = test_ascii.clone();
asc: test_ascii.replace( asc.lines = asc
.lines
.join("\n")
.replace(
"{txt}", "{txt}",
&format!( &format!(
"{lightness:^5}", "{lightness:^5}",
lightness = format!("{lightness:.0}%", lightness = r * 100.0) lightness = format!("{lightness:.0}%", lightness = r * 100.0)
), ),
), )
fg: Vec::new(), .lines()
bg: Vec::new(), .map(ToOwned::to_owned)
}; .collect();
let asc = asc
.to_normalized()
.expect("normalizing test ascii should not fail");
let asc = asc let asc = asc
.to_recolored( .to_recolored(
&color_align, &color_align,
@ -690,7 +690,7 @@ fn create_config(
asc.lines asc.lines
}) })
.collect(); .collect();
for i in 0..NonZeroUsize::from(test_ascii_height).get() { for i in 0..usize::from(test_ascii.h) {
let mut line = Vec::new(); let mut line = Vec::new();
for lines in &row { for lines in &row {
line.push(&*lines[i]); line.push(&*lines[i]);
@ -769,7 +769,7 @@ fn create_config(
terminal_size().context("failed to get terminal size")?; terminal_size().context("failed to get terminal size")?;
let ascii_per_row = cmp::max( let ascii_per_row = cmp::max(
1, 1,
term_w.div_euclid(NonZeroU16::from(asc.w).checked_add(2).unwrap().get()), term_w.div_euclid(u16::from(asc.w).checked_add(2).unwrap()),
); );
let ascii_per_row: u8 = ascii_per_row let ascii_per_row: u8 = ascii_per_row
.try_into() .try_into()
@ -778,7 +778,7 @@ fn create_config(
1, 1,
term_h term_h
.saturating_sub(8) .saturating_sub(8)
.div_euclid(NonZeroU16::from(asc.h).checked_add(1).unwrap().get()), .div_euclid(u16::from(asc.h).checked_add(1).unwrap()),
); );
let ascii_rows: u8 = ascii_rows let ascii_rows: u8 = ascii_rows
.try_into() .try_into()
@ -866,10 +866,7 @@ fn create_config(
.to_recolored(ca, &color_profile, color_mode, theme) .to_recolored(ca, &color_profile, color_mode, theme)
.context("failed to recolor ascii")? .context("failed to recolor ascii")?
.lines; .lines;
v.push(format!( v.push(format!("{k:^asc_width$}", asc_width = usize::from(asc.w)));
"{k:^asc_width$}",
asc_width = NonZeroUsize::from(asc.w).get()
));
Ok(v) Ok(v)
}) })
.collect::<Result<_>>()?; .collect::<Result<_>>()?;
@ -878,7 +875,7 @@ fn create_config(
let row: Vec<Vec<String>> = row.collect(); let row: Vec<Vec<String>> = row.collect();
// Print by row // Print by row
for i in 0..NonZeroUsize::from(asc.h).checked_add(1).unwrap().get() { for i in 0..usize::from(asc.h).checked_add(1).unwrap() {
let mut line = Vec::new(); let mut line = Vec::new();
for lines in &row { for lines in &row {
line.push(&*lines[i]); line.push(&*lines[i]);

View file

@ -5,7 +5,6 @@ use std::fs;
#[cfg(windows)] #[cfg(windows)]
use std::io; use std::io;
use std::io::{self, Write as _}; use std::io::{self, Write as _};
use std::num::{NonZeroU8, NonZeroUsize};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::sync::OnceLock; use std::sync::OnceLock;
@ -290,9 +289,9 @@ where
// Try new codegen-based detection method // Try new codegen-based detection method
if let Some(distro) = Distro::detect(&distro) { if let Some(distro) = Distro::detect(&distro) {
let asc = distro.ascii_art().to_owned(); let asc = distro.ascii_art().to_owned();
let (fg, bg) = fore_back(&distro); let fg = ascii_foreground(&distro);
return Ok(RawAsciiArt { asc, fg, bg }); return Ok(RawAsciiArt { asc, fg });
} }
debug!(%distro, "could not find a match for distro; falling back to neofetch"); debug!(%distro, "could not find a match for distro; falling back to neofetch");
@ -308,7 +307,6 @@ where
Ok(RawAsciiArt { Ok(RawAsciiArt {
asc, asc,
fg: Vec::new(), fg: Vec::new(),
bg: Vec::new(),
}) })
} }
@ -333,12 +331,16 @@ pub fn run(asc: RecoloredAsciiArt, backend: Backend, args: Option<&Vec<String>>)
} }
/// Gets distro ascii width and height, ignoring color code. /// Gets distro ascii width and height, ignoring color code.
pub fn ascii_size<S>(asc: S) -> Result<(NonZeroU8, NonZeroU8)> pub fn ascii_size<S>(asc: S) -> Result<(u8, u8)>
where where
S: AsRef<str>, S: AsRef<str>,
{ {
let asc = asc.as_ref(); let asc = asc.as_ref();
if asc.is_empty() {
return Ok((0, 0));
}
let asc = { let asc = {
let ac = let ac =
NEOFETCH_COLORS_AC.get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap()); NEOFETCH_COLORS_AC.get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap());
@ -347,21 +349,23 @@ where
ac.replace_all(asc, &REPLACEMENTS) ac.replace_all(asc, &REPLACEMENTS)
}; };
if asc.is_empty() {
return Ok((0, 0));
}
let width = asc let width = asc
.lines() .lines()
.map(|line| line.graphemes(true).count()) .map(|line| line.graphemes(true).count())
.max() .max()
.expect("line iterator should not be empty"); .expect("line iterator should not be empty");
let width: NonZeroUsize = width.try_into().context("`asc` should not be empty")?; let width: u8 = width.try_into().with_context(|| {
let width: NonZeroU8 = width.try_into().with_context(|| {
format!( format!(
"`asc` should not have more than {limit} characters per line", "`asc` should not have more than {limit} characters per line",
limit = u8::MAX limit = u8::MAX
) )
})?; })?;
let height = asc.lines().count(); let height = asc.lines().count();
let height: NonZeroUsize = height.try_into().context("`asc` should not be empty")?; let height: u8 = height.try_into().with_context(|| {
let height: NonZeroU8 = height.try_into().with_context(|| {
format!( format!(
"`asc` should not have more than {limit} lines", "`asc` should not have more than {limit} lines",
limit = u8::MAX limit = u8::MAX
@ -827,52 +831,41 @@ fn run_macchina(asc: String, args: Option<&Vec<String>>) -> Result<()> {
Ok(()) Ok(())
} }
/// Gets recommended foreground-background configuration for distro. /// Gets the color indices that should be considered as foreground, for a
fn fore_back( /// particular distro's ascii art.
distro: &Distro, fn ascii_foreground(distro: &Distro) -> Vec<NeofetchAsciiIndexedColor> {
) -> ( let fg: Vec<u8> = match distro {
Vec<NeofetchAsciiIndexedColor>, Distro::Anarchy => vec![2],
Vec<NeofetchAsciiIndexedColor>, Distro::Antergos => vec![1],
) { Distro::ArchStrike => vec![2],
let (fg, bg): (Vec<u8>, Vec<u8>) = match distro { Distro::Astra_Linux => vec![2],
Distro::Anarchy => (vec![2], vec![1]), Distro::Chapeau => vec![2],
Distro::Antergos => (vec![1], vec![2]), Distro::Fedora => vec![2],
Distro::ArchStrike => (vec![2], vec![1]), Distro::Fedora_Silverblue => vec![2],
Distro::Astra_Linux => (vec![2], vec![1]), Distro::GalliumOS => vec![2],
Distro::Chapeau => (vec![2], vec![1]), Distro::KrassOS => vec![2],
Distro::Fedora => (vec![2], vec![1]), Distro::Kubuntu => vec![2],
Distro::Fedora_Silverblue => (vec![2], vec![1, 3]), Distro::Lubuntu => vec![2],
Distro::GalliumOS => (vec![2], vec![1]), Distro::openEuler => vec![2],
Distro::KrassOS => (vec![2], vec![1]), Distro::Peppermint => vec![2],
Distro::Kubuntu => (vec![2], vec![1]), Distro::Pop__OS => vec![2],
Distro::Lubuntu => (vec![2], vec![1]), Distro::Ubuntu_Cinnamon => vec![2],
Distro::openEuler => (vec![2], vec![1]), Distro::Ubuntu_Kylin => vec![2],
Distro::Peppermint => (vec![2], vec![1]), Distro::Ubuntu_MATE => vec![2],
Distro::Pop__OS => (vec![2], vec![1]), Distro::Ubuntu_old => vec![2],
Distro::Ubuntu_Cinnamon => (vec![2], vec![1]), Distro::Ubuntu_Studio => vec![2],
Distro::Ubuntu_Kylin => (vec![2], vec![1]), Distro::Ubuntu_Sway => vec![2],
Distro::Ubuntu_MATE => (vec![2], vec![1]), Distro::Ultramarine_Linux => vec![2],
Distro::Ubuntu_old => (vec![2], vec![1]), Distro::Univention => vec![2],
Distro::Ubuntu_Studio => (vec![2], vec![1]), Distro::Vanilla => vec![2],
Distro::Ubuntu_Sway => (vec![2], vec![1]), Distro::Xubuntu => vec![2],
Distro::Ultramarine_Linux => (vec![2], vec![1]), _ => Vec::new(),
Distro::Univention => (vec![2], vec![1]),
Distro::Vanilla => (vec![2], vec![1]),
Distro::Xubuntu => (vec![2], vec![1]),
_ => (Vec::new(), Vec::new()),
}; };
(
fg.into_iter() fg.into_iter()
.map(|fore| { .map(|fore| {
fore.try_into() fore.try_into()
.expect("`fore` should be a valid neofetch color index") .expect("`fore` should be a valid neofetch color index")
}) })
.collect(), .collect()
bg.into_iter()
.map(|back| {
back.try_into()
.expect("`back` should be a valid neofetch color index")
})
.collect(),
)
} }

View file

@ -1,5 +1,5 @@
use std::io::{self, Write as _}; use std::io::{self, Write as _};
use std::num::{NonZeroU16, NonZeroU8, NonZeroUsize, Wrapping}; use std::num::{NonZeroU16, NonZeroUsize, Wrapping};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -48,54 +48,47 @@ pub fn start_animation(color_mode: AnsiMode) -> Result<()> {
const TEXT_BORDER_WIDTH: u16 = 2; const TEXT_BORDER_WIDTH: u16 = 2;
const NOTICE_BORDER_WIDTH: u16 = 1; const NOTICE_BORDER_WIDTH: u16 = 1;
const VERTICAL_MARGIN: u16 = 1; const VERTICAL_MARGIN: u16 = 1;
let notice_w: NonZeroUsize = NOTICE let notice_w = NOTICE.len();
.len() let notice_w: u8 = notice_w
.try_into()
.expect("`NOTICE` should not be empty");
let notice_w: NonZeroU8 = notice_w
.try_into() .try_into()
.expect("`NOTICE` width should fit in `u8`"); .expect("`NOTICE` width should fit in `u8`");
let notice_h: NonZeroUsize = NOTICE let notice_h = NOTICE.lines().count();
.lines() let notice_h: u8 = notice_h
.count()
.try_into()
.expect("`NOTICE` should not be empty");
let notice_h: NonZeroU8 = notice_h
.try_into() .try_into()
.expect("`NOTICE` height should fit in `u8`"); .expect("`NOTICE` height should fit in `u8`");
let term_w_min = cmp::max( let term_w_min = cmp::max(
NonZeroU16::from(text_width) u16::from(text_width)
.checked_add(TEXT_BORDER_WIDTH.checked_mul(2).unwrap()) .checked_add(TEXT_BORDER_WIDTH.checked_mul(2).unwrap())
.unwrap(), .unwrap(),
NonZeroU16::from(notice_w) u16::from(notice_w)
.checked_add(NOTICE_BORDER_WIDTH.checked_mul(2).unwrap()) .checked_add(NOTICE_BORDER_WIDTH.checked_mul(2).unwrap())
.unwrap(), .unwrap(),
); );
let term_h_min = NonZeroU16::from(text_height) let term_h_min = u16::from(text_height)
.checked_add(notice_h.get().into()) .checked_add(notice_h.into())
.unwrap() .unwrap()
.checked_add(VERTICAL_MARGIN.checked_mul(2).unwrap()) .checked_add(VERTICAL_MARGIN.checked_mul(2).unwrap())
.unwrap(); .unwrap();
if w >= term_w_min && h >= term_h_min { if w.get() >= term_w_min && h.get() >= term_h_min {
(text, text_width, text_height) (text, text_width, text_height)
} else { } else {
let text = &TEXT_ASCII_SMALL[1..TEXT_ASCII_SMALL.len().checked_sub(1).unwrap()]; let text = &TEXT_ASCII_SMALL[1..TEXT_ASCII_SMALL.len().checked_sub(1).unwrap()];
let (text_width, text_height) = let (text_width, text_height) =
ascii_size(text).expect("text ascii should have valid width and height"); ascii_size(text).expect("text ascii should have valid width and height");
let term_w_min = cmp::max( let term_w_min = cmp::max(
NonZeroU16::from(text_width) u16::from(text_width)
.checked_add(TEXT_BORDER_WIDTH.checked_mul(2).unwrap()) .checked_add(TEXT_BORDER_WIDTH.checked_mul(2).unwrap())
.unwrap(), .unwrap(),
NonZeroU16::from(notice_w) u16::from(notice_w)
.checked_add(NOTICE_BORDER_WIDTH.checked_mul(2).unwrap()) .checked_add(NOTICE_BORDER_WIDTH.checked_mul(2).unwrap())
.unwrap(), .unwrap(),
); );
let term_h_min = NonZeroU16::from(text_height) let term_h_min = u16::from(text_height)
.checked_add(notice_h.get().into()) .checked_add(notice_h.into())
.unwrap() .unwrap()
.checked_add(VERTICAL_MARGIN.checked_mul(2).unwrap()) .checked_add(VERTICAL_MARGIN.checked_mul(2).unwrap())
.unwrap(); .unwrap();
if w < term_w_min || h < term_h_min { if w.get() < term_w_min || h.get() < term_h_min {
return Err(anyhow!( return Err(anyhow!(
"terminal size should be at least ({term_w_min} * {term_h_min})" "terminal size should be at least ({term_w_min} * {term_h_min})"
)); ));
@ -115,19 +108,15 @@ pub fn start_animation(color_mode: AnsiMode) -> Result<()> {
let text_start_y = h let text_start_y = h
.get() .get()
.div_euclid(2) .div_euclid(2)
.checked_sub(u16::from(text_height.get() / 2)) .checked_sub((text_height / 2).into())
.unwrap();
let text_end_y = text_start_y
.checked_add(NonZeroU16::from(text_height).get())
.unwrap(); .unwrap();
let text_end_y = text_start_y.checked_add(text_height.into()).unwrap();
let text_start_x = w let text_start_x = w
.get() .get()
.div_euclid(2) .div_euclid(2)
.checked_sub(u16::from(text_width.get() / 2)) .checked_sub((text_width / 2).into())
.unwrap();
let text_end_x = text_start_x
.checked_add(NonZeroU16::from(text_width).get())
.unwrap(); .unwrap();
let text_end_x = text_start_x.checked_add(text_width.into()).unwrap();
let notice_start_x = w let notice_start_x = w
.get() .get()