Skip to main content

matchmaker/
config.rs

1//! Config Types.
2//! See `src/bin/mm/config.rs` for an example
3
4use matchmaker_partial_macros::partial;
5
6use std::{fmt, ops::Deref};
7
8pub use crate::utils::{Percentage, When, serde::StringOrVec};
9
10use crate::{
11    MAX_SPLITS,
12    tui::IoStream,
13    utils::{
14        seperator::HorizontalSeparator,
15        serde::{escaped_opt_char, escaped_opt_string, serde_duration_ms},
16    },
17};
18
19use cli_boilerplate_automation::define_transparent_wrapper;
20use cli_boilerplate_automation::serde::{
21    // one_or_many,
22    through_string,
23    transform::camelcase_normalized,
24};
25use ratatui::{
26    style::{Color, Modifier, Style},
27    text::Span,
28    widgets::{BorderType, Borders, Padding},
29};
30
31use regex::Regex;
32
33use serde::{
34    Deserialize, Deserializer, Serialize,
35    de::{self, IntoDeserializer, Visitor},
36    ser::SerializeSeq,
37};
38
39/// Settings unrelated to event loop/picker_ui.
40///
41/// Does not deny unknown fields.
42#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
43#[partial(recurse, path, derive(Debug, Deserialize))]
44pub struct MatcherConfig {
45    #[serde(flatten)]
46    #[partial(skip)]
47    pub matcher: NucleoMatcherConfig,
48    #[serde(flatten)]
49    pub worker: WorkerConfig,
50    // only nit is that we would really prefer this config sit at top level since its irrelevant to [`crate::Matchmaker::new_from_config`] but it makes more sense under matcher in the config file
51    #[serde(flatten)]
52    pub start: StartConfig,
53    #[serde(flatten)]
54    pub exit: ExitConfig,
55}
56
57/// "Input/output specific". Configures the matchmaker worker.
58///
59/// Does not deny unknown fields.
60#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[serde(default)]
62#[partial(path, derive(Debug, Deserialize))]
63pub struct WorkerConfig {
64    #[partial(recurse)]
65    #[partial(alias = "c")]
66    /// How columns are parsed from input lines
67    pub columns: ColumnsConfig,
68    /// Trim the input
69    #[partial(alias = "t")]
70    pub trim: bool,
71    /// How "stable" the results are. Higher values prioritize the initial ordering.
72    pub sort_threshold: u32,
73    /// Whether to parse ansi sequences from input
74    #[partial(alias = "a")]
75    pub ansi: bool,
76
77    /// TODO: Enable raw mode where non-matching items are also displayed in a dimmed color.
78    #[partial(alias = "r")]
79    pub raw: bool,
80    /// TODO: Track the current selection when the result list is updated.
81    pub track: bool,
82    /// TODO: Reverse the order of the input
83    pub reverse: bool,
84}
85
86/// Configures how input is fed to to the worker(s).
87///
88/// Does not deny unknown fields.
89#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(default)]
91#[partial(path, derive(Debug, Deserialize))]
92pub struct StartConfig {
93    #[serde(deserialize_with = "escaped_opt_char")]
94    #[partial(alias = "is")]
95    pub input_separator: Option<char>,
96    #[serde(deserialize_with = "escaped_opt_string")]
97    #[partial(alias = "os")]
98    pub output_separator: Option<String>,
99
100    /// Format string to print accepted items as.
101    pub output_template: Option<String>,
102
103    /// Default command to execute when stdin is not being read.
104    #[partial(alias = "cmd", alias = "x")]
105    pub command: String,
106    #[partial(alias = "s")]
107    pub sync: bool,
108}
109
110/// Exit conditions of the render loop.
111///
112/// Does not deny unknown fields.
113#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
114#[serde(default)]
115#[partial(path, derive(Debug, Deserialize))]
116pub struct ExitConfig {
117    /// Exit automatically if there is only one match.
118    pub select_1: bool,
119    /// Allow returning without any items selected.
120    pub allow_empty: bool,
121    /// Abort if no items.
122    pub abort_empty: bool,
123}
124
125/// The ui config.
126#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[serde(default, deny_unknown_fields)]
128#[partial(recurse, path, derive(Debug, Deserialize))]
129pub struct RenderConfig {
130    /// The default overlay style
131    pub ui: UiConfig,
132    /// The input bar style
133    pub input: InputConfig,
134    /// The results table style
135    #[partial(alias = "r")]
136    pub results: ResultsConfig,
137    /// The preview panel style
138    #[partial(alias = "p")]
139    pub preview: PreviewConfig,
140    pub footer: DisplayConfig,
141    #[partial(alias = "h")]
142    pub header: DisplayConfig,
143}
144
145impl RenderConfig {
146    pub fn tick_rate(&self) -> u8 {
147        self.ui.tick_rate
148    }
149}
150
151/// Terminal settings.
152#[partial(path, derive(Debug, Deserialize))]
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
154#[serde(default, deny_unknown_fields)]
155pub struct TerminalConfig {
156    pub stream: IoStream, // consumed
157    pub restore_fullscreen: bool,
158    pub redraw_on_resize: bool,
159    // https://docs.rs/crossterm/latest/crossterm/event/struct.PushKeyboardEnhancementFlags.html
160    pub extended_keys: bool,
161    #[serde(with = "serde_duration_ms")]
162    pub sleep_ms: std::time::Duration, // necessary to give ratatui a small delay before resizing after entering and exiting
163    // todo: lowpri: will need a value which can deserialize to none when implementing cli parsing
164    #[serde(flatten)]
165    #[partial(recurse)]
166    pub layout: Option<TerminalLayoutSettings>, // None for fullscreen
167    pub clear_on_exit: bool,
168}
169
170impl Default for TerminalConfig {
171    fn default() -> Self {
172        Self {
173            stream: IoStream::default(),
174            restore_fullscreen: true,
175            redraw_on_resize: bool::default(),
176            sleep_ms: std::time::Duration::default(),
177            layout: Option::default(),
178            extended_keys: true,
179            clear_on_exit: true,
180        }
181    }
182}
183#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
184#[serde(default, deny_unknown_fields)]
185pub struct TerminalSettings {}
186
187/// The container ui.
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189#[serde(default, deny_unknown_fields)]
190#[partial(path, derive(Debug, Deserialize))]
191pub struct UiConfig {
192    #[partial(recurse)]
193    pub border: BorderSetting,
194    pub tick_rate: u8, // separate from render, but best place ig
195}
196
197impl Default for UiConfig {
198    fn default() -> Self {
199        Self {
200            border: Default::default(),
201            tick_rate: 60,
202        }
203    }
204}
205
206/// The input bar ui.
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
208#[serde(default, deny_unknown_fields)]
209#[partial(path, derive(Debug, Deserialize))]
210pub struct InputConfig {
211    #[partial(recurse)]
212    pub border: BorderSetting,
213
214    // text styles
215    #[serde(deserialize_with = "camelcase_normalized")]
216    pub fg: Color,
217    // #[serde(deserialize_with = "transform_uppercase")]
218    pub modifier: Modifier,
219
220    #[serde(deserialize_with = "camelcase_normalized")]
221    pub prompt_fg: Color,
222    // #[serde(deserialize_with = "transform_uppercase")]
223    pub prompt_modifier: Modifier,
224
225    #[serde(deserialize_with = "deserialize_string_or_char_as_double_width")]
226    pub prompt: String,
227    pub cursor: CursorSetting,
228    pub initial: String,
229
230    /// Maintain padding when moving the cursor in the bar.
231    pub scroll_padding: bool,
232}
233
234impl Default for InputConfig {
235    fn default() -> Self {
236        Self {
237            border: Default::default(),
238            fg: Default::default(),
239            modifier: Default::default(),
240            prompt_fg: Default::default(),
241            prompt_modifier: Default::default(),
242            prompt: "> ".to_string(),
243            cursor: Default::default(),
244            initial: Default::default(),
245
246            scroll_padding: true,
247        }
248    }
249}
250
251#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
252#[serde(default, deny_unknown_fields)]
253#[partial(path, derive(Debug, Deserialize))]
254pub struct OverlayConfig {
255    #[partial(recurse)]
256    pub border: BorderSetting,
257    pub outer_dim: bool,
258    pub layout: OverlayLayoutSettings,
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
262#[partial(path, derive(Debug, Deserialize))]
263pub struct OverlayLayoutSettings {
264    /// w, h
265    #[partial(alias = "p")]
266    pub percentage: [Percentage; 2],
267    /// w, h
268    pub min: [u16; 2],
269    /// w, h
270    pub max: [u16; 2],
271}
272
273impl Default for OverlayLayoutSettings {
274    fn default() -> Self {
275        Self {
276            percentage: [Percentage::new(60), Percentage::new(30)],
277            min: [10, 5],
278            max: [200, 30],
279        }
280    }
281}
282
283#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
284pub enum RowConnectionStyle {
285    #[default]
286    Disjoint,
287    Capped,
288    Full,
289}
290
291// pub struct OverlaySize
292
293#[partial(path, derive(Debug, Deserialize))]
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295#[serde(default, deny_unknown_fields)]
296pub struct ResultsConfig {
297    #[partial(recurse)]
298    pub border: BorderSetting,
299
300    // prefixes
301    #[serde(deserialize_with = "deserialize_string_or_char_as_double_width")]
302    pub multi_prefix: String,
303    pub default_prefix: String,
304
305    // text styles
306    #[serde(deserialize_with = "camelcase_normalized")]
307    pub fg: Color,
308    // #[serde(deserialize_with = "transform_uppercase")]
309    pub modifier: Modifier,
310
311    #[serde(deserialize_with = "camelcase_normalized")]
312    pub match_fg: Color,
313    // #[serde(deserialize_with = "transform_uppercase")]
314    pub match_modifier: Modifier,
315
316    /// foreground of the current item.
317    #[serde(deserialize_with = "camelcase_normalized")]
318    pub current_fg: Color,
319    /// background of the current item.
320    #[serde(deserialize_with = "camelcase_normalized")]
321    pub current_bg: Color,
322    /// modifier of the current item.
323    // #[serde(deserialize_with = "transform_uppercase")]
324    pub current_modifier: Modifier,
325    /// How the current_* styles are applied across the row.
326    #[serde(deserialize_with = "camelcase_normalized")]
327    pub row_connection_style: RowConnectionStyle,
328
329    // status
330    #[serde(deserialize_with = "camelcase_normalized")]
331    pub status_fg: Color,
332    // #[serde(deserialize_with = "transform_uppercase")]
333    pub status_modifier: Modifier,
334    pub status_show: bool,
335
336    // pub selected_fg: Color,
337    // pub selected_bg: Color,
338    // pub selected_modifier: Color,
339
340    // scroll
341    pub scroll_wrap: bool,
342    pub scroll_padding: u16,
343    #[partial(alias = "r")]
344    pub reverse: When,
345
346    // wrap
347    #[partial(alias = "w")]
348    pub wrap: bool,
349    pub wrap_scaling_min_width: u16,
350
351    // experimental
352    pub column_spacing: Count,
353    pub current_prefix: String,
354
355    // lowpri: maybe space-around/space-between instead?
356    #[partial(alias = "ra")]
357    pub right_align_last: bool,
358
359    #[partial(alias = "v")]
360    #[serde(alias = "vertical")]
361    pub stacked_columns: bool,
362
363    #[serde(alias = "hr")]
364    #[serde(deserialize_with = "camelcase_normalized")]
365    pub horizontal_separator: HorizontalSeparator,
366}
367define_transparent_wrapper!(
368    #[derive(Copy, Clone)]
369    Count: u16 = 1
370);
371
372// #[derive(Default, Deserialize)]
373// pub enum HorizontalSeperator {
374//     None,
375
376// }
377
378impl Default for ResultsConfig {
379    fn default() -> Self {
380        ResultsConfig {
381            border: Default::default(),
382
383            multi_prefix: "▌ ".to_string(),
384            default_prefix: Default::default(),
385
386            fg: Default::default(),
387            modifier: Default::default(),
388            match_fg: Color::Green,
389            match_modifier: Modifier::ITALIC,
390
391            current_fg: Default::default(),
392            current_bg: Color::Black,
393            current_modifier: Modifier::BOLD,
394            row_connection_style: RowConnectionStyle::Disjoint,
395
396            status_fg: Color::Green,
397            status_modifier: Modifier::ITALIC,
398            status_show: true,
399
400            scroll_wrap: true,
401            scroll_padding: 2,
402            reverse: Default::default(),
403
404            wrap: Default::default(),
405            wrap_scaling_min_width: 5,
406
407            column_spacing: Default::default(),
408            current_prefix: Default::default(),
409            right_align_last: false,
410            stacked_columns: false,
411            horizontal_separator: Default::default(),
412        }
413    }
414}
415
416#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
417#[serde(default, deny_unknown_fields)]
418#[partial(path, derive(Debug, Deserialize))]
419pub struct DisplayConfig {
420    #[partial(recurse)]
421    pub border: BorderSetting,
422
423    #[serde(deserialize_with = "camelcase_normalized")]
424    pub fg: Color,
425    // #[serde(deserialize_with = "transform_uppercase")]
426    pub modifier: Modifier,
427
428    pub match_indent: bool,
429    pub wrap: bool,
430
431    #[serde(deserialize_with = "deserialize_option_auto")]
432    pub content: Option<StringOrVec>,
433
434    /// This setting controls the effective width of the displayed content.
435    /// - Full: Effective width is the full ui width.
436    /// - Capped: Effective width is the full ui width, but
437    ///   any width exceeding the width of the Results UI is occluded by the preview pane.
438    /// - Disjoint: Effective width is same as the Results UI.
439    ///
440    /// # Note
441    /// The width effect only applies on the footer, and when the content is singular.
442    #[serde(deserialize_with = "camelcase_normalized")]
443    pub row_connection_style: RowConnectionStyle,
444
445    /// This setting controls how many lines are read from the input for display with the header.
446    ///
447    /// # Note
448    /// This only affects the header and is only implemented in the binary.
449    #[partial(alias = "h")]
450    pub header_lines: usize,
451}
452
453impl Default for DisplayConfig {
454    fn default() -> Self {
455        DisplayConfig {
456            border: Default::default(),
457            match_indent: true,
458            fg: Color::Green,
459            wrap: false,
460            row_connection_style: Default::default(),
461            modifier: Modifier::ITALIC, // whatever your `deserialize_modifier` default uses
462            content: None,
463            header_lines: 0,
464        }
465    }
466}
467
468/// # Example
469/// ```rust
470/// use matchmaker::config::{PreviewConfig, PreviewSetting, PreviewLayout};
471///
472/// let _ = PreviewConfig {
473///     layout: vec![
474///         PreviewSetting {
475///             layout: PreviewLayout::default(),
476///             command: String::new()
477///         }
478///     ],
479///     ..Default::default()
480/// };
481/// ```
482#[partial(path, derive(Debug, Deserialize))]
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484#[serde(default)]
485pub struct PreviewConfig {
486    #[partial(recurse)]
487    pub border: BorderSetting,
488    #[partial(recurse)]
489    #[partial(alias = "l")]
490    pub layout: Vec<PreviewSetting>,
491    #[partial(recurse)]
492    #[serde(flatten)]
493    pub scroll: PreviewScrollSetting,
494    /// Whether to cycle to top after scrolling to the bottom and vice versa.
495    #[partial(alias = "c")]
496    pub scroll_wrap: bool,
497    pub wrap: bool,
498    pub show: bool,
499}
500
501impl Default for PreviewConfig {
502    fn default() -> Self {
503        PreviewConfig {
504            border: BorderSetting {
505                padding: Padding::left(2),
506                ..Default::default()
507            },
508            scroll: Default::default(),
509            layout: Default::default(),
510            scroll_wrap: true,
511            wrap: Default::default(),
512            show: Default::default(),
513        }
514    }
515}
516
517/// Determines the initial scroll offset of the preview window.
518#[partial(path, derive(Debug, Deserialize))]
519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
520#[serde(default, deny_unknown_fields)]
521#[derive(Default)]
522pub struct PreviewScrollSetting {
523    /// Extract the initial display index `n` of the preview window from this column.
524    /// `n` lines are skipped after the header lines are consumed.
525    pub index: Option<String>,
526    /// For adjusting the initial scroll index.
527    #[partial(alias = "o")]
528    pub offset: isize,
529    /// How far from the bottom of the preview window the scroll offset should appear.
530    #[partial(alias = "p")]
531    pub percentage: Percentage,
532    /// Keep the top N lines as the fixed header so that they are always visible.
533    #[partial(alias = "h")]
534    pub header_lines: usize,
535}
536
537#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
538#[serde(default, deny_unknown_fields)]
539pub struct PreviewerConfig {
540    pub try_lossy: bool,
541
542    // todo
543    pub cache: u8,
544
545    pub help_colors: TomlColorConfig,
546}
547
548/// Help coloring
549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
550pub struct TomlColorConfig {
551    #[serde(deserialize_with = "camelcase_normalized")]
552    pub section: Color,
553    #[serde(deserialize_with = "camelcase_normalized")]
554    pub key: Color,
555    #[serde(deserialize_with = "camelcase_normalized")]
556    pub string: Color,
557    #[serde(deserialize_with = "camelcase_normalized")]
558    pub number: Color,
559    pub section_bold: bool,
560}
561
562impl Default for TomlColorConfig {
563    fn default() -> Self {
564        Self {
565            section: Color::Blue,
566            key: Color::Yellow,
567            string: Color::Green,
568            number: Color::Cyan,
569            section_bold: true,
570        }
571    }
572}
573
574// ----------- SETTING TYPES -------------------------
575// Default config file -> write if not exists, then load
576
577#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
578#[serde(transparent)]
579pub struct FormatString(String);
580
581impl Deref for FormatString {
582    type Target = str;
583
584    fn deref(&self) -> &Self::Target {
585        &self.0
586    }
587}
588
589#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
590#[serde(default, deny_unknown_fields)]
591#[partial(path, derive(Debug, Deserialize))]
592pub struct BorderSetting {
593    #[serde(with = "through_string")]
594    pub r#type: BorderType,
595    #[serde(deserialize_with = "camelcase_normalized")]
596    pub color: Color,
597    /// Given as sides joined by `|`. i.e.:
598    /// `sides = "TOP | BOTTOM"``
599    /// When omitted, this is ALL if either padding or type are specified, otherwose NONE.
600    ///
601    /// An empty string enforces no sides:
602    /// `sides = ""`
603    // #[serde(deserialize_with = "uppercase_normalized_option")] // need ratatui bitflags to use transparent
604    pub sides: Option<Borders>,
605    /// Supply as either 1, 2, or 4 numbers for:
606    ///
607    /// - Same padding on all sides
608    /// - Vertical and horizontal padding values
609    /// - Top, Right, Bottom, Left padding values
610    ///
611    /// respectively.
612    #[serde(with = "padding")]
613    pub padding: Padding,
614    pub title: String,
615    // #[serde(deserialize_with = "transform_uppercase")]
616    pub title_modifier: Modifier,
617    #[serde(deserialize_with = "camelcase_normalized")]
618    pub bg: Color,
619}
620
621impl BorderSetting {
622    pub fn as_block(&self) -> ratatui::widgets::Block<'_> {
623        let mut ret = ratatui::widgets::Block::default()
624            .padding(self.padding)
625            .style(Style::default().bg(self.bg));
626
627        if !self.title.is_empty() {
628            let title = Span::styled(
629                &self.title,
630                Style::default().add_modifier(self.title_modifier),
631            );
632
633            ret = ret.title(title)
634        };
635
636        if !self.is_empty() {
637            ret = ret
638                .borders(self.sides())
639                .border_type(self.r#type)
640                .border_style(ratatui::style::Style::default().fg(self.color))
641        }
642
643        ret
644    }
645
646    pub fn sides(&self) -> Borders {
647        if let Some(s) = self.sides {
648            s
649        } else if self.color != Default::default() || self.r#type != Default::default() {
650            Borders::ALL
651        } else {
652            Borders::NONE
653        }
654    }
655
656    pub fn as_static_block(&self) -> ratatui::widgets::Block<'static> {
657        let mut ret = ratatui::widgets::Block::default()
658            .padding(self.padding)
659            .style(Style::default().bg(self.bg));
660
661        if !self.title.is_empty() {
662            let title: Span<'static> = Span::styled(
663                self.title.clone(),
664                Style::default().add_modifier(self.title_modifier),
665            );
666
667            ret = ret.title(title)
668        };
669
670        if !self.is_empty() {
671            ret = ret
672                .borders(self.sides())
673                .border_type(self.r#type)
674                .border_style(ratatui::style::Style::default().fg(self.color))
675        }
676
677        ret
678    }
679
680    pub fn is_empty(&self) -> bool {
681        self.sides() == Borders::NONE
682    }
683
684    pub fn height(&self) -> u16 {
685        let mut height = 0;
686        height += 2 * !self.is_empty() as u16;
687        height += self.padding.top + self.padding.bottom;
688        height += (!self.title.is_empty() as u16).saturating_sub(!self.is_empty() as u16);
689
690        height
691    }
692
693    pub fn width(&self) -> u16 {
694        let mut width = 0;
695        width += 2 * !self.is_empty() as u16;
696        width += self.padding.left + self.padding.right;
697
698        width
699    }
700
701    pub fn left(&self) -> u16 {
702        let mut width = 0;
703        width += !self.is_empty() as u16;
704        width += self.padding.left;
705
706        width
707    }
708
709    pub fn top(&self) -> u16 {
710        let mut height = 0;
711        height += !self.is_empty() as u16;
712        height += self.padding.top;
713        height += (!self.title.is_empty() as u16).saturating_sub(!self.is_empty() as u16);
714
715        height
716    }
717}
718
719// how to determine how many rows to allocate?
720#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
721#[partial(path, derive(Debug, Deserialize))]
722pub struct TerminalLayoutSettings {
723    /// Percentage of total rows to occupy.
724    #[partial(alias = "p")]
725    pub percentage: Percentage,
726    pub min: u16,
727    pub max: u16, // 0 for terminal height cap
728}
729
730impl Default for TerminalLayoutSettings {
731    fn default() -> Self {
732        Self {
733            percentage: Percentage::new(50),
734            min: 10,
735            max: 120,
736        }
737    }
738}
739
740#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
741#[serde(rename_all = "lowercase")]
742pub enum Side {
743    Top,
744    Bottom,
745    Left,
746    #[default]
747    Right,
748}
749
750#[partial(path, derive(Debug, Deserialize))]
751#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
752pub struct PreviewSetting {
753    #[serde(flatten)]
754    #[partial(recurse)]
755    pub layout: PreviewLayout,
756    #[serde(default, alias = "cmd", alias = "x")]
757    pub command: String,
758}
759
760#[partial(path, derive(Debug, Deserialize))]
761#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
762pub struct PreviewLayout {
763    pub side: Side,
764    /// Percentage of total rows/columns to occupy.
765
766    #[serde(alias = "p")]
767    // we need serde here since its specified inside the value but i don't think there's another case for it.
768    pub percentage: Percentage,
769    pub min: i16,
770    pub max: i16,
771}
772
773impl Default for PreviewLayout {
774    fn default() -> Self {
775        Self {
776            side: Side::Right,
777            percentage: Percentage::new(60),
778            min: 30,
779            max: 120,
780        }
781    }
782}
783
784#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
785#[serde(rename_all = "lowercase")]
786pub enum CursorSetting {
787    None,
788    #[default]
789    Default,
790}
791
792use crate::utils::serde::bounded_usize;
793// todo: pass filter and hidden to mm
794#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
795#[serde(default, deny_unknown_fields)]
796#[partial(path, derive(Debug, Deserialize))]
797pub struct ColumnsConfig {
798    /// The strategy of how columns are parsed from input lines
799    pub split: Split,
800    /// Column names
801    #[partial(alias = "n")]
802    pub names: Vec<ColumnSetting>,
803    /// Maximum number of columns to autogenerate when names is unspecified. Maximum of 10, minimum of 1.
804    #[serde(deserialize_with = "bounded_usize::<_, 1, {crate::MAX_SPLITS}>")]
805    max_columns: usize,
806}
807
808impl ColumnsConfig {
809    pub fn max_cols(&self) -> usize {
810        self.max_columns.min(MAX_SPLITS).max(1)
811    }
812}
813
814impl Default for ColumnsConfig {
815    fn default() -> Self {
816        Self {
817            split: Default::default(),
818            names: Default::default(),
819            max_columns: 5,
820        }
821    }
822}
823
824#[derive(Default, Debug, Clone, PartialEq)]
825pub struct ColumnSetting {
826    pub filter: bool,
827    pub hidden: bool,
828    pub name: String,
829}
830
831#[derive(Default, Debug, Clone)]
832pub enum Split {
833    /// Split by delimiter. Supports regex.
834    Delimiter(Regex),
835    /// A sequence of regexes.
836    Regexes(Vec<Regex>),
837    /// No splitting.
838    #[default]
839    None,
840}
841
842impl PartialEq for Split {
843    fn eq(&self, other: &Self) -> bool {
844        match (self, other) {
845            (Split::Delimiter(r1), Split::Delimiter(r2)) => r1.as_str() == r2.as_str(),
846            (Split::Regexes(v1), Split::Regexes(v2)) => {
847                if v1.len() != v2.len() {
848                    return false;
849                }
850                v1.iter()
851                    .zip(v2.iter())
852                    .all(|(r1, r2)| r1.as_str() == r2.as_str())
853            }
854            (Split::None, Split::None) => true,
855            _ => false,
856        }
857    }
858}
859
860// --------- Deserialize Helpers ------------
861pub fn serialize_borders<S>(borders: &Borders, serializer: S) -> Result<S::Ok, S::Error>
862where
863    S: serde::Serializer,
864{
865    use serde::ser::SerializeSeq;
866    let mut seq = serializer.serialize_seq(None)?;
867    if borders.contains(Borders::TOP) {
868        seq.serialize_element("top")?;
869    }
870    if borders.contains(Borders::BOTTOM) {
871        seq.serialize_element("bottom")?;
872    }
873    if borders.contains(Borders::LEFT) {
874        seq.serialize_element("left")?;
875    }
876    if borders.contains(Borders::RIGHT) {
877        seq.serialize_element("right")?;
878    }
879    seq.end()
880}
881
882pub fn deserialize_string_or_char_as_double_width<'de, D, T>(deserializer: D) -> Result<T, D::Error>
883where
884    D: Deserializer<'de>,
885    T: From<String>,
886{
887    struct GenericVisitor<T> {
888        _marker: std::marker::PhantomData<T>,
889    }
890
891    impl<'de, T> Visitor<'de> for GenericVisitor<T>
892    where
893        T: From<String>,
894    {
895        type Value = T;
896
897        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
898            formatter.write_str("a string or single character")
899        }
900
901        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
902        where
903            E: de::Error,
904        {
905            let s = if v.chars().count() == 1 {
906                let mut s = String::with_capacity(2);
907                s.push(v.chars().next().unwrap());
908                s.push(' ');
909                s
910            } else {
911                v.to_string()
912            };
913            Ok(T::from(s))
914        }
915
916        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
917        where
918            E: de::Error,
919        {
920            self.visit_str(&v)
921        }
922    }
923
924    deserializer.deserialize_string(GenericVisitor {
925        _marker: std::marker::PhantomData,
926    })
927}
928
929// ----------- Nucleo config helper
930#[derive(Debug, Clone, PartialEq)]
931pub struct NucleoMatcherConfig(pub nucleo::Config);
932
933impl Default for NucleoMatcherConfig {
934    fn default() -> Self {
935        Self(nucleo::Config::DEFAULT)
936    }
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize)]
940#[serde(default)]
941#[derive(Default)]
942struct MatcherConfigHelper {
943    pub normalize: Option<bool>,
944    pub ignore_case: Option<bool>,
945    pub prefer_prefix: Option<bool>,
946}
947
948impl serde::Serialize for NucleoMatcherConfig {
949    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
950    where
951        S: serde::Serializer,
952    {
953        let helper = MatcherConfigHelper {
954            normalize: Some(self.0.normalize),
955            ignore_case: Some(self.0.ignore_case),
956            prefer_prefix: Some(self.0.prefer_prefix),
957        };
958        helper.serialize(serializer)
959    }
960}
961
962impl<'de> Deserialize<'de> for NucleoMatcherConfig {
963    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
964    where
965        D: serde::Deserializer<'de>,
966    {
967        let helper = MatcherConfigHelper::deserialize(deserializer)?;
968        let mut config = nucleo::Config::DEFAULT;
969
970        if let Some(norm) = helper.normalize {
971            config.normalize = norm;
972        }
973        if let Some(ic) = helper.ignore_case {
974            config.ignore_case = ic;
975        }
976        if let Some(pp) = helper.prefer_prefix {
977            config.prefer_prefix = pp;
978        }
979
980        Ok(NucleoMatcherConfig(config))
981    }
982}
983
984impl serde::Serialize for Split {
985    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
986    where
987        S: serde::Serializer,
988    {
989        match self {
990            Split::Delimiter(r) => serializer.serialize_str(r.as_str()),
991            Split::Regexes(rs) => {
992                let mut seq = serializer.serialize_seq(Some(rs.len()))?;
993                for r in rs {
994                    seq.serialize_element(r.as_str())?;
995                }
996                seq.end()
997            }
998            Split::None => serializer.serialize_none(),
999        }
1000    }
1001}
1002
1003impl<'de> Deserialize<'de> for Split {
1004    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1005    where
1006        D: Deserializer<'de>,
1007    {
1008        struct SplitVisitor;
1009
1010        impl<'de> Visitor<'de> for SplitVisitor {
1011            type Value = Split;
1012
1013            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1014                formatter.write_str("string for delimiter or array of strings for regexes")
1015            }
1016
1017            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1018            where
1019                E: de::Error,
1020            {
1021                // Try to compile single regex
1022                Regex::new(value)
1023                    .map(Split::Delimiter)
1024                    .map_err(|e| E::custom(format!("Invalid regex: {}", e)))
1025            }
1026
1027            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1028            where
1029                A: serde::de::SeqAccess<'de>,
1030            {
1031                let mut regexes = Vec::new();
1032                while let Some(s) = seq.next_element::<String>()? {
1033                    let r = Regex::new(&s)
1034                        .map_err(|e| de::Error::custom(format!("Invalid regex: {}", e)))?;
1035                    regexes.push(r);
1036                }
1037                Ok(Split::Regexes(regexes))
1038            }
1039        }
1040
1041        deserializer.deserialize_any(SplitVisitor)
1042    }
1043}
1044
1045impl serde::Serialize for ColumnSetting {
1046    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1047    where
1048        S: serde::Serializer,
1049    {
1050        use serde::ser::SerializeStruct;
1051        let mut state = serializer.serialize_struct("ColumnSetting", 3)?;
1052        state.serialize_field("filter", &self.filter)?;
1053        state.serialize_field("hidden", &self.hidden)?;
1054        state.serialize_field("name", &self.name)?;
1055        state.end()
1056    }
1057}
1058
1059impl<'de> Deserialize<'de> for ColumnSetting {
1060    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1061    where
1062        D: Deserializer<'de>,
1063    {
1064        #[derive(Deserialize)]
1065        #[serde(deny_unknown_fields)]
1066        struct ColumnStruct {
1067            #[serde(default = "default_true")]
1068            filter: bool,
1069            #[serde(default)]
1070            hidden: bool,
1071            name: String,
1072        }
1073
1074        fn default_true() -> bool {
1075            true
1076        }
1077
1078        #[derive(Deserialize)]
1079        #[serde(untagged)]
1080        enum Input {
1081            Str(String),
1082            Obj(ColumnStruct),
1083        }
1084
1085        match Input::deserialize(deserializer)? {
1086            Input::Str(name) => Ok(ColumnSetting {
1087                filter: true,
1088                hidden: false,
1089                name,
1090            }),
1091            Input::Obj(obj) => Ok(ColumnSetting {
1092                filter: obj.filter,
1093                hidden: obj.hidden,
1094                name: obj.name,
1095            }),
1096        }
1097    }
1098}
1099
1100mod padding {
1101    use super::*;
1102
1103    pub fn serialize<S>(padding: &Padding, serializer: S) -> Result<S::Ok, S::Error>
1104    where
1105        S: serde::Serializer,
1106    {
1107        use serde::ser::SerializeSeq;
1108        if padding.top == padding.bottom
1109            && padding.left == padding.right
1110            && padding.top == padding.left
1111        {
1112            serializer.serialize_u16(padding.top)
1113        } else if padding.top == padding.bottom && padding.left == padding.right {
1114            let mut seq = serializer.serialize_seq(Some(2))?;
1115            seq.serialize_element(&padding.left)?;
1116            seq.serialize_element(&padding.top)?;
1117            seq.end()
1118        } else {
1119            let mut seq = serializer.serialize_seq(Some(4))?;
1120            seq.serialize_element(&padding.top)?;
1121            seq.serialize_element(&padding.right)?;
1122            seq.serialize_element(&padding.bottom)?;
1123            seq.serialize_element(&padding.left)?;
1124            seq.end()
1125        }
1126    }
1127
1128    pub fn deserialize<'de, D>(deserializer: D) -> Result<Padding, D::Error>
1129    where
1130        D: Deserializer<'de>,
1131    {
1132        struct PaddingVisitor;
1133
1134        impl<'de> Visitor<'de> for PaddingVisitor {
1135            type Value = Padding;
1136
1137            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1138                formatter.write_str("a number or an array of 1, 2, or 4 numbers")
1139            }
1140
1141            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1142            where
1143                E: de::Error,
1144            {
1145                let v = u16::try_from(value).map_err(|_| {
1146                    E::custom(format!("padding value {} is out of range for u16", value))
1147                })?;
1148
1149                Ok(Padding {
1150                    top: v,
1151                    right: v,
1152                    bottom: v,
1153                    left: v,
1154                })
1155            }
1156
1157            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1158            where
1159                E: de::Error,
1160            {
1161                let v = u16::try_from(value).map_err(|_| {
1162                    E::custom(format!("padding value {} is out of range for u16", value))
1163                })?;
1164
1165                Ok(Padding {
1166                    top: v,
1167                    right: v,
1168                    bottom: v,
1169                    left: v,
1170                })
1171            }
1172
1173            // 3. Handle Sequences (Arrays)
1174            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1175            where
1176                A: de::SeqAccess<'de>,
1177            {
1178                let first: u16 = seq
1179                    .next_element()?
1180                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
1181
1182                let second: Option<u16> = seq.next_element()?;
1183                let third: Option<u16> = seq.next_element()?;
1184                let fourth: Option<u16> = seq.next_element()?;
1185
1186                match (second, third, fourth) {
1187                    (None, None, None) => Ok(Padding {
1188                        top: first,
1189                        right: first,
1190                        bottom: first,
1191                        left: first,
1192                    }),
1193                    (Some(v2), None, None) => Ok(Padding {
1194                        top: first,
1195                        bottom: first,
1196                        left: v2,
1197                        right: v2,
1198                    }),
1199                    (Some(v2), Some(v3), Some(v4)) => Ok(Padding {
1200                        top: first,
1201                        right: v2,
1202                        bottom: v3,
1203                        left: v4,
1204                    }),
1205                    _ => Err(de::Error::invalid_length(3, &self)),
1206                }
1207            }
1208        }
1209
1210        deserializer.deserialize_any(PaddingVisitor)
1211    }
1212}
1213
1214pub fn deserialize_option_auto<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
1215where
1216    D: serde::Deserializer<'de>,
1217    T: Deserialize<'de>,
1218{
1219    let opt = Option::<String>::deserialize(deserializer)?;
1220    match opt.as_deref() {
1221        Some("auto") => Ok(None),
1222        Some(s) => Ok(Some(T::deserialize(s.into_deserializer())?)),
1223        None => Ok(None),
1224    }
1225}