Implement recolored ascii output
This commit is contained in:
parent
ff46c8f4ca
commit
be70233b03
8 changed files with 694 additions and 118 deletions
|
|
@ -10,23 +10,22 @@ license = { workspace = true }
|
|||
default-run = "hyfetch"
|
||||
|
||||
[dependencies]
|
||||
# ansi_colours = { workspace = true, features = ["rgb"] }
|
||||
aho-corasick = { workspace = true, features = ["perf-literal", "std"] }
|
||||
ansi_colours = { workspace = true, features = [] }
|
||||
# anstream = { workspace = true, features = ["auto"] }
|
||||
anyhow = { workspace = true, features = ["std"] }
|
||||
bpaf = { workspace = true, features = [] }
|
||||
# bytemuck = { workspace = true, features = [] }
|
||||
chrono = { workspace = true, features = ["clock", "std"] }
|
||||
deranged = { workspace = true, features = ["serde", "std"] }
|
||||
derive_more = { workspace = true, features = ["from", "from_str", "into", "std"] }
|
||||
directories = { workspace = true, features = [] }
|
||||
indexmap = { workspace = true, features = ["serde", "std"] }
|
||||
palette = { workspace = true, features = ["std"] }
|
||||
regex = { workspace = true, features = ["perf", "std", "unicode"] }
|
||||
# rgb = { workspace = true, features = [] }
|
||||
serde = { workspace = true, features = ["derive", "std"] }
|
||||
serde_json = { workspace = true, features = ["std"] }
|
||||
serde_path_to_error = { workspace = true, features = [] }
|
||||
shell-words = { workspace = true, features = ["std"] }
|
||||
strum = { workspace = true, features = ["derive", "std"] }
|
||||
tempfile = { workspace = true, features = [] }
|
||||
thiserror = { workspace = true, features = [] }
|
||||
tracing = { workspace = true, features = ["attributes", "std"] }
|
||||
tracing-subscriber = { workspace = true, features = ["ansi", "fmt", "smallvec", "std", "tracing-log"] }
|
||||
|
|
@ -36,6 +35,9 @@ indexmap = { workspace = true, features = ["std"] }
|
|||
regex = { workspace = true, features = ["perf", "std", "unicode"] }
|
||||
unicode-normalization = { workspace = true, features = ["std"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
enable-ansi-support = { workspace = true, features = [] }
|
||||
|
||||
[features]
|
||||
default = ["autocomplete", "color"]
|
||||
autocomplete = ["bpaf/autocomplete"]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ use hyfetch::utils::get_cache_path;
|
|||
use tracing::debug;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
enable_ansi_support::enable_ansi_support();
|
||||
|
||||
let options = options().run();
|
||||
|
||||
init_tracing_subsriber(options.debug).context("failed to init tracing subscriber")?;
|
||||
|
|
@ -97,7 +100,8 @@ fn main() -> Result<()> {
|
|||
};
|
||||
let asc = config
|
||||
.color_align
|
||||
.recolor_ascii(asc, color_profile, color_mode, config.light_dark);
|
||||
.recolor_ascii(asc, color_profile, color_mode, config.light_dark)
|
||||
.context("failed to recolor ascii")?;
|
||||
neofetch_util::run(asc, backend, args).context("failed to run")?;
|
||||
|
||||
if options.ask_exit {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,63 @@
|
|||
use std::num::ParseFloatError;
|
||||
use std::num::{ParseFloatError, ParseIntError};
|
||||
use std::str::FromStr;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use ansi_colours::AsRGB;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use deranged::RangedU8;
|
||||
use derive_more::{From, FromStr, Into};
|
||||
use palette::Srgb;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::types::AnsiMode;
|
||||
|
||||
const MINECRAFT_COLORS: [(&str, &str); 30] = [
|
||||
// Minecraft formatting codes
|
||||
// ==========================
|
||||
("&0", "\x1b[38;5;0m"),
|
||||
("&1", "\x1b[38;5;4m"),
|
||||
("&2", "\x1b[38;5;2m"),
|
||||
("&3", "\x1b[38;5;6m"),
|
||||
("&4", "\x1b[38;5;1m"),
|
||||
("&5", "\x1b[38;5;5m"),
|
||||
("&6", "\x1b[38;5;3m"),
|
||||
("&7", "\x1b[38;5;7m"),
|
||||
("&8", "\x1b[38;5;8m"),
|
||||
("&9", "\x1b[38;5;12m"),
|
||||
("&a", "\x1b[38;5;10m"),
|
||||
("&b", "\x1b[38;5;14m"),
|
||||
("&c", "\x1b[38;5;9m"),
|
||||
("&d", "\x1b[38;5;13m"),
|
||||
("&e", "\x1b[38;5;11m"),
|
||||
("&f", "\x1b[38;5;15m"),
|
||||
("&l", "\x1b[1m"), // Enable bold text
|
||||
("&o", "\x1b[3m"), // Enable italic text
|
||||
("&n", "\x1b[4m"), // Enable underlined text
|
||||
("&k", "\x1b[8m"), // Enable hidden text
|
||||
("&m", "\x1b[9m"), // Enable strikethrough text
|
||||
("&r", "\x1b[0m"), // Reset everything
|
||||
// Extended codes (not officially in Minecraft)
|
||||
// ============================================
|
||||
("&-", "\n"), // Line break
|
||||
("&~", "\x1b[39m"), // Reset text color
|
||||
("&*", "\x1b[49m"), // Reset background color
|
||||
("&L", "\x1b[22m"), // Disable bold text
|
||||
("&O", "\x1b[23m"), // Disable italic text
|
||||
("&N", "\x1b[24m"), // Disable underlined text
|
||||
("&K", "\x1b[28m"), // Disable hidden text
|
||||
("&M", "\x1b[29m"), // Disable strikethrough text
|
||||
];
|
||||
const RGB_COLOR_PATTERNS: [&str; 2] = ["&gf(", "&gb("];
|
||||
|
||||
static MINECRAFT_COLORS_AC: OnceLock<(AhoCorasick, Box<[&str; 30]>)> = OnceLock::new();
|
||||
static RGB_COLORS_AC: OnceLock<AhoCorasick> = OnceLock::new();
|
||||
|
||||
/// Represents the lightness component in HSL.
|
||||
///
|
||||
/// The range of valid values is
|
||||
/// `(`[`Lightness::MIN`]`..=`[`Lightness::MAX`]`)`.
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Into, Serialize)]
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)]
|
||||
pub struct Lightness(f32);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -37,21 +84,7 @@ pub enum ParseLightnessError {
|
|||
/// The range of valid values as supported in neofetch is
|
||||
/// `(`[`NeofetchAsciiIndexedColor::MIN`]`..
|
||||
/// =`[`NeofetchAsciiIndexedColor::MAX`]`)`.
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Hash,
|
||||
Debug,
|
||||
Deserialize,
|
||||
From,
|
||||
FromStr,
|
||||
Into,
|
||||
Serialize,
|
||||
)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
|
||||
pub struct NeofetchAsciiIndexedColor(
|
||||
RangedU8<{ NeofetchAsciiIndexedColor::MIN }, { NeofetchAsciiIndexedColor::MAX }>,
|
||||
);
|
||||
|
|
@ -61,22 +94,21 @@ pub struct NeofetchAsciiIndexedColor(
|
|||
///
|
||||
/// The range of valid values depends on the number of unique colors in a
|
||||
/// certain preset.
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Hash,
|
||||
Debug,
|
||||
Deserialize,
|
||||
From,
|
||||
FromStr,
|
||||
Into,
|
||||
Serialize,
|
||||
)]
|
||||
pub struct PresetIndexedColor(usize);
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
|
||||
pub struct PresetIndexedColor(u8);
|
||||
|
||||
/// Whether the color is for foreground text or background color.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum ForegroundBackground {
|
||||
Foreground,
|
||||
Background,
|
||||
}
|
||||
|
||||
pub trait ToAnsiString {
|
||||
/// Converts RGB to ANSI escape code.
|
||||
fn to_ansi_string(&self, mode: AnsiMode, foreground_background: ForegroundBackground)
|
||||
-> String;
|
||||
}
|
||||
|
||||
impl Lightness {
|
||||
const MAX: f32 = 1.0f32;
|
||||
|
|
@ -107,7 +139,175 @@ impl FromStr for Lightness {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Lightness> for f32 {
|
||||
fn from(value: Lightness) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl NeofetchAsciiIndexedColor {
|
||||
const MAX: u8 = 6;
|
||||
const MIN: u8 = 1;
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for NeofetchAsciiIndexedColor {
|
||||
type Error = deranged::TryFromIntError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
Ok(Self(value.try_into()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for NeofetchAsciiIndexedColor {
|
||||
type Err = deranged::ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(s.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NeofetchAsciiIndexedColor> for u8 {
|
||||
fn from(value: NeofetchAsciiIndexedColor) -> Self {
|
||||
value.0.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for PresetIndexedColor {
|
||||
fn from(value: u8) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PresetIndexedColor {
|
||||
type Err = ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(s.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PresetIndexedColor> for u8 {
|
||||
fn from(value: PresetIndexedColor) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAnsiString for Srgb<u8> {
|
||||
fn to_ansi_string(
|
||||
&self,
|
||||
mode: AnsiMode,
|
||||
foreground_background: ForegroundBackground,
|
||||
) -> String {
|
||||
let c: u8 = match foreground_background {
|
||||
ForegroundBackground::Foreground => 38,
|
||||
ForegroundBackground::Background => 48,
|
||||
};
|
||||
match mode {
|
||||
AnsiMode::Rgb => {
|
||||
let [r, g, b]: [u8; 3] = (*self).into();
|
||||
format!("\x1b[{c};2;{r};{g};{b}m")
|
||||
},
|
||||
AnsiMode::Ansi256 => {
|
||||
let rgb: [u8; 3] = (*self).into();
|
||||
let indexed = rgb.to_ansi256();
|
||||
format!("\x1b[{c};5;{indexed}m")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces extended minecraft color codes in message.
|
||||
///
|
||||
/// Returns message with escape codes.
|
||||
pub fn color<S>(msg: S, mode: AnsiMode) -> Result<String>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let msg = msg.as_ref();
|
||||
|
||||
let msg = {
|
||||
let (ac, escape_codes) = MINECRAFT_COLORS_AC.get_or_init(|| {
|
||||
let (color_codes, escape_codes): (Vec<_>, Vec<_>) =
|
||||
MINECRAFT_COLORS.into_iter().unzip();
|
||||
let ac = AhoCorasick::new(color_codes).unwrap();
|
||||
(
|
||||
ac,
|
||||
escape_codes.try_into().expect(
|
||||
"`MINECRAFT_COLORS` should have the same number of elements as \
|
||||
`MINECRAFT_COLORS_AC.get_or_init(...).1`",
|
||||
),
|
||||
)
|
||||
});
|
||||
ac.replace_all(msg, &escape_codes[..])
|
||||
};
|
||||
|
||||
let ac = RGB_COLORS_AC.get_or_init(|| AhoCorasick::new(RGB_COLOR_PATTERNS).unwrap());
|
||||
let mut dst = String::new();
|
||||
let mut ret_err = None;
|
||||
ac.replace_all_with(&msg, &mut dst, |m, _, dst| {
|
||||
let start = m.end();
|
||||
let end = msg[start..]
|
||||
.find(')')
|
||||
.ok_or_else(|| anyhow!("missing closing brace for color code"));
|
||||
let end = match end {
|
||||
Ok(end) => end,
|
||||
Err(err) => {
|
||||
ret_err = Some(err);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
let code = &msg[start..end];
|
||||
let foreground_background = if m.pattern().as_usize() == 0 {
|
||||
ForegroundBackground::Foreground
|
||||
} else {
|
||||
ForegroundBackground::Background
|
||||
};
|
||||
|
||||
let rgb: Srgb<u8> = if code.starts_with('#') {
|
||||
let rgb = code.parse().context("failed to parse hex color");
|
||||
match rgb {
|
||||
Ok(rgb) => rgb,
|
||||
Err(err) => {
|
||||
ret_err = Some(err);
|
||||
return false;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let rgb: Result<[&str; 3], _> = code
|
||||
.split(&[',', ';', ' '])
|
||||
.filter(|x| x.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("wrong number of rgb components"));
|
||||
let rgb = match rgb {
|
||||
Ok(rgb) => rgb,
|
||||
Err(err) => {
|
||||
ret_err = Some(err);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
let rgb = rgb
|
||||
.into_iter()
|
||||
.map(u8::from_str)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("failed to parse rgb components");
|
||||
let rgb: [u8; 3] = match rgb {
|
||||
Ok(rgb) => rgb.try_into().unwrap(),
|
||||
Err(err) => {
|
||||
ret_err = Some(err);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
rgb.into()
|
||||
};
|
||||
|
||||
dst.push_str(&rgb.to_ansi_string(mode, foreground_background));
|
||||
|
||||
true
|
||||
});
|
||||
if let Some(err) = ret_err {
|
||||
Err(err)?;
|
||||
}
|
||||
|
||||
Ok(dst)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,12 @@ pub struct Config {
|
|||
impl Config {
|
||||
pub fn default_lightness(term: &LightDark) -> Lightness {
|
||||
match term {
|
||||
LightDark::Dark => Lightness::new(0.65).unwrap(),
|
||||
LightDark::Light => Lightness::new(0.4).unwrap(),
|
||||
LightDark::Dark => {
|
||||
Lightness::new(0.65).expect("default lightness should not be invalid")
|
||||
},
|
||||
LightDark::Light => {
|
||||
Lightness::new(0.4).expect("default lightness should not be invalid")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::Write;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::process::{Command, ExitStatus};
|
||||
use std::sync::OnceLock;
|
||||
use std::{env, fmt};
|
||||
use std::{env, fmt, iter};
|
||||
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use indexmap::IndexMap;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tempfile::NamedTempFile;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::color_util::{NeofetchAsciiIndexedColor, PresetIndexedColor};
|
||||
use crate::color_util::{
|
||||
color, ForegroundBackground, NeofetchAsciiIndexedColor, PresetIndexedColor, ToAnsiString,
|
||||
};
|
||||
use crate::distros::Distro;
|
||||
use crate::presets::ColorProfile;
|
||||
use crate::types::{AnsiMode, Backend, LightDark};
|
||||
|
||||
const NEOFETCH_COLOR_PATTERN: &str = r"\$\{c[0-6]\}";
|
||||
static NEOFETCH_COLOR_RE: OnceLock<Regex> = OnceLock::new();
|
||||
const NEOFETCH_COLOR_PATTERNS: [&str; 6] = ["${c1}", "${c2}", "${c3}", "${c4}", "${c5}", "${c6}"];
|
||||
static NEOFETCH_COLORS_AC: OnceLock<AhoCorasick> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "mode")]
|
||||
|
|
@ -39,14 +43,101 @@ pub enum ColorAlignment {
|
|||
|
||||
impl ColorAlignment {
|
||||
/// Uses the color alignment to recolor an ascii art.
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub fn recolor_ascii(
|
||||
&self,
|
||||
asc: String,
|
||||
color_profile: ColorProfile,
|
||||
color_mode: AnsiMode,
|
||||
term: LightDark,
|
||||
) -> String {
|
||||
todo!()
|
||||
) -> Result<String> {
|
||||
let asc = fill_starting(asc).context("failed to fill in starting neofetch color codes")?;
|
||||
|
||||
let reset = color("&~&*", color_mode).expect("color reset should not be invalid");
|
||||
|
||||
let asc = match self {
|
||||
Self::Horizontal {
|
||||
fore_back: Some((fore, back)),
|
||||
}
|
||||
| Self::Vertical {
|
||||
fore_back: Some((fore, back)),
|
||||
} => {
|
||||
let fore: u8 = (*fore).into();
|
||||
let back: u8 = (*back).into();
|
||||
|
||||
// Replace foreground colors
|
||||
let asc = asc.replace(
|
||||
&format!("${{c{fore}}}"),
|
||||
&color(
|
||||
match term {
|
||||
LightDark::Light => "&0",
|
||||
LightDark::Dark => "&f",
|
||||
},
|
||||
color_mode,
|
||||
)
|
||||
.expect("foreground color should not be invalid"),
|
||||
);
|
||||
|
||||
let lines: Vec<_> = asc.split('\n').collect();
|
||||
|
||||
// Add new colors
|
||||
let asc = match self {
|
||||
Self::Horizontal { .. } => {
|
||||
let length = lines.len();
|
||||
let length: u8 = length.try_into().expect("`length` should fit in `u8`");
|
||||
let ColorProfile { colors } = color_profile
|
||||
.with_length(length)
|
||||
.context("failed to spread color profile to length")?;
|
||||
let mut asc = String::new();
|
||||
for (i, line) in lines.into_iter().enumerate() {
|
||||
let line = line.replace(
|
||||
&format!("${{c{back}}}"),
|
||||
&colors[i].to_ansi_string(color_mode, {
|
||||
// note: this is "background" in the ascii art, but foreground
|
||||
// text in terminal
|
||||
ForegroundBackground::Foreground
|
||||
}),
|
||||
);
|
||||
asc.push_str(&line);
|
||||
asc.push_str(&reset);
|
||||
asc.push('\n');
|
||||
}
|
||||
asc
|
||||
},
|
||||
Self::Vertical { .. } => {
|
||||
unimplemented!(
|
||||
"vertical color alignment with fore and back colors not implemented"
|
||||
);
|
||||
},
|
||||
_ => {
|
||||
unreachable!();
|
||||
},
|
||||
};
|
||||
|
||||
// Remove existing colors
|
||||
let asc = {
|
||||
let ac = NEOFETCH_COLORS_AC
|
||||
.get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap());
|
||||
const N: usize = NEOFETCH_COLOR_PATTERNS.len();
|
||||
let replacements: [&str; N] = iter::repeat("")
|
||||
.take(N)
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
ac.replace_all(&asc, &replacements)
|
||||
};
|
||||
|
||||
asc
|
||||
},
|
||||
Self::Horizontal { fore_back: None } | Self::Vertical { fore_back: None } => {
|
||||
todo!()
|
||||
},
|
||||
Self::Custom { colors } => {
|
||||
todo!()
|
||||
},
|
||||
};
|
||||
|
||||
Ok(asc)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,8 +200,24 @@ where
|
|||
todo!()
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub fn run(asc: String, backend: Backend, args: Option<&Vec<String>>) -> Result<()> {
|
||||
todo!()
|
||||
match backend {
|
||||
Backend::Neofetch => {
|
||||
run_neofetch(asc, args).context("failed to run neofetch")?;
|
||||
},
|
||||
Backend::Fastfetch => {
|
||||
todo!();
|
||||
},
|
||||
Backend::FastfetchOld => {
|
||||
todo!();
|
||||
},
|
||||
Backend::Qwqfetch => {
|
||||
todo!();
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets distro ascii width and height, ignoring color code.
|
||||
|
|
@ -120,18 +227,26 @@ where
|
|||
{
|
||||
let asc = asc.as_ref();
|
||||
|
||||
let Some(width) = NEOFETCH_COLOR_RE
|
||||
.get_or_init(|| Regex::new(NEOFETCH_COLOR_PATTERN).unwrap())
|
||||
.replace_all(asc, "")
|
||||
.split('\n')
|
||||
.map(|line| line.len())
|
||||
.max()
|
||||
else {
|
||||
let asc = {
|
||||
let ac =
|
||||
NEOFETCH_COLORS_AC.get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap());
|
||||
const N: usize = NEOFETCH_COLOR_PATTERNS.len();
|
||||
let replacements: [&str; N] = iter::repeat("")
|
||||
.take(N)
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
ac.replace_all(asc, &replacements)
|
||||
};
|
||||
|
||||
let Some(width) = asc.split('\n').map(|line| line.len()).max() else {
|
||||
unreachable!();
|
||||
};
|
||||
let width: u8 = width.try_into().expect("`width` should fit in `u8`");
|
||||
let height = asc.split('\n').count();
|
||||
let height: u8 = height.try_into().expect("`height` should fit in `u8`");
|
||||
|
||||
(width as u8, height as u8)
|
||||
(width, height)
|
||||
}
|
||||
|
||||
/// Makes sure every line are the same width.
|
||||
|
|
@ -146,13 +261,69 @@ where
|
|||
let mut buf = String::new();
|
||||
for line in asc.split('\n') {
|
||||
let (line_w, _) = ascii_size(line);
|
||||
buf.push_str(line);
|
||||
let pad = " ".repeat((w - line_w) as usize);
|
||||
buf.push_str(&format!("{line}{pad}\n"))
|
||||
buf.push_str(&pad);
|
||||
buf.push('\n');
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
/// Fills the missing starting placeholders.
|
||||
///
|
||||
/// e.g. `"${c1}...\n..."` -> `"${c1}...\n${c1}..."`
|
||||
fn fill_starting<S>(asc: S) -> Result<String>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let asc = asc.as_ref();
|
||||
|
||||
let ac = NEOFETCH_COLORS_AC.get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap());
|
||||
|
||||
let mut new = String::new();
|
||||
let mut last = None;
|
||||
for line in asc.split('\n') {
|
||||
let mut matches = ac.find_iter(line).peekable();
|
||||
|
||||
match matches.peek() {
|
||||
Some(m) if m.start() == 0 => {
|
||||
// line starts with neofetch color code, do nothing
|
||||
},
|
||||
_ => {
|
||||
new.push_str(last.ok_or_else(|| {
|
||||
anyhow!("failed to find neofetch color code from a previous line")
|
||||
})?);
|
||||
},
|
||||
}
|
||||
new.push_str(line);
|
||||
new.push('\n');
|
||||
|
||||
// Get the last placeholder for the next line
|
||||
if let Some(m) = matches.last() {
|
||||
last = Some(&line[m.span()])
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
/// Runs neofetch command.
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn run_neofetch_command<S>(args: &[S]) -> Result<()>
|
||||
where
|
||||
S: AsRef<OsStr> + fmt::Debug,
|
||||
{
|
||||
let mut command = make_neofetch_command(args).context("failed to make neofetch command")?;
|
||||
|
||||
let status = command
|
||||
.status()
|
||||
.context("failed to execute neofetch command as child process")?;
|
||||
process_command_status(&status).context("neofetch command exited with error")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs neofetch command, returning the piped stdout output.
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn run_neofetch_command_piped<S>(args: &[S]) -> Result<String>
|
||||
|
|
@ -165,26 +336,7 @@ where
|
|||
.output()
|
||||
.context("failed to execute neofetch as child process")?;
|
||||
debug!(?output, "neofetch output");
|
||||
|
||||
if !output.status.success() {
|
||||
let err = if let Some(code) = output.status.code() {
|
||||
anyhow!("neofetch process exited with status code: {code}")
|
||||
} else {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
anyhow!(
|
||||
"neofetch process terminated by signal: {}",
|
||||
output
|
||||
.status
|
||||
.signal()
|
||||
.expect("either one of status code or signal should be set")
|
||||
)
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
unimplemented!("status code not expected to be `None` on non-Unix platforms")
|
||||
};
|
||||
Err(err)?;
|
||||
}
|
||||
process_command_status(&output.status).context("neofetch command exited with error")?;
|
||||
|
||||
let out = String::from_utf8(output.stdout)
|
||||
.context("failed to process neofetch output as it contains invalid UTF-8")?
|
||||
|
|
@ -210,8 +362,67 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn process_command_status(status: &ExitStatus) -> Result<()> {
|
||||
if status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let err = if let Some(code) = status.code() {
|
||||
anyhow!("child process exited with status code: {code}")
|
||||
} else {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
anyhow!(
|
||||
"child process terminated by signal: {}",
|
||||
status
|
||||
.signal()
|
||||
.expect("either one of status code or signal should be set")
|
||||
)
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
unimplemented!("status code not expected to be `None` on non-Unix platforms")
|
||||
};
|
||||
Err(err)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn get_distro_name() -> Result<String> {
|
||||
run_neofetch_command_piped(&["ascii_distro_name"])
|
||||
.context("failed to get distro name from neofetch")
|
||||
}
|
||||
|
||||
/// Runs neofetch with colors.
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn run_neofetch(asc: String, args: Option<&Vec<String>>) -> Result<()> {
|
||||
// Escape backslashes here because backslashes are escaped in neofetch for
|
||||
// printf
|
||||
let asc = asc.replace('\\', r"\\");
|
||||
|
||||
// Write temp file
|
||||
let mut temp_file =
|
||||
NamedTempFile::with_prefix("ascii.txt").context("failed to create temp file for ascii")?;
|
||||
temp_file
|
||||
.write_all(asc.as_bytes())
|
||||
.context("failed to write ascii to temp file")?;
|
||||
|
||||
// Call neofetch with the temp file
|
||||
let temp_file_path = temp_file.into_temp_path();
|
||||
let args = {
|
||||
let mut v = vec![
|
||||
"--ascii",
|
||||
"--source",
|
||||
temp_file_path
|
||||
.to_str()
|
||||
.expect("temp file path should not contain invalid UTF-8"),
|
||||
"--ascii-colors",
|
||||
];
|
||||
if let Some(args) = args {
|
||||
let args: Vec<_> = args.iter().map(|s| &**s).collect();
|
||||
v.extend(args);
|
||||
}
|
||||
v
|
||||
};
|
||||
run_neofetch_command(&args).context("failed to run neofetch command")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,8 +146,9 @@ impl Preset {
|
|||
// sourced from https://www.flagcolorcodes.com/autoromantic
|
||||
Self::Autoromantic => ColorProfile::from_hex_colors(
|
||||
// symbol interpreted
|
||||
vec!["#99D9EA", "#99D9EA", "#3DA542", "#7F7F7F", "#7F7F7F"],
|
||||
),
|
||||
vec!["#99D9EA", "#3DA542", "#7F7F7F"],
|
||||
)
|
||||
.and_then(|c| c.with_weights(vec![2, 1, 2])),
|
||||
|
||||
// sourced from https://www.flagcolorcodes.com/autosexual
|
||||
Self::Autosexual => ColorProfile::from_hex_colors(vec!["#99D9EA", "#7F7F7F"]),
|
||||
|
|
@ -192,15 +193,17 @@ impl Preset {
|
|||
|
||||
// used colorpicker to source form https://www.deviantart.com/pride-flags/art/Demifae-870194777
|
||||
Self::Demifae => ColorProfile::from_hex_colors(vec![
|
||||
"#7F7F7F", "#7F7F7F", "#C5C5C5", "#C5C5C5", "#97C3A4", "#C4DEAE", "#FFFFFF",
|
||||
"#FCA2C5", "#AB7EDF", "#C5C5C5", "#C5C5C5", "#7F7F7F", "#7F7F7F",
|
||||
]),
|
||||
"#7F7F7F", "#C5C5C5", "#97C3A4", "#C4DEAE", "#FFFFFF", "#FCA2C5", "#AB7EDF",
|
||||
"#C5C5C5", "#7F7F7F",
|
||||
])
|
||||
.and_then(|c| c.with_weights(vec![2, 2, 1, 1, 1, 1, 1, 2, 2])),
|
||||
|
||||
// sourced from https://www.flagcolorcodes.com/demifaun
|
||||
Self::Demifaun => ColorProfile::from_hex_colors(vec![
|
||||
"#7F7F7F", "#7F7F7F", "#C6C6C6", "#C6C6C6", "#FCC688", "#FFF19C", "#FFFFFF",
|
||||
"#8DE0D5", "#9682EC", "#C6C6C6", "#C6C6C6", "#7F7F7F", "#7F7F7F",
|
||||
]),
|
||||
"#7F7F7F", "#C6C6C6", "#FCC688", "#FFF19C", "#FFFFFF", "#8DE0D5", "#9682EC",
|
||||
"#C6C6C6", "#7F7F7F",
|
||||
])
|
||||
.and_then(|c| c.with_weights(vec![2, 2, 1, 1, 1, 1, 1, 2, 2])),
|
||||
|
||||
// yellow sourced from https://lgbtqia.fandom.com/f/p/4400000000000041031
|
||||
// other colors sourced from demiboy and demigirl flags
|
||||
|
|
@ -272,9 +275,9 @@ impl Preset {
|
|||
|
||||
// sourced from https://www.flagcolorcodes.com/greygender
|
||||
Self::Greygender => ColorProfile::from_hex_colors(vec![
|
||||
"#B3B3B3", "#B3B3B3", "#FFFFFF", "#062383", "#062383", "#FFFFFF", "#535353",
|
||||
"#535353",
|
||||
]),
|
||||
"#B3B3B3", "#FFFFFF", "#062383", "#FFFFFF", "#535353",
|
||||
])
|
||||
.and_then(|c| c.with_weights(vec![2, 1, 2, 1, 2])),
|
||||
|
||||
// sourced from https://www.flagcolorcodes.com/greysexual
|
||||
Self::Greysexual => ColorProfile::from_hex_colors(vec![
|
||||
|
|
@ -381,7 +384,7 @@ impl Preset {
|
|||
"#FF6692", "#FF9A98", "#FFB883", "#FBFFA8", "#85BCFF", "#9D85FF", "#A510FF",
|
||||
]),
|
||||
})
|
||||
.expect("presets should not be invalid")
|
||||
.expect("preset color profiles should not be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -424,6 +427,46 @@ impl ColorProfile {
|
|||
Ok(Self::new(weighted_colors))
|
||||
}
|
||||
|
||||
/// Creates a new color profile, with the colors spread to the specified
|
||||
/// length.
|
||||
pub fn with_length(&self, length: u8) -> Result<Self> {
|
||||
let orig_len = self.colors.len();
|
||||
let orig_len: u8 = orig_len.try_into().expect("`orig_len` should fit in `u8`");
|
||||
if length < orig_len {
|
||||
unimplemented!("compressing length of color profile not implemented");
|
||||
}
|
||||
let center_i = (orig_len as f32 / 2.0).floor() as usize;
|
||||
|
||||
// How many copies of each color should be displayed at least?
|
||||
let repeats = (length as f32 / orig_len as f32).floor() as usize;
|
||||
let repeats: u8 = repeats.try_into().expect("`repeats` should fit in `u8`");
|
||||
let mut weights: Vec<u8> = iter::repeat(repeats).take(orig_len as usize).collect();
|
||||
|
||||
// How many extra spaces left?
|
||||
let mut extras = length % orig_len;
|
||||
|
||||
// If there is an odd 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)
|
||||
// TODO: this gives a horrible result when `extras` is still large relative to
|
||||
// `orig_len` - we should probably distribute even if slightly uneven
|
||||
let mut border_i = 0;
|
||||
while extras > 0 {
|
||||
extras -= 2;
|
||||
weights[border_i] += 1;
|
||||
let weights_len = weights.len();
|
||||
weights[weights_len - border_i - 1] += 1;
|
||||
border_i += 1;
|
||||
}
|
||||
|
||||
self.with_weights(weights)
|
||||
}
|
||||
|
||||
/// Creates a new color profile, with the colors lightened by a multiplier.
|
||||
pub fn lighten(&self, multiplier: f32) -> Self {
|
||||
let mut rgb_f32_colors: Vec<LinSrgb> =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue