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