From a7c277cb764dd0aa2b0e9113a7cd83443d1c984b Mon Sep 17 00:00:00 2001 From: Teoh Han Hui Date: Mon, 22 Jul 2024 17:16:19 +0800 Subject: [PATCH] Move `create_config` back to hyfetch.rs p/s: sorry... --- crates/hyfetch/src/bin/hyfetch.rs | 859 ++++++++++++++++++++++++++- crates/hyfetch/src/models.rs | 885 +--------------------------- crates/hyfetch/src/neofetch_util.rs | 41 +- 3 files changed, 882 insertions(+), 903 deletions(-) diff --git a/crates/hyfetch/src/bin/hyfetch.rs b/crates/hyfetch/src/bin/hyfetch.rs index 522631c3..933c545d 100644 --- a/crates/hyfetch/src/bin/hyfetch.rs +++ b/crates/hyfetch/src/bin/hyfetch.rs @@ -1,15 +1,42 @@ +use std::borrow::Cow; +use std::cmp; +use std::fmt::Write as _; use std::fs::{self, File}; -use std::io::{self, IsTerminal as _}; +use std::io::{self, IsTerminal as _, Read as _}; +use std::iter::zip; +use std::path::PathBuf; +use aho_corasick::AhoCorasick; use anyhow::{Context as _, Result}; +use deranged::RangedU8; +use enterpolation::bspline::BSpline; +use enterpolation::{Curve as _, Generator as _}; use hyfetch::cli_options::options; +use hyfetch::color_util::{ + clear_screen, color, printc, ContrastGrayscale as _, ForegroundBackground, Lightness, + NeofetchAsciiIndexedColor, PresetIndexedColor, Theme as _, ToAnsiString as _, +}; use hyfetch::models::Config; -use hyfetch::neofetch_util::{self, get_distro_ascii, ColorAlignment}; -use hyfetch::presets::AssignLightness; -use hyfetch::types::Backend; +#[cfg(feature = "macchina")] +use hyfetch::neofetch_util::macchina_path; +use hyfetch::neofetch_util::{ + self, ascii_size, fastfetch_path, get_distro_ascii, literal_input, ColorAlignment, + NEOFETCH_COLORS_AC, NEOFETCH_COLOR_PATTERNS, TEST_ASCII, +}; +use hyfetch::presets::{AssignLightness, Preset}; +use hyfetch::types::{AnsiMode, Backend, TerminalTheme}; use hyfetch::utils::{get_cache_path, input}; +use indexmap::{IndexMap, IndexSet}; +use itertools::Itertools as _; +use palette::{LinSrgb, Srgb}; +use serde::Serialize as _; +use serde_json::ser::PrettyFormatter; +use strum::{EnumCount as _, VariantArray, VariantNames}; +use terminal_colorsaurus::{background_color, QueryOptions}; +use terminal_size::{terminal_size, Height, Width}; use time::{Month, OffsetDateTime}; use tracing::debug; +use unicode_segmentation::UnicodeSegmentation as _; fn main() -> Result<()> { #[cfg(windows)] @@ -38,7 +65,7 @@ fn main() -> Result<()> { } let config = if options.config { - Config::create_config( + create_config( &options.config_file, distro, backend, @@ -47,11 +74,11 @@ fn main() -> Result<()> { ) .context("failed to create config")? } else if let Some(config) = - Config::read_from_file(&options.config_file).context("failed to read config")? + load_config(&options.config_file).context("failed to load config")? { config } else { - Config::create_config( + create_config( &options.config_file, distro, backend, @@ -139,6 +166,824 @@ fn main() -> Result<()> { Ok(()) } +/// Loads config from file. +/// +/// Returns `None` if the config file does not exist. +#[tracing::instrument(level = "debug")] +pub fn load_config(path: &PathBuf) -> Result> { + let mut file = match File::open(path) { + Ok(file) => file, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + return Ok(None); + }, + Err(err) => { + return Err(err).with_context(|| format!("failed to open file {path:?} for reading")); + }, + }; + + let mut buf = String::new(); + + file.read_to_string(&mut buf) + .with_context(|| format!("failed to read from file {path:?}"))?; + + let deserializer = &mut serde_json::Deserializer::from_str(&buf); + let config: Config = serde_path_to_error::deserialize(deserializer) + .with_context(|| format!("failed to parse config from file {path:?}"))?; + + debug!(?config, "loaded config"); + + Ok(Some(config)) +} + +/// Creates config interactively. +/// +/// The config is automatically stored to file. +#[tracing::instrument(level = "debug")] +pub fn create_config( + path: &PathBuf, + distro: Option<&String>, + backend: Backend, + use_overlay: bool, + debug_mode: bool, +) -> Result { + // Detect terminal environment (doesn't work for all terminal emulators, + // especially on Windows) + let det_bg = if io::stdout().is_terminal() { + match background_color(QueryOptions::default()) { + Ok(bg) => Some(Srgb::::new(bg.r, bg.g, bg.b).into_format::()), + Err(terminal_colorsaurus::Error::UnsupportedTerminal) => None, + Err(err) => { + return Err(err).context("failed to get terminal background color"); + }, + } + } else { + None + }; + debug!(?det_bg, "detected background color"); + let det_ansi = supports_color::on(supports_color::Stream::Stdout).map(|color_level| { + if color_level.has_16m { + AnsiMode::Rgb + } 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() + ); + } else { + unreachable!(); + } + }); + debug!(?det_ansi, "detected color mode"); + + let (asc, fore_back) = + get_distro_ascii(distro, backend).context("failed to get distro ascii")?; + let (asc_width, asc_lines) = ascii_size(&asc); + let theme = det_bg.map(|bg| bg.theme()).unwrap_or(TerminalTheme::Light); + let color_mode = det_ansi.unwrap_or(AnsiMode::Ansi256); + let mut title = format!( + "Welcome to {logo} Let's set up some colors first.", + logo = color( + match theme { + TerminalTheme::Light => "&l&bhyfetch&~&L", + TerminalTheme::Dark => "&l&bhy&ffetch&~&L", + }, + color_mode, + ) + .expect("logo should not contain invalid color codes") + ); + clear_screen(Some(&title), color_mode, debug_mode) + .expect("title should not contain invalid color codes"); + + let mut option_counter: u8 = 1; + + fn update_title(title: &mut String, option_counter: &mut u8, k: &str, v: &str) { + let k: Cow = if k.ends_with(':') { + k.into() + } else { + format!("{k}:").into() + }; + write!(title, "\n&e{option_counter}. {k:<30} &~{v}").unwrap(); + *option_counter += 1; + } + + fn print_title_prompt(option_counter: u8, prompt: &str, color_mode: AnsiMode) { + printc(format!("&a{option_counter}. {prompt}"), color_mode) + .expect("prompt should not contain invalid color codes"); + } + + ////////////////////////////// + // 0. Check term size + + { + let (Width(term_w), Height(term_h)) = + terminal_size().context("failed to get terminal size")?; + let (term_w_min, term_h_min) = (2 * u16::from(asc_width) + 4, 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, + ) + .expect("message should not contain invalid color codes"); + input(Some("Press enter to continue...")).context("failed to read input")?; + } + } + + ////////////////////////////// + // 1. Select color mode + + let default_color_profile = Preset::Rainbow.color_profile(); + + let select_color_mode = || -> Result<(AnsiMode, &str)> { + if det_ansi == Some(AnsiMode::Rgb) { + return Ok((AnsiMode::Rgb, "Detected color mode")); + } + + clear_screen(Some(&title), color_mode, debug_mode) + .expect("title should not contain invalid color codes"); + + let (Width(term_w), _) = terminal_size().context("failed to get terminal size")?; + + let spline = BSpline::builder() + .clamped() + .elements( + default_color_profile + .unique_colors() + .colors + .iter() + .map(|rgb_u8_color| rgb_u8_color.into_linear()) + .collect::>(), + ) + .equidistant::() + .degree(1) + .normalized() + .constant::<2>() + .build() + .expect("building spline should not fail"); + let [dmin, dmax] = spline.domain(); + let gradient: Vec = (0..term_w) + .map(|i| spline.gen(remap(i as f32, 0.0, term_w as f32, dmin, dmax))) + .collect(); + + /// Maps `t` in range `[a, b)` to range `[c, d)`. + fn remap(t: f32, a: f32, b: f32, c: f32, d: f32) -> f32 { + (t - a) * ((d - c) / (b - a)) + c + } + + { + let label = format!( + "{label:^term_w$}", + label = "8bit 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::::from_linear(rgb_f32_color); + let back = rgb_u8_color + .to_ansi_string(AnsiMode::Ansi256, 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::Ansi256) + .expect("message should not contain invalid color codes"); + } + { + 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::::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).expect("message should not contain invalid color codes"); + } + + println!(); + print_title_prompt( + option_counter, + "Which &bcolor system &ado you want to use?", + color_mode, + ); + println!(r#"(If you can't see colors under "RGB Color Testing", please choose 8bit)"#); + println!(); + + let choice = literal_input( + "Your choice?", + AnsiMode::VARIANTS, + AnsiMode::Rgb.as_ref(), + true, + color_mode, + )?; + Ok(( + choice.parse().expect("selected color mode should be valid"), + "Selected color mode", + )) + }; + + let color_mode = { + let (color_mode, ttl) = select_color_mode().context("failed to select color mode")?; + debug!(?color_mode, "selected color mode"); + update_title(&mut title, &mut option_counter, ttl, color_mode.as_ref()); + color_mode + }; + + ////////////////////////////// + // 2. Select theme (light/dark mode) + + let select_theme = || -> Result<(TerminalTheme, &str)> { + if let Some(det_bg) = det_bg { + return Ok((det_bg.theme(), "Detected background color")); + } + + clear_screen(Some(&title), color_mode, debug_mode) + .expect("title should not contain invalid color codes"); + + print_title_prompt( + option_counter, + "Is your terminal in &blight mode&~ or &4dark mode&~?", + color_mode, + ); + let choice = literal_input( + "", + TerminalTheme::VARIANTS, + TerminalTheme::Dark.as_ref(), + true, + color_mode, + )?; + Ok(( + choice.parse().expect("selected theme should be valid"), + "Selected background color", + )) + }; + + let theme = { + let (theme, ttl) = select_theme().context("failed to select theme")?; + debug!(?theme, "selected theme"); + update_title(&mut title, &mut option_counter, ttl, theme.as_ref()); + theme + }; + + ////////////////////////////// + // 3. Choose preset + + // Create flag lines + let mut flags = Vec::with_capacity(Preset::COUNT); + let spacing = { + let spacing = ::VARIANTS + .iter() + .map(|name| name.chars().count()) + .max() + .expect("preset name iterator should not be empty"); + let spacing = u8::try_from(spacing).expect("`spacing` should fit in `u8`"); + cmp::max(spacing, 20) + }; + for preset in ::VARIANTS { + let color_profile = preset.color_profile(); + let flag = color_profile + .color_text( + " ".repeat(usize::from(spacing)), + color_mode, + ForegroundBackground::Background, + false, + ) + .with_context(|| format!("failed to color flag using preset: {preset:?}"))?; + let name = format!( + "{name:^spacing$}", + name = preset.as_ref(), + spacing = usize::from(spacing) + ); + flags.push([name, flag.clone(), flag.clone(), flag]); + } + + // Calculate flags per row + let (flags_per_row, rows_per_page) = { + let (Width(term_w), Height(term_h)) = + terminal_size().context("failed to get terminal size")?; + let flags_per_row = term_w / (u16::from(spacing) + 2); + let flags_per_row = + u8::try_from(flags_per_row).expect("`flags_per_row` should fit in `u8`"); + let rows_per_page = cmp::max(1, (term_h - 13) / 5); + let rows_per_page = + u8::try_from(rows_per_page).expect("`rows_per_page` should fit in `u8`"); + (flags_per_row, rows_per_page) + }; + let num_pages = + (Preset::COUNT as f32 / (flags_per_row as f32 * rows_per_page as f32)).ceil() as usize; + let num_pages = u8::try_from(num_pages).expect("`num_pages` should fit in `u8`"); + + // Create pages + let mut pages = Vec::with_capacity(usize::from(num_pages)); + for flags in flags.chunks(usize::from(flags_per_row) * usize::from(rows_per_page)) { + let mut page = Vec::with_capacity(usize::from(rows_per_page)); + for flags in flags.chunks(usize::from(flags_per_row)) { + page.push(flags); + } + pages.push(page); + } + + let print_flag_page = |page, page_num| { + clear_screen(Some(&title), color_mode, debug_mode) + .expect("title should not contain invalid color codes"); + print_title_prompt(option_counter, "Let's choose a flag!", color_mode); + println!("Available flag presets:"); + println!("Page: {page_num} of {num_pages}", page_num = page_num + 1); + println!(); + for &row in page { + print_flag_row(row, color_mode); + } + println!(); + }; + + fn print_flag_row(row: &[[String; 4]], color_mode: AnsiMode) { + for i in 0..4 { + let mut line = Vec::new(); + for flag in row { + line.push(&*flag[i]); + } + printc(line.join(" "), color_mode) + .expect("flag line should not contain invalid color codes"); + } + println!(); + } + + let default_lightness = Config::default_lightness(theme); + let preset_default_colored = default_color_profile + .with_lightness_adaptive(default_lightness, theme, use_overlay) + .color_text( + "preset", + color_mode, + ForegroundBackground::Foreground, + false, + ) + .expect("coloring text with default preset should not fail"); + + let preset: Preset; + let color_profile; + + let mut page: u8 = 0; + loop { + print_flag_page(&pages[usize::from(page)], page); + + let mut opts: Vec<&str> = ::VARIANTS.into(); + if page < num_pages - 1 { + opts.push("next"); + } + if page > 0 { + opts.push("prev"); + } + println!("Enter 'next' to go to the next page and 'prev' to go to the previous page."); + let selection = literal_input( + format!( + "Which {preset} do you want to use? ", + preset = preset_default_colored + ), + &opts[..], + Preset::Rainbow.as_ref(), + false, + color_mode, + ) + .context("failed to select preset")?; + if selection == "next" { + page += 1; + } else if selection == "prev" { + page -= 1; + } else { + preset = selection.parse().expect("selected preset should be valid"); + debug!(?preset, "selected preset"); + color_profile = preset.color_profile(); + update_title( + &mut title, + &mut option_counter, + "Selected flag", + &color_profile + .with_lightness_adaptive(default_lightness, theme, use_overlay) + .color_text( + preset.as_ref(), + color_mode, + ForegroundBackground::Foreground, + false, + ) + .expect("coloring text with selected preset should not fail"), + ); + break; + } + } + + ////////////////////////////// + // 4. Dim/lighten colors + + let test_ascii = &TEST_ASCII[1..TEST_ASCII.len() - 1]; + let test_ascii_width = test_ascii + .split('\n') + .map(|line| line.graphemes(true).count()) + .max() + .expect("line iterator should not be empty"); + let test_ascii_width = + u8::try_from(test_ascii_width).expect("`test_ascii_width` should fit in `u8`"); + let test_ascii_height = test_ascii.split('\n').count(); + let test_ascii_height = + u8::try_from(test_ascii_height).expect("`test_ascii_height` should fit in `u8`"); + + let select_lightness = || -> Result { + clear_screen(Some(&title), color_mode, debug_mode) + .expect("title should not contain invalid color codes"); + print_title_prompt( + option_counter, + "Let's adjust the color brightness!", + color_mode, + ); + println!( + "The colors might be a little bit too {bright_dark} for {light_dark} mode.", + bright_dark = match theme { + TerminalTheme::Light => "bright", + TerminalTheme::Dark => "dark", + }, + light_dark = theme.as_ref() + ); + println!(); + + let color_align = ColorAlignment::Horizontal { fore_back: None }; + + // Print cats + { + let (Width(term_w), _) = terminal_size().context("failed to get terminal size")?; + let num_cols = cmp::max(1, term_w / (u16::from(test_ascii_width) + 2)); + let num_cols = u8::try_from(num_cols).expect("`num_cols` should fit in `u8`"); + const MIN: f32 = 0.15; + const MAX: f32 = 0.85; + let ratios = + (0..num_cols) + .map(|col| col as f32 / num_cols as f32) + .map(|r| match theme { + TerminalTheme::Light => r * (MAX - MIN) / 2.0 + MIN, + TerminalTheme::Dark => (r * (MAX - MIN) + (MAX + MIN)) / 2.0, + }); + let row: Vec> = ratios + .map(|r| { + let asc = color_align + .recolor_ascii( + test_ascii.replace( + "{txt}", + &format!( + "{lightness:^5}", + lightness = format!("{lightness:.0}%", lightness = r * 100.0) + ), + ), + &color_profile.with_lightness_adaptive( + Lightness::new(r) + .expect("generated lightness should not be invalid"), + theme, + use_overlay, + ), + color_mode, + theme, + ) + .expect("recoloring test ascii should not fail"); + asc.split('\n').map(ToOwned::to_owned).collect() + }) + .collect(); + for i in 0..usize::from(test_ascii_height) { + let mut line = Vec::new(); + for lines in &row { + line.push(&*lines[i]); + } + printc(line.join(" "), color_mode) + .expect("test ascii line should not contain invalid color codes"); + } + } + + loop { + println!(); + println!( + "Which brightness level looks the best? (Default: {default:.0}% for {light_dark} \ + mode)", + default = f32::from(default_lightness) * 100.0, + light_dark = theme.as_ref() + ); + let lightness = input(Some("> ")) + .context("failed to read input")? + .trim() + .to_lowercase(); + + match parse_lightness(lightness, default_lightness) { + Ok(lightness) => { + return Ok(lightness); + }, + 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, + ) + .expect("message should not contain invalid color codes"); + }, + } + } + }; + + fn parse_lightness(lightness: String, default: Lightness) -> Result { + if lightness.is_empty() || ["unset", "none"].contains(&&*lightness) { + return Ok(default); + } + + let lightness = if let Some(lightness) = lightness.strip_suffix('%') { + let lightness: RangedU8<0, 100> = lightness.parse()?; + lightness.get() as f32 / 100.0 + } else { + match lightness.parse::>() { + Ok(lightness) => lightness.get() as f32 / 100.0, + Err(_) => lightness.parse::()?, + } + }; + + Ok(Lightness::new(lightness)?) + } + + let lightness = select_lightness().context("failed to select lightness")?; + debug!(?lightness, "selected lightness"); + let color_profile = color_profile.with_lightness_adaptive(lightness, theme, use_overlay); + update_title( + &mut title, + &mut option_counter, + "Selected brightness", + &format!("{lightness:.2}", lightness = f32::from(lightness)), + ); + + ////////////////////////////// + // 5. Color arrangement + + let color_align: ColorAlignment; + + // Calculate amount of row/column that can be displayed on screen + let (ascii_per_row, ascii_rows) = { + let (Width(term_w), Height(term_h)) = + terminal_size().context("failed to get terminal size")?; + let ascii_per_row = cmp::max(1, term_w / (u16::from(asc_width) + 2)); + let ascii_per_row = + u8::try_from(ascii_per_row).expect("`ascii_per_row` should fit in `u8`"); + let ascii_rows = cmp::max(1, (term_h - 8) / (u16::from(asc_lines) + 1)); + let ascii_rows = u8::try_from(ascii_rows).expect("`ascii_rows` should fit in `u8`"); + (ascii_per_row, ascii_rows) + }; + + // Displays horizontal and vertical arrangements in the first iteration, but + // hide them in later iterations + let hv_arrangements = [ + ("Horizontal", ColorAlignment::Horizontal { fore_back }), + ("Vertical", ColorAlignment::Vertical { fore_back }), + ]; + let mut arrangements: IndexMap, ColorAlignment> = + hv_arrangements.map(|(k, ca)| (k.into(), ca)).into(); + + // Loop for random rolling + let mut rng = fastrand::Rng::new(); + loop { + clear_screen(Some(&title), color_mode, debug_mode) + .expect("title should not contain invalid color codes"); + + // Random color schemes + let mut preset_indices: Vec = + (0..color_profile.unique_colors().colors.len()) + .map(|pi| u8::try_from(pi).expect("`pi` should fit in `u8`").into()) + .collect(); + let slots: IndexSet = { + let ac = NEOFETCH_COLORS_AC + .get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap()); + ac.find_iter(&asc) + .map(|m| { + asc[m.start() + 3..m.end() - 1] + .parse() + .expect("neofetch ascii color index should not be invalid") + }) + .collect() + }; + while preset_indices.len() < slots.len() { + preset_indices.extend_from_within(0..); + } + let preset_index_permutations: IndexSet> = preset_indices + .into_iter() + .permutations(slots.len()) + .collect(); + let random_count = cmp::max( + 0, + usize::from(ascii_per_row) * usize::from(ascii_rows) - arrangements.len(), + ); + let random_count = u8::try_from(random_count).expect("`random_count` should fit in `u8`"); + let choices: IndexSet> = + if usize::from(random_count) > preset_index_permutations.len() { + preset_index_permutations + } else { + rng.choose_multiple( + preset_index_permutations.into_iter(), + usize::from(random_count), + ) + .into_iter() + .collect() + }; + let choices: Vec> = choices + .into_iter() + .map(|c| { + c.into_iter() + .enumerate() + .map(|(ai, pi)| (slots[ai], pi)) + .collect() + }) + .collect(); + arrangements.extend(choices.into_iter().enumerate().map(|(i, colors)| { + (format!("random{i}").into(), ColorAlignment::Custom { + colors, + }) + })); + let asciis = arrangements.iter().map(|(k, ca)| { + let mut v: Vec = ca + .recolor_ascii(&asc, &color_profile, color_mode, theme) + .expect("recoloring ascii should not fail") + .split('\n') + .map(ToOwned::to_owned) + .collect(); + v.push(format!( + "{k:^asc_width$}", + asc_width = usize::from(asc_width) + )); + v + }); + + for row in &asciis.chunks(usize::from(ascii_per_row)) { + let row: Vec> = row.into_iter().collect(); + + // Print by row + for i in 0..usize::from(asc_lines) + 1 { + let mut line = Vec::new(); + for lines in &row { + line.push(&*lines[i]); + } + printc(line.join(" "), color_mode) + .expect("ascii line should not contain invalid color codes"); + } + + println!(); + } + + print_title_prompt( + option_counter, + "Let's choose a color arrangement!", + color_mode, + ); + println!( + "You can choose standard horizontal or vertical alignment, or use one of the random \ + color schemes." + ); + println!(r#"You can type "roll" to randomize again."#); + println!(); + let mut opts: Vec> = ["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) + .context("failed to select color alignment")?; + + if choice == "roll" { + arrangements.clear(); + continue; + } + + // Save choice + color_align = arrangements + .into_iter() + .find_map(|(k, ca)| { + if k.to_lowercase() == choice { + Some(ca) + } else { + None + } + }) + .expect("selected color alignment should be valid"); + debug!(?color_align, "selected color alignment"); + break; + } + + update_title( + &mut title, + &mut option_counter, + "Selected color alignment", + color_align.as_ref(), + ); + + ////////////////////////////// + // 6. Select *fetch backend + + let select_backend = || -> Result { + clear_screen(Some(&title), color_mode, debug_mode) + .expect("title should not contain invalid color codes"); + print_title_prompt(option_counter, "Select a *fetch backend", color_mode); + + // Check if fastfetch is installed + let fastfetch_path = fastfetch_path().context("failed to get fastfetch path")?; + + // Check if macchina is installed + #[cfg(feature = "macchina")] + let macchina_path = macchina_path().context("failed to get macchina path")?; + + printc( + "- &bneofetch&r: Written in bash, &nbest compatibility&r on Unix systems", + color_mode, + ) + .expect("message should not contain invalid color codes"); + 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, + ) + .expect("message should not contain invalid color codes"); + #[cfg(feature = "macchina")] + printc( + format!( + "- &bmacchina&r: Written in Rust, &nbest performance&r {installed_not_installed}", + 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, + ) + .expect("message should not contain invalid color codes"); + println!(); + + let choice = literal_input( + "Your choice?", + Backend::VARIANTS, + backend.as_ref(), + true, + color_mode, + )?; + Ok(choice.parse().expect("selected backend should be valid")) + }; + + let backend = select_backend().context("failed to select backend")?; + update_title( + &mut title, + &mut option_counter, + "Selected backend", + backend.as_ref(), + ); + + // Create config + clear_screen(Some(&title), color_mode, debug_mode) + .expect("title should not contain invalid color codes"); + let config = Config { + preset, + mode: color_mode, + light_dark: theme, + lightness: Some(lightness), + color_align, + backend, + args: None, + distro: distro.cloned(), + pride_month_disable: false, + }; + debug!(?config, "created config"); + + // Save config + let save = literal_input("Save config?", &["y", "n"], "y", true, color_mode)?; + if save == "y" { + let file = File::options() + .write(true) + .create(true) + .truncate(true) + .open(path) + .with_context(|| format!("failed to open file {path:?} for writing"))?; + let mut serializer = + serde_json::Serializer::with_formatter(file, PrettyFormatter::with_indent(b" ")); + config + .serialize(&mut serializer) + .with_context(|| format!("failed to write config to file {path:?}"))?; + debug!(?path, "saved config"); + } + + Ok(config) +} + fn init_tracing_subsriber(debug_mode: bool) -> Result<()> { use std::env; use std::str::FromStr as _; diff --git a/crates/hyfetch/src/models.rs b/crates/hyfetch/src/models.rs index 2a1fb363..30571f1f 100644 --- a/crates/hyfetch/src/models.rs +++ b/crates/hyfetch/src/models.rs @@ -1,62 +1,16 @@ -use std::borrow::Cow; -use std::cmp; -use std::fmt::{self, Write as _}; -use std::fs::File; -use std::io::{self, IsTerminal as _, Read as _}; -use std::iter::zip; -use std::path::Path; - -use aho_corasick::AhoCorasick; -use anyhow::{Context as _, Result}; -use deranged::RangedU8; -use enterpolation::bspline::BSpline; -use enterpolation::{Curve as _, Generator as _}; -use indexmap::{IndexMap, IndexSet}; -use itertools::Itertools as _; -use palette::{LinSrgb, Srgb}; use serde::{Deserialize, Serialize}; -use serde_json::ser::PrettyFormatter; -use strum::{EnumCount as _, VariantArray, VariantNames}; -use terminal_colorsaurus::{background_color, QueryOptions}; -use terminal_size::{terminal_size, Height, Width}; -use tracing::debug; -use unicode_segmentation::UnicodeSegmentation as _; -use crate::color_util::{ - clear_screen, color, printc, ContrastGrayscale as _, ForegroundBackground, Lightness, - NeofetchAsciiIndexedColor, PresetIndexedColor, Theme as _, ToAnsiString as _, -}; -#[cfg(feature = "macchina")] -use crate::neofetch_util::macchina_path; -use crate::neofetch_util::{ - ascii_size, fastfetch_path, get_distro_ascii, literal_input, ColorAlignment, - NEOFETCH_COLORS_AC, NEOFETCH_COLOR_PATTERNS, -}; +use crate::color_util::Lightness; +use crate::neofetch_util::ColorAlignment; use crate::presets::Preset; use crate::types::{AnsiMode, Backend, TerminalTheme}; -use crate::utils::input; - -const TEST_ASCII: &str = r####################" -### |\___/| ### -### ) ( ### -## =\ /= ## -#### )===( #### -### / \ ### -### | | ### -## / {txt} \ ## -## \ / ## -_/\_\_ _/_/\_ -|##| ( ( |##| -|##| ) ) |##| -|##| (_( |##| -"####################; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Config { pub preset: Preset, pub mode: AnsiMode, pub light_dark: TerminalTheme, - lightness: Option, + pub lightness: Option, pub color_align: ColorAlignment, pub backend: Backend, #[serde(default)] @@ -67,839 +21,6 @@ pub struct Config { } impl Config { - /// Reads config from file. - /// - /// Returns `None` if the config file does not exist. - #[tracing::instrument(level = "debug")] - pub fn read_from_file

