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