matchmaker/
config.rs

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    // configure the ui
16    #[serde(flatten)]
17    pub render: RenderConfig,
18    
19    // binds
20    pub binds: BindMap,
21    
22    // Ideally, this would deserialize flattened from PreviewConfig, but its way too much trouble
23    pub previewer: PreviewerConfig,
24    
25    // instantiate the picker
26    pub matcher: MatcherConfig,
27    
28    // similarly, maybe this would prefer to be in ui
29    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// for now, just stores all misc options for render_loop
46#[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, // seperate from render, but best place ig
74    
75}
76
77#[derive(Default, Debug, Clone, Deserialize)]
78#[serde(default, deny_unknown_fields)]
79pub struct TerminalConfig {
80    pub stream: IoStream,
81    pub sleep: u16, // necessary to give ratatui a small delay before resizing after entering and exiting
82    #[serde(flatten)]
83    pub layout: Option<TerminalLayoutSettings> // None for fullscreen
84}
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    // experimental
125    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    // TODO
169    pub wrap: bool,
170    pub cache: bool,
171    
172}
173
174// ----------- SETTING TYPES -------------------------
175// Default config file -> write if not exists, then load
176
177#[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// how to determine how many rows to allocate?
240#[derive(Debug, Clone, Deserialize)]
241pub struct TerminalLayoutSettings {
242    pub percentage: Percentage,
243    pub min: u16,
244    pub max: u16, // 0 for terminal height cap
245}
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
338// ----------- UTILS -------------------------
339
340pub 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
373// --------- Deserialize Helpers ------------
374fn 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            // "slow_blink" => {
458            //     *m |= Modifier::SLOW_BLINK;
459            //     Ok(())
460            // }
461            // "rapid_blink" => {
462            //     *m |= Modifier::RAPID_BLINK;
463            //     Ok(())
464            // }
465            // "reversed" => {
466            //     *m |= Modifier::REVERSED;
467            //     Ok(())
468            // }
469            // "dim" => {
470            //     *m |= Modifier::DIM;
471            //     Ok(())
472            // }
473            // "crossed_out" => {
474            //     *m |= Modifier::CROSSED_OUT;
475            //     Ok(())
476            // }
477            "none" => {
478                *m = Modifier::empty();
479                Ok(())
480            } // reset all modifiers
481            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// ----------- Nucleo config helper
536#[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                // Try to compile single regex
604                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
705// Implement Deserialize
706impl<'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('%'); // allow optional trailing '%'
724        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}