use console::{style, Style};
use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, FuzzySelect, MultiSelect, Select};
use std::fmt::Display;
const INDIGO_BG: u8 = 54;
const INDIGO_MID: u8 = 61;
const INDIGO_LIGHT: u8 = 141;
const INDIGO_MATCH: u8 = 183;
const INDIGO_MUTED: u8 = 103;
pub fn xbp_theme() -> ColorfulTheme {
ColorfulTheme {
defaults_style: Style::new().for_stderr().color256(INDIGO_LIGHT),
prompt_style: Style::new().for_stderr().white().bold(),
prompt_prefix: style("◆".to_string()).for_stderr().color256(INDIGO_LIGHT).bold(),
prompt_suffix: style("›".to_string()).for_stderr().color256(INDIGO_MID),
success_prefix: style("✔".to_string()).for_stderr().green().bright(),
success_suffix: style("·".to_string()).for_stderr().black().bright(),
error_prefix: style("✘".to_string()).for_stderr().red().bright(),
error_style: Style::new().for_stderr().red().bright(),
hint_style: Style::new().for_stderr().color256(INDIGO_MUTED),
values_style: Style::new().for_stderr().color256(INDIGO_LIGHT).bold(),
active_item_style: Style::new()
.for_stderr()
.white()
.bold()
.on_color256(INDIGO_BG),
inactive_item_style: Style::new().for_stderr().color256(252),
active_item_prefix: style("❯".to_string())
.for_stderr()
.color256(INDIGO_LIGHT)
.bold(),
inactive_item_prefix: style(" ".to_string()).for_stderr().color256(INDIGO_MUTED),
checked_item_prefix: style("✔".to_string()).for_stderr().color256(INDIGO_LIGHT),
unchecked_item_prefix: style("⬚".to_string()).for_stderr().color256(INDIGO_MID),
picked_item_prefix: style("❯".to_string()).for_stderr().color256(INDIGO_LIGHT),
unpicked_item_prefix: style(" ".to_string()).for_stderr(),
fuzzy_cursor_style: Style::new()
.for_stderr()
.white()
.bold()
.on_color256(INDIGO_BG),
fuzzy_match_highlight_style: Style::new()
.for_stderr()
.color256(INDIGO_MATCH)
.bold()
.underlined(),
}
}
pub fn xbp_selection_style() -> ratatui::style::Style {
use ratatui::style::{Color, Modifier, Style};
Style::default()
.bg(Color::Indexed(INDIGO_BG))
.fg(Color::White)
.add_modifier(Modifier::BOLD)
}
pub fn xbp_accent_color() -> ratatui::style::Color {
ratatui::style::Color::Indexed(INDIGO_LIGHT)
}
pub fn print_picker_header(title: &str, subtitle: &str) {
use colored::Colorize;
println!();
println!(
"{} {}",
"◆".truecolor(0x63, 0x66, 0xf1).bold(),
title.bright_white().bold()
);
if !subtitle.is_empty() {
println!("{}", subtitle.truecolor(0x67, 0x65, 0x8b));
}
}
pub fn searchable_select<T: Display>(
prompt: &str,
items: &[T],
default: usize,
) -> Result<Option<usize>, String> {
if items.is_empty() {
return Ok(None);
}
let default = default.min(items.len().saturating_sub(1));
FuzzySelect::with_theme(&xbp_theme())
.with_prompt(format!("{prompt} (type to search)"))
.items(items)
.default(default)
.highlight_matches(true)
.interact_opt()
.map_err(|error| format!("Prompt failed: {error}"))
}
pub fn searchable_select_required<T: Display>(
prompt: &str,
items: &[T],
default: usize,
) -> Result<usize, String> {
searchable_select(prompt, items, default)?
.ok_or_else(|| "Selection cancelled".to_string())
}
pub fn select_one<T: Display>(
prompt: &str,
items: &[T],
default: usize,
) -> Result<Option<usize>, String> {
if items.is_empty() {
return Ok(None);
}
let default = default.min(items.len().saturating_sub(1));
Select::with_theme(&xbp_theme())
.with_prompt(prompt)
.items(items)
.default(default)
.interact_opt()
.map_err(|error| format!("Prompt failed: {error}"))
}
pub fn searchable_multi_select<T: Display>(
prompt: &str,
items: &[T],
defaults: &[bool],
) -> Result<Vec<usize>, String> {
if items.is_empty() {
return Ok(Vec::new());
}
let defaults = if defaults.len() == items.len() {
defaults.to_vec()
} else {
vec![false; items.len()]
};
MultiSelect::with_theme(&xbp_theme())
.with_prompt(format!("{prompt} (space to toggle)"))
.items(items)
.defaults(&defaults)
.interact()
.map_err(|error| format!("Prompt failed: {error}"))
}
pub fn confirm(prompt: &str, default: bool) -> Result<bool, String> {
Confirm::with_theme(&xbp_theme())
.with_prompt(prompt)
.default(default)
.interact()
.map_err(|error| format!("Prompt failed: {error}"))
}
pub fn pad_label(s: &str, width: usize) -> String {
let len = s.chars().count();
if len >= width {
s.chars().take(width).collect()
} else {
format!("{s}{}", " ".repeat(width - len))
}
}