[+] Add custom_presets config option (#481)
* [+] Add custom_presets config option * [F] Resolve Copilot issues
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
use std::cmp;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, IsTerminal as _, Read as _, Write};
|
||||
@@ -23,7 +24,7 @@ use hyfetch::color_util::{
|
||||
NeofetchAsciiIndexedColor, PresetIndexedColor, Theme as _, ToAnsiString as _,
|
||||
};
|
||||
use hyfetch::distros::Distro;
|
||||
use hyfetch::models::Config;
|
||||
use hyfetch::models::{build_hex_color_profile, Config};
|
||||
#[cfg(feature = "macchina")]
|
||||
use hyfetch::neofetch_util::macchina_path;
|
||||
use hyfetch::neofetch_util::{self, add_pkg_path, fastfetch_path, get_distro_ascii, get_distro_name, literal_input, ColorAlignment, NEOFETCH_COLORS_AC, NEOFETCH_COLOR_PATTERNS, TEST_ASCII};
|
||||
@@ -135,43 +136,52 @@ fn main() -> Result<()> {
|
||||
let backend = options.backend.unwrap_or(config.backend);
|
||||
let args = options.args.as_ref().or(config.args.as_ref());
|
||||
|
||||
fn parse_preset_string(preset_string: &str) -> Result<ColorProfile> {
|
||||
fn parse_preset_string(preset_string: &str, config: &Config) -> Result<ColorProfile> {
|
||||
if preset_string.contains('#') {
|
||||
let colors: Vec<&str> = preset_string.split(',').map(|s| s.trim()).collect();
|
||||
for color in &colors {
|
||||
if !color.starts_with('#') ||
|
||||
(color.len() != 4 && color.len() != 7) ||
|
||||
!color[1..].chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(anyhow::anyhow!("invalid hex color: {}", color));
|
||||
}
|
||||
let colors: Vec<String> = preset_string
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_owned())
|
||||
.collect();
|
||||
let color_profile = build_hex_color_profile(&colors)
|
||||
.context("failed to create color profile from hex")?;
|
||||
return Ok(color_profile);
|
||||
}
|
||||
|
||||
let mut preset_profiles: HashMap<String, ColorProfile> = <Preset as VariantArray>::VARIANTS
|
||||
.iter()
|
||||
.map(|preset| (preset.as_ref().to_owned(), preset.color_profile()))
|
||||
.collect();
|
||||
preset_profiles.extend(config.custom_preset_profiles()?);
|
||||
|
||||
let presets: Vec<ColorProfile> = preset_profiles.values().cloned().collect();
|
||||
|
||||
if preset_string == "random" {
|
||||
if presets.is_empty() {
|
||||
return Err(anyhow::anyhow!("preset iterator should not be empty"));
|
||||
}
|
||||
ColorProfile::from_hex_colors(colors)
|
||||
.context("failed to create color profile from hex")
|
||||
} else if preset_string == "random" {
|
||||
let mut rng = fastrand::Rng::new();
|
||||
let preset = *rng
|
||||
.choice(<Preset as VariantArray>::VARIANTS)
|
||||
.expect("preset iterator should not be empty");
|
||||
Ok(preset.color_profile())
|
||||
let selected_index = rng.usize(0..presets.len());
|
||||
return Ok(presets[selected_index].clone());
|
||||
}
|
||||
|
||||
if let Some(color_profile) = preset_profiles.get(preset_string) {
|
||||
Ok(color_profile.clone())
|
||||
} else {
|
||||
use std::str::FromStr;
|
||||
let preset = Preset::from_str(preset_string)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"PRESET should be comma-separated hex colors or one of {{{presets}}}",
|
||||
presets = <Preset as VariantNames>::VARIANTS
|
||||
.iter()
|
||||
.chain(iter::once(&"random"))
|
||||
.join(",")
|
||||
)
|
||||
})?;
|
||||
Ok(preset.color_profile())
|
||||
let presets = preset_profiles
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.chain(iter::once("random"))
|
||||
.sorted()
|
||||
.join(",");
|
||||
Err(anyhow::anyhow!(
|
||||
"PRESET should be comma-separated hex colors or one of {{{presets}}}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Get preset
|
||||
let preset_string = options.preset.as_deref().unwrap_or(&config.preset);
|
||||
let color_profile = parse_preset_string(preset_string)?;
|
||||
let color_profile = parse_preset_string(preset_string, &config)?;
|
||||
debug!(?color_profile, "color profile");
|
||||
|
||||
// Lighten
|
||||
@@ -1219,6 +1229,7 @@ fn create_config(
|
||||
distro: logo_chosen,
|
||||
pride_month_disable: false,
|
||||
custom_ascii_path,
|
||||
custom_presets: None,
|
||||
};
|
||||
debug!(?config, "created config");
|
||||
|
||||
@@ -1297,4 +1308,4 @@ fn init_tracing_subsriber(debug_mode: bool) -> Result<()> {
|
||||
subscriber
|
||||
.try_init()
|
||||
.context("failed to set the global default subscriber")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::color_profile::ColorProfile;
|
||||
use crate::color_util::Lightness;
|
||||
use crate::neofetch_util::ColorAlignment;
|
||||
use crate::types::{AnsiMode, Backend, TerminalTheme};
|
||||
@@ -19,6 +23,7 @@ pub struct Config {
|
||||
pub distro: Option<String>,
|
||||
pub pride_month_disable: bool,
|
||||
pub custom_ascii_path: Option<String>,
|
||||
pub custom_presets: Option<HashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -32,6 +37,40 @@ impl Config {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn custom_preset_profiles(&self) -> Result<HashMap<String, ColorProfile>> {
|
||||
let mut profiles = HashMap::new();
|
||||
if let Some(custom_presets) = &self.custom_presets {
|
||||
for (preset_name, colors) in custom_presets {
|
||||
if preset_name == "random" {
|
||||
return Err(anyhow::anyhow!("custom preset key `random` is reserved"));
|
||||
}
|
||||
let color_profile = build_hex_color_profile(colors).with_context(|| {
|
||||
format!("failed to validate custom preset key `{preset_name}`")
|
||||
})?;
|
||||
profiles.insert(preset_name.clone(), color_profile);
|
||||
}
|
||||
}
|
||||
Ok(profiles)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_hex_color_profile(hex_colors: &[String]) -> Result<ColorProfile> {
|
||||
if hex_colors.is_empty() {
|
||||
return Err(anyhow::anyhow!("hex color list cannot be empty"));
|
||||
}
|
||||
|
||||
for color in hex_colors {
|
||||
if !color.starts_with('#')
|
||||
|| (color.len() != 4 && color.len() != 7)
|
||||
|| !color[1..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
{
|
||||
return Err(anyhow::anyhow!("invalid hex color: {color}"));
|
||||
}
|
||||
}
|
||||
|
||||
ColorProfile::from_hex_colors(hex_colors.to_vec())
|
||||
.context("failed to create color profile from hex")
|
||||
}
|
||||
|
||||
mod args_serde {
|
||||
|
||||
+29
-18
@@ -17,7 +17,7 @@ from .color_scale import Scale
|
||||
from .color_util import clear_screen
|
||||
from .constants import *
|
||||
from .font_logo import get_font_logo
|
||||
from .models import Config
|
||||
from .models import build_hex_color_profile, Config
|
||||
from .neofetch_util import *
|
||||
from .presets import PRESETS, ColorProfile
|
||||
|
||||
@@ -555,30 +555,41 @@ def run():
|
||||
config.backend = args.backend
|
||||
if args.args:
|
||||
config.args = args.args
|
||||
|
||||
# Random preset
|
||||
if config.preset == 'random':
|
||||
config.preset = random.choice(list(PRESETS.keys()))
|
||||
|
||||
# Override global color mode
|
||||
GLOBAL_CFG.color_mode = config.mode
|
||||
GLOBAL_CFG.is_light = config.light_dark == 'light'
|
||||
|
||||
# Get preset
|
||||
preset = None
|
||||
if config.preset in PRESETS:
|
||||
preset = PRESETS.get(config.preset)
|
||||
elif '#' in config.preset:
|
||||
colors = [color.strip() for color in config.preset.split(',')]
|
||||
def parse_preset_string(preset_string: str, config: Config) -> ColorProfile:
|
||||
if '#' in preset_string:
|
||||
colors = [s.strip() for s in preset_string.split(',')]
|
||||
return build_hex_color_profile(colors)
|
||||
|
||||
for color in colors:
|
||||
if not (color.startswith('#') and len(color) in [4, 7] and all(c in '0123456789abcdefABCDEF' for c in color[1:])):
|
||||
print(f'Error: invalid hex color "{color}"')
|
||||
preset = ColorProfile(colors)
|
||||
else:
|
||||
print(f'Preset should be a comma-separated list of hex colors, or one of the following: {", ".join(sorted(PRESETS.keys()))}')
|
||||
preset_profiles: dict[str, ColorProfile] = {
|
||||
name: preset for name, preset in PRESETS.items()
|
||||
}
|
||||
preset_profiles.update(config.custom_preset_profiles())
|
||||
|
||||
if preset is None:
|
||||
presets = list(preset_profiles.values())
|
||||
|
||||
if preset_string == 'random':
|
||||
if not presets:
|
||||
raise ValueError('preset iterator should not be empty')
|
||||
return random.choice(presets)
|
||||
|
||||
if preset_string in preset_profiles:
|
||||
return preset_profiles[preset_string]
|
||||
|
||||
available = sorted([*preset_profiles.keys(), 'random'])
|
||||
raise ValueError(
|
||||
"PRESET should be comma-separated hex colors or one of "
|
||||
f"{{{','.join(available)}}}"
|
||||
)
|
||||
|
||||
try:
|
||||
preset = parse_preset_string(config.preset, config)
|
||||
except ValueError as err:
|
||||
print(f'Error: {err}')
|
||||
exit(1)
|
||||
|
||||
# Lighten (args > config)
|
||||
|
||||
@@ -4,10 +4,25 @@ from dataclasses import dataclass, field
|
||||
|
||||
from .constants import CONFIG_PATH
|
||||
from .neofetch_util import ColorAlignment
|
||||
from .presets import ColorProfile
|
||||
from .serializer import json_stringify, from_dict
|
||||
from .types import AnsiMode, LightDark, BackendLiteral
|
||||
|
||||
|
||||
def build_hex_color_profile(hex_colors: list[str]) -> ColorProfile:
|
||||
if not hex_colors:
|
||||
raise ValueError('hex color list cannot be empty')
|
||||
|
||||
for color in hex_colors:
|
||||
if not (
|
||||
color.startswith('#')
|
||||
and len(color) in [4, 7]
|
||||
and all(c in '0123456789abcdefABCDEF' for c in color[1:])
|
||||
):
|
||||
raise ValueError(f'invalid hex color: {color}')
|
||||
return ColorProfile(hex_colors)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
preset: str
|
||||
@@ -21,6 +36,7 @@ class Config:
|
||||
pride_month_shown: list[int] = field(default_factory=list) # This is deprecated, see issue #136
|
||||
pride_month_disable: bool = False
|
||||
custom_ascii_path: str | None = None
|
||||
custom_presets: dict[str, list[str]] | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict):
|
||||
@@ -30,3 +46,17 @@ class Config:
|
||||
def save(self):
|
||||
CONFIG_PATH.parent.mkdir(exist_ok=True, parents=True)
|
||||
CONFIG_PATH.write_text(json_stringify(self, indent=4), 'utf-8')
|
||||
|
||||
def custom_preset_profiles(self) -> dict[str, ColorProfile]:
|
||||
profiles: dict[str, ColorProfile] = {}
|
||||
if self.custom_presets:
|
||||
for preset_name, colors in self.custom_presets.items():
|
||||
if preset_name == 'random':
|
||||
raise ValueError('custom preset key "random" is reserved')
|
||||
try:
|
||||
profiles[preset_name] = build_hex_color_profile(colors)
|
||||
except ValueError as err:
|
||||
raise ValueError(
|
||||
f'failed to validate custom preset key "{preset_name}": {err}'
|
||||
) from err
|
||||
return profiles
|
||||
|
||||
Reference in New Issue
Block a user