Implement recolored ascii output

This commit is contained in:
Teoh Han Hui 2024-07-07 04:39:41 +08:00
parent ff46c8f4ca
commit be70233b03
No known key found for this signature in database
GPG key ID: D43E2BABAF97DCAE
8 changed files with 694 additions and 118 deletions

155
Cargo.lock generated
View file

@ -26,6 +26,12 @@ dependencies = [
"libc",
]
[[package]]
name = "ansi_colours"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1558bd2075d341b9ca698ec8eb6fcc55a746b1fc4255585aad5b141d918a80"
[[package]]
name = "anyhow"
version = "1.0.86"
@ -114,26 +120,6 @@ dependencies = [
"serde",
]
[[package]]
name = "derive_more"
version = "1.0.0-beta.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7abbfc297053be59290e3152f8cbcd52c8642e0728b69ee187d991d4c1af08d"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "1.0.0-beta.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bba3e9872d7c58ce7ef0fcf1844fcc3e23ef2a58377b50df35dd98e42a5726e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "directories"
version = "5.0.1"
@ -152,7 +138,16 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "enable-ansi-support"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4ff3ae2a9aa54bf7ee0983e59303224de742818c1822d89f07da9856d9bc60"
dependencies = [
"windows-sys 0.42.0",
]
[[package]]
@ -161,12 +156,28 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]]
name = "fastrand"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "getrandom"
version = "0.2.15"
@ -194,12 +205,14 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
name = "hyfetch"
version = "1.4.11"
dependencies = [
"aho-corasick",
"ansi_colours",
"anyhow",
"bpaf",
"chrono",
"deranged",
"derive_more",
"directories",
"enable-ansi-support",
"indexmap",
"palette",
"regex",
@ -208,6 +221,7 @@ dependencies = [
"serde_path_to_error",
"shell-words",
"strum",
"tempfile",
"thiserror",
"tracing",
"tracing-subscriber",
@ -291,6 +305,12 @@ dependencies = [
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "log"
version = "0.4.21"
@ -433,6 +453,19 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.17"
@ -549,6 +582,18 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"rustix",
"windows-sys 0.52.0",
]
[[package]]
name = "thiserror"
version = "1.0.61"
@ -763,6 +808,21 @@ dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -772,6 +832,15 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@ -803,6 +872,12 @@ dependencies = [
"windows_x86_64_msvc 0.52.5",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@ -815,6 +890,12 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@ -827,6 +908,12 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@ -845,6 +932,12 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@ -857,6 +950,12 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@ -869,6 +968,12 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@ -881,6 +986,12 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"

View file

@ -12,23 +12,24 @@ repository = "https://github.com/hykilpikonna/hyfetch"
license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.3", default-features = false }
ansi_colours = { version = "1.2.2", default-features = false }
# anstream = { version = "0.6.14", default-features = false }
anyhow = { version = "1.0.86", default-features = false }
bpaf = { version = "0.9.12", default-features = false }
bytemuck = { version = "1.16.1", default-features = false }
chrono = { version = "0.4.38", default-features = false }
deranged = { version = "0.3.11", default-features = false }
derive_more = { version = "1.0.0-beta.6", default-features = false }
directories = { version = "5.0.1", default-features = false }
enable-ansi-support = { version = "0.2.1", default-features = false }
indexmap = { version = "2.2.6", default-features = false }
palette = { version = "0.7.6", default-features = false }
regex = { version = "1.10.5", default-features = false }
rgb = { version = "0.8.37", default-features = false }
serde = { version = "1.0.203", default-features = false }
serde_json = { version = "1.0.118", default-features = false }
serde_path_to_error = { version = "0.1.16", default-features = false }
shell-words = { version = "1.1.0", default-features = false }
strum = { version = "0.26.3", default-features = false }
tempfile = { version = "3.10.1", default-features = false }
thiserror = { version = "1.0.61", default-features = false }
tracing = { version = "0.1.40", default-features = false }
tracing-subscriber = { version = "0.3.18", default-features = false }

View file

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

View file

@ -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 {

View file

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

View file

@ -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")
},
}
}

View file

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

View file

@ -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> =