Compare commits

...

15 Commits

Author SHA1 Message Date
azalea 15ca855a04 [O] Shorten 2025-10-02 02:11:58 +08:00
azalea 666d2dc90a Update hyfetch.rs 2025-10-02 01:33:25 +08:00
azalea 34583294c6 [O] Make code more readable 2025-10-02 01:25:07 +08:00
azalea 03615ab4ee [O] Make code more readable 2025-10-02 01:06:39 +08:00
azalea c722c73e79 [O] shorten code 2025-10-02 00:44:05 +08:00
azalea 8f5199974b [F] Fix windows build #439 2025-10-02 00:43:25 +08:00
azalea 8168877fb1 [U] Update crates 2025-10-02 00:40:05 +08:00
azalea 4e20b18d45 [U] Upgrade deps 2025-10-01 23:58:01 +08:00
thea 5dc1709f58 [+] Allow passing hex colors as preset (#435) 2025-10-01 08:13:09 -07:00
Un1q32 fb1e35172e Support old Apple TV models (#438) 2025-10-01 08:12:37 -07:00
ObsoleteDev fc9292be3f 🌈 Support custom ASCII art file path (#429)
* Feature: Add custom ascii file saving to python version of hyfetch

* Feature: Add custom ascii file saving to rust version of hyfetch

* [-] Remove test ascii

---------

Co-authored-by: Azalea <22280294+hykilpikonna@users.noreply.github.com>
2025-10-01 08:12:05 -07:00
ObsoleteDev 3f41cb40e2 [+] Add fluidflux flags (#437)
* [+] Add fluidflux flags to py

* [+] Add fluidflux flags rust
2025-09-18 21:48:54 -07:00
ObsoleteDev 729024a45f [+] libragender flags (#433)
* [+] libragender flags py

* [+] libragender flags rust
2025-09-13 02:21:01 +09:00
ObsoleteDev ef1407d00e [F] Only mark pride month easter egg as displayed when its june (rust) (#430)
fix(rust): Only mark pride month easter egg as displayed when its june
2025-09-10 07:49:32 -07:00
Thundertides 075fc467d2 Temporary fix to GH-399 (#428) 2025-09-07 09:59:02 -07:00
16 changed files with 734 additions and 588 deletions
Generated
+323 -242
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -17,14 +17,14 @@ 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 }
crossterm = { version = "0.27.0", default-features = false }
crossterm = { version = "0.29.0", default-features = false }
deranged = { version = "0.3.11", default-features = false }
directories = { version = "5.0.1", default-features = false }
directories = { version = "6.0.0", default-features = false }
enable-ansi-support = { version = "0.2.1", default-features = false }
enterpolation = { version = "0.2.1", default-features = false }
fastrand = { version = "2.1.0", default-features = false }
indexmap = { version = "2.2.6", default-features = false }
itertools = { version = "0.13.0", default-features = false }
itertools = { version = "0.14.0", default-features = false }
normpath = { version = "1.2.0", default-features = false }
palette = { version = "0.7.6", default-features = false }
regex = { version = "1.10.5", default-features = false }
@@ -33,14 +33,14 @@ 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 }
strum = { version = "0.27.2", default-features = false }
supports-color = { version = "3.0.0", default-features = false }
tempfile = { version = "3.10.1", default-features = false }
terminal-colorsaurus = { version = "0.4.3", default-features = false }
terminal_size = { version = "0.3.0", default-features = false }
terminal-colorsaurus = { version = "1.0.0", default-features = false }
terminal_size = { version = "0.4.3", default-features = false }
thiserror = { version = "1.0.61", default-features = false }
time = { version = "0.3.36", default-features = false }
toml_edit = { version = "0.22.16", default-features = false }
toml_edit = { version = "0.23.6", default-features = false }
tracing = { version = "0.1.40", default-features = false }
tracing-subscriber = { version = "0.3.18", default-features = false }
unicode-normalization = { version = "0.1.23", default-features = false }
+2 -1
View File
@@ -34,7 +34,7 @@ fn main() {
let dir = PathBuf::from(env::var_os("CARGO_WORKSPACE_DIR").unwrap_or_else(|| env::var_os("CARGO_MANIFEST_DIR").unwrap()));
let o = PathBuf::from(env::var_os("OUT_DIR").unwrap());
for file in &["neofetch", "hyfetch"] {
for file in &["neofetch", "hyfetch/data"] {
let src = anything_that_exist(&[
&dir.join(file),
&dir.join("../../").join(file),
@@ -45,6 +45,7 @@ fn main() {
// Copy either file or directory
if src.is_dir() {
let opt = CopyOptions { overwrite: true, copy_inside: true, ..CopyOptions::default() };
println!("copying {} to {}", src.display(), dst.display());
fs_extra::dir::copy(&src, &dst, &opt).expect("Failed to copy directory to OUT_DIR");
}
else { fs::copy(&src, &dst).expect("Failed to copy file to OUT_DIR"); }
+2 -2
View File
@@ -61,8 +61,8 @@ impl RawAsciiArt {
Ok(NormalizedAsciiArt {
lines,
w,
h,
w: w.try_into().context("width does not fit in u8")?,
h: h.try_into().context("height does not fit in u8")?,
fg: self.fg.clone(),
})
}
+150 -180
View File
@@ -2,7 +2,8 @@ use std::borrow::Cow;
use std::cmp;
use std::fmt::Write as _;
use std::fs::{self, File};
use std::io::{self, IsTerminal as _, Read as _, Write as _};
use std::io::{self, IsTerminal as _, Read as _};
use std::iter;
use std::iter::zip;
use std::num::NonZeroU8;
use std::path::{Path, PathBuf};
@@ -24,8 +25,8 @@ use hyfetch::models::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};
use hyfetch::presets::{AssignLightness, Preset};
use hyfetch::pride_month;
use hyfetch::presets::{AssignLightness, ColorProfile, Preset};
use hyfetch::{pride_month, printc};
use hyfetch::types::{AnsiMode, Backend, TerminalTheme};
use hyfetch::utils::{get_cache_path, input};
use hyfetch::font_logo::get_font_logo;
@@ -64,15 +65,12 @@ fn main() -> Result<()> {
});
if options.test_print {
let asc = get_distro_ascii(distro, backend).context("failed to get distro ascii")?;
writeln!(io::stdout(), "{asc}", asc = asc.asc)
.context("failed to write ascii to stdout")?;
println!("{asc}", asc = get_distro_ascii(distro, backend)?.asc);
return Ok(());
}
if options.print_font_logo {
let logo = get_font_logo(backend).context("failed to get font logo")?;
writeln!(io::stdout(), "{}", logo).context("failed to write logo to stdout")?;
println!("{}", get_font_logo(backend)?);
return Ok(());
}
@@ -110,14 +108,9 @@ fn main() -> Result<()> {
if show_pride_month && !config.pride_month_disable {
pride_month::start_animation(color_mode).context("failed to draw pride month animation")?;
writeln!(
io::stdout(),
"\nHappy pride month!\n(You can always view the animation again with `hyfetch \
--june`)\n"
)
.context("failed to write message to stdout")?;
println!("\nHappy pride month!\n(You can always view the animation again with `hyfetch --june`)\n");
if !june_path.is_file() {
if !june_path.is_file() && !options.june {
File::create(&june_path)
.with_context(|| format!("failed to create file {june_path:?}"))?;
}
@@ -129,9 +122,43 @@ 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> {
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));
}
}
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())
} 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())
}
}
// Get preset
let preset = options.preset.unwrap_or(config.preset);
let color_profile = preset.color_profile();
let preset_string = options.preset.as_deref().unwrap_or(&config.preset);
let color_profile = parse_preset_string(preset_string)?;
debug!(?color_profile, "color profile");
// Lighten
@@ -149,7 +176,14 @@ fn main() -> Result<()> {
};
debug!(?color_profile, "lightened color profile");
let asc = if let Some(path) = options.ascii_file {
let asc = if let Some(path_str) = config.custom_ascii_path {
let path = PathBuf::from(path_str);
RawAsciiArt {
asc: fs::read_to_string(&path)
.with_context(|| format!("failed to read ascii from {path:?}"))?,
fg: Vec::new(),
}
} else if let Some(path) = options.ascii_file {
RawAsciiArt {
asc: fs::read_to_string(&path)
.with_context(|| format!("failed to read ascii from {path:?}"))?,
@@ -207,9 +241,9 @@ fn det_bg() -> Result<Option<Srgb<u8>>, terminal_colorsaurus::Error> {
}
background_color(QueryOptions::default())
.map(|terminal_colorsaurus::Color { r, g, b }| Some(Srgb::new(r, g, b).into_format()))
.map(|terminal_colorsaurus::Color { r, g, b , .. }| Some(Srgb::new(r, g, b).into_format()))
.or_else(|err| {
if matches!(err, terminal_colorsaurus::Error::UnsupportedTerminal) {
if matches!(err, terminal_colorsaurus::Error::UnsupportedTerminal(_)) {
Ok(None)
} else {
Err(err)
@@ -236,10 +270,6 @@ fn create_config(
} else if color_level.has_256 {
AnsiMode::Ansi256
} else if color_level.has_basic {
// unimplemented!(
// "{mode} color mode not supported",
// mode = AnsiMode::Ansi16.as_ref()
// );
AnsiMode::Ansi256
} else {
unreachable!();
@@ -279,13 +309,8 @@ fn create_config(
.expect("`option_counter` should not overflow `u8`");
}
fn print_title_prompt(
option_counter: NonZeroU8,
prompt: &str,
color_mode: AnsiMode,
) -> Result<()> {
printc(format!("&a{option_counter}. {prompt}"), color_mode)
.context("failed to print prompt")
fn print_title_prompt(option_counter: NonZeroU8, prompt: &str) {
printc!("&a{option_counter}. {prompt}");
}
//////////////////////////////
@@ -295,14 +320,8 @@ fn create_config(
let (Width(term_w), Height(term_h)) = terminal_size().context("failed to get terminal size")?;
let (term_w_min, term_h_min) = ((asc.w as u32 * 2 + 4).clamp(0, u16::MAX.into()) as u16, 30);
if term_w < term_w_min || term_h < term_h_min {
printc(
format!(
"&cWarning: Your terminal is too small ({term_w} * {term_h}).\nPlease resize \
it to at least ({term_w_min} * {term_h_min}) for better experience."
),
color_mode,
)
.context("failed to print message")?;
printc!("&cWarning: Your terminal is too small ({term_w} * {term_h}).\n\
Please resize it to at least ({term_w_min} * {term_h_min}) for better experience.");
input(Some("Press enter to continue...")).context("failed to read input")?;
}
}
@@ -347,18 +366,14 @@ fn create_config(
(t - a) * ((d - c) / (b - a)) + c
}
{
let label = format!(
"{label:^term_w$}",
label = "8bit Color Testing",
term_w = usize::from(term_w)
);
let print_color_testing = |label: &str, mode: AnsiMode| {
let label = format!("{label:^term_w$}", term_w = usize::from(term_w));
let line = zip(gradient.iter(), label.chars()).fold(
String::new(),
|mut s, (&rgb_f32_color, t)| {
let rgb_u8_color = Srgb::<u8>::from_linear(rgb_f32_color);
let back = rgb_u8_color
.to_ansi_string(AnsiMode::Ansi256, ForegroundBackground::Background);
.to_ansi_string(mode, ForegroundBackground::Background);
let fore = rgb_u8_color
.contrast_grayscale()
.to_ansi_string(AnsiMode::Ansi256, ForegroundBackground::Foreground);
@@ -366,42 +381,15 @@ fn create_config(
s
},
);
printc(line, AnsiMode::Ansi256).context("failed to print 8-bit color test line")?;
}
{
let label = format!(
"{label:^term_w$}",
label = "RGB Color Testing",
term_w = usize::from(term_w)
);
let line = zip(gradient.iter(), label.chars()).fold(
String::new(),
|mut s, (&rgb_f32_color, t)| {
let rgb_u8_color = Srgb::<u8>::from_linear(rgb_f32_color);
let back = rgb_u8_color
.to_ansi_string(AnsiMode::Rgb, ForegroundBackground::Background);
let fore = rgb_u8_color
.contrast_grayscale()
.to_ansi_string(AnsiMode::Ansi256, ForegroundBackground::Foreground);
write!(s, "{back}{fore}{t}").unwrap();
s
},
);
printc(line, AnsiMode::Rgb).context("failed to print RGB color test line")?;
}
printc!("{line}");
};
writeln!(io::stdout()).context("failed to write to stdout")?;
print_title_prompt(
option_counter,
"Which &bcolor system &ado you want to use?",
color_mode,
)
.context("failed to print title prompt")?;
writeln!(
io::stdout(),
"(If you can't see colors under \"RGB Color Testing\", please choose 8bit)\n"
)
.context("failed to write message to stdout")?;
print_color_testing("8bit Color Testing", AnsiMode::Ansi256);
print_color_testing("RGB Color Testing", AnsiMode::Rgb);
println!();
print_title_prompt(option_counter, "Which &bcolor system &ado you want to use?");
println!("(If you can't see colors under \"RGB Color Testing\", please choose 8bit)\n");
let choice = literal_input(
"Your choice?",
@@ -434,20 +422,14 @@ fn create_config(
clear_screen(Some(&title), color_mode, debug_mode).context("failed to clear screen")?;
print_title_prompt(
option_counter,
"Is your terminal in &blight mode&~ or &4dark mode&~?",
color_mode,
)
.context("failed to print title prompt")?;
print_title_prompt(option_counter, "Is your terminal in &blight mode&~ or &4dark mode&~?");
let choice = literal_input(
"",
TerminalTheme::VARIANTS,
TerminalTheme::Dark.as_ref(),
true,
color_mode,
)
.context("failed to ask for choice input")?;
)?;
Ok((
choice.parse().expect("selected theme should be valid"),
"Selected background color",
@@ -518,18 +500,12 @@ fn create_config(
let print_flag_page = |page, page_num: u8| -> Result<()> {
clear_screen(Some(&title), color_mode, debug_mode).context("failed to clear screen")?;
print_title_prompt(option_counter, "Let's choose a flag!", color_mode)
.context("failed to print title prompt")?;
writeln!(
io::stdout(),
"Available flag presets:\nPage: {page_num} of {num_pages}\n",
page_num = page_num.checked_add(1).unwrap()
)
.context("failed to write header to stdout")?;
print_title_prompt(option_counter, "Let's choose a flag!");
println!("Available flag presets:\nPage: {page_num} of {num_pages}\n", page_num = page_num + 1);
for &row in page {
print_flag_row(row, color_mode).context("failed to print flag row")?;
}
writeln!(io::stdout()).context("failed to write to stdout")?;
println!();
Ok(())
};
@@ -541,7 +517,7 @@ fn create_config(
}
printc(line.join(" "), color_mode).context("failed to print line")?;
}
writeln!(io::stdout()).context("failed to write to stdout")?;
println!();
Ok(())
}
@@ -566,16 +542,9 @@ fn create_config(
let mut opts: Vec<&str> = <Preset as VariantNames>::VARIANTS.into();
opts.extend(["next", "n", "prev", "p"]);
writeln!(
io::stdout(),
"Enter '[n]ext' to go to the next page and '[p]rev' to go to the previous page."
)
.context("failed to write message to stdout")?;
println!("Enter '[n]ext' to go to the next page and '[p]rev' to go to the previous page.");
let selection = literal_input(
format!(
"Which {preset} do you want to use? ",
preset = preset_default_colored
),
format!("Which {preset_default_colored} do you want to use? "),
&opts[..],
Preset::Rainbow.as_ref(),
false,
@@ -624,22 +593,15 @@ fn create_config(
let select_lightness = || -> Result<Lightness> {
clear_screen(Some(&title), color_mode, debug_mode).context("failed to clear screen")?;
print_title_prompt(
option_counter,
"Let's adjust the color brightness!",
color_mode,
)
.context("failed to print title prompt")?;
writeln!(
io::stdout(),
print_title_prompt(option_counter, "Let's adjust the color brightness!");
println!(
"The colors might be a little bit too {bright_dark} for {light_dark} mode.\n",
bright_dark = match theme {
TerminalTheme::Light => "bright",
TerminalTheme::Dark => "dark",
},
light_dark = theme.as_ref()
)
.context("failed to write message to stdout")?;
);
let color_align = ColorAlignment::Horizontal;
@@ -697,14 +659,10 @@ fn create_config(
}
loop {
writeln!(
io::stdout(),
"\nWhich brightness level looks the best? (Default: {default:.0}% for \
{light_dark} mode)",
println!("\nWhich brightness level looks the best? (Default: {default:.0}% for {light_dark} mode)",
default = f32::from(default_lightness) * 100.0,
light_dark = theme.as_ref()
)
.context("failed to write prompt to stdout")?;
);
let lightness = input(Some("> "))
.context("failed to read input")?
.trim()
@@ -716,12 +674,7 @@ fn create_config(
},
Err(err) => {
debug!(%err, "could not parse lightness");
printc(
"&cUnable to parse lightness value, please enter a lightness value such \
as 45%, .45, or 45",
color_mode,
)
.context("failed to print message")?;
printc!("&cUnable to parse lightness value, please enter a lightness value such as 45%, .45, or 45");
},
}
}
@@ -845,16 +798,11 @@ fn create_config(
}
printc(line.join(" "), color_mode).context("failed to print ascii line")?;
}
writeln!(io::stdout()).context("failed to write to stdout")?;
println!();
}
print_title_prompt(
option_counter,
"Do you want the default logo, or the small logo?",
color_mode,
)
.context("failed to print title prompt")?;
print_title_prompt(option_counter, "Do you want the default logo, or the small logo?");
let opts: Vec<Cow<str>> = ["default", "small"].map(Into::into).into();
let choice = literal_input("Your choice?", &opts[..], "default", true, color_mode)
.context("failed to ask for choice input")
@@ -969,21 +917,11 @@ fn create_config(
}
printc(line.join(" "), color_mode).context("failed to print ascii line")?;
}
writeln!(io::stdout()).context("failed to write to stdout")?;
println!();
}
print_title_prompt(
option_counter,
"Let's choose a color arrangement!",
color_mode,
)
.context("failed to print title prompt")?;
writeln!(
io::stdout(),
"You can choose standard horizontal or vertical alignment, or use one of the random \
color schemes.\nYou can type \"roll\" to randomize again.\n"
)
.context("failed to write message to stdout")?;
print_title_prompt(option_counter, "Let's choose a color arrangement!");
println!("You can choose standard horizontal or vertical alignment, or use one of the random color schemes.\nYou can type \"roll\" to randomize again.\n");
let mut opts: Vec<Cow<str>> = ["horizontal", "vertical", "roll"].map(Into::into).into();
opts.extend((0..random_count).map(|i| format!("random{i}").into()));
let choice = literal_input("Your choice?", &opts[..], "horizontal", true, color_mode)
@@ -1022,42 +960,27 @@ fn create_config(
let select_backend = || -> Result<Backend> {
clear_screen(Some(&title), color_mode, debug_mode).context("failed to clear screen")?;
print_title_prompt(option_counter, "Select a *fetch backend", color_mode)
.context("failed to print title prompt")?;
print_title_prompt(option_counter, "Select a *fetch backend");
// Check if fastfetch is installed
let fastfetch_path = fastfetch_path().ok();
// Check if macchina is installed
#[cfg(feature = "macchina")]
let macchina_path = macchina_path().context("failed to get macchina path")?;
let macchina_path = macchina_path().unwrap_or(None);
printc(
"- &bneofetch&r: Written in bash, &nbest compatibility&r on Unix systems",
color_mode,
)
.context("failed to print message")?;
printc(
format!(
"- &bfastfetch&r: Written in C, &nbest performance&r {installed_not_installed}",
installed_not_installed = fastfetch_path
.map(|path| format!("&a(Installed at {path})", path = path.display()))
.unwrap_or_else(|| "&c(Not installed)".to_owned())
),
color_mode,
)
.context("failed to print message")?;
printc!("- &bneofetch&r: Written in bash, &nbest compatibility&r on Unix systems");
printc!("- &bfastfetch&r: Written in C, &nbest performance&r {}",
fastfetch_path
.map(|path| format!("&a(Installed at {path})", path = path.display()))
.unwrap_or_else(|| "&c(Not installed)".to_owned())
);
#[cfg(feature = "macchina")]
printc(
format!(
"- &bmacchina&r: Written in Rust, &nbest performance&r {installed_not_installed}\n",
installed_not_installed = macchina_path
.map(|path| format!("&a(Installed at {path})", path = path.display()))
.unwrap_or_else(|| "&c(Not installed)".to_owned())
),
color_mode,
)
.context("failed to print message")?;
printc!("- &bmacchina&r: Written in Rust, &nbest performance&r {}\n",
macchina_path
.map(|path| format!("&a(Installed at {path})", path = path.display()))
.unwrap_or_else(|| "&c(Not installed)".to_owned())
);
let choice = literal_input(
"Your choice?",
@@ -1078,10 +1001,56 @@ fn create_config(
backend.as_ref(),
);
//////////////////////////////
// 8. Custom ASCII file
let mut custom_ascii_path: Option<String> = None;
clear_screen(Some(&title), color_mode, debug_mode).context("failed to clear screen")?;
let choice = literal_input(
"Do you want to specify a custom ASCII file?",
&["y", "n"],
"n",
true,
color_mode,
)
.context("failed to ask for choice input")?;
if choice == "y" {
loop {
let pth = input(Some("Path to custom ASCII file (must be UTF-8 encoded, empty to skip): "))?.trim().to_owned();
if pth.is_empty() {
printc!("&cNo path entered. Skipping custom ASCII file.");
break;
}
let pth_buf = PathBuf::from(&pth);
if !pth_buf.is_file() {
printc!("&cError: File not found at {pth}");
continue;
}
match fs::read_to_string(&pth_buf) {
Ok(_) => {
custom_ascii_path = Some(pth);
update_title(
&mut title,
&mut option_counter,
"Custom ASCII file",
custom_ascii_path.as_ref().unwrap(),
);
break;
}
Err(e) => {
printc!("&cError: File is not UTF-8 encoded or an unexpected error occurred: {e}");
continue;
}
}
}
}
// Create config
clear_screen(Some(&title), color_mode, debug_mode).context("failed to clear screen")?;
let config = Config {
preset,
preset: preset.as_ref().to_string(),
mode: color_mode,
light_dark: Some(theme),
auto_detect_light_dark: Some(det_bg.is_some()),
@@ -1091,6 +1060,7 @@ fn create_config(
args: None,
distro: logo_chosen,
pride_month_disable: false,
custom_ascii_path,
};
debug!(?config, "created config");
+4 -27
View File
@@ -8,7 +8,7 @@ use bpaf::ShellComp;
use bpaf::{construct, long, OptionParser, Parser as _};
use directories::BaseDirs;
use itertools::Itertools as _;
use strum::{VariantArray, VariantNames};
use strum::VariantNames;
use crate::color_util::{color, Lightness};
use crate::presets::Preset;
@@ -18,7 +18,7 @@ use crate::types::{AnsiMode, Backend};
pub struct Options {
pub config: bool,
pub config_file: PathBuf,
pub preset: Option<Preset>,
pub preset: Option<String>,
pub mode: Option<AnsiMode>,
pub backend: Option<Backend>,
pub args: Option<Vec<String>>,
@@ -55,7 +55,7 @@ pub fn options() -> OptionParser<Options> {
let preset = long("preset")
.short('p')
.help(&*format!(
"Use preset
"Use preset or comma-separated color list or comma-separated hex colors (e.g., \"#ff0000,#00ff00,#0000ff\")
PRESET={{{presets}}}",
presets = <Preset as VariantNames>::VARIANTS
.iter()
@@ -65,30 +65,7 @@ PRESET={{{presets}}}",
.argument::<String>("PRESET");
#[cfg(feature = "autocomplete")]
let preset = preset.complete(complete_preset);
let preset = preset
.parse(|s| {
Preset::from_str(&s)
.or_else(|e| {
if s == "random" {
let mut rng = fastrand::Rng::new();
Ok(*rng
.choice(<Preset as VariantArray>::VARIANTS)
.expect("preset iterator should not be empty"))
} else {
Err(e)
}
})
.with_context(|| {
format!(
"PRESET should be one of {{{presets}}}",
presets = <Preset as VariantNames>::VARIANTS
.iter()
.chain(iter::once(&"random"))
.join(",")
)
})
})
.optional();
let preset = preset.optional();
let mode = long("mode")
.short('m')
.help(&*format!(
+9 -7
View File
@@ -403,18 +403,20 @@ where
Ok(dst)
}
#[macro_export]
macro_rules! printc {
($($arg:tt)*) => {
println!("{}", color(format!("{}&r", format!($($arg)*)), AnsiMode::Rgb).expect("failed to color message"));
};
}
/// Prints with color.
pub fn printc<S>(msg: S, mode: AnsiMode) -> Result<()>
where
S: AsRef<str>,
{
writeln!(
io::stdout(),
"{msg}",
msg = color(format!("{msg}&r", msg = msg.as_ref()), mode)
.context("failed to color message")?
)
.context("failed to write message to stdout")
println!("{msg}", msg = color(format!("{msg}&r", msg = msg.as_ref()), mode).context("failed to color message")?);
Ok(())
}
/// Clears screen using ANSI escape codes.
+2 -2
View File
@@ -2,12 +2,11 @@ use serde::{Deserialize, Serialize};
use crate::color_util::Lightness;
use crate::neofetch_util::ColorAlignment;
use crate::presets::Preset;
use crate::types::{AnsiMode, Backend, TerminalTheme};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Config {
pub preset: Preset,
pub preset: String,
pub mode: AnsiMode,
pub auto_detect_light_dark: Option<bool>,
pub light_dark: Option<TerminalTheme>,
@@ -19,6 +18,7 @@ pub struct Config {
pub args: Option<Vec<String>>,
pub distro: Option<String>,
pub pride_month_disable: bool,
pub custom_ascii_path: Option<String>,
}
impl Config {
+7 -25
View File
@@ -113,16 +113,12 @@ where
};
if let Some(selected) = find_selection(&selection, options) {
writeln!(io::stdout()).context("failed to write to stdout")?;
println!();
return Ok(selected);
} else {
let options_text = options.iter().map(AsRef::as_ref).join("|");
writeln!(
io::stdout(),
"Invalid selection! {selection} is not one of {options_text}"
)
.context("failed to write message to stdout")?;
println!("Invalid selection! {selection} is not one of {options_text}");
}
}
@@ -285,7 +281,7 @@ pub fn run(asc: RecoloredAsciiArt, backend: Backend, args: Option<&Vec<String>>)
}
/// Gets distro ascii width and height, ignoring color code.
pub fn ascii_size<S>(asc: S) -> Result<(u8, u8)>
pub fn ascii_size<S>(asc: S) -> Result<(u16, u16)>
where
S: AsRef<str>,
{
@@ -307,25 +303,11 @@ where
return Ok((0, 0));
}
let width = asc
.lines()
.map(|line| line.graphemes(true).count())
.max()
let width = asc.lines()
.map(|line| line.graphemes(true).count()).max()
.expect("line iterator should not be empty");
let width: u8 = width.try_into().with_context(|| {
format!(
"`asc` should not have more than {limit} characters per line",
limit = u8::MAX
)
})?;
let height = asc.lines().count();
let height: u8 = height.try_into().with_context(|| {
format!(
"`asc` should not have more than {limit} lines",
limit = u8::MAX
)
})?;
let width: u16 = width.try_into().context("ascii art width should fit in u16")?;
let height: u16 = asc.lines().count().try_into().context("ascii art height should fit in u16")?;
Ok((width, height))
}
+31
View File
@@ -229,6 +229,10 @@ pub enum Preset {
/// Meme flag
Band,
Libragender, Librafeminine, Libramasculine, Libraandrogyne, Libranonbinary,
Fluidfluxa, Fluidfluxb,
}
#[derive(Clone, Eq, PartialEq, Debug)]
@@ -687,6 +691,33 @@ impl Preset {
"#2670C0", "#F5BD00", "#DC0045", "#E0608E"
]),
Self::Libragender => ColorProfile::from_hex_colors(vec![
"#000000", "#808080", "#92D8E9", "#FFF544", "#FFB0CA", "#808080", "#000000"
]),
Self::Librafeminine => ColorProfile::from_hex_colors(vec![
"#000000", "#A3A3A3", "#FFFFFF", "#C6568F", "#FFFFFF", "#A3A3A3", "#000000"
]),
Self::Libramasculine => ColorProfile::from_hex_colors(vec![
"#000000", "#A3A3A3", "#FFFFFF", "#56C5C5", "#FFFFFF", "#A3A3A3", "#000000"
]),
Self::Libraandrogyne => ColorProfile::from_hex_colors(vec![
"#000000", "#A3A3A3", "#FFFFFF", "#9186B1", "#FFFFFF", "#A3A3A3", "#000000"
]),
Self::Libranonbinary => ColorProfile::from_hex_colors(vec![
"#000000", "#A3A3A3", "#FFFFFF", "#FFF987", "#FFFFFF", "#A3A3A3", "#000000"
]),
Self::Fluidfluxa => ColorProfile::from_hex_colors(vec![
"#ff115f", "#a34aa3", "#00a4e7", "#ffdf00", "#000000", "#ffed71", "#85daff", "#dbadda", "#fe8db1"
]),
Self::Fluidfluxb => ColorProfile::from_hex_colors(vec![
"#c6d1d2", "#f47b9d", "#f09f9b", "#e3f09e", "#75eeea", "#52d2ed", "#c6d1d2"
]),
})
.expect("preset color profiles should be valid")
}
+25 -85
View File
@@ -47,58 +47,32 @@ pub fn start_animation(color_mode: AnsiMode) -> Result<()> {
};
let text = &TEXT_ASCII[1..TEXT_ASCII.len().checked_sub(1).unwrap()];
let (text_width, text_height) =
ascii_size(text).expect("text ascii should have valid width and height");
let (text_w, text_h) = ascii_size(text)?;
let (text, text_width, text_height) = {
const TEXT_BORDER_WIDTH: u16 = 2;
const NOTICE_BORDER_WIDTH: u16 = 1;
const VERTICAL_MARGIN: u16 = 1;
let notice_w = NOTICE.len();
let notice_w: u8 = notice_w
.try_into()
.expect("`NOTICE` width should fit in `u8`");
let notice_h = NOTICE.lines().count();
let notice_h: u8 = notice_h
.try_into()
.expect("`NOTICE` height should fit in `u8`");
let notice_w: u16 = NOTICE.len().try_into()?;
let notice_h: u16 = NOTICE.lines().count().try_into()?;
let term_w_min = cmp::max(
u16::from(text_width)
.checked_add(TEXT_BORDER_WIDTH.checked_mul(2).unwrap())
.unwrap(),
u16::from(notice_w)
.checked_add(NOTICE_BORDER_WIDTH.checked_mul(2).unwrap())
.unwrap(),
text_w + TEXT_BORDER_WIDTH * 2,
notice_w + NOTICE_BORDER_WIDTH * 2,
);
let term_h_min = u16::from(text_height)
.checked_add(notice_h.into())
.unwrap()
.checked_add(VERTICAL_MARGIN.checked_mul(2).unwrap())
.unwrap();
let term_h_min = u16::from(text_h) + notice_h + VERTICAL_MARGIN * 2;
if w.get() >= term_w_min && h.get() >= term_h_min {
(text, text_width, text_height)
(text, text_w, text_h)
} else {
let text = &TEXT_ASCII_SMALL[1..TEXT_ASCII_SMALL.len().checked_sub(1).unwrap()];
let (text_width, text_height) =
ascii_size(text).expect("text ascii should have valid width and height");
let (text_w, text_h) = ascii_size(text)?;
let term_w_min = cmp::max(
u16::from(text_width)
.checked_add(TEXT_BORDER_WIDTH.checked_mul(2).unwrap())
.unwrap(),
u16::from(notice_w)
.checked_add(NOTICE_BORDER_WIDTH.checked_mul(2).unwrap())
.unwrap(),
text_w + TEXT_BORDER_WIDTH * 2,
notice_w + NOTICE_BORDER_WIDTH * 2,
);
let term_h_min = u16::from(text_height)
.checked_add(notice_h.into())
.unwrap()
.checked_add(VERTICAL_MARGIN.checked_mul(2).unwrap())
.unwrap();
let term_h_min = text_h + notice_h + VERTICAL_MARGIN * 2;
if w.get() < term_w_min || h.get() < term_h_min {
return Err(anyhow!(
"terminal size should be at least ({term_w_min} * {term_h_min})"
));
return Err(anyhow!("terminal size should be at least ({term_w_min} * {term_h_min})"));
}
(text, text_width, text_height)
(text, text_w, text_h)
}
};
let text_lines: Vec<&str> = text.lines().collect();
@@ -165,8 +139,7 @@ pub fn start_animation(color_mode: AnsiMode) -> Result<()> {
.rem_euclid(colors.len())]
.to_ansi_string(color_mode, ForegroundBackground::Background),
fg = fg.to_ansi_string(color_mode, ForegroundBackground::Foreground)
)
.unwrap();
)?;
// Loop over the width
for x in 0..w.get() {
@@ -176,27 +149,11 @@ pub fn start_animation(color_mode: AnsiMode) -> Result<()> {
.wrapping_add_signed((2.0 * (y as f64 + 0.5 * frame as f64).sin()) as isize);
let y_text = text_start_y <= y && y < text_end_y;
let border = 1u16
.checked_add(
if y == text_start_y || y == text_end_y.checked_sub(1).unwrap() {
0
} else {
1
},
)
.unwrap();
let text_bounds_x1 = text_start_x
.checked_sub(border)
.expect("`text_start_x - border` should not underflow `u16`");
let text_bounds_x2 = text_end_x
.checked_add(border)
.expect("`text_end_x + border` should not overflow `u16`");
let notice_bounds_x1 = notice_start_x
.checked_sub(1)
.expect("`notice_start_x - 1` should not underflow `u16`");
let notice_bounds_x2 = notice_end_x
.checked_add(1)
.expect("`notice_end_x + 1` should not overflow `u16`");
let border = 1u16 + if y == text_start_y || y == (text_end_y - 1) { 0 } else { 1 };
let text_bounds_x1 = text_start_x - border;
let text_bounds_x2 = text_end_x - border;
let notice_bounds_x1 = notice_start_x - 1;
let notice_bounds_x2 = notice_end_x - 1;
// If it's a switching point
if idx.rem_euclid(NonZeroUsize::from(block_width).get()) == 0
@@ -215,19 +172,9 @@ pub fn start_animation(color_mode: AnsiMode) -> Result<()> {
{
let c: LinSrgba = c.with_alpha(1.0).into_linear();
let c = Srgb::<u8>::from_linear(c.overlay(black).without_alpha());
write!(
buf,
"{bg}",
bg = c.to_ansi_string(color_mode, ForegroundBackground::Background),
)
.unwrap();
write!(buf, "{bg}", bg = c.to_ansi_string(color_mode, ForegroundBackground::Background))?;
} else {
write!(
buf,
"{bg}",
bg = c.to_ansi_string(color_mode, ForegroundBackground::Background),
)
.unwrap();
write!(buf, "{bg}", bg = c.to_ansi_string(color_mode, ForegroundBackground::Background))?;
}
}
@@ -240,8 +187,7 @@ pub fn start_animation(color_mode: AnsiMode) -> Result<()> {
.chars()
.nth(usize::from(x.checked_sub(text_start_x).unwrap()))
.unwrap(),
)
.unwrap();
)?;
} else if y == notice_y && notice_start_x <= x && x < notice_end_x {
write!(
buf,
@@ -250,21 +196,15 @@ pub fn start_animation(color_mode: AnsiMode) -> Result<()> {
.chars()
.nth(usize::from(x.checked_sub(notice_start_x).unwrap()))
.unwrap(),
)
.unwrap();
)?;
} else {
write!(buf, " ").unwrap();
write!(buf, " ")?;
}
}
// New line if it isn't the last line
if y != h.get().checked_sub(1).unwrap() {
writeln!(
buf,
"{reset}",
reset = color("&r", color_mode).expect("reset should be valid"),
)
.unwrap();
writeln!(buf, "{reset}", reset = color("&r", color_mode)?)?;
}
}
+11 -3
View File
@@ -135,9 +135,17 @@ class RGB:
:return: RGB object
"""
hex = hex.lstrip("#")
r = int(hex[0:2], 16)
g = int(hex[2:4], 16)
b = int(hex[4:6], 16)
if len(hex) == 6:
r = int(hex[0:2], 16)
g = int(hex[2:4], 16)
b = int(hex[4:6], 16)
elif len(hex) == 3:
r = int(hex[0], 16)
g = int(hex[1], 16)
b = int(hex[2], 16)
else:
raise ValueError(f"Error: invalid hex length")
return cls(r, g, b)
def to_ansi_rgb(self, foreground: bool = True) -> str:
+66 -5
View File
@@ -5,6 +5,7 @@ import argparse
import datetime
import importlib.util
import json
import os
import random
import traceback
from itertools import permutations, islice
@@ -17,7 +18,7 @@ from .constants import *
from .font_logo import get_font_logo
from .models import Config
from .neofetch_util import *
from .presets import PRESETS
from .presets import PRESETS, ColorProfile
def check_config(path) -> Config:
@@ -318,9 +319,49 @@ def create_config() -> Config:
backend = select_backend()
update_title('Selected backend', backend)
##############################
# 7. Custom ASCII file
custom_ascii_path = None
clear_screen(title)
if literal_input('Do you want to specify a custom ASCII file?', ['y', 'n'], 'n') == 'y':
while True:
path_input = input('Path to custom ASCII file (must be .txt and UTF-8 encoded): ').strip()
if not path_input:
printc('&cNo path entered. Skipping custom ASCII file.')
break
custom_path = Path(path_input)
if not custom_path.is_file():
printc(f'&cError: File not found at {path_input}')
if literal_input('Try again?', ['y', 'n'], 'y') == 'n':
break
continue
if not custom_path.suffix == '.txt':
printc(f'&cError: File must have a .txt extension. Found {custom_path.suffix}')
if literal_input('Try again?', ['y', 'n'], 'y') == 'n':
break
continue
try:
custom_path.read_text('utf-8')
custom_ascii_path = str(custom_path)
update_title('Custom ASCII file', custom_ascii_path)
break
except UnicodeDecodeError:
printc(f'&cError: File is not UTF-8 encoded.')
if literal_input('Try again?', ['y', 'n'], 'y') == 'n':
break
continue
except Exception as e:
printc(f'&cAn unexpected error occurred: {e}')
if literal_input('Try again?', ['y', 'n'], 'y') == 'n':
break
continue
# Create config
clear_screen(title)
c = Config(preset, color_system, light_dark, lightness, color_alignment, backend)
c = Config(preset, color_system, light_dark, lightness, color_alignment, backend, custom_ascii_path=custom_ascii_path)
# Save config
print()
@@ -338,7 +379,7 @@ def create_parser() -> argparse.ArgumentParser:
parser.add_argument('-c', '--config', action='store_true', help=color(f'Configure hyfetch'))
parser.add_argument('-C', '--config-file', dest='config_file', default=CONFIG_PATH, help=f'Use another config file')
parser.add_argument('-p', '--preset', help=f'Use preset', choices=list(PRESETS.keys()) + ['random'])
parser.add_argument('-p', '--preset', help=f'Use preset or comma-separated hex color list (e.g., "#ff0000,#00ff00,#0000ff")')
parser.add_argument('-m', '--mode', help=f'Color mode', choices=['8bit', 'rgb'])
parser.add_argument('-b', '--backend', help=f'Choose a *fetch backend', choices=['qwqfetch', 'neofetch', 'fastfetch', 'fastfetch-old'])
parser.add_argument('--args', help=f'Additional arguments pass-through to backend')
@@ -451,7 +492,21 @@ def run():
GLOBAL_CFG.is_light = config.light_dark == 'light'
# Get preset
preset = PRESETS.get(config.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(',')]
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()))}')
if preset is None:
exit(1)
# Lighten (args > config)
if args.scale:
@@ -463,8 +518,14 @@ def run():
# Run
try:
asc = get_distro_ascii() if not args.ascii_file else Path(args.ascii_file).read_text("utf-8")
if config.custom_ascii_path:
asc = Path(config.custom_ascii_path).read_text("utf-8")
elif args.ascii_file:
asc = Path(args.ascii_file).read_text("utf-8")
else:
asc = get_distro_ascii()
asc = config.color_align.recolor_ascii(asc, preset)
asc = '\n'.join(asc.split('\n')[1:])
neofetch_util.run(asc, config.backend, config.args or '')
except Exception as e:
print(f'Error: {e}')
+1
View File
@@ -20,6 +20,7 @@ class Config:
distro: str | None = None
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
@classmethod
def from_dict(cls, d: dict):
+81
View File
@@ -1011,4 +1011,85 @@ PRESETS: dict[str, ColorProfile] = {
"#dc0045",
"#e0608e"
]),
# Adding libragender flags https://lgbtqia.wiki/wiki/Libragender
# Sourced from https://lgbtqia.wiki/wiki/Libragender
'libragender': ColorProfile([
"#000000",
"#808080",
"#92D8E9",
"#FFF544",
"#FFB0CA",
"#808080",
"#000000"
]),
# Sourced from https://lgbtqia.wiki/wiki/Librafeminine
'librafeminine': ColorProfile([
"#000000",
"#A3A3A3",
"#FFFFFF",
"#C6568F",
"#FFFFFF",
"#A3A3A3",
"#000000"
]),
# Sourced from https://lgbtqia.wiki/wiki/Libramasculine
'libramasculine': ColorProfile([
"#000000",
"#A3A3A3",
"#FFFFFF",
"#56C5C5",
"#FFFFFF",
"#A3A3A3",
"#000000"
]),
# Sourced from https://lgbtqia.wiki/wiki/Librandrogyne
'libraandrogyne': ColorProfile([
"#000000",
"#A3A3A3",
"#FFFFFF",
"#9186B1",
"#FFFFFF",
"#A3A3A3",
"#000000"
]),
# Sourced from https://lgbtqia.wiki/wiki/Libranonbinary
'libranonbinary': ColorProfile([
"#000000",
"#A3A3A3",
"#FFFFFF",
"#FFF987",
"#FFFFFF",
"#A3A3A3",
"#000000"
]),
# Adding Fluidflux flags - ObsoleteDev
# Sourced from https://gender.fandom.com/wiki/Fluidflux?file=FC90B24D-CA36-4FE2-A752-C9ABFC65E332.jpeg
'fluidflux A': ColorProfile([
"#ff115f",
"#a34aa3",
"#00a4e7",
"#ffdf00",
"#000000",
"#ffed71",
"#85daff",
"#dbadda",
"#fe8db1"
]),
'fluidflux B': ColorProfile([
"#c6d1d2",
"#f47b9d",
"#f09f9b",
"#e3f09e",
"#75eeea",
"#52d2ed",
"#c6d1d2"
]),
}
+13 -2
View File
@@ -1823,6 +1823,10 @@ get_model() {
iPod5,1): "iPod touch 5G" ;;
iPod7,1): "iPod touch 6G" ;;
iPod9,1): "iPod touch 7G" ;;
AppleTV2,1): "Apple TV 2" ;;
AppleTV3,1): "Apple TV 3" ;;
AppleTV3,2): "Apple TV 3 (2013)" ;;
esac
model=$_
@@ -3232,6 +3236,10 @@ END
"iPad7,"[1-4]): "Apple A10X Fusion (6) @ 2.39GHz" ;;
"iPad8,"[1-8]): "Apple A12X Bionic (8) @ 2.49GHz" ;;
"iPad8,9" | "iPad8,1"[0-2]): "Apple A12Z Bionic (8) @ 2.49GHz" ;;
"AppleTV2,1"): "Apple A4 (1) @ 1GHz" ;;
"AppleTV3,1"): "Apple A5 (1) @ 1GHz" ;;
"AppleTV3,2"): "Apple A5 (1) @ 1GHz" ;;
esac
cpu="$_"
;;
@@ -3537,10 +3545,12 @@ get_gpu() {
"iPhone OS")
case $kernel_machine in
"iPhone1,"[1-2]): "PowerVR MBX Lite 3D" ;;
"iPhone2,1" | "iPhone3,"[1-3] | "iPod3,1" | "iPod4,1" | "iPad1,"[1-2]):
"iPhone2,1" | "iPhone3,"[1-3] | "iPod3,1" | "iPod4,1" | "iPad1,"[1-2] | "AppleTV2,1"):
"PowerVR SGX535"
;;
"iPhone4,1" | "iPad2,"[1-7] | "iPod5,1"): "PowerVR SGX543MP2" ;;
"iPhone4,1" | "iPad2,"[1-7] | "iPod5,1" | "AppleTV3,"[1-2]):
"PowerVR SGX543MP2"
;;
"iPhone5,"[1-4]): "PowerVR SGX543MP3" ;;
"iPhone6,"[1-2] | "iPad4,"[1-9]): "PowerVR G6430" ;;
"iPhone7,"[1-2] | "iPod7,1" | "iPad5,"[1-2]): "PowerVR GX6450" ;;
@@ -4167,6 +4177,7 @@ get_resolution() {
"iPad13,"[1-2] | "iPad13,1"[6-9]): "1640x2360" ;;
"iPad8,"[1-4] | "iPad8,"[9-10] | "iPad13,"[4-7] | "iPad14,"[3-6]): "1668x2388" ;;
"iPad6,"[7-8] | "iPad7,"[1-2] | "iPad8,"[5-8] | "iPad8,1"[1-2] | "iPad13,"[8-9] | "iPad13,1"[0-1] | "iPad14,"[5-6]): "2048x2732" ;;
"AppleTV"*) return ;;
esac
resolution="$_"
;;