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