Merge pull request #16 from teohhanhui/riir

Finish implementation of recolor_ascii
This commit is contained in:
Teoh Han Hui 2024-07-08 03:46:13 +08:00 committed by GitHub
commit 1239e86d1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 265 additions and 37 deletions

7
Cargo.lock generated
View file

@ -226,6 +226,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"unicode-normalization", "unicode-normalization",
"unicode-segmentation",
] ]
[[package]] [[package]]
@ -711,6 +712,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View file

@ -34,3 +34,4 @@ thiserror = { version = "1.0.61", default-features = false }
tracing = { version = "0.1.40", default-features = false } tracing = { version = "0.1.40", default-features = false }
tracing-subscriber = { version = "0.3.18", default-features = false } tracing-subscriber = { version = "0.3.18", default-features = false }
unicode-normalization = { version = "0.1.23", default-features = false } unicode-normalization = { version = "0.1.23", default-features = false }
unicode-segmentation = { version = "1.11.0", default-features = false }

View file

@ -29,6 +29,7 @@ tempfile = { workspace = true, features = [] }
thiserror = { workspace = true, features = [] } thiserror = { workspace = true, features = [] }
tracing = { workspace = true, features = ["attributes", "std"] } tracing = { workspace = true, features = ["attributes", "std"] }
tracing-subscriber = { workspace = true, features = ["ansi", "fmt", "smallvec", "std", "tracing-log"] } tracing-subscriber = { workspace = true, features = ["ansi", "fmt", "smallvec", "std", "tracing-log"] }
unicode-segmentation = { workspace = true, features = [] }
[build-dependencies] [build-dependencies]
indexmap = { workspace = true, features = ["std"] } indexmap = { workspace = true, features = ["std"] }

View file

