1use crate::OutputLabel;
4use dialoguer::{
5 console::{style, Style, StyledObject},
6 theme::Theme,
7};
8use std::fmt;
9
10macro_rules! write_label {
12 ($dst:expr, $label:expr, $($arg:tt)*) => {
13 write!(
14 $dst,
15 "{}",
16 $crate::pretty_output($label, format!($($arg)*))
17 )
18 };
19}
20
21pub struct LabelTheme<'a> {
23 pub defaults_style: Style,
25 pub prompt_style: Style,
27 pub prompt_prefix: StyledObject<&'a str>,
29 pub prompt_suffix: StyledObject<&'a str>,
31 pub success_prefix: StyledObject<&'a str>,
33 pub success_suffix: StyledObject<&'a str>,
35 pub error_prefix: StyledObject<&'a str>,
37 pub error_style: Style,
39 pub hint_style: Style,
41 pub values_style: Style,
43 pub active_item_style: Style,
45 pub inactive_item_style: Style,
47 pub active_item_prefix: StyledObject<&'a str>,
49 pub inactive_item_prefix: StyledObject<&'a str>,
51 pub checked_item_prefix: StyledObject<&'a str>,
53 pub unchecked_item_prefix: StyledObject<&'a str>,
55 pub picked_item_prefix: StyledObject<&'a str>,
57 pub unpicked_item_prefix: StyledObject<&'a str>,
59 pub fuzzy_cursor_style: Style,
61 pub inline_selections: bool,
63}
64
65impl Default for LabelTheme<'_> {
66 fn default() -> Self {
67 Self {
68 defaults_style: Style::new().for_stderr().cyan(),
69 prompt_style: Style::new().for_stderr().bold(),
70 prompt_prefix: style("?").for_stderr().yellow(),
71 prompt_suffix: style("›").for_stderr().black().bright(),
72 success_prefix: style("✔").for_stderr().green(),
73 success_suffix: style("·").for_stderr().black().bright(),
74 error_prefix: style("✘").for_stderr().red(),
75 error_style: Style::new().for_stderr().red(),
76 hint_style: Style::new().for_stderr().black().bright(),
77 values_style: Style::new().for_stderr().green(),
78 active_item_style: Style::new().for_stderr().cyan(),
79 inactive_item_style: Style::new().for_stderr(),
80 active_item_prefix: style("❯").for_stderr().green(),
81 inactive_item_prefix: style(" ").for_stderr(),
82 checked_item_prefix: style("✔").for_stderr().green(),
83 unchecked_item_prefix: style("✔").for_stderr().black(),
84 picked_item_prefix: style("❯").for_stderr().green(),
85 unpicked_item_prefix: style(" ").for_stderr(),
86 fuzzy_cursor_style: Style::new().for_stderr().black().on_white(),
87 inline_selections: true,
88 }
89 }
90}
91
92impl Theme for LabelTheme<'_> {
93 fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
95 write_label!(
96 f,
97 OutputLabel::Custom(self.prompt_prefix.clone()),
98 "{}",
99 self.prompt_style.apply_to(prompt)
100 )
101 }
102
103 fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result {
105 write_label!(
106 f,
107 OutputLabel::Custom(self.error_prefix.clone()),
108 "{}",
109 self.error_style.apply_to(err)
110 )
111 }
112
113 fn format_input_prompt(
115 &self,
116 f: &mut dyn fmt::Write,
117 prompt: &str,
118 default: Option<&str>,
119 ) -> fmt::Result {
120 let suffix = default.map_or_else(
121 || format!("{} ", self.prompt_suffix),
122 |default| {
123 format!(
124 "{} {} ",
125 self.hint_style.apply_to(&format!("({default})")),
126 &self.prompt_suffix
127 )
128 },
129 );
130
131 write_label!(
132 f,
133 OutputLabel::Custom(self.prompt_prefix.clone()),
134 "{} {}",
135 self.prompt_style.apply_to(prompt),
136 suffix
137 )
138 }
139
140 fn format_confirm_prompt(
142 &self,
143 f: &mut dyn fmt::Write,
144 prompt: &str,
145 default: Option<bool>,
146 ) -> fmt::Result {
147 let yes_no_hint = match default {
148 None => format!(
149 "{} {}",
150 self.hint_style.apply_to("(y/n)"),
151 &self.prompt_suffix
152 ),
153 Some(true) => format!(
154 "{} {} {}",
155 self.hint_style.apply_to("(y/n)"),
156 &self.prompt_suffix,
157 self.defaults_style.apply_to("yes")
158 ),
159 Some(false) => format!(
160 "{} {} {}",
161 self.hint_style.apply_to("(y/n)"),
162 &self.prompt_suffix,
163 self.defaults_style.apply_to("no")
164 ),
165 };
166
167 write_label!(
168 f,
169 OutputLabel::Custom(self.prompt_prefix.clone()),
170 "{} {}",
171 self.prompt_style.apply_to(prompt),
172 yes_no_hint
173 )
174 }
175
176 fn format_confirm_prompt_selection(
178 &self,
179 f: &mut dyn fmt::Write,
180 prompt: &str,
181 selection: Option<bool>,
182 ) -> fmt::Result {
183 if !prompt.is_empty() {
184 write_label!(
185 f,
186 OutputLabel::Success(self.success_prefix.to_string().as_str()),
187 "{}",
188 self.prompt_style.apply_to(prompt)
189 )?;
190 }
191
192 let selection = selection.map(|b| if b { "yes" } else { "no" });
193
194 match selection {
195 Some(selection) => {
196 write!(
197 f,
198 " {} {}",
199 &self.success_suffix,
200 self.values_style.apply_to(selection)
201 )
202 }
203 None => {
204 write!(f, " {}", &self.success_suffix)
205 }
206 }
207 }
208
209 fn format_input_prompt_selection(
211 &self,
212 f: &mut dyn fmt::Write,
213 prompt: &str,
214 sel: &str,
215 ) -> fmt::Result {
216 if !prompt.is_empty() {
217 write_label!(
218 f,
219 OutputLabel::Success(self.success_prefix.to_string().as_str()),
220 "{}",
221 self.prompt_style.apply_to(prompt)
222 )?;
223 }
224
225 if !sel.is_empty() {
226 write!(
227 f,
228 " {} {}",
229 &self.success_suffix,
230 self.values_style.apply_to(sel)
231 )?;
232 }
233
234 Ok(())
235 }
236
237 fn format_password_prompt_selection(
239 &self,
240 f: &mut dyn fmt::Write,
241 prompt: &str,
242 ) -> fmt::Result {
243 self.format_input_prompt_selection(f, prompt, "********")
244 }
245
246 fn format_multi_select_prompt_selection(
248 &self,
249 f: &mut dyn fmt::Write,
250 prompt: &str,
251 selections: &[&str],
252 ) -> fmt::Result {
253 if !prompt.is_empty() {
254 write_label!(
255 f,
256 OutputLabel::Success(self.success_prefix.to_string().as_str()),
257 "{}",
258 self.prompt_style.apply_to(prompt)
259 )?;
260 }
261
262 write!(f, "{} ", &self.success_suffix)?;
263
264 if self.inline_selections {
265 for (idx, sel) in selections.iter().enumerate() {
266 write!(
267 f,
268 "{}{}",
269 if idx == 0 { "" } else { ", " },
270 self.values_style.apply_to(sel)
271 )?;
272 }
273 }
274
275 Ok(())
276 }
277
278 fn format_select_prompt_item(
280 &self,
281 f: &mut dyn fmt::Write,
282 text: &str,
283 active: bool,
284 ) -> fmt::Result {
285 let (prefix, item) = if active {
286 (
287 &self.active_item_prefix,
288 self.active_item_style.apply_to(text),
289 )
290 } else {
291 (
292 &self.inactive_item_prefix,
293 self.inactive_item_style.apply_to(text),
294 )
295 };
296
297 write_label!(
298 f,
299 OutputLabel::Success(prefix.to_string().as_str()),
300 "{}",
301 item
302 )
303 }
304
305 fn format_multi_select_prompt_item(
307 &self,
308 f: &mut dyn fmt::Write,
309 text: &str,
310 checked: bool,
311 active: bool,
312 ) -> fmt::Result {
313 let (prefix, item) = match (checked, active) {
314 (true, true) => (
315 &self.checked_item_prefix,
316 self.active_item_style.apply_to(text),
317 ),
318 (true, false) => (
319 &self.checked_item_prefix,
320 self.inactive_item_style.apply_to(text),
321 ),
322 (false, true) => (
323 &self.unchecked_item_prefix,
324 self.active_item_style.apply_to(text),
325 ),
326 (false, false) => (
327 &self.unchecked_item_prefix,
328 self.inactive_item_style.apply_to(text),
329 ),
330 };
331
332 write_label!(
333 f,
334 OutputLabel::Success(prefix.to_string().as_str()),
335 "{}",
336 item
337 )
338 }
339
340 fn format_sort_prompt_item(
342 &self,
343 f: &mut dyn fmt::Write,
344 text: &str,
345 picked: bool,
346 active: bool,
347 ) -> fmt::Result {
348 let (prefix, item) = match (picked, active) {
349 (true, true) => (
350 &self.picked_item_prefix,
351 self.active_item_style.apply_to(text),
352 ),
353 (false, true) => (
354 &self.unpicked_item_prefix,
355 self.active_item_style.apply_to(text),
356 ),
357 (_, false) => (
358 &self.unpicked_item_prefix,
359 self.inactive_item_style.apply_to(text),
360 ),
361 };
362
363 write_label!(
364 f,
365 OutputLabel::Success(prefix.to_string().as_str()),
366 "{}",
367 item
368 )
369 }
370
371 fn format_fuzzy_select_prompt(
373 &self,
374 f: &mut dyn fmt::Write,
375 prompt: &str,
376 search_term: &str,
377 cursor_pos: usize,
378 ) -> fmt::Result {
379 if !prompt.is_empty() {
380 write_label!(
381 f,
382 OutputLabel::Success(self.success_prefix.to_string().as_str()),
383 "{}",
384 self.prompt_style.apply_to(prompt)
385 )?;
386 }
387
388 if cursor_pos < search_term.len() {
389 let st_head = search_term[0..cursor_pos].to_string();
390 let st_tail = search_term[cursor_pos + 1..search_term.len()].to_string();
391 let st_cursor = self
392 .fuzzy_cursor_style
393 .apply_to(search_term.to_string().chars().nth(cursor_pos).unwrap());
394
395 write!(
396 f,
397 " {} {}{}{}",
398 &self.prompt_suffix, st_head, st_cursor, st_tail
399 )
400 } else {
401 let cursor = self.fuzzy_cursor_style.apply_to(" ");
402 write!(f, " {} {}{}", &self.prompt_suffix, search_term, cursor)
403 }
404 }
405}