xbp_cli/cli/
interactive.rs1use console::{style, Style};
8use dialoguer::theme::ColorfulTheme;
9use dialoguer::{Confirm, FuzzySelect, MultiSelect, Select};
10use std::fmt::Display;
11
12const INDIGO_BG: u8 = 54;
14const INDIGO_MID: u8 = 61;
16const INDIGO_LIGHT: u8 = 141;
18const INDIGO_MATCH: u8 = 183;
20const INDIGO_MUTED: u8 = 103;
22
23pub fn xbp_theme() -> ColorfulTheme {
29 ColorfulTheme {
30 defaults_style: Style::new().for_stderr().color256(INDIGO_LIGHT),
31 prompt_style: Style::new().for_stderr().white().bold(),
32 prompt_prefix: style("◆".to_string()).for_stderr().color256(INDIGO_LIGHT).bold(),
33 prompt_suffix: style("›".to_string()).for_stderr().color256(INDIGO_MID),
34 success_prefix: style("✔".to_string()).for_stderr().green().bright(),
35 success_suffix: style("·".to_string()).for_stderr().black().bright(),
36 error_prefix: style("✘".to_string()).for_stderr().red().bright(),
37 error_style: Style::new().for_stderr().red().bright(),
38 hint_style: Style::new().for_stderr().color256(INDIGO_MUTED),
39 values_style: Style::new().for_stderr().color256(INDIGO_LIGHT).bold(),
40 active_item_style: Style::new()
42 .for_stderr()
43 .white()
44 .bold()
45 .on_color256(INDIGO_BG),
46 inactive_item_style: Style::new().for_stderr().color256(252),
47 active_item_prefix: style("❯".to_string())
48 .for_stderr()
49 .color256(INDIGO_LIGHT)
50 .bold(),
51 inactive_item_prefix: style(" ".to_string()).for_stderr().color256(INDIGO_MUTED),
52 checked_item_prefix: style("✔".to_string()).for_stderr().color256(INDIGO_LIGHT),
53 unchecked_item_prefix: style("⬚".to_string()).for_stderr().color256(INDIGO_MID),
54 picked_item_prefix: style("❯".to_string()).for_stderr().color256(INDIGO_LIGHT),
55 unpicked_item_prefix: style(" ".to_string()).for_stderr(),
56 fuzzy_cursor_style: Style::new()
58 .for_stderr()
59 .white()
60 .bold()
61 .on_color256(INDIGO_BG),
62 fuzzy_match_highlight_style: Style::new()
63 .for_stderr()
64 .color256(INDIGO_MATCH)
65 .bold()
66 .underlined(),
67 }
68}
69
70pub fn xbp_selection_style() -> ratatui::style::Style {
73 use ratatui::style::{Color, Modifier, Style};
74 Style::default()
75 .bg(Color::Indexed(INDIGO_BG))
76 .fg(Color::White)
77 .add_modifier(Modifier::BOLD)
78}
79
80pub fn xbp_accent_color() -> ratatui::style::Color {
82 ratatui::style::Color::Indexed(INDIGO_LIGHT)
83}
84
85pub fn print_picker_header(title: &str, subtitle: &str) {
87 use colored::Colorize;
88 println!();
89 println!(
90 "{} {}",
91 "◆".truecolor(0x63, 0x66, 0xf1).bold(),
92 title.bright_white().bold()
93 );
94 if !subtitle.is_empty() {
95 println!("{}", subtitle.truecolor(0x67, 0x65, 0x8b));
96 }
97}
98
99pub fn searchable_select<T: Display>(
101 prompt: &str,
102 items: &[T],
103 default: usize,
104) -> Result<Option<usize>, String> {
105 if items.is_empty() {
106 return Ok(None);
107 }
108 let default = default.min(items.len().saturating_sub(1));
109 FuzzySelect::with_theme(&xbp_theme())
110 .with_prompt(format!("{prompt} (type to search)"))
111 .items(items)
112 .default(default)
113 .highlight_matches(true)
114 .interact_opt()
115 .map_err(|error| format!("Prompt failed: {error}"))
116}
117
118pub fn searchable_select_required<T: Display>(
120 prompt: &str,
121 items: &[T],
122 default: usize,
123) -> Result<usize, String> {
124 searchable_select(prompt, items, default)?
125 .ok_or_else(|| "Selection cancelled".to_string())
126}
127
128pub fn select_one<T: Display>(
130 prompt: &str,
131 items: &[T],
132 default: usize,
133) -> Result<Option<usize>, String> {
134 if items.is_empty() {
135 return Ok(None);
136 }
137 let default = default.min(items.len().saturating_sub(1));
138 Select::with_theme(&xbp_theme())
139 .with_prompt(prompt)
140 .items(items)
141 .default(default)
142 .interact_opt()
143 .map_err(|error| format!("Prompt failed: {error}"))
144}
145
146pub fn searchable_multi_select<T: Display>(
148 prompt: &str,
149 items: &[T],
150 defaults: &[bool],
151) -> Result<Vec<usize>, String> {
152 if items.is_empty() {
153 return Ok(Vec::new());
154 }
155 let defaults = if defaults.len() == items.len() {
156 defaults.to_vec()
157 } else {
158 vec![false; items.len()]
159 };
160 MultiSelect::with_theme(&xbp_theme())
161 .with_prompt(format!("{prompt} (space to toggle)"))
162 .items(items)
163 .defaults(&defaults)
164 .interact()
165 .map_err(|error| format!("Prompt failed: {error}"))
166}
167
168pub fn confirm(prompt: &str, default: bool) -> Result<bool, String> {
170 Confirm::with_theme(&xbp_theme())
171 .with_prompt(prompt)
172 .default(default)
173 .interact()
174 .map_err(|error| format!("Prompt failed: {error}"))
175}
176
177pub fn pad_label(s: &str, width: usize) -> String {
179 let len = s.chars().count();
180 if len >= width {
181 s.chars().take(width).collect()
182 } else {
183 format!("{s}{}", " ".repeat(width - len))
184 }
185}