@ -14,7 +14,7 @@ pub struct Config {
pub color_align: ColorAlignment, pub color_align: ColorAlignment,
pub backend: Backend, pub backend: Backend,
#[serde(default)] #[serde(default)]
#[serde(with = "self::args_serde_with")] #[serde(with = "self::args_serde")]
pub args: Option<Vec<String>>, pub args: Option<Vec<String>>,
pub distro: Option<String>, pub distro: Option<String>,
pub pride_month_disable: bool, pub pride_month_disable: bool,
@ -38,16 +38,15 @@ impl Config {
} }
} }
mod args_serde_with { mod args_serde {
use std::fmt; use std::fmt;
use serde::de::{self, value, Deserialize, Deserializer, SeqAccess, Visitor}; use serde::de::{self, value, Deserialize, Deserializer, SeqAccess, Visitor};
use serde::ser::Serializer; use serde::ser::Serializer;
pub(super) fn serialize<S>( type Value = Option<Vec<String>>;
value: &Option<Vec<String>>,
serializer: S, pub(super) fn serialize<S>(value: &Value, serializer: S) -> Result<S::Ok, S::Error>
) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
@ -57,7 +56,7 @@ mod args_serde_with {
} }
} }
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error> pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Value, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
@ -88,7 +87,7 @@ mod args_serde_with {
} }
impl<'de> Visitor<'de> for OptionVisitor { impl<'de> Visitor<'de> for OptionVisitor {
type Value = Option<Vec<String>>; type Value = Value;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("option") formatter.write_str("option")

View file

@ -6,7 +6,7 @@ use std::os::unix::process::ExitStatusExt as _;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus}; use std::process::{Command, ExitStatus};
use std::sync::OnceLock; use std::sync::OnceLock;
use std::{env, fmt, iter}; use std::{env, fmt};
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
@ -37,6 +37,7 @@ pub enum ColorAlignment {
}, },
Custom { Custom {
#[serde(rename = "custom_colors")] #[serde(rename = "custom_colors")]
#[serde(deserialize_with = "crate::utils::index_map_serde::deserialize")]
colors: IndexMap<NeofetchAsciiIndexedColor, PresetIndexedColor>, colors: IndexMap<NeofetchAsciiIndexedColor, PresetIndexedColor>,
}, },
} }
@ -83,11 +84,14 @@ impl ColorAlignment {
// Add new colors // Add new colors
let asc = match self { let asc = match self {
Self::Horizontal { .. } => { Self::Horizontal { .. } => {
let length = lines.len(); let ColorProfile { colors } = {
let length: u8 = length.try_into().expect("`length` should fit in `u8`"); let length = lines.len();
let ColorProfile { colors } = color_profile let length: u8 =
.with_length(length) length.try_into().expect("`length` should fit in `u8`");
.context("failed to spread color profile to length")?; color_profile
.with_length(length)
.context("failed to spread color profile to length")?
};
let mut asc = String::new(); let mut asc = String::new();
for (i, line) in lines.into_iter().enumerate() { for (i, line) in lines.into_iter().enumerate() {
let line = line.replace( let line = line.replace(
@ -119,21 +123,91 @@ impl ColorAlignment {
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());
const N: usize = NEOFETCH_COLOR_PATTERNS.len(); const N: usize = NEOFETCH_COLOR_PATTERNS.len();
let replacements: [&str; N] = iter::repeat("") const REPLACEMENTS: [&str; N] = [""; N];
.take(N) ac.replace_all(&asc, &REPLACEMENTS)
.collect::<Vec<_>>()
.try_into()
.unwrap();
ac.replace_all(&asc, &replacements)
}; };
asc asc
}, },
Self::Horizontal { fore_back: None } | Self::Vertical { fore_back: None } => { Self::Horizontal { fore_back: None } | Self::Vertical { fore_back: None } => {
todo!() // 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();
const REPLACEMENTS: [&str; N] = [""; N];
ac.replace_all(&asc, &REPLACEMENTS)
};
let lines: Vec<_> = asc.split('\n').collect();
// Add new colors
match self {
Self::Horizontal { .. } => {
let ColorProfile { colors } = {
let length = lines.len();
let length: u8 =
length.try_into().expect("`length` should fit in `u8`");
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() {
asc.push_str(
&colors[i]
.to_ansi_string(color_mode, ForegroundBackground::Foreground),
);
asc.push_str(line);
asc.push_str(&reset);
asc.push('\n');
}
asc
},
Self::Vertical { .. } => {
let mut asc = String::new();
for line in lines {
let line = color_profile
.color_text(
line,
color_mode,
ForegroundBackground::Foreground,
false,
)
.context("failed to color text using color profile")?;
asc.push_str(&line);
asc.push_str(&reset);
asc.push('\n');
}
asc
},
_ => {
unreachable!();
},
}
}, },
Self::Custom { colors } => { Self::Custom {
todo!() colors: custom_colors,
} => {
let ColorProfile { colors } = color_profile.unique_colors();
// Apply 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 mut replacements = vec![Cow::from(""); N];
for (&ai, &pi) in custom_colors {
let ai: u8 = ai.into();
let pi: u8 = pi.into();
replacements[ai as usize - 1] = colors[pi as usize]
.to_ansi_string(color_mode, ForegroundBackground::Foreground)
.into();
}
ac.replace_all(&asc, &replacements)
};
asc
}, },
}; };
@ -193,11 +267,22 @@ where
}; };
debug!(%distro, "distro name"); debug!(%distro, "distro name");
// Try new codegen-based detection method
if let Some(distro) = Distro::detect(&distro) { if let Some(distro) = Distro::detect(&distro) {
return Ok(normalize_ascii(distro.ascii_art())); return Ok(normalize_ascii(distro.ascii_art()));
} }
todo!() debug!(%distro, "could not find a match for distro; falling back to neofetch");
// Old detection method that calls neofetch
let asc = run_neofetch_command_piped(&["print_ascii", "--ascii_distro", distro.as_ref()])
.context("failed to get ascii art from neofetch")?;
// Unescape backslashes here because backslashes are escaped in neofetch for
// printf
let asc = asc.replace(r"\\", r"\");
Ok(normalize_ascii(asc))
} }
#[tracing::instrument(level = "debug")] #[tracing::instrument(level = "debug")]
@ -231,12 +316,8 @@ where
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());
const N: usize = NEOFETCH_COLOR_PATTERNS.len(); const N: usize = NEOFETCH_COLOR_PATTERNS.len();
let replacements: [&str; N] = iter::repeat("") const REPLACEMENTS: [&str; N] = [""; N];
.take(N) ac.replace_all(asc, &REPLACEMENTS)
.collect::<Vec<_>>()
.try_into()
.unwrap();
ac.replace_all(asc, &replacements)
}; };
let Some(width) = asc.split('\n').map(|line| line.len()).max() else { let Some(width) = asc.split('\n').map(|line| line.len()).max() else {

View file

@ -7,9 +7,10 @@ use palette::num::ClampAssign;
use palette::{Hsl, IntoColorMut, LinSrgb, Srgb}; use palette::{Hsl, IntoColorMut, LinSrgb, Srgb};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{EnumString, VariantNames}; use strum::{EnumString, VariantNames};
use unicode_segmentation::UnicodeSegmentation;
use crate::color_util::Lightness; use crate::color_util::{ForegroundBackground, Lightness, ToAnsiString};
use crate::types::LightDark; use crate::types::{AnsiMode, LightDark};
#[derive(Copy, Clone, Hash, Debug, Deserialize, EnumString, Serialize, VariantNames)] #[derive(Copy, Clone, Hash, Debug, Deserialize, EnumString, Serialize, VariantNames)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@ -290,10 +291,10 @@ impl Preset {
}, },
// sourced from https://www.flagcolorcodes.com/intergender // sourced from https://www.flagcolorcodes.com/intergender
Self::Intergender => ColorProfile::from_hex_colors( Self::Intergender => {
// todo: use weighted spacing ColorProfile::from_hex_colors(vec!["#900DC2", "#FFE54F", "#900DC2"])
vec!["#900DC2", "#900DC2", "#FFE54F", "#900DC2", "#900DC2"], .and_then(|c| c.with_weights(vec![2, 1, 2]))
), },
Self::Lesbian => ColorProfile::from_hex_colors(vec![ Self::Lesbian => ColorProfile::from_hex_colors(vec![
"#D62800", "#FF9B56", "#FFFFFF", "#D462A6", "#A40062", "#D62800", "#FF9B56", "#FFFFFF", "#D462A6", "#A40062",
@ -440,7 +441,7 @@ impl ColorProfile {
// How many copies of each color should be displayed at least? // 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 = (length as f32 / orig_len as f32).floor() as usize;
let repeats: u8 = repeats.try_into().expect("`repeats` should fit in `u8`"); 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(); let mut weights = vec![repeats; orig_len as usize];
// How many extra spaces left? // How many extra spaces left?
let mut extras = length % orig_len; let mut extras = length % orig_len;
@ -467,6 +468,50 @@ impl ColorProfile {
self.with_weights(weights) self.with_weights(weights)
} }
/// Colors a text.
///
/// # Arguments
///
/// * `foreground_background` - Whether the color is shown on the foreground
/// text or the background block
/// * `space_only` - Whether to only color spaces
pub fn color_text<S>(
&self,
txt: S,
color_mode: AnsiMode,
foreground_background: ForegroundBackground,
space_only: bool,
) -> Result<String>
where
S: AsRef<str>,
{
let txt = txt.as_ref();
let ColorProfile { colors } = {
let length = txt.len();
let length: u8 = length.try_into().expect("`length` should fit in `u8`");
self.with_length(length)
.context("failed to spread color profile to length")?
};
let mut buf = String::new();
let txt: Vec<&str> = txt.graphemes(true).collect();
for (i, &gr) in txt.iter().enumerate() {
if space_only && gr != " " {
if i > 0 && txt[i - 1] == " " {
buf.push_str("\x1b[39;49m");
}
buf.push_str(gr);
} else {
buf.push_str(&colors[i].to_ansi_string(color_mode, foreground_background));
buf.push_str(gr);
}
}
buf.push_str("\x1b[39;49m");
Ok(buf)
}
/// Creates a new color profile, with the colors lightened by a multiplier. /// Creates a new color profile, with the colors lightened by a multiplier.
pub fn lighten(&self, multiplier: f32) -> Self { pub fn lighten(&self, multiplier: f32) -> Self {
let mut rgb_f32_colors: Vec<LinSrgb> = let mut rgb_f32_colors: Vec<LinSrgb> =

View file

@ -10,3 +10,97 @@ pub fn get_cache_path() -> Result<PathBuf> {
.to_owned(); .to_owned();
Ok(path) Ok(path)
} }
pub(crate) mod index_map_serde {
use std::fmt;
use std::fmt::Display;
use std::hash::Hash;
use std::marker::PhantomData;
use std::str::FromStr;
use indexmap::IndexMap;
use serde::de::{self, DeserializeSeed, MapAccess, Visitor};
use serde::{Deserialize, Deserializer};
pub(crate) fn deserialize<'de, D, K, V>(deserializer: D) -> Result<IndexMap<K, V>, D::Error>
where
D: Deserializer<'de>,
K: Eq + Hash + FromStr,
K::Err: Display,
V: Deserialize<'de>,
{
struct KeySeed<K> {
k: PhantomData<K>,
}
impl<'de, K> DeserializeSeed<'de> for KeySeed<K>
where
K: FromStr,
K::Err: Display,
{
type Value = K;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(self)
}
}
impl<'de, K> Visitor<'de> for KeySeed<K>
where
K: FromStr,
K::Err: Display,
{
type Value = K;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
K::from_str(s).map_err(de::Error::custom)
}
}
struct MapVisitor<K, V> {
k: PhantomData<K>,
v: PhantomData<V>,
}
impl<'de, K, V> Visitor<'de> for MapVisitor<K, V>
where
K: Eq + Hash + FromStr,
K::Err: Display,
V: Deserialize<'de>,
{
type Value = IndexMap<K, V>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map")
}
fn visit_map<A>(self, mut input: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut map = IndexMap::new();
while let Some((k, v)) =
input.next_entry_seed(KeySeed { k: PhantomData }, PhantomData)?
{
map.insert(k, v);
}
Ok(map)
}
}
deserializer.deserialize_map(MapVisitor {
k: PhantomData,
v: PhantomData,
})
}
}