1use std::fmt;
2
3use console::{style, Style, StyledObject};
4#[cfg(feature = "fuzzy-select")]
5use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
6
7use crate::theme::Theme;
8
9pub struct ColorfulTheme {
11 pub defaults_style: Style,
13 pub prompt_style: Style,
15 pub prompt_prefix: StyledObject<String>,
17 pub prompt_suffix: StyledObject<String>,
19 pub success_prefix: StyledObject<String>,
21 pub success_suffix: StyledObject<String>,
23 pub error_prefix: StyledObject<String>,
25 pub error_style: Style,
27 pub hint_style: Style,
29 pub values_style: Style,
31 pub active_item_style: Style,
33 pub inactive_item_style: Style,
35 pub active_item_prefix: StyledObject<String>,
37 pub inactive_item_prefix: StyledObject<String>,
39 pub checked_item_prefix: StyledObject<String>,
41 pub unchecked_item_prefix: StyledObject<String>,
43 pub picked_item_prefix: StyledObject<String>,
45 pub unpicked_item_prefix: StyledObject<String>,
47 #[cfg(feature = "fuzzy-select")]
49 pub fuzzy_cursor_style: Style,
50 #[cfg(feature = "fuzzy-select")]
52 pub fuzzy_match_highlight_style: Style,
53}
54
55impl Default for ColorfulTheme {
56 fn default() -> ColorfulTheme {
57 ColorfulTheme {
58 defaults_style: Style::new().for_stderr().cyan(),
59 prompt_style: Style::new().for_stderr().bold(),
60 prompt_prefix: style("?".to_string()).for_stderr().yellow(),
61 prompt_suffix: style("›".to_string()).for_stderr().black().bright(),
62 success_prefix: style("✔".to_string()).for_stderr().green(),
63 success_suffix: style("·".to_string()).for_stderr().black().bright(),
64 error_prefix: style("✘".to_string()).for_stderr().red(),
65 error_style: Style::new().for_stderr().red(),
66 hint_style: Style::new().for_stderr().black().bright(),
67 values_style: Style::new().for_stderr().green(),
68 active_item_style: Style::new().for_stderr().cyan(),
69 inactive_item_style: Style::new().for_stderr(),
70 active_item_prefix: style("❯".to_string()).for_stderr().green(),
71 inactive_item_prefix: style(" ".to_string()).for_stderr(),
72 checked_item_prefix: style("✔".to_string()).for_stderr().green(),
73 unchecked_item_prefix: style("⬚".to_string()).for_stderr().magenta(),
74 picked_item_prefix: style("❯".to_string()).for_stderr().green(),
75 unpicked_item_prefix: style(" ".to_string()).for_stderr(),
76 #[cfg(feature = "fuzzy-select")]
77 fuzzy_cursor_style: Style::new().for_stderr().black().on_white(),
78 #[cfg(feature = "fuzzy-select")]
79 fuzzy_match_highlight_style: Style::new().for_stderr().bold(),
80 }
81 }
82}
83
84impl Theme for ColorfulTheme {
85 fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
87 if !prompt.is_empty() {
88 write!(
89 f,
90 "{} {} ",
91 &self.prompt_prefix,
92 self.prompt_style.apply_to(prompt)
93 )?;
94 }
95
96 write!(f, "{}", &self.prompt_suffix)
97 }
98
99 fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result {
101 write!(
102 f,
103 "{} {}",
104 &self.error_prefix,
105 self.error_style.apply_to(err)
106 )
107 }
108
109 fn format_input_prompt(
111 &self,
112 f: &mut dyn fmt::Write,
113 prompt: &str,
114 default: Option<&str>,
115 ) -> fmt::Result {
116 if !prompt.is_empty() {
117 write!(
118 f,
119 "{} {} ",
120 &self.prompt_prefix,
121 self.prompt_style.apply_to(prompt)
122 )?;
123 }
124
125 match default {
126 Some(default) => write!(
127 f,
128 "{} {} ",
129 self.hint_style.apply_to(&format!("({})", default)),
130 &self.prompt_suffix
131 ),
132 None => write!(f, "{} ", &self.prompt_suffix),
133 }
134 }
135
136 fn format_confirm_prompt(
138 &self,
139 f: &mut dyn fmt::Write,
140 prompt: &str,
141 default: Option<bool>,
142 ) -> fmt::Result {
143 if !prompt.is_empty() {
144 write!(
145 f,
146 "{} {} ",
147 &self.prompt_prefix,
148 self.prompt_style.apply_to(prompt)
149 )?;
150 }
151
152 match default {
153 None => write!(
154 f,
155 "{} {}",
156 self.hint_style.apply_to("(y/n)"),
157 &self.prompt_suffix
158 ),
159 Some(true) => write!(
160 f,
161 "{} {} {}",
162 self.hint_style.apply_to("(y/n)"),
163 &self.prompt_suffix,
164 self.defaults_style.apply_to("yes")
165 ),
166 Some(false) => write!(
167 f,
168 "{} {} {}",
169 self.hint_style.apply_to("(y/n)"),
170 &self.prompt_suffix,
171 self.defaults_style.apply_to("no")
172 ),
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!(
185 f,
186 "{} {} ",
187 &self.success_prefix,
188 self.prompt_style.apply_to(prompt)
189 )?;
190 }
191 let selection = selection.map(|b| if b { "yes" } else { "no" });
192
193 match selection {
194 Some(selection) => {
195 write!(
196 f,
197 "{} {}",
198 &self.success_suffix,
199 self.values_style.apply_to(selection)
200 )
201 }
202 None => {
203 write!(f, "{}", &self.success_suffix)
204 }
205 }
206 }
207
208 fn format_input_prompt_selection(
210 &self,
211 f: &mut dyn fmt::Write,
212 prompt: &str,
213 sel: &str,
214 ) -> fmt::Result {
215 if !prompt.is_empty() {
216 write!(
217 f,
218 "{} {} ",
219 &self.success_prefix,
220 self.prompt_style.apply_to(prompt)
221 )?;
222 }
223
224 write!(
225 f,
226 "{} {}",
227 &self.success_suffix,
228 self.values_style.apply_to(sel)
229 )
230 }
231
232 #[cfg(feature = "password")]
234 fn format_password_prompt_selection(
235 &self,
236 f: &mut dyn fmt::Write,
237 prompt: &str,
238 ) -> fmt::Result {
239 self.format_input_prompt_selection(f, prompt, "********")
240 }
241
242 fn format_multi_select_prompt_selection(
244 &self,
245 f: &mut dyn fmt::Write,
246 prompt: &str,
247 selections: &[&str],
248 ) -> fmt::Result {
249 if !prompt.is_empty() {
250 write!(
251 f,
252 "{} {} ",
253 &self.success_prefix,
254 self.prompt_style.apply_to(prompt)
255 )?;
256 }
257
258 write!(f, "{} ", &self.success_suffix)?;
259
260 for (idx, sel) in selections.iter().enumerate() {
261 write!(
262 f,
263 "{}{}",
264 if idx == 0 { "" } else { ", " },
265 self.values_style.apply_to(sel)
266 )?;
267 }
268
269 Ok(())
270 }
271
272 fn format_select_prompt_item(
274 &self,
275 f: &mut dyn fmt::Write,
276 text: &str,
277 active: bool,
278 ) -> fmt::Result {
279 let details = if active {
280 (
281 &self.active_item_prefix,
282 self.active_item_style.apply_to(text),
283 )
284 } else {
285 (
286 &self.inactive_item_prefix,
287 self.inactive_item_style.apply_to(text),
288 )
289 };
290
291 write!(f, "{} {}", details.0, details.1)
292 }
293
294 fn format_multi_select_prompt_item(
296 &self,
297 f: &mut dyn fmt::Write,
298 text: &str,
299 checked: bool,
300 active: bool,
301 ) -> fmt::Result {
302 let details = match (checked, active) {
303 (true, true) => (
304 &self.checked_item_prefix,
305 self.active_item_style.apply_to(text),
306 ),
307 (true, false) => (
308 &self.checked_item_prefix,
309 self.inactive_item_style.apply_to(text),
310 ),
311 (false, true) => (
312 &self.unchecked_item_prefix,
313 self.active_item_style.apply_to(text),
314 ),
315 (false, false) => (
316 &self.unchecked_item_prefix,
317 self.inactive_item_style.apply_to(text),
318 ),
319 };
320
321 write!(f, "{} {}", details.0, details.1)
322 }
323
324 fn format_sort_prompt_item(
326 &self,
327 f: &mut dyn fmt::Write,
328 text: &str,
329 picked: bool,
330 active: bool,
331 ) -> fmt::Result {
332 let details = match (picked, active) {
333 (true, true) => (
334 &self.picked_item_prefix,
335 self.active_item_style.apply_to(text),
336 ),
337 (false, true) => (
338 &self.unpicked_item_prefix,
339 self.active_item_style.apply_to(text),
340 ),
341 (_, false) => (
342 &self.unpicked_item_prefix,
343 self.inactive_item_style.apply_to(text),
344 ),
345 };
346
347 write!(f, "{} {}", details.0, details.1)
348 }
349
350 #[cfg(feature = "fuzzy-select")]
352 fn format_fuzzy_select_prompt_item(
353 &self,
354 f: &mut dyn fmt::Write,
355 text: &str,
356 active: bool,
357 highlight_matches: bool,
358 matcher: &SkimMatcherV2,
359 search_term: &str,
360 ) -> fmt::Result {
361 write!(
362 f,
363 "{} ",
364 if active {
365 &self.active_item_prefix
366 } else {
367 &self.inactive_item_prefix
368 }
369 )?;
370
371 if highlight_matches {
372 if let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) {
373 for (idx, c) in text.chars().enumerate() {
374 if indices.contains(&idx) {
375 if active {
376 write!(
377 f,
378 "{}",
379 self.active_item_style
380 .apply_to(self.fuzzy_match_highlight_style.apply_to(c))
381 )?;
382 } else {
383 write!(f, "{}", self.fuzzy_match_highlight_style.apply_to(c))?;
384 }
385 } else if active {
386 write!(f, "{}", self.active_item_style.apply_to(c))?;
387 } else {
388 write!(f, "{}", c)?;
389 }
390 }
391
392 return Ok(());
393 }
394 }
395
396 write!(f, "{}", text)
397 }
398
399 #[cfg(feature = "fuzzy-select")]
401 fn format_fuzzy_select_prompt(
402 &self,
403 f: &mut dyn fmt::Write,
404 prompt: &str,
405 search_term: &str,
406 bytes_pos: usize,
407 ) -> fmt::Result {
408 if !prompt.is_empty() {
409 write!(
410 f,
411 "{} {} ",
412 self.prompt_prefix,
413 self.prompt_style.apply_to(prompt)
414 )?;
415 }
416
417 let (st_head, remaining) = search_term.split_at(bytes_pos);
418 let mut chars = remaining.chars();
419 let chr = chars.next().unwrap_or(' ');
420 let st_cursor = self.fuzzy_cursor_style.apply_to(chr);
421 let st_tail = chars.as_str();
422
423 let prompt_suffix = &self.prompt_suffix;
424 write!(f, "{prompt_suffix} {st_head}{st_cursor}{st_tail}",)
425 }
426}