Prompt for preset selection

This commit is contained in:
Teoh Han Hui 2024-07-12 22:42:58 +08:00
parent 4861744b50
commit 114de9be12
No known key found for this signature in database
GPG key ID: D43E2BABAF97DCAE
4 changed files with 171 additions and 20 deletions

View file

@ -1,7 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp; use std::cmp;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{self, ErrorKind, IsTerminal, Read}; use std::io::{self, ErrorKind, IsTerminal, Read, Write};
use std::path::Path; use std::path::Path;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -10,8 +10,8 @@ use hyfetch::color_util::{clear_screen, color, printc, ForegroundBackground, The
use hyfetch::models::Config; use hyfetch::models::Config;
#[cfg(windows)] #[cfg(windows)]
use hyfetch::neofetch_util::ensure_git_bash; use hyfetch::neofetch_util::ensure_git_bash;
use hyfetch::neofetch_util::{self, ascii_size, get_distro_ascii, ColorAlignment}; use hyfetch::neofetch_util::{self, ascii_size, get_distro_ascii, literal_input, ColorAlignment};
use hyfetch::presets::{AssignLightness, Preset}; use hyfetch::presets::{AssignLightness, ColorProfile, Preset};
use hyfetch::types::{AnsiMode, LightDark}; use hyfetch::types::{AnsiMode, LightDark};
use hyfetch::utils::get_cache_path; use hyfetch::utils::get_cache_path;
use palette::Srgb; use palette::Srgb;
@ -45,14 +45,14 @@ fn main() -> Result<()> {
} }
let config = if options.config { let config = if options.config {
create_config(distro, &options.config_file, options.debug) create_config(distro, &options.config_file, options.overlay, options.debug)
.context("failed to create config")? .context("failed to create config")?
} else if let Some(config) = } else if let Some(config) =
read_config(&options.config_file).context("failed to read config")? read_config(&options.config_file).context("failed to read config")?
{ {
config config
} else { } else {
create_config(distro, &options.config_file, options.debug) create_config(distro, &options.config_file, options.overlay, options.debug)
.context("failed to create config")? .context("failed to create config")?
}; };
@ -130,6 +130,7 @@ fn main() -> Result<()> {
if options.ask_exit { if options.ask_exit {
print!("Press any key to exit..."); print!("Press any key to exit...");
io::stdout().flush()?;
let mut buf = String::new(); let mut buf = String::new();
io::stdin() io::stdin()
.read_line(&mut buf) .read_line(&mut buf)
@ -172,7 +173,12 @@ fn read_config(path: &Path) -> Result<Option<Config>> {
/// ///
/// The config is automatically stored to file. /// The config is automatically stored to file.
#[tracing::instrument(level = "debug")] #[tracing::instrument(level = "debug")]
fn create_config(distro: Option<&String>, path: &Path, debug_: bool) -> Result<Config> { fn create_config(
distro: Option<&String>,
path: &Path,
use_overlay: bool,
debug_mode: bool,
) -> Result<Config> {
// Detect terminal environment (doesn't work for all terminal emulators, // Detect terminal environment (doesn't work for all terminal emulators,
// especially on Windows) // especially on Windows)
let det_bg = match termbg::rgb(std::time::Duration::from_millis(100)) { let det_bg = match termbg::rgb(std::time::Duration::from_millis(100)) {
@ -209,7 +215,7 @@ fn create_config(distro: Option<&String>, path: &Path, debug_: bool) -> Result<C
) )
.expect("logo should not contain invalid color codes"); .expect("logo should not contain invalid color codes");
let mut title = format!("Welcome to {logo} Let's set up some colors first."); let mut title = format!("Welcome to {logo} Let's set up some colors first.");
clear_screen(Some(&title), color_mode, debug_) clear_screen(Some(&title), color_mode, debug_mode)
.expect("title should not contain invalid color codes"); .expect("title should not contain invalid color codes");
let mut option_counter: u8 = 1; let mut option_counter: u8 = 1;
@ -239,12 +245,13 @@ fn create_config(distro: Option<&String>, path: &Path, debug_: bool) -> Result<C
////////////////////////////// //////////////////////////////
// 1. Select color system // 1. Select color system
let select_color_system = || -> Result<(AnsiMode, &str)> { let select_color_system = || -> Result<(AnsiMode, &str)> {
if det_ansi == Some(AnsiMode::Rgb) { if det_ansi == Some(AnsiMode::Rgb) {
return Ok((AnsiMode::Rgb, "Detected color mode")); return Ok((AnsiMode::Rgb, "Detected color mode"));
} }
clear_screen(Some(&title), color_mode, debug_) clear_screen(Some(&title), color_mode, debug_mode)
.expect("title should not contain invalid color codes"); .expect("title should not contain invalid color codes");
// TODO // TODO
@ -274,12 +281,13 @@ fn create_config(distro: Option<&String>, path: &Path, debug_: bool) -> Result<C
////////////////////////////// //////////////////////////////
// 2. Select light/dark mode // 2. Select light/dark mode
let select_light_dark = || -> Result<(LightDark, &str)> { let select_light_dark = || -> Result<(LightDark, &str)> {
if let Some(det_bg) = det_bg { if let Some(det_bg) = det_bg {
return Ok((det_bg.theme(), "Detected background color")); return Ok((det_bg.theme(), "Detected background color"));
} }
clear_screen(Some(&title), color_mode, debug_) clear_screen(Some(&title), color_mode, debug_mode)
.expect("title should not contain invalid color codes"); .expect("title should not contain invalid color codes");
todo!() todo!()
@ -295,6 +303,7 @@ fn create_config(distro: Option<&String>, path: &Path, debug_: bool) -> Result<C
////////////////////////////// //////////////////////////////
// 3. Choose preset // 3. Choose preset
// Create flag lines // Create flag lines
let mut flags = Vec::with_capacity(Preset::COUNT); let mut flags = Vec::with_capacity(Preset::COUNT);
let spacing = { let spacing = {
@ -372,7 +381,7 @@ fn create_config(distro: Option<&String>, path: &Path, debug_: bool) -> Result<C
}; };
let print_flag_page = |page, page_num| { let print_flag_page = |page, page_num| {
clear_screen(Some(&title), color_mode, debug_) clear_screen(Some(&title), color_mode, debug_mode)
.expect("title should not contain invalid color codes"); .expect("title should not contain invalid color codes");
print_title_prompt(option_counter, "Let's choose a flag!", color_mode); print_title_prompt(option_counter, "Let's choose a flag!", color_mode);
printc("Available flag presets:", color_mode) printc("Available flag presets:", color_mode)
@ -388,17 +397,77 @@ fn create_config(distro: Option<&String>, path: &Path, debug_: bool) -> Result<C
println!(); println!();
}; };
let page: u8 = 0; let color_profile: ColorProfile;
let preset_rainbow = Preset::Rainbow
.color_profile()
.with_lightness_dl(Config::default_lightness(theme), theme, use_overlay)
.color_text(
"preset",
color_mode,
ForegroundBackground::Foreground,
false,
)
.expect("coloring text with rainbow preset should not fail");
let mut page: u8 = 0;
loop { loop {
print_flag_page(&pages[page as usize], page); print_flag_page(&pages[page as usize], page);
todo!(); let mut opts = Vec::from(<Preset as VariantNames>::VARIANTS);
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 preset = literal_input(
format!("Which {preset_rainbow} do you want to use? "),
&opts[..],
Preset::Rainbow.into(),
false,
color_mode,
)
.context("failed to select preset")?;
if preset == "next" {
page += 1;
} else if preset == "prev" {
page -= 1;
} else {
let preset: Preset = preset.parse().expect("selected preset should be valid");
debug!(?preset, "selected preset");
color_profile = preset.color_profile();
{
let preset_name: &'static str = preset.into();
let preset_colored_name = color_profile
.with_lightness_dl(Config::default_lightness(theme), theme, use_overlay)
.color_text(
preset_name,
color_mode,
ForegroundBackground::Foreground,
false,
)
.expect("coloring text with selected preset should not fail");
update_title(
&mut title,
&mut option_counter,
"Selected flag",
&preset_colored_name,
);
}
break;
}
} }
//////////////////////////////
// 4. Dim/lighten colors
// TODO
todo!() todo!()
} }
fn init_tracing_subsriber(debug: bool) -> Result<()> { fn init_tracing_subsriber(debug_mode: bool) -> Result<()> {
use std::env; use std::env;
use std::str::FromStr; use std::str::FromStr;
@ -432,7 +501,7 @@ fn init_tracing_subsriber(debug: bool) -> Result<()> {
Targets::new().with_default(Subscriber::DEFAULT_MAX_LEVEL) Targets::new().with_default(Subscriber::DEFAULT_MAX_LEVEL)
}, },
}; };
let targets = if debug { let targets = if debug_mode {
targets.with_target(env!("CARGO_CRATE_NAME"), Level::DEBUG) targets.with_target(env!("CARGO_CRATE_NAME"), Level::DEBUG)
} else { } else {
targets targets

View file

@ -1,3 +1,4 @@
use std::io::{self, Write};
use std::num::{ParseFloatError, ParseIntError}; use std::num::{ParseFloatError, ParseIntError};
use std::str::FromStr; use std::str::FromStr;
use std::sync::OnceLock; use std::sync::OnceLock;
@ -337,6 +338,7 @@ where
Ok(dst) Ok(dst)
} }
/// Prints with color.
pub fn printc<S>(msg: S, mode: AnsiMode) -> Result<()> pub fn printc<S>(msg: S, mode: AnsiMode) -> Result<()>
where where
S: AsRef<str>, S: AsRef<str>,
@ -351,9 +353,11 @@ where
Ok(()) Ok(())
} }
pub fn clear_screen(title: Option<&str>, mode: AnsiMode, debug: bool) -> Result<()> { /// Clears screen using ANSI escape codes.
if !debug { pub fn clear_screen(title: Option<&str>, mode: AnsiMode, debug_mode: bool) -> Result<()> {
if !debug_mode {
print!("\x1b[2J\x1b[H"); print!("\x1b[2J\x1b[H");
io::stdout().flush()?;
} }
if let Some(title) = title { if let Some(title) = title {

View file

@ -21,7 +21,7 @@ pub struct Config {
} }
impl Config { impl Config {
pub fn default_lightness(term: &LightDark) -> Lightness { pub fn default_lightness(term: LightDark) -> Lightness {
match term { match term {
LightDark::Dark => { LightDark::Dark => {
Lightness::new(0.65).expect("default lightness should not be invalid") Lightness::new(0.65).expect("default lightness should not be invalid")
@ -34,7 +34,7 @@ impl Config {
pub fn lightness(&self) -> Lightness { pub fn lightness(&self) -> Lightness {
self.lightness self.lightness
.unwrap_or_else(|| Self::default_lightness(&self.light_dark)) .unwrap_or_else(|| Self::default_lightness(self.light_dark))
} }
} }

View file

@ -1,6 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io::Write; use std::io::{self, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::sync::OnceLock; use std::sync::OnceLock;
@ -17,7 +17,8 @@ use tracing::debug;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::color_util::{ use crate::color_util::{
color, ForegroundBackground, NeofetchAsciiIndexedColor, PresetIndexedColor, ToAnsiString, color, printc, ForegroundBackground, NeofetchAsciiIndexedColor, PresetIndexedColor,
ToAnsiString,
}; };
use crate::distros::Distro; use crate::distros::Distro;
use crate::presets::ColorProfile; use crate::presets::ColorProfile;
@ -285,6 +286,83 @@ impl ColorAlignment {
} }
} }
/// Asks the user to provide an input among a list of options.
pub fn literal_input<'a, S>(
prompt: S,
options: &[&'a str],
default: &str,
show_options: bool,
color_mode: AnsiMode,
) -> Result<&'a str>
where
S: AsRef<str>,
{
let prompt = prompt.as_ref();
if show_options {
let options_text = options
.iter()
.map(|&o| {
if o == default {
format!("&l&n{o}&L&N")
} else {
o.to_owned()
}
})
.collect::<Vec<_>>()
.join("|");
printc(format!("{prompt} ({options_text})"), color_mode)
.context("failed to print input prompt")?;
} else {
printc(format!("{prompt} (default: {default})"), color_mode)
.context("failed to print input prompt")?;
}
let find_selection = |sel: &str| {
if sel.is_empty() {
return None;
}
// Find exact match
if let Some(selected) = options.iter().find(|&&o| o.to_lowercase() == sel) {
return Some(selected);
}
// Find starting abbreviation
if let Some(selected) = options.iter().find(|&&o| o.to_lowercase().starts_with(sel)) {
return Some(selected);
}
None
};
loop {
let mut buf = String::new();
print!("> ");
io::stdout().flush()?;
io::stdin()
.read_line(&mut buf)
.context("failed to read line from input")?;
let selection = {
let selection = buf.trim_end_matches(&['\r', '\n']);
if selection.is_empty() {
default.to_owned()
} else {
selection.to_lowercase()
}
};
if let Some(selected) = find_selection(&selection) {
println!();
return Ok(selected);
} else {
let options_text = options.join("|");
println!("Invalid selection! {selection} is not one of {options_text}");
}
}
}
/// Gets the absolute path of the neofetch command. /// Gets the absolute path of the neofetch command.
pub fn neofetch_path() -> Result<Option<PathBuf>> { pub fn neofetch_path() -> Result<Option<PathBuf>> {
if let Some(workspace_dir) = env::var_os("CARGO_WORKSPACE_DIR") { if let Some(workspace_dir) = env::var_os("CARGO_WORKSPACE_DIR") {