1use std::{fmt, ops::Deref, u8};
2
3use crate::{impl_int_wrapper};
4
5use crate::{Result, action::{Count}, binds::BindMap, tui::IoStream};
6use ratatui::{
7 style::{Color, Modifier}, widgets::{BorderType, Borders, Padding}
8};
9use regex::Regex;
10use serde::{Deserialize, Deserializer, de::{self, Visitor, Error}};
11
12#[derive(Default, Debug, Clone, Deserialize)]
13#[serde(default, deny_unknown_fields)]
14pub struct Config {
15 #[serde(flatten)]
17 pub render: RenderConfig,
18
19 pub binds: BindMap,
21
22 pub previewer: PreviewerConfig,
24
25 pub matcher: MatcherConfig,
27
28 pub tui: TerminalConfig,
30}
31
32#[derive(Default, Debug, Clone, Deserialize)]
33#[serde(default, deny_unknown_fields)]
34pub struct MatcherConfig {
35 #[serde(flatten)]
36 pub matcher: NucleoMatcherConfig,
37 pub columns: ColumnsConfig,
38 pub trim: bool,
39 pub delimiter: Option<char>,
40 #[serde(flatten)]
41 pub exit: ExitConfig,
42 pub format: FormatString
43}
44
45#[derive(Default, Debug, Clone, Deserialize)]
47#[serde(default, deny_unknown_fields)]
48pub struct ExitConfig {
49 pub select_1: bool,
50 pub sync: bool
51}
52
53#[derive(Default, Debug, Clone, Deserialize)]
54#[serde(default, deny_unknown_fields)]
55pub struct RenderConfig {
56 pub ui: UiConfig,
57 pub input: InputConfig,
58 pub results: ResultsConfig,
59 pub preview: PreviewConfig,
60}
61
62impl RenderConfig {
63 pub fn tick_rate(&self) -> u16 {
64 self.ui.tick_rate.0
65 }
66}
67
68#[derive(Default, Debug, Clone, Deserialize)]
69#[serde(default, deny_unknown_fields)]
70pub struct UiConfig {
71 pub border_fg: Color,
72 pub background: Option<Color>,
73 pub tick_rate: TickRate, }
76
77#[derive(Default, Debug, Clone, Deserialize)]
78#[serde(default, deny_unknown_fields)]
79pub struct TerminalConfig {
80 pub stream: IoStream,
81 pub sleep: u16, #[serde(flatten)]
83 pub layout: Option<TerminalLayoutSettings> }
85
86#[derive(Default, Debug, Clone, Deserialize)]
87#[serde(default, deny_unknown_fields)]
88pub struct InputConfig {
89 pub input_fg: Color,
90 pub count_fg: Color,
91 pub cursor: CursorSetting,
92 pub border: BorderSetting,
93 pub title: String,
94 #[serde(deserialize_with = "deserialize_char")]
95 pub prompt: String,
96 pub initial: String,
97}
98
99#[derive(Default, Debug, Clone, Deserialize)]
100#[serde(default, deny_unknown_fields)]
101pub struct ResultsConfig {
102 #[serde(deserialize_with = "deserialize_char")]
103 pub multi_prefix: String,
104 pub default_prefix: String,
105 #[serde(deserialize_with = "deserialize_option_bool")]
106 pub reverse: Option<bool>,
107
108 pub border: BorderSetting,
109 pub result_fg: Color,
110 pub current_fg: Color,
111 pub current_bg: Color,
112 pub match_fg: Color,
113 pub count_fg: Color,
114 #[serde(deserialize_with = "deserialize_modifier")]
115 pub current_modifier: Modifier,
116
117 #[serde(deserialize_with = "deserialize_modifier")]
118 pub count_modifier: Modifier,
119
120 pub title: String,
121 pub scroll_wrap: bool,
122 pub scroll_padding: u16,
123
124 pub column_spacing: Count,
126 pub current_prefix: String,
127}
128
129#[derive(Default, Debug, Clone, Deserialize)]
130#[serde(default, deny_unknown_fields)]
131pub struct HeaderConfig {
132 pub border: BorderSetting,
133 #[serde(deserialize_with = "deserialize_modifier")]
134 pub modifier: Modifier,
135 pub title: String,
136
137 pub content: StringOrVec,
138}
139
140#[derive(Debug, Clone, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub enum StringOrVec {
143 String(String),
144 Vec(Vec<String>)
145}
146impl Default for StringOrVec {
147 fn default() -> Self {
148 StringOrVec::String(String::new())
149 }
150}
151
152
153#[derive(Default, Debug, Clone, Deserialize)]
154#[serde(default, deny_unknown_fields)]
155pub struct PreviewConfig {
156 pub border: BorderSetting,
157 pub layout: Vec<PreviewSetting>,
158 pub scroll_wrap: bool,
159 pub wrap: bool,
160 pub show: bool,
161}
162
163#[derive(Default, Debug, Clone, Deserialize)]
164#[serde(default, deny_unknown_fields)]
165pub struct PreviewerConfig {
166 pub try_lossy: bool,
167
168 pub wrap: bool,
170 pub cache: bool,
171
172}
173
174#[derive(Default, Debug, Clone, Deserialize)]
178#[serde(transparent)]
179pub struct FormatString(String);
180
181impl Deref for FormatString {
182 type Target = str;
183
184 fn deref(&self) -> &Self::Target {
185 &self.0
186 }
187}
188
189#[derive(Default, Debug, Clone, Deserialize)]
190#[serde(default, deny_unknown_fields)]
191pub struct BorderSetting {
192 #[serde(deserialize_with = "fromstr_deserialize")]
193 pub r#type: BorderType,
194 pub color: Color,
195 #[serde(deserialize_with = "deserialize_borders")]
196 pub sides: Borders,
197 #[serde(deserialize_with = "deserialize_padding")]
198 pub padding: Padding,
199 pub title: String,
200}
201
202impl BorderSetting {
203 pub fn as_block(&self) -> ratatui::widgets::Block<'static> {
204 let mut ret = ratatui::widgets::Block::default();
205
206 if !self.title.is_empty() {
207 let title = self.title.to_string();
208 ret = ret.title(title)
209 };
210
211 if self.sides != Borders::NONE {
212 ret = ret.borders(self.sides)
213 .border_type(self.r#type)
214 .border_style(ratatui::style::Style::default().fg(self.color))
215 }
216
217 ret
218 }
219
220 pub fn height(&self) -> u16 {
221 let mut height = 0;
222 height += 2 * !self.sides.is_empty() as u16;
223 height += self.padding.top + self.padding.bottom;
224 height += (!self.title.is_empty() as u16).saturating_sub(!self.sides.is_empty() as u16);
225
226 height
227 }
228
229 pub fn width(&self) -> u16 {
230 let mut width = 0;
231 width += 2 * !self.sides.is_empty() as u16;
232 width += self.padding.left + self.padding.right;
233 width += (!self.title.is_empty() as u16).saturating_sub(!self.sides.is_empty() as u16);
234
235 width
236 }
237}
238
239#[derive(Debug, Clone, Deserialize)]
241pub struct TerminalLayoutSettings {
242 pub percentage: Percentage,
243 pub min: u16,
244 pub max: u16, }
246
247impl Default for TerminalLayoutSettings {
248 fn default() -> Self {
249 Self {
250 percentage: Percentage(40),
251 min: 10,
252 max: 120
253 }
254 }
255}
256
257
258#[derive(Default, Debug, Clone, Deserialize)]
259pub enum Side {
260 Top,
261 Bottom,
262 Left,
263 #[default]
264 Right,
265}
266
267#[derive(Debug, Clone, Deserialize)]
268pub struct PreviewSetting {
269 #[serde(flatten)]
270 pub layout: PreviewLayoutSetting,
271 #[serde(default)]
272 pub command: String
273}
274
275#[derive(Debug, Clone, Deserialize)]
276pub struct PreviewLayoutSetting {
277 pub side: Side,
278 pub percentage: Percentage,
279 pub min: i16,
280 pub max: i16,
281}
282
283impl Default for PreviewLayoutSetting {
284 fn default() -> Self {
285 Self {
286 side: Side::Right,
287 percentage: Percentage(40),
288 min: 30,
289 max: 120
290 }
291 }
292}
293
294
295#[derive(Default, Debug, Clone, Deserialize)]
296#[serde(rename_all = "lowercase")]
297pub enum CursorSetting {
298 None,
299 #[default]
300 Default,
301}
302
303#[derive(Debug, Clone, Deserialize)]
304#[serde(transparent)]
305pub struct TickRate(pub u16);
306impl Default for TickRate {
307 fn default() -> Self {
308 Self(60)
309 }
310}
311
312#[derive(Default, Debug, Clone, Deserialize)]
313#[serde(default, deny_unknown_fields)]
314pub struct ColumnsConfig {
315 pub split: Split,
316 pub names: Vec<ColumnSetting>,
317 pub max_columns: MaxCols,
318}
319
320impl_int_wrapper!(MaxCols, u8, u8::MAX);
321
322#[derive(Default, Debug, Clone)]
323pub struct ColumnSetting {
324 pub filter: bool,
325 pub hidden: bool,
326 pub name: String,
327}
328
329#[derive(Default, Debug, Clone)]
330pub enum Split {
331 Delimiter(Regex),
332 Regexes(Vec<Regex>),
333 #[default]
334 None
335}
336
337
338pub mod utils {
341 use crate::Result;
342 use crate::config::Config;
343 use std::borrow::Cow;
344 use std::path::{Path};
345
346 pub fn get_config(dir: &Path) -> Result<Config> {
347 let config_path = dir.join("config.toml");
348
349 let config_content: Cow<'static, str> = if !config_path.exists() {
350 Cow::Borrowed(include_str!("../assets/config.toml"))
351 } else {
352 Cow::Owned(std::fs::read_to_string(config_path)?)
353 };
354
355 let config: Config = toml::from_str(&config_content)?;
356
357 Ok(config)
358 }
359
360 pub fn write_config(dir: &Path) -> Result<()> {
361 let config_path = dir.join("config.toml");
362
363 let default_config_content = include_str!("../assets/config.toml");
364 let parent_dir = config_path.parent().unwrap();
365 std::fs::create_dir_all(parent_dir)?;
366 std::fs::write(&config_path, default_config_content)?;
367
368 println!("Config written to: {}", config_path.display());
369 Ok(())
370 }
371}
372
373fn fromstr_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
375where
376T: std::str::FromStr,
377T::Err: std::fmt::Display,
378D: serde::Deserializer<'de>,
379{
380 let s = String::deserialize(deserializer)?;
381 T::from_str(&s).map_err(serde::de::Error::custom)
382}
383
384
385
386pub fn deserialize_borders<'de, D>(deserializer: D) -> Result<Borders, D::Error>
387where
388D: Deserializer<'de>,
389{
390 #[derive(Deserialize)]
391 #[serde(untagged)]
392 enum Input {
393 Str(String),
394 List(Vec<String>),
395 }
396
397 let input = Input::deserialize(deserializer)?;
398 let mut borders = Borders::NONE;
399
400 match input {
401 Input::Str(s) => match s.as_str() {
402 "none" => return Ok(Borders::NONE),
403 "all" => return Ok(Borders::ALL),
404 other => {
405 return Err(de::Error::custom(format!(
406 "invalid border value '{}'",
407 other
408 )));
409 }
410 },
411 Input::List(list) => {
412 for item in list {
413 match item.as_str() {
414 "top" => borders |= Borders::TOP,
415 "bottom" => borders |= Borders::BOTTOM,
416 "left" => borders |= Borders::LEFT,
417 "right" => borders |= Borders::RIGHT,
418 "all" => borders |= Borders::ALL,
419 "none" => borders = Borders::NONE,
420 other => return Err(de::Error::custom(format!("invalid side '{}'", other))),
421 }
422 }
423 }
424 }
425
426 Ok(borders)
427}
428
429pub fn deserialize_modifier<'de, D>(deserializer: D) -> Result<Modifier, D::Error>
430where
431D: Deserializer<'de>,
432{
433 #[derive(Deserialize)]
434 #[serde(untagged)]
435 enum Input {
436 Str(String),
437 List(Vec<String>),
438 }
439
440 let input = Input::deserialize(deserializer)?;
441 let mut modifier = Modifier::empty();
442
443 let add_modifier = |name: &str, m: &mut Modifier| -> Result<(), D::Error> {
444 match name {
445 "bold" => {
446 *m |= Modifier::BOLD;
447 Ok(())
448 }
449 "italic" => {
450 *m |= Modifier::ITALIC;
451 Ok(())
452 }
453 "underlined" => {
454 *m |= Modifier::UNDERLINED;
455 Ok(())
456 }
457 "none" => {
478 *m = Modifier::empty();
479 Ok(())
480 } other => Err(de::Error::custom(format!("invalid modifier '{}'", other))),
482 }
483 };
484
485 match input {
486 Input::Str(s) => add_modifier(&s, &mut modifier)?,
487 Input::List(list) => {
488 for item in list {
489 add_modifier(&item, &mut modifier)?;
490 }
491 }
492 }
493
494 Ok(modifier)
495}
496
497pub fn deserialize_char<'de, D>(deserializer: D) -> Result<String, D::Error>
498where
499D: Deserializer<'de>,
500{
501 struct CharVisitor;
502
503 impl<'de> Visitor<'de> for CharVisitor {
504 type Value = String;
505
506 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
507 formatter.write_str("a string or single character")
508 }
509
510 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
511 where
512 E: de::Error,
513 {
514 if v.chars().count() == 1 {
515 let mut s = String::with_capacity(2);
516 s.push(v.chars().next().unwrap());
517 s.push(' ');
518 Ok(s)
519 } else {
520 Ok(v.to_string())
521 }
522 }
523
524 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
525 where
526 E: de::Error,
527 {
528 self.visit_str(&v)
529 }
530 }
531
532 deserializer.deserialize_string(CharVisitor)
533}
534
535#[derive(Debug, Clone)]
537pub struct NucleoMatcherConfig(pub nucleo::Config);
538
539impl Default for NucleoMatcherConfig {
540 fn default() -> Self {
541 Self(nucleo::Config::DEFAULT)
542 }
543}
544
545#[derive(Debug, Clone, Deserialize)]
546#[serde(default)]
547struct MatcherConfigHelper {
548 pub normalize: Option<bool>,
549 pub ignore_case: Option<bool>,
550 pub prefer_prefix: Option<bool>,
551}
552
553impl Default for MatcherConfigHelper {
554 fn default() -> Self {
555 Self {
556 normalize: None,
557 ignore_case: None,
558 prefer_prefix: None,
559 }
560 }
561}
562
563impl<'de> Deserialize<'de> for NucleoMatcherConfig {
564 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
565 where
566 D: serde::Deserializer<'de>,
567 {
568 let helper = MatcherConfigHelper::deserialize(deserializer)?;
569 let mut config = nucleo::Config::DEFAULT;
570
571 if let Some(norm) = helper.normalize {
572 config.normalize = norm;
573 }
574 if let Some(ic) = helper.ignore_case {
575 config.ignore_case = ic;
576 }
577 if let Some(pp) = helper.prefer_prefix {
578 config.prefer_prefix = pp;
579 }
580
581 Ok(NucleoMatcherConfig(config))
582 }
583}
584
585impl<'de> Deserialize<'de> for Split {
586 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
587 where
588 D: Deserializer<'de>,
589 {
590 struct SplitVisitor;
591
592 impl<'de> Visitor<'de> for SplitVisitor {
593 type Value = Split;
594
595 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
596 formatter.write_str("string for delimiter or array of strings for regexes")
597 }
598
599 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
600 where
601 E: de::Error,
602 {
603 Regex::new(value)
605 .map(Split::Delimiter)
606 .map_err(|e| E::custom(format!("Invalid regex: {}", e)))
607 }
608
609 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
610 where
611 A: serde::de::SeqAccess<'de>,
612 {
613 let mut regexes = Vec::new();
614 while let Some(s) = seq.next_element::<String>()? {
615 let r = Regex::new(&s).map_err(|e| de::Error::custom(format!("Invalid regex: {}", e)))?;
616 regexes.push(r);
617 }
618 Ok(Split::Regexes(regexes))
619 }
620 }
621
622 deserializer.deserialize_any(SplitVisitor)
623 }
624}
625
626
627impl<'de> Deserialize<'de> for ColumnSetting {
628 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
629 where
630 D: Deserializer<'de>,
631 {
632 #[derive(Deserialize)]
633 #[serde(deny_unknown_fields)]
634 struct ColumnStruct {
635 #[serde(default = "default_true")]
636 filter: bool,
637 #[serde(default)]
638 hidden: bool,
639 name: String,
640 }
641
642 fn default_true() -> bool { true }
643
644 #[derive(Deserialize)]
645 #[serde(untagged)]
646 enum Input {
647 Str(String),
648 Obj(ColumnStruct),
649 }
650
651 match Input::deserialize(deserializer)? {
652 Input::Str(name) => Ok(ColumnSetting {
653 filter: true,
654 hidden: false,
655 name,
656 }),
657 Input::Obj(obj) => Ok(ColumnSetting {
658 filter: obj.filter,
659 hidden: obj.hidden,
660 name: obj.name,
661 }),
662 }
663 }
664}
665
666
667#[derive(Debug, Clone, Copy, PartialEq, Eq)]
668pub struct Percentage(u8);
669
670impl Percentage {
671 pub fn new(value: u8) -> Option<Self> {
672 if value <= 100 {
673 Some(Self(value))
674 } else {
675 None
676 }
677 }
678
679 pub fn get(&self) -> u16 {
680 self.0 as u16
681 }
682
683 pub fn get_max(&self, total: u16, max: u16) -> u16 {
684 let pct_height = (total * self.get()).div_ceil(100);
685 let max_height = if max == 0 { total } else { max };
686 pct_height.min(max_height)
687 }
688}
689
690
691impl Deref for Percentage {
692 type Target = u8;
693
694 fn deref(&self) -> &Self::Target {
695 &self.0
696 }
697}
698
699impl fmt::Display for Percentage {
700 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
701 write!(f, "{}%", self.0)
702 }
703}
704
705impl<'de> Deserialize<'de> for Percentage {
707 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
708 where
709 D: Deserializer<'de>,
710 {
711 let v = u8::deserialize(deserializer)?;
712 if v <= 100 {
713 Ok(Percentage(v))
714 } else {
715 Err(serde::de::Error::custom(format!("percentage out of range: {}", v)))
716 }
717 }
718}
719impl std::str::FromStr for Percentage {
720 type Err = String;
721
722 fn from_str(s: &str) -> Result<Self, Self::Err> {
723 let s = s.trim_end_matches('%'); let value: u8 = s
725 .parse()
726 .map_err(|e: std::num::ParseIntError| format!("Invalid number: {}", e))?;
727 Self::new(value).ok_or_else(|| format!("Percentage out of range: {}", value))
728 }
729}
730
731
732pub fn deserialize_padding<'de, D>(deserializer: D) -> Result<Padding, D::Error>
733where
734D: Deserializer<'de>,
735{
736 struct PaddingVisitor;
737
738 impl<'de> de::Visitor<'de> for PaddingVisitor {
739 type Value = Padding;
740
741 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
742 formatter.write_str("a number or an array of 1, 2, or 4 numbers")
743 }
744
745 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
746 where
747 E: de::Error,
748 {
749 let v = value as u16;
750 Ok(Padding { top: v, right: v, bottom: v, left: v })
751 }
752
753 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
754 where
755 A: de::SeqAccess<'de>,
756 {
757 let first: u16 = seq.next_element()?.ok_or_else(|| de::Error::invalid_length(0, &self))?;
758 let second: Option<u16> = seq.next_element()?;
759 let third: Option<u16> = seq.next_element()?;
760 let fourth: Option<u16> = seq.next_element()?;
761
762 match (second, third, fourth) {
763 (None, None, None) => Ok(Padding { top: first, right: first, bottom: first, left: first }),
764 (Some(v2), None, None) => Ok(Padding { top: v2, bottom: v2, left: first, right: first }),
765 (Some(v2), Some(v3), Some(v4)) => Ok(Padding { top: first, right: v2, bottom: v3, left: v4 }),
766 _ => Err(de::Error::invalid_length(2, &self)),
767 }
768 }
769 }
770
771 deserializer.deserialize_any(PaddingVisitor)
772}
773
774fn deserialize_option_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
775where
776D: serde::Deserializer<'de>,
777{
778 use serde::Deserialize;
779 let opt = Option::<String>::deserialize(deserializer)?;
780 Ok(match opt.as_deref() {
781 Some("true") => Some(true),
782 Some("false") => Some(false),
783 Some("auto") => None,
784 None => None,
785 Some(other) => return Err(D::Error::custom(format!("invalid value: {}", other))),
786 })
787}