use std::{
io,
str::FromStr,
thread,
time::{Duration, Instant},
};
use crate::{
control::{clear_line, flush, move_cursor_down, move_cursor_up, Visibility},
read::{key_pressed_within, read_key, Key},
styled::{Color, StyledText},
};
#[derive(Clone, Copy, Debug, Default)]
pub struct Empty<T>(pub Option<T>);
impl<T> FromStr for Empty<T>
where
T: FromStr,
T::Err: std::fmt::Debug,
{
type Err = T::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.trim().is_empty() {
Ok(Empty(None))
} else {
s.trim().parse::<T>().map(|v| Empty(Some(v)))
}
}
}
pub fn input<T>(before: &str) -> T
where
T: std::str::FromStr,
T::Err: std::fmt::Debug,
{
loop {
let quest = StyledText::new("?").fg(Color::Red);
let caret = StyledText::new("›").fg(Color::BrightBlack);
print!("{quest} {before} {caret} ");
flush();
let mut cli = String::new();
io::stdin().read_line(&mut cli).unwrap();
match cli.parse() {
Ok(value) => return value,
Err(_) => {
let x = StyledText::new("X").fg(Color::Red);
println!("\n{x} Invalid Input Type\n")
}
}
}
}
pub fn select<'a>(before: &'a str, options: &'a [&'a str]) -> usize {
let mut i = 0;
let quest = StyledText::new("?").fg(Color::Red);
let caret = StyledText::new("›").fg(Color::BrightBlack);
println!("{quest} {before} {caret} ");
populate(options, None, 0);
let vis = Visibility::new();
vis.hide_cursor();
loop {
if let Ok(character) = read_key() {
match character {
Key::ArrowUp | Key::Char('w') | Key::Char('W') => {
if i > 0 {
i -= 1;
populate(options, None, i);
}
}
Key::ArrowDown | Key::Char('s') | Key::Char('S') => {
if i < options.len() - 1 {
i += 1;
populate(options, None, i);
}
}
Key::Enter => {
break;
}
_ => {}
}
}
}
move_cursor_down(options.len());
i
}
pub fn multiselect(before: &str, options: &[&str]) -> Vec<bool> {
let mut matrix: Vec<bool> = vec![false; options.len()];
let mut i = 0;
let quest = StyledText::new("?").fg(Color::Red);
let caret = StyledText::new("›").fg(Color::BrightBlack);
println!("{quest} {before} {caret} ");
populate(options, Some(&matrix), 0);
let vis = Visibility::new();
vis.hide_cursor();
loop {
if let Ok(character) = read_key() {
match character {
Key::ArrowUp | Key::Char('w') | Key::Char('W') => {
if i > 0 {
i -= 1;
populate(options, Some(&matrix), i);
}
}
Key::ArrowDown | Key::Char('s') | Key::Char('S') => {
if i < options.len() - 1 {
i += 1;
populate(options, Some(&matrix), i);
}
}
Key::Char(' ') => {
move_cursor_down(i);
clear_line();
matrix[i] = !matrix[i];
flush();
move_cursor_up(i);
populate(options, Some(&matrix), i);
}
Key::Enter => {
break;
}
_ => {}
}
}
}
move_cursor_down(options.len());
matrix
}
fn populate(options: &[&str], matrix: Option<&[bool]>, cursor: usize) {
for (i, option) in options.iter().enumerate() {
clear_line();
if i == cursor {
let caret = StyledText::new("›").fg(Color::Green);
let option = if matrix.is_some() && matrix.unwrap()[i] {
StyledText::new(option).fg(Color::Green)
} else {
StyledText::new(option).fg(Color::Cyan)
};
println!(" {caret} {option}");
} else if matrix.is_some() && matrix.unwrap()[i] {
let option = StyledText::new(option).fg(Color::Green);
println!(" {}", option);
} else {
println!(" {}", option);
}
}
move_cursor_up(options.len());
}
#[derive(Debug, Clone)]
pub enum SpinnerType {
Standard,
Dots,
Box,
Flip,
Custom(&'static [&'static str]),
}
impl SpinnerType {
pub fn frames(&self) -> &'static [&'static str] {
match self {
SpinnerType::Standard => &["/", "-", "\\", "|"],
SpinnerType::Dots => &[".", "..", "...", "....", "...", ".."],
SpinnerType::Box => &["▌", "▀", "▐", "▄"],
SpinnerType::Flip => &["_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_"],
SpinnerType::Custom(frames) => frames,
}
}
}
pub fn spinner(mut time: f64, spinner_type: SpinnerType) {
let frames = spinner_type.frames();
let mut i = 0;
while time > 0.0 {
clear_line();
print!("{}", frames[i]);
flush();
thread::sleep(Duration::from_secs_f64(0.075));
time -= 0.075;
if i < frames.len() - 1 {
i += 1
} else {
i = 0
}
}
clear_line();
}
const FAST_GRACE_MS: u64 = 120;
const FAST_GRACE: Duration = Duration::from_millis(FAST_GRACE_MS);
pub fn reveal(str: &str, time_between: f64, skip: Option<(Key, f64)>) {
let clamped = if time_between.is_finite() && time_between >= 0.0 {
time_between
} else {
0.0
};
let normal_delay = Duration::from_secs_f64(clamped);
let (skip_key, fast_delay) = if let Some((key, fast_time_between)) = skip {
let clamped = if fast_time_between.is_finite() && fast_time_between >= 0.0 {
fast_time_between
} else {
0.0
};
let delay = Duration::from_secs_f64(clamped);
(Some(key), delay)
} else {
(None, Duration::from_millis(0))
};
let mut fast_until: Option<Instant> = None;
for ch in str.chars() {
print!("{ch}");
flush();
let now = Instant::now();
let fast_active = fast_until.map_or(false, |t| now < t);
let delay = if fast_active {
fast_delay
} else {
normal_delay
};
let Some(skip_key) = skip_key.clone() else {
std::thread::sleep(delay);
continue;
};
match key_pressed_within(delay) {
Ok(Some(k)) if k == skip_key => {
fast_until = Some(Instant::now() + FAST_GRACE);
while let Ok(Some(k2)) = key_pressed_within(Duration::from_millis(0)) {
if k2 == skip_key {
fast_until = Some(Instant::now() + FAST_GRACE);
} else {
break;
}
}
}
_ => {
if let Some(t) = fast_until {
if Instant::now() >= t {
fast_until = None;
}
}
}
}
}
}