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