(path: P) -> Result> - where - P: AsRef + fmt::Debug, - { - let path = path.as_ref(); - - let mut file = match File::open(path) { - Ok(file) => file, - Err(err) if err.kind() == io::ErrorKind::NotFound => { - return Ok(None); - }, - Err(err) => { - return Err(err) - .with_context(|| format!("failed to open file {path:?} for reading")); - }, - }; - - let mut buf = String::new(); - - file.read_to_string(&mut buf) - .with_context(|| format!("failed to read from file {path:?}"))?; - - let deserializer = &mut serde_json::Deserializer::from_str(&buf); - let config: Config = serde_path_to_error::deserialize(deserializer) - .with_context(|| format!("failed to parse config from file {path:?}"))?; - - debug!(?config, "read config"); - - Ok(Some(config)) - } - - /// Creates config interactively. - /// - /// The config is automatically stored to file. - #[tracing::instrument(level = "debug")] - pub fn create_config

( - path: P, - distro: Option<&String>, - backend: Backend, - use_overlay: bool, - debug_mode: bool, - ) -> Result - where - P: AsRef + fmt::Debug, - { - // Detect terminal environment (doesn't work for all terminal emulators, - // especially on Windows) - let det_bg = if io::stdout().is_terminal() { - match background_color(QueryOptions::default()) { - Ok(bg) => Some(Srgb::::new(bg.r, bg.g, bg.b).into_format::()), - Err(terminal_colorsaurus::Error::UnsupportedTerminal) => None, - Err(err) => { - return Err(err).context("failed to get terminal background color"); - }, - } - } else { - None - }; - debug!(?det_bg, "detected background color"); - let det_ansi = supports_color::on(supports_color::Stream::Stdout).map(|color_level| { - if color_level.has_16m { - AnsiMode::Rgb - } 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() - ); - } else { - unreachable!(); - } - }); - debug!(?det_ansi, "detected color mode"); - - let (asc, fore_back) = - get_distro_ascii(distro, backend).context("failed to get distro ascii")?; - let (asc_width, asc_lines) = ascii_size(&asc); - let theme = det_bg.map(|bg| bg.theme()).unwrap_or(TerminalTheme::Light); - let color_mode = det_ansi.unwrap_or(AnsiMode::Ansi256); - let mut title = format!( - "Welcome to {logo} Let's set up some colors first.", - logo = color( - match theme { - TerminalTheme::Light => "&l&bhyfetch&~&L", - TerminalTheme::Dark => "&l&bhy&ffetch&~&L", - }, - color_mode, - ) - .expect("logo should not contain invalid color codes") - ); - clear_screen(Some(&title), color_mode, debug_mode) - .expect("title should not contain invalid color codes"); - - let mut option_counter: u8 = 1; - - fn update_title(title: &mut String, option_counter: &mut u8, k: &str, v: &str) { - let k: Cow = if k.ends_with(':') { - k.into() - } else { - format!("{k}:").into() - }; - write!(title, "\n&e{option_counter}. {k:<30} &~{v}").unwrap(); - *option_counter += 1; - } - - fn print_title_prompt(option_counter: u8, prompt: &str, color_mode: AnsiMode) { - printc(format!("&a{option_counter}. {prompt}"), color_mode) - .expect("prompt should not contain invalid color codes"); - } - - ////////////////////////////// - // 0. Check term size - - { - let (Width(term_w), Height(term_h)) = - terminal_size().context("failed to get terminal size")?; - let (term_w_min, term_h_min) = (2 * u16::from(asc_width) + 4, 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, - ) - .expect("message should not contain invalid color codes"); - input(Some("Press enter to continue...")).context("failed to read input")?; - } - } - - ////////////////////////////// - // 1. Select color mode - - let default_color_profile = Preset::Rainbow.color_profile(); - - let select_color_mode = || -> Result<(AnsiMode, &str)> { - if det_ansi == Some(AnsiMode::Rgb) { - return Ok((AnsiMode::Rgb, "Detected color mode")); - } - - clear_screen(Some(&title), color_mode, debug_mode) - .expect("title should not contain invalid color codes"); - - let (Width(term_w), _) = terminal_size().context("failed to get terminal size")?; - - let spline = BSpline::builder() - .clamped() - .elements( - default_color_profile - .unique_colors() - .colors - .iter() - .map(|rgb_u8_color| rgb_u8_color.into_linear()) - .collect::>(), - ) - .equidistant::() - .degree(1) - .normalized() - .constant::<2>() - .build() - .expect("building spline should not fail"); - let [dmin, dmax] = spline.domain(); - let gradient: Vec = (0..term_w) - .map(|i| spline.gen(remap(i as f32, 0.0, term_w as f32, dmin, dmax))) - .collect(); - - /// Maps `t` in range `[a, b)` to range `[c, d)`. - fn remap(t: f32, a: f32, b: f32, c: f32, d: f32) -> f32 { - (t - a) * ((d - c) / (b - a)) + c - } - - { - let label = format!( - "{label:^term_w$}", - label = "8bit 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::::from_linear(rgb_f32_color); - let back = rgb_u8_color - .to_ansi_string(AnsiMode::Ansi256, 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::Ansi256) - .expect("message should not contain invalid color codes"); - } - { - 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::::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) - .expect("message should not contain invalid color codes"); - } - - println!(); - print_title_prompt( - option_counter, - "Which &bcolor system &ado you want to use?", - color_mode, - ); - println!(r#"(If you can't see colors under "RGB Color Testing", please choose 8bit)"#); - println!(); - - let choice = literal_input( - "Your choice?", - AnsiMode::VARIANTS, - AnsiMode::Rgb.as_ref(), - true, - color_mode, - )?; - Ok(( - choice.parse().expect("selected color mode should be valid"), - "Selected color mode", - )) - }; - - let color_mode = { - let (color_mode, ttl) = select_color_mode().context("failed to select color mode")?; - debug!(?color_mode, "selected color mode"); - update_title(&mut title, &mut option_counter, ttl, color_mode.as_ref()); - color_mode - }; - - ////////////////////////////// - // 2. Select theme (light/dark mode) - - let select_theme = || -> Result<(TerminalTheme, &str)> { - if let Some(det_bg) = det_bg { - return Ok((det_bg.theme(), "Detected background color")); - } - - clear_screen(Some(&title), color_mode, debug_mode) - .expect("title should not contain invalid color codes"); - - print_title_prompt( - option_counter, - "Is your terminal in &blight mode&~ or &4dark mode&~?", - color_mode, - ); - let choice = literal_input( - "", - TerminalTheme::VARIANTS, - TerminalTheme::Dark.as_ref(), - true, - color_mode, - )?; - Ok(( - choice.parse().expect("selected theme should be valid"), - "Selected background color", - )) - }; - - let theme = { - let (theme, ttl) = select_theme().context("failed to select theme")?; - debug!(?theme, "selected theme"); - update_title(&mut title, &mut option_counter, ttl, theme.as_ref()); - theme - }; - - ////////////////////////////// - // 3. Choose preset - - // Create flag lines - let mut flags = Vec::with_capacity(Preset::COUNT); - let spacing = { - let spacing = ::VARIANTS - .iter() - .map(|name| name.chars().count()) - .max() - .expect("preset name iterator should not be empty"); - let spacing = u8::try_from(spacing).expect("`spacing` should fit in `u8`"); - cmp::max(spacing, 20) - }; - for preset in ::VARIANTS { - let color_profile = preset.color_profile(); - let flag = color_profile - .color_text( - " ".repeat(usize::from(spacing)), - color_mode, - ForegroundBackground::Background, - false, - ) - .with_context(|| format!("failed to color flag using preset: {preset:?}"))?; - let name = format!( - "{name:^spacing$}", - name = preset.as_ref(), - spacing = usize::from(spacing) - ); - flags.push([name, flag.clone(), flag.clone(), flag]); - } - - // Calculate flags per row - let (flags_per_row, rows_per_page) = { - let (Width(term_w), Height(term_h)) = - terminal_size().context("failed to get terminal size")?; - let flags_per_row = term_w / (u16::from(spacing) + 2); - let flags_per_row = - u8::try_from(flags_per_row).expect("`flags_per_row` should fit in `u8`"); - let rows_per_page = cmp::max(1, (term_h - 13) / 5); - let rows_per_page = - u8::try_from(rows_per_page).expect("`rows_per_page` should fit in `u8`"); - (flags_per_row, rows_per_page) - }; - let num_pages = - (Preset::COUNT as f32 / (flags_per_row as f32 * rows_per_page as f32)).ceil() as usize; - let num_pages = u8::try_from(num_pages).expect("`num_pages` should fit in `u8`"); - - // Create pages - let mut pages = Vec::with_capacity(usize::from(num_pages)); - for flags in flags.chunks(usize::from(flags_per_row) * usize::from(rows_per_page)) { - let mut page = Vec::with_capacity(usize::from(rows_per_page)); - for flags in flags.chunks(usize::from(flags_per_row)) { - page.push(flags); - } - pages.push(page); - } - - let print_flag_page = |page, page_num| { - clear_screen(Some(&title), color_mode, debug_mode) - .expect("title should not contain invalid color codes"); - print_title_prompt(option_counter, "Let's choose a flag!", color_mode); - println!("Available flag presets:"); - println!("Page: {page_num} of {num_pages}", page_num = page_num + 1); - println!(); - for &row in page { - print_flag_row(row, color_mode); - } - println!(); - }; - - fn print_flag_row(row: &[[String; 4]], color_mode: AnsiMode) { - for i in 0..4 { - let mut line = Vec::new(); - for flag in row { - line.push(&*flag[i]); - } - printc(line.join(" "), color_mode) - .expect("flag line should not contain invalid color codes"); - } - println!(); - } - - let default_lightness = Config::default_lightness(theme); - let preset_default_colored = default_color_profile - .with_lightness_adaptive(default_lightness, theme, use_overlay) - .color_text( - "preset", - color_mode, - ForegroundBackground::Foreground, - false, - ) - .expect("coloring text with default preset should not fail"); - - let preset: Preset; - let color_profile; - - let mut page: u8 = 0; - loop { - print_flag_page(&pages[usize::from(page)], page); - - let mut opts: Vec<&str> = ::VARIANTS.into(); - if page < num_pages - 1 { - opts.push("next"); - } - if page > 0 { - opts.push("prev"); - } - println!("Enter 'next' to go to the next page and 'prev' to go to the previous page."); - let selection = literal_input( - format!( - "Which {preset} do you want to use? ", - preset = preset_default_colored - ), - &opts[..], - Preset::Rainbow.as_ref(), - false, - color_mode, - ) - .context("failed to select preset")?; - if selection == "next" { - page += 1; - } else if selection == "prev" { - page -= 1; - } else { - preset = selection.parse().expect("selected preset should be valid"); - debug!(?preset, "selected preset"); - color_profile = preset.color_profile(); - update_title( - &mut title, - &mut option_counter, - "Selected flag", - &color_profile - .with_lightness_adaptive(default_lightness, theme, use_overlay) - .color_text( - preset.as_ref(), - color_mode, - ForegroundBackground::Foreground, - false, - ) - .expect("coloring text with selected preset should not fail"), - ); - break; - } - } - - ////////////////////////////// - // 4. Dim/lighten colors - - let test_ascii = &TEST_ASCII[1..TEST_ASCII.len() - 1]; - let test_ascii_width = test_ascii - .split('\n') - .map(|line| line.graphemes(true).count()) - .max() - .expect("line iterator should not be empty"); - let test_ascii_width = - u8::try_from(test_ascii_width).expect("`test_ascii_width` should fit in `u8`"); - let test_ascii_height = test_ascii.split('\n').count(); - let test_ascii_height = - u8::try_from(test_ascii_height).expect("`test_ascii_height` should fit in `u8`"); - - let select_lightness = || -> Result { - clear_screen(Some(&title), color_mode, debug_mode) - .expect("title should not contain invalid color codes"); - print_title_prompt( - option_counter, - "Let's adjust the color brightness!", - color_mode, - ); - println!( - "The colors might be a little bit too {bright_dark} for {light_dark} mode.", - bright_dark = match theme { - TerminalTheme::Light => "bright", - TerminalTheme::Dark => "dark", - }, - light_dark = theme.as_ref() - ); - println!(); - - let color_align = ColorAlignment::Horizontal { fore_back: None }; - - // Print cats - { - let (Width(term_w), _) = terminal_size().context("failed to get terminal size")?; - let num_cols = cmp::max(1, term_w / (u16::from(test_ascii_width) + 2)); - let num_cols = u8::try_from(num_cols).expect("`num_cols` should fit in `u8`"); - const MIN: f32 = 0.15; - const MAX: f32 = 0.85; - let ratios = (0..num_cols) - .map(|col| col as f32 / num_cols as f32) - .map(|r| match theme { - TerminalTheme::Light => r * (MAX - MIN) / 2.0 + MIN, - TerminalTheme::Dark => (r * (MAX - MIN) + (MAX + MIN)) / 2.0, - }); - let row: Vec> = ratios - .map(|r| { - let asc = color_align - .recolor_ascii( - test_ascii.replace( - "{txt}", - &format!( - "{lightness:^5}", - lightness = - format!("{lightness:.0}%", lightness = r * 100.0) - ), - ), - &color_profile.with_lightness_adaptive( - Lightness::new(r) - .expect("generated lightness should not be invalid"), - theme, - use_overlay, - ), - color_mode, - theme, - ) - .expect("recoloring test ascii should not fail"); - asc.split('\n').map(ToOwned::to_owned).collect() - }) - .collect(); - for i in 0..usize::from(test_ascii_height) { - let mut line = Vec::new(); - for lines in &row { - line.push(&*lines[i]); - } - printc(line.join(" "), color_mode) - .expect("test ascii line should not contain invalid color codes"); - } - } - - loop { - println!(); - println!( - "Which brightness level looks the best? (Default: {default:.0}% for \ - {light_dark} mode)", - default = f32::from(default_lightness) * 100.0, - light_dark = theme.as_ref() - ); - let lightness = input(Some("> ")) - .context("failed to read input")? - .trim() - .to_lowercase(); - - match parse_lightness(lightness, default_lightness) { - Ok(lightness) => { - return Ok(lightness); - }, - 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, - ) - .expect("message should not contain invalid color codes"); - }, - } - } - }; - - fn parse_lightness(lightness: String, default: Lightness) -> Result { - if lightness.is_empty() || ["unset", "none"].contains(&&*lightness) { - return Ok(default); - } - - let lightness = if let Some(lightness) = lightness.strip_suffix('%') { - let lightness: RangedU8<0, 100> = lightness.parse()?; - lightness.get() as f32 / 100.0 - } else { - match lightness.parse::>() { - Ok(lightness) => lightness.get() as f32 / 100.0, - Err(_) => lightness.parse::()?, - } - }; - - Ok(Lightness::new(lightness)?) - } - - let lightness = select_lightness().context("failed to select lightness")?; - debug!(?lightness, "selected lightness"); - let color_profile = color_profile.with_lightness_adaptive(lightness, theme, use_overlay); - update_title( - &mut title, - &mut option_counter, - "Selected brightness", - &format!("{lightness:.2}", lightness = f32::from(lightness)), - ); - - ////////////////////////////// - // 5. Color arrangement - - let color_align: ColorAlignment; - - // Calculate amount of row/column that can be displayed on screen - let (ascii_per_row, ascii_rows) = { - let (Width(term_w), Height(term_h)) = - terminal_size().context("failed to get terminal size")?; - let ascii_per_row = cmp::max(1, term_w / (u16::from(asc_width) + 2)); - let ascii_per_row = - u8::try_from(ascii_per_row).expect("`ascii_per_row` should fit in `u8`"); - let ascii_rows = cmp::max(1, (term_h - 8) / (u16::from(asc_lines) + 1)); - let ascii_rows = u8::try_from(ascii_rows).expect("`ascii_rows` should fit in `u8`"); - (ascii_per_row, ascii_rows) - }; - - // Displays horizontal and vertical arrangements in the first iteration, but - // hide them in later iterations - let hv_arrangements = [ - ("Horizontal", ColorAlignment::Horizontal { fore_back }), - ("Vertical", ColorAlignment::Vertical { fore_back }), - ]; - let mut arrangements: IndexMap, ColorAlignment> = - hv_arrangements.map(|(k, ca)| (k.into(), ca)).into(); - - // Loop for random rolling - let mut rng = fastrand::Rng::new(); - loop { - clear_screen(Some(&title), color_mode, debug_mode) - .expect("title should not contain invalid color codes"); - - // Random color schemes - let mut preset_indices: Vec = - (0..color_profile.unique_colors().colors.len()) - .map(|pi| u8::try_from(pi).expect("`pi` should fit in `u8`").into()) - .collect(); - let slots: IndexSet = { - let ac = NEOFETCH_COLORS_AC - .get_or_init(|| AhoCorasick::new(NEOFETCH_COLOR_PATTERNS).unwrap()); - ac.find_iter(&asc) - .map(|m| { - asc[m.start() + 3..m.end() - 1] - .parse() - .expect("neofetch ascii color index should not be invalid") - }) - .collect() - }; - while preset_indices.len() < slots.len() { - preset_indices.extend_from_within(0..); - } - let preset_index_permutations: IndexSet> = preset_indices - .into_iter() - .permutations(slots.len()) - .collect(); - let random_count = cmp::max( - 0, - usize::from(ascii_per_row) * usize::from(ascii_rows) - arrangements.len(), - ); - let random_count = - u8::try_from(random_count).expect("`random_count` should fit in `u8`"); - let choices: IndexSet> = - if usize::from(random_count) > preset_index_permutations.len() { - preset_index_permutations - } else { - rng.choose_multiple( - preset_index_permutations.into_iter(), - usize::from(random_count), - ) - .into_iter() - .collect() - }; - let choices: Vec> = choices - .into_iter() - .map(|c| { - c.into_iter() - .enumerate() - .map(|(ai, pi)| (slots[ai], pi)) - .collect() - }) - .collect(); - arrangements.extend(choices.into_iter().enumerate().map(|(i, colors)| { - (format!("random{i}").into(), ColorAlignment::Custom { - colors, - }) - })); - let asciis = arrangements.iter().map(|(k, ca)| { - let mut v: Vec = ca - .recolor_ascii(&asc, &color_profile, color_mode, theme) - .expect("recoloring ascii should not fail") - .split('\n') - .map(ToOwned::to_owned) - .collect(); - v.push(format!( - "{k:^asc_width$}", - asc_width = usize::from(asc_width) - )); - v - }); - - for row in &asciis.chunks(usize::from(ascii_per_row)) { - let row: Vec> = row.into_iter().collect(); - - // Print by row - for i in 0..usize::from(asc_lines) + 1 { - let mut line = Vec::new(); - for lines in &row { - line.push(&*lines[i]); - } - printc(line.join(" "), color_mode) - .expect("ascii line should not contain invalid color codes"); - } - - println!(); - } - - print_title_prompt( - option_counter, - "Let's choose a color arrangement!", - color_mode, - ); - println!( - "You can choose standard horizontal or vertical alignment, or use one of the \ - random color schemes." - ); - println!(r#"You can type "roll" to randomize again."#); - println!(); - let mut opts: Vec> = ["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) - .context("failed to select color alignment")?; - - if choice == "roll" { - arrangements.clear(); - continue; - } - - // Save choice - color_align = arrangements - .into_iter() - .find_map(|(k, ca)| { - if k.to_lowercase() == choice { - Some(ca) - } else { - None - } - }) - .expect("selected color alignment should be valid"); - debug!(?color_align, "selected color alignment"); - break; - } - - update_title( - &mut title, - &mut option_counter, - "Selected color alignment", - color_align.as_ref(), - ); - - ////////////////////////////// - // 6. Select *fetch backend - - let select_backend = || -> Result { - clear_screen(Some(&title), color_mode, debug_mode) - .expect("title should not contain invalid color codes"); - print_title_prompt(option_counter, "Select a *fetch backend", color_mode); - - // Check if fastfetch is installed - let fastfetch_path = fastfetch_path().context("failed to get fastfetch path")?; - - // Check if macchina is installed - #[cfg(feature = "macchina")] - let macchina_path = macchina_path().context("failed to get macchina path")?; - - printc( - "- &bneofetch&r: Written in bash, &nbest compatibility&r on Unix systems", - color_mode, - ) - .expect("message should not contain invalid color codes"); - 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, - ) - .expect("message should not contain invalid color codes"); - #[cfg(feature = "macchina")] - printc( - format!( - "- &bmacchina&r: Written in Rust, &nbest performance&r \ - {installed_not_installed}", - 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, - ) - .expect("message should not contain invalid color codes"); - println!(); - - let choice = literal_input( - "Your choice?", - Backend::VARIANTS, - backend.as_ref(), - true, - color_mode, - )?; - Ok(choice.parse().expect("selected backend should be valid")) - }; - - let backend = select_backend().context("failed to select backend")?; - update_title( - &mut title, - &mut option_counter, - "Selected backend", - backend.as_ref(), - ); - - // Create config - clear_screen(Some(&title), color_mode, debug_mode) - .expect("title should not contain invalid color codes"); - let config = Config { - preset, - mode: color_mode, - light_dark: theme, - lightness: Some(lightness), - color_align, - backend, - args: None, - distro: distro.cloned(), - pride_month_disable: false, - }; - debug!(?config, "created config"); - - // Save config - let save = literal_input("Save config?", &["y", "n"], "y", true, color_mode)?; - if save == "y" { - let path = path.as_ref(); - - let file = File::options() - .write(true) - .create(true) - .truncate(true) - .open(path) - .with_context(|| format!("failed to open file {path:?} for writing"))?; - let mut serializer = - serde_json::Serializer::with_formatter(file, PrettyFormatter::with_indent(b" ")); - config - .serialize(&mut serializer) - .with_context(|| format!("failed to write config to file {path:?}"))?; - debug!(?path, "saved config"); - } - - Ok(config) - } - pub fn default_lightness(theme: TerminalTheme) -> Lightness { match theme { TerminalTheme::Dark => { diff --git a/crates/hyfetch/src/neofetch_util.rs b/crates/hyfetch/src/neofetch_util.rs index 4804f957..fa4587e1 100644 --- a/crates/hyfetch/src/neofetch_util.rs +++ b/crates/hyfetch/src/neofetch_util.rs @@ -35,6 +35,21 @@ use crate::presets::ColorProfile; use crate::types::{AnsiMode, Backend, TerminalTheme}; use crate::utils::{find_file, find_in_path, input, process_command_status}; +pub const TEST_ASCII: &str = r####################" +### |\___/| ### +### ) ( ### +## =\ /= ## +#### )===( #### +### / \ ### +### | | ### +## / {txt} \ ## +## \ / ## +_/\_\_ _/_/\_ +|##| ( ( |##| +|##| ) ) |##| +|##| (_( |##| +"####################; + pub const NEOFETCH_COLOR_PATTERNS: [&str; 6] = ["${c1}", "${c2}", "${c3}", "${c4}", "${c5}", "${c6}"]; pub static NEOFETCH_COLORS_AC: OnceLock = OnceLock::new(); @@ -511,7 +526,7 @@ pub fn fastfetch_path() -> Result> { } }; - // Fall back to `fastfetch/fastfetch.exe` in directory of current executable + // Fall back to `fastfetch\fastfetch.exe` in directory of current executable #[cfg(windows)] let fastfetch_path = fastfetch_path.map_or_else( || { @@ -521,7 +536,7 @@ pub fn fastfetch_path() -> Result> { let current_exe_dir_path = current_exe_path .parent() .expect("parent should not be `None`"); - let fastfetch_path = current_exe_dir_path.join("fastfetch/fastfetch.exe"); + let fastfetch_path = current_exe_dir_path.join(r"fastfetch\fastfetch.exe"); find_file(&fastfetch_path) .with_context(|| format!("failed to check existence of file {fastfetch_path:?}")) }, @@ -714,13 +729,11 @@ where .join("\n")) } -/// Ensures git bash installation for Windows. -/// -/// Returns the path of git bash. +/// Gets the absolute path of the bash command. #[cfg(windows)] -fn ensure_git_bash() -> Result { +fn bash_path() -> Result { // Find `bash.exe` in `PATH`, but exclude the known bad paths - let git_bash_path = find_in_path("bash.exe") + let bash_path = find_in_path("bash.exe") .context("failed to check existence of `bash.exe` in `PATH`")? .map_or_else( || Ok(None), @@ -745,7 +758,7 @@ fn ensure_git_bash() -> Result { )?; // Detect any Git for Windows installation in `PATH` - let git_bash_path = git_bash_path.map_or_else( + let bash_path = bash_path.map_or_else( || { let git_path = find_in_path("git.exe") .context("failed to check existence of `git.exe` in `PATH`")?; @@ -770,7 +783,7 @@ fn ensure_git_bash() -> Result { )?; // Fall back to default Git for Windows installation paths - let git_bash_path = git_bash_path + let bash_path = bash_path .or_else(|| { let program_files_dir = env::var_os("ProgramFiles")?; let bash_path = Path::new(&program_files_dir).join(r"Git\bin\bash.exe"); @@ -791,7 +804,7 @@ fn ensure_git_bash() -> Result { }); // Bundled git bash - let git_bash_path = git_bash_path.map_or_else( + let bash_path = bash_path.map_or_else( || { let current_exe_path: PathBuf = env::current_exe() .and_then(|p| p.normalize().map(|p| p.into())) @@ -809,9 +822,9 @@ fn ensure_git_bash() -> Result { |path| Ok(Some(path)), )?; - let git_bash_path = git_bash_path.context("failed to find git bash executable")?; + let bash_path = bash_path.context("bash command not found")?; - Ok(git_bash_path) + Ok(bash_path) } /// Runs neofetch command, returning the piped stdout output. @@ -854,8 +867,8 @@ where } #[cfg(windows)] { - let git_bash_path = ensure_git_bash().context("failed to get git bash path")?; - let mut command = Command::new(git_bash_path); + let bash_path = bash_path().context("failed to get bash path")?; + let mut command = Command::new(bash_path); command.arg(neofetch_path); command.args(args); Ok(command)