[+] 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::borrow::Cow;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{self, IsTerminal as _, Read as _, Write};
|
use std::io::{self, IsTerminal as _, Read as _, Write};
|
||||||
@@ -23,7 +24,7 @@ use hyfetch::color_util::{
|
|||||||
NeofetchAsciiIndexedColor, PresetIndexedColor, Theme as _, ToAnsiString as _,
|
NeofetchAsciiIndexedColor, PresetIndexedColor, Theme as _, ToAnsiString as _,
|
||||||
};
|
};
|
||||||
use hyfetch::distros::Distro;
|
use hyfetch::distros::Distro;
|
||||||
use hyfetch::models::Config;
|
use hyfetch::models::{build_hex_color_profile, Config};
|
||||||
#[cfg(feature = "macchina")]
|
#[cfg(feature = "macchina")]
|
||||||
use hyfetch::neofetch_util::macchina_path;
|
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};
|
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 backend = options.backend.unwrap_or(config.backend);
|
||||||
let args = options.args.as_ref().or(config.args.as_ref());
|
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('#') {
|
if preset_string.contains('#') {
|
||||||
let colors: Vec<&str> = preset_string.split(',').map(|s| s.trim()).collect();
|
let colors: Vec<String> = preset_string
|
||||||
for color in &colors {
|
.split(',')
|
||||||
if !color.starts_with('#') ||
|
.map(|s| s.trim().to_owned())
|
||||||
(color.len() != 4 && color.len() != 7) ||
|
.collect();
|
||||||
!color[1..].chars().all(|c| c.is_ascii_hexdigit()) {
|
let color_profile = build_hex_color_profile(&colors)
|
||||||
return Err(anyhow::anyhow!("invalid hex color: {}", color));
|
.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 mut rng = fastrand::Rng::new();
|
||||||
let preset = *rng
|
let selected_index = rng.usize(0..presets.len());
|
||||||
.choice(<Preset as VariantArray>::VARIANTS)
|
return Ok(presets[selected_index].clone());
|
||||||
.expect("preset iterator should not be empty");
|
}
|
||||||
Ok(preset.color_profile())
|
|
||||||
|
if let Some(color_profile) = preset_profiles.get(preset_string) {
|
||||||
|
Ok(color_profile.clone())
|
||||||
} else {
|
} else {
|
||||||
use std::str::FromStr;
|
let presets = preset_profiles
|
||||||
let preset = Preset::from_str(preset_string)
|
.keys()
|
||||||
.with_context(|| {
|
.map(String::as_str)
|
||||||
format!(
|
.chain(iter::once("random"))
|
||||||
"PRESET should be comma-separated hex colors or one of {{{presets}}}",
|
.sorted()
|
||||||
presets = <Preset as VariantNames>::VARIANTS
|
.join(",");
|
||||||
.iter()
|
Err(anyhow::anyhow!(
|
||||||
.chain(iter::once(&"random"))
|
"PRESET should be comma-separated hex colors or one of {{{presets}}}"
|
||||||
.join(",")
|
))
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Ok(preset.color_profile())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get preset
|
// Get preset
|
||||||
let preset_string = options.preset.as_deref().unwrap_or(&config.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");
|
debug!(?color_profile, "color profile");
|
||||||
|
|
||||||
// Lighten
|
// Lighten
|
||||||
@@ -1219,6 +1229,7 @@ fn create_config(
|
|||||||
distro: logo_chosen,
|
distro: logo_chosen,
|
||||||
pride_month_disable: false,
|
pride_month_disable: false,
|
||||||
custom_ascii_path,
|
custom_ascii_path,
|
||||||
|
custom_presets: None,
|
||||||
};
|
};
|
||||||
debug!(?config, "created config");
|
debug!(?config, "created config");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::color_profile::ColorProfile;
|
||||||
use crate::color_util::Lightness;
|
use crate::color_util::Lightness;
|
||||||
use crate::neofetch_util::ColorAlignment;
|
use crate::neofetch_util::ColorAlignment;
|
||||||
use crate::types::{AnsiMode, Backend, TerminalTheme};
|
use crate::types::{AnsiMode, Backend, TerminalTheme};
|
||||||
@@ -19,6 +23,7 @@ pub struct Config {
|
|||||||
pub distro: Option<String>,
|
pub distro: Option<String>,
|
||||||
pub pride_month_disable: bool,
|
pub pride_month_disable: bool,
|
||||||
pub custom_ascii_path: Option<String>,
|
pub custom_ascii_path: Option<String>,
|
||||||
|
pub custom_presets: Option<HashMap<String, Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
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 {
|
mod args_serde {
|
||||||
|
|||||||
+29
-18
@@ -17,7 +17,7 @@ from .color_scale import Scale
|
|||||||
from .color_util import clear_screen
|
from .color_util import clear_screen
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .font_logo import get_font_logo
|
from .font_logo import get_font_logo
|
||||||
from .models import Config
|
from .models import build_hex_color_profile, Config
|
||||||
from .neofetch_util import *
|
from .neofetch_util import *
|
||||||
from .presets import PRESETS, ColorProfile
|
from .presets import PRESETS, ColorProfile
|
||||||
|
|
||||||
@@ -555,30 +555,41 @@ def run():
|
|||||||
config.backend = args.backend
|
config.backend = args.backend
|
||||||
if args.args:
|
if args.args:
|
||||||
config.args = args.args
|
config.args = args.args
|
||||||
|
|
||||||
# Random preset
|
|
||||||
if config.preset == 'random':
|
|
||||||
config.preset = random.choice(list(PRESETS.keys()))
|
|
||||||
|
|
||||||
# Override global color mode
|
# Override global color mode
|
||||||
GLOBAL_CFG.color_mode = config.mode
|
GLOBAL_CFG.color_mode = config.mode
|
||||||
GLOBAL_CFG.is_light = config.light_dark == 'light'
|
GLOBAL_CFG.is_light = config.light_dark == 'light'
|
||||||
|
|
||||||
# Get preset
|
# Get preset
|
||||||
preset = None
|
def parse_preset_string(preset_string: str, config: Config) -> ColorProfile:
|
||||||
if config.preset in PRESETS:
|
if '#' in preset_string:
|
||||||
preset = PRESETS.get(config.preset)
|
colors = [s.strip() for s in preset_string.split(',')]
|
||||||
elif '#' in config.preset:
|
return build_hex_color_profile(colors)
|
||||||
colors = [color.strip() for color in config.preset.split(',')]
|
|
||||||
|
|
||||||
for color in colors:
|
preset_profiles: dict[str, ColorProfile] = {
|
||||||
if not (color.startswith('#') and len(color) in [4, 7] and all(c in '0123456789abcdefABCDEF' for c in color[1:])):
|
name: preset for name, preset in PRESETS.items()
|
||||||
print(f'Error: invalid hex color "{color}"')
|
}
|
||||||
preset = ColorProfile(colors)
|
preset_profiles.update(config.custom_preset_profiles())
|
||||||
else:
|
|
||||||
print(f'Preset should be a comma-separated list of hex colors, or one of the following: {", ".join(sorted(PRESETS.keys()))}')
|
|
||||||
|
|
||||||
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)
|
exit(1)
|
||||||
|
|
||||||
# Lighten (args > config)
|
# Lighten (args > config)
|
||||||
|
|||||||
@@ -4,10 +4,25 @@ from dataclasses import dataclass, field
|
|||||||
|
|
||||||
from .constants import CONFIG_PATH
|
from .constants import CONFIG_PATH
|
||||||
from .neofetch_util import ColorAlignment
|
from .neofetch_util import ColorAlignment
|
||||||
|
from .presets import ColorProfile
|
||||||
from .serializer import json_stringify, from_dict
|
from .serializer import json_stringify, from_dict
|
||||||
from .types import AnsiMode, LightDark, BackendLiteral
|
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
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
preset: str
|
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_shown: list[int] = field(default_factory=list) # This is deprecated, see issue #136
|
||||||
pride_month_disable: bool = False
|
pride_month_disable: bool = False
|
||||||
custom_ascii_path: str | None = None
|
custom_ascii_path: str | None = None
|
||||||
|
custom_presets: dict[str, list[str]] | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, d: dict):
|
def from_dict(cls, d: dict):
|
||||||
@@ -30,3 +46,17 @@ class Config:
|
|||||||
def save(self):
|
def save(self):
|
||||||
CONFIG_PATH.parent.mkdir(exist_ok=True, parents=True)
|
CONFIG_PATH.parent.mkdir(exist_ok=True, parents=True)
|
||||||
CONFIG_PATH.write_text(json_stringify(self, indent=4), 'utf-8')
|
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