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
6pub use crate::config_types::*;
7pub use crate::utils::{Percentage, serde::StringOrVec};
8
9use crate::{
10    MAX_SPLITS,
11    tui::IoStream,
12    utils::serde::{escaped_opt_char, escaped_opt_string, serde_duration_ms},
13};
14
15use cba::serde::transform::{camelcase_normalized, camelcase_normalized_option};
16use ratatui::{
17    style::{Color, Modifier, Style},
18    text::Span,
19    widgets::{BorderType, Borders},
20};
21
22use serde::{Deserialize, Serialize};
23
24/// Settings unrelated to event loop/picker_ui.
25///
26/// Does not deny unknown fields.
27#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[partial(recurse, path, derive(Debug, Deserialize))]
29pub struct MatcherConfig {
30    #[serde(flatten)]
31    #[partial(skip)]
32    pub matcher: NucleoMatcherConfig,
33    #[serde(flatten)]
34    pub worker: WorkerConfig,
35}
36
37/// "Input/output specific". Configures the matchmaker worker.
38///
39/// Does not deny unknown fields.
40#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
41#[serde(default)]
42#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
43pub struct WorkerConfig {
44    #[partial(recurse)]
45    #[serde(flatten)]
46    /// How columns are parsed from input lines
47    pub columns: ColumnsConfig,
48    /// How "stable" the results are. Higher values prioritize the initial ordering.
49    pub sort_threshold: u32,
50    /// The name of the default column
51    #[partial(alias = "i")]
52    pub default_column: Option<String>,
53
54    /// TODO: Enable raw mode where non-matching items are also displayed in a dimmed color.
55    #[partial(alias = "r")]
56    pub raw: bool,
57    /// TODO: Track the current selection when the result list is updated.
58    pub track: bool,
59    /// Reverse the order of the input
60    pub reverse: bool, // TODO: test with sort_threshold
61}
62
63/// Configures how input is fed to to the worker(s).
64///
65#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
66#[serde(default, deny_unknown_fields)]
67#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
68pub struct StartConfig {
69    #[serde(deserialize_with = "escaped_opt_char")]
70    #[partial(alias = "is")]
71    pub input_separator: Option<char>,
72    #[serde(deserialize_with = "escaped_opt_string")]
73    #[partial(alias = "os")]
74    pub output_separator: Option<String>,
75
76    /// Format string to print accepted items as.
77    #[partial(alias = "ot")]
78    #[serde(alias = "output")]
79    pub output_template: Option<String>,
80
81    /// Default command to execute when stdin is not being read.
82    #[partial(alias = "cmd", alias = "x")]
83    pub command: String,
84    /// (cli only) Additional command which can be cycled through using Action::ReloadNext
85    #[partial(alias = "ax")]
86    pub additional_commands: Vec<String>,
87    pub sync: bool,
88
89    /// Whether to parse ansi sequences from input
90    #[partial(alias = "a")]
91    pub ansi: bool,
92    /// Trim the input
93    #[partial(alias = "t")]
94    pub trim: bool,
95}
96
97/// Exit conditions of the render loop.
98#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
99#[serde(default, deny_unknown_fields)]
100#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
101pub struct ExitConfig {
102    /// Exit automatically if there is only one match.
103    pub select_1: bool,
104    /// Allow returning without any items selected.
105    pub allow_empty: bool,
106    /// Abort if no items.
107    pub abort_empty: bool,
108    /// Last processed key is written here.
109    /// Set to an empty path to disable.
110    pub last_key_path: Option<std::path::PathBuf>,
111}
112
113/// The ui config.
114#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
115#[serde(default, deny_unknown_fields)]
116#[partial(recurse, path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
117pub struct RenderConfig {
118    /// The default overlay style
119    pub ui: UiConfig,
120    /// The input bar style
121    #[partial(alias = "i")]
122    pub input: InputConfig,
123    /// The results table style
124    #[partial(alias = "r")]
125    pub results: ResultsConfig,
126
127    /// The results status style
128    pub status: StatusConfig,
129    /// The preview panel style
130    #[partial(alias = "p")]
131    pub preview: PreviewConfig,
132    #[partial(alias = "f")]
133    pub footer: DisplayConfig,
134    #[partial(alias = "h")]
135    pub header: DisplayConfig,
136}
137
138impl RenderConfig {
139    pub fn tick_rate(&self) -> u8 {
140        self.ui.tick_rate
141    }
142}
143
144/// Terminal settings.
145#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[serde(default, deny_unknown_fields)]
148pub struct TerminalConfig {
149    pub stream: IoStream, // consumed
150    pub restore_fullscreen: bool,
151    pub redraw_on_resize: bool,
152    // https://docs.rs/crossterm/latest/crossterm/event/struct.PushKeyboardEnhancementFlags.html
153    pub extended_keys: bool,
154    #[serde(with = "serde_duration_ms")]
155    pub sleep_ms: std::time::Duration, // necessary to give ratatui a small delay before resizing after entering and exiting
156    #[serde(flatten)]
157    #[partial(recurse)]
158    pub layout: Option<TerminalLayoutSettings>, // None for fullscreen
159    pub clear_on_exit: bool,
160    // experimental: makes exits cleaner, but success get joined
161    pub move_up_on_exit: bool,
162}
163
164impl Default for TerminalConfig {
165    fn default() -> Self {
166        Self {
167            stream: IoStream::default(),
168            restore_fullscreen: true,
169            redraw_on_resize: bool::default(),
170            sleep_ms: std::time::Duration::default(),
171            layout: Option::default(),
172            extended_keys: true,
173            clear_on_exit: true,
174            move_up_on_exit: false,
175        }
176    }
177}
178
179/// The container ui.
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181#[serde(default, deny_unknown_fields)]
182#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
183pub struct UiConfig {
184    #[partial(recurse)]
185    pub border: BorderSetting,
186    pub tick_rate: u8, // separate from render, but best place ig
187}
188
189impl Default for UiConfig {
190    fn default() -> Self {
191        Self {
192            border: Default::default(),
193            tick_rate: 60,
194        }
195    }
196}
197
198/// The query (input) bar ui.
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200#[serde(default, deny_unknown_fields)]
201#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
202pub struct InputConfig {
203    #[partial(recurse)]
204    pub border: BorderSetting,
205
206    // text styles
207    #[serde(deserialize_with = "camelcase_normalized")]
208    pub fg: Color,
209    // #[serde(deserialize_with = "transform_uppercase")]
210    pub modifier: Modifier,
211
212    #[serde(deserialize_with = "camelcase_normalized")]
213    pub prompt_fg: Color,
214    pub prompt_bg: Color,
215    // #[serde(deserialize_with = "transform_uppercase")]
216    pub prompt_modifier: Modifier,
217
218    /// The prompt prefix.
219    #[serde(deserialize_with = "deserialize_string_or_char_as_double_width")]
220    pub prompt: String,
221    /// Cursor style.
222    pub cursor: CursorSetting,
223
224    /// Initial text in the input bar.
225    #[partial(alias = "i")]
226    pub initial: String,
227
228    /// Maintain padding when moving the cursor in the bar.
229    pub scroll_padding: bool,
230}
231
232impl Default for InputConfig {
233    fn default() -> Self {
234        Self {
235            border: Default::default(),
236            fg: Default::default(),
237            modifier: Default::default(),
238            prompt_fg: Default::default(),
239            prompt_bg: 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
250impl InputConfig {
251    pub fn text_style(&self) -> Style {
252        Style::default().fg(self.fg).add_modifier(self.modifier)
253    }
254
255    pub fn prompt_style(&self) -> Style {
256        Style::default()
257            .fg(self.prompt_fg)
258            .bg(self.prompt_bg)
259            .add_modifier(self.prompt_modifier)
260    }
261}
262
263#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
264#[serde(default, deny_unknown_fields)]
265#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
266pub struct OverlayConfig {
267    #[partial(recurse)]
268    pub border: BorderSetting,
269    pub outer_dim: bool,
270    pub layout: OverlayLayoutSettings,
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
275pub struct OverlayLayoutSettings {
276    /// w, h
277    #[partial(alias = "p")]
278    pub percentage: [Percentage; 2],
279    /// w, h
280    pub min: [u16; 2],
281    /// w, h
282    pub max: [u16; 2],
283
284    /// y_offset as a percentage of total height: 50 for neutral, (default: 55)
285    pub y_offset: Percentage,
286}
287
288impl Default for OverlayLayoutSettings {
289    fn default() -> Self {
290        Self {
291            percentage: [Percentage::new(60), Percentage::new(30)],
292            min: [10, 10],
293            max: [200, 30],
294            y_offset: Percentage::new(55),
295        }
296    }
297}
298
299// pub struct OverlaySize
300
301#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
303#[serde(default, deny_unknown_fields)]
304pub struct ResultsConfig {
305    #[partial(recurse)]
306    pub border: BorderSetting,
307
308    // prefixes
309    #[serde(deserialize_with = "deserialize_string_or_char_as_double_width")]
310    pub multi_prefix: String,
311    pub default_prefix: String,
312
313    /// Enable selections
314    pub multi: bool,
315
316    // text styles
317    #[serde(deserialize_with = "camelcase_normalized")]
318    pub fg: Color,
319    #[serde(deserialize_with = "camelcase_normalized")]
320    pub bg: Color,
321    // #[serde(deserialize_with = "transform_uppercase")]
322    pub modifier: Modifier,
323
324    // inactive_col styles
325    #[serde(deserialize_with = "camelcase_normalized")]
326    pub inactive_fg: Color,
327    #[serde(deserialize_with = "camelcase_normalized")]
328    pub inactive_bg: Color,
329    // #[serde(deserialize_with = "transform_uppercase")]
330    pub inactive_modifier: Modifier,
331
332    // inactive_col styles on the current item
333    #[serde(deserialize_with = "camelcase_normalized")]
334    pub inactive_current_fg: Color,
335    #[serde(deserialize_with = "camelcase_normalized")]
336    pub inactive_current_bg: Color,
337    // #[serde(deserialize_with = "transform_uppercase")]
338    pub inactive_current_modifier: Modifier,
339
340    #[serde(deserialize_with = "camelcase_normalized")]
341    pub match_fg: Color,
342    // #[serde(deserialize_with = "transform_uppercase")]
343    pub match_modifier: Modifier,
344
345    /// foreground of the current item.
346    #[serde(deserialize_with = "camelcase_normalized")]
347    pub current_fg: Color,
348    /// background of the current item.
349    #[serde(deserialize_with = "camelcase_normalized")]
350    pub current_bg: Color,
351    /// modifier of the current item.
352    // #[serde(deserialize_with = "transform_uppercase")]
353    pub current_modifier: Modifier,
354
355    /// How the styles are applied across the row:
356    /// Disjoint: Styles are applied per column.
357    /// Capped: The inactive styles are applied per row, and the active styles applied on the active column.
358    /// Full: Inactive column styles are ignored, the current style is applied on the current row.
359    #[serde(deserialize_with = "camelcase_normalized")]
360    pub row_connection_style: RowConnectionStyle,
361
362    // scroll
363    #[partial(alias = "c")]
364    #[serde(alias = "cycle")]
365    pub scroll_wrap: bool,
366    #[partial(alias = "sp")]
367    pub scroll_padding: u16,
368    #[partial(alias = "r")]
369    pub reverse: Option<bool>,
370
371    // wrap
372    #[partial(alias = "w")]
373    pub wrap: bool,
374    pub min_wrap_width: u16,
375
376    // autoscroll
377    pub autoscroll_initial_preserved: usize,
378    pub autoscroll: bool,
379    pub autoscroll_context: usize,
380
381    // ------------
382    // experimental
383    // ------------
384    pub column_spacing: Count,
385    pub current_prefix: String,
386
387    // lowpri: maybe space-around/space-between instead?
388    #[partial(alias = "ra")]
389    pub right_align_last: bool,
390
391    #[partial(alias = "v")]
392    #[serde(alias = "vertical")]
393    pub stacked_columns: bool,
394
395    #[serde(alias = "hr")]
396    #[serde(deserialize_with = "camelcase_normalized")]
397    pub horizontal_separator: HorizontalSeparator,
398}
399
400impl Default for ResultsConfig {
401    fn default() -> Self {
402        ResultsConfig {
403            border: Default::default(),
404
405            multi_prefix: "▌ ".to_string(),
406            default_prefix: Default::default(),
407            multi: true,
408
409            fg: Default::default(),
410            modifier: Default::default(),
411            bg: Default::default(),
412
413            inactive_fg: Default::default(),
414            inactive_modifier: Modifier::DIM,
415            inactive_bg: Default::default(),
416
417            inactive_current_fg: Default::default(),
418            inactive_current_modifier: Default::default(),
419            inactive_current_bg: Default::default(),
420
421            match_fg: Color::Green,
422            match_modifier: Modifier::ITALIC,
423
424            current_fg: Default::default(),
425            current_bg: Color::Black,
426            current_modifier: Modifier::BOLD,
427            row_connection_style: RowConnectionStyle::Disjoint,
428
429            scroll_wrap: true,
430            scroll_padding: 2,
431            reverse: Default::default(),
432
433            wrap: Default::default(),
434            min_wrap_width: 6,
435
436            autoscroll: true,
437            autoscroll_initial_preserved: 0,
438            autoscroll_context: 4,
439
440            column_spacing: Default::default(),
441            current_prefix: Default::default(),
442            right_align_last: false,
443            stacked_columns: false,
444            horizontal_separator: Default::default(),
445        }
446    }
447}
448
449#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
450#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
451#[serde(default, deny_unknown_fields)]
452pub struct StatusConfig {
453    #[serde(deserialize_with = "camelcase_normalized")]
454    pub fg: Color,
455    #[serde(deserialize_with = "camelcase_normalized")]
456    pub bg: Color,
457    // #[serde(deserialize_with = "transform_uppercase")]
458    pub modifier: Modifier,
459
460    /// Whether the status is visible.
461    pub show: bool,
462    /// Indent the status to match the results.
463    pub match_indent: bool,
464
465    /// Supports replacements:
466    /// - `\r` -> cursor index
467    /// - `\m` -> match count
468    /// - `\t` -> total count
469    /// - `\s` -> available whitespace / # appearances
470    #[partial(alias = "t")]
471    pub template: String,
472
473    /// - Full: available whitespace is computed using the full ui width when replacing `\s` in the template.
474    /// - Disjoint: no effect.
475    /// - Capped: no effect.
476    pub row_connection_style: RowConnectionStyle,
477}
478impl Default for StatusConfig {
479    fn default() -> Self {
480        Self {
481            fg: Color::Green,
482            bg: Default::default(),
483            modifier: Modifier::ITALIC,
484            show: true,
485            match_indent: true,
486            template: r#"\m/\t"#.to_string(),
487            row_connection_style: RowConnectionStyle::Full,
488        }
489    }
490}
491
492impl StatusConfig {
493    pub fn base_style(&self) -> Style {
494        Style::default()
495            .fg(self.fg)
496            .bg(self.bg)
497            .add_modifier(self.modifier)
498    }
499}
500
501#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
502#[serde(default, deny_unknown_fields)]
503#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
504pub struct DisplayConfig {
505    #[partial(recurse)]
506    pub border: BorderSetting,
507
508    #[serde(deserialize_with = "camelcase_normalized")]
509    pub fg: Color,
510    // #[serde(deserialize_with = "transform_uppercase")]
511    pub modifier: Modifier,
512
513    /// Indent content to match the results table.
514    pub match_indent: bool,
515    /// Enable line wrapping.
516    pub wrap: bool,
517
518    /// Static content to display.
519    pub content: Option<StringOrVec>,
520
521    /// This setting controls the effective width of the displayed content.
522    /// - Full: Effective width is the full ui width.
523    /// - Capped: Effective width is the full ui width, but
524    ///   any width exceeding the width of the Results UI is occluded by the preview pane.
525    /// - Disjoint: Effective width is same as the Results UI.
526    ///
527    /// # Note
528    /// The width effect only applies on the footer, and when the content is singular.
529    #[serde(deserialize_with = "camelcase_normalized")]
530    pub row_connection_style: RowConnectionStyle,
531
532    /// (cli only) This setting controls how many lines are read from the input for display with the header.
533    #[partial(alias = "h")]
534    pub header_lines: usize,
535}
536
537impl Default for DisplayConfig {
538    fn default() -> Self {
539        DisplayConfig {
540            border: Default::default(),
541            match_indent: true,
542            fg: Color::Green,
543            wrap: false,
544            row_connection_style: Default::default(),
545            modifier: Modifier::ITALIC, // whatever your `deserialize_modifier` default uses
546            content: None,
547            header_lines: 0,
548        }
549    }
550}
551
552/// # Example
553/// ```rust
554/// use matchmaker::config::{PreviewConfig, PreviewSetting, PreviewLayout};
555///
556/// let _ = PreviewConfig {
557///     layout: vec![
558///         PreviewSetting {
559///             layout: PreviewLayout::default(),
560///             command: String::new(),
561///             ..Default::default()
562///         }
563///     ],
564///     ..Default::default()
565/// };
566/// ```
567#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
568#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
569#[serde(default)]
570pub struct PreviewConfig {
571    #[partial(recurse)]
572    pub border: BorderSetting,
573    #[partial(recurse, set = "recurse")]
574    #[partial(alias = "l")]
575    pub layout: Vec<PreviewSetting>,
576    #[partial(recurse)]
577    #[serde(flatten)]
578    pub scroll: PreviewScrollSetting,
579    /// Whether to cycle to top after scrolling to the bottom and vice versa.
580    #[partial(alias = "c")]
581    #[serde(alias = "cycle")]
582    pub scroll_wrap: bool,
583    pub wrap: bool,
584    /// Whether to show the preview pane initially.
585    /// Can either be a boolean or a number which the relevant dimension of the available ui area must exceed.
586    pub show: ShowCondition,
587
588    pub reevaluate_show_on_resize: bool,
589}
590
591impl Default for PreviewConfig {
592    fn default() -> Self {
593        PreviewConfig {
594            border: BorderSetting {
595                padding: Padding(ratatui::widgets::Padding::left(2)),
596                ..Default::default()
597            },
598            scroll: Default::default(),
599            layout: Default::default(),
600            scroll_wrap: true,
601            wrap: Default::default(),
602            show: Default::default(),
603            reevaluate_show_on_resize: false,
604        }
605    }
606}
607
608/// Determines the initial scroll offset of the preview window.
609#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
610#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611#[serde(default, deny_unknown_fields)]
612pub struct PreviewScrollSetting {
613    /// Extract the initial display index `n` of the preview window from this column.
614    /// `n` lines are skipped after the header lines are consumed.
615    pub index: Option<String>,
616    /// For adjusting the initial scroll index.
617    #[partial(alias = "o")]
618    pub offset: isize,
619    /// How far from the bottom of the preview window the scroll offset should appear.
620    #[partial(alias = "p")]
621    pub percentage: Percentage,
622    /// Keep the top N lines as the fixed header so that they are always visible.
623    #[partial(alias = "h")]
624    pub header_lines: usize,
625}
626
627impl Default for PreviewScrollSetting {
628    fn default() -> Self {
629        Self {
630            index: Default::default(),
631            offset: -1,
632            percentage: Default::default(),
633            header_lines: Default::default(),
634        }
635    }
636}
637
638#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
639#[serde(default, deny_unknown_fields)]
640pub struct PreviewerConfig {
641    pub try_lossy: bool,
642
643    // todo
644    pub cache: u8,
645
646    pub help_colors: HelpColorConfig,
647}
648
649/// Help coloring
650#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
651pub struct HelpColorConfig {
652    #[serde(deserialize_with = "camelcase_normalized")]
653    pub section: Color,
654    #[serde(deserialize_with = "camelcase_normalized")]
655    pub key: Color,
656    #[serde(deserialize_with = "camelcase_normalized")]
657    pub value: Color,
658}
659
660impl Default for HelpColorConfig {
661    fn default() -> Self {
662        Self {
663            section: Color::Blue,
664            key: Color::Green,
665            value: Color::White,
666        }
667    }
668}
669
670// ----------- SETTING TYPES -------------------------
671
672#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
673#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
674#[serde(default, deny_unknown_fields)]
675pub struct BorderSetting {
676    #[serde(deserialize_with = "camelcase_normalized_option")]
677    pub r#type: Option<BorderType>,
678    #[serde(deserialize_with = "camelcase_normalized")]
679    pub color: Color,
680    /// Given as sides joined by `|`. i.e.:
681    /// `sides = "TOP | BOTTOM"``
682    /// `sides = "ALL"`
683    /// 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.
684    ///
685    /// An empty string enforces no sides:
686    /// `sides = ""`
687    // #[serde(deserialize_with = "uppercase_normalized_option")] // need ratatui bitflags to use transparent
688    pub sides: Option<Borders>,
689    /// Supply as either 1, 2, or 4 numbers for:
690    ///
691    /// - Same padding on all sides
692    /// - Vertical and horizontal padding values
693    /// - Top, Right, Bottom, Left padding values
694    ///
695    /// respectively.
696    pub padding: Padding,
697    pub title: String,
698    // #[serde(deserialize_with = "transform_uppercase")]
699    pub title_modifier: Modifier,
700    pub modifier: Modifier,
701    #[serde(deserialize_with = "camelcase_normalized")]
702    pub bg: Color,
703}
704
705impl BorderSetting {
706    pub fn as_block(&self) -> ratatui::widgets::Block<'_> {
707        let mut ret = ratatui::widgets::Block::default()
708            .padding(self.padding.0)
709            .style(Style::default().bg(self.bg).add_modifier(self.modifier));
710
711        if !self.title.is_empty() {
712            let title = Span::styled(
713                &self.title,
714                Style::default().add_modifier(self.title_modifier),
715            );
716
717            ret = ret.title(title)
718        };
719
720        if !self.is_empty() {
721            ret = ret
722                .borders(self.sides())
723                .border_type(self.r#type.unwrap_or_default())
724                .border_style(ratatui::style::Style::default().fg(self.color))
725        }
726
727        ret
728    }
729
730    pub fn sides(&self) -> Borders {
731        if let Some(s) = self.sides {
732            s
733        } else if self.color != Default::default() || self.r#type != Default::default() {
734            Borders::ALL
735        } else {
736            Borders::NONE
737        }
738    }
739
740    pub fn as_static_block(&self) -> ratatui::widgets::Block<'static> {
741        let mut ret = ratatui::widgets::Block::default()
742            .padding(self.padding.0)
743            .style(Style::default().bg(self.bg).add_modifier(self.modifier));
744
745        if !self.title.is_empty() {
746            let title: Span<'static> = Span::styled(
747                self.title.clone(),
748                Style::default().add_modifier(self.title_modifier),
749            );
750
751            ret = ret.title(title)
752        };
753
754        if !self.is_empty() {
755            ret = ret
756                .borders(self.sides())
757                .border_type(self.r#type.unwrap_or_default())
758                .border_style(ratatui::style::Style::default().fg(self.color))
759        }
760
761        ret
762    }
763
764    pub fn is_empty(&self) -> bool {
765        self.sides() == Borders::NONE
766    }
767
768    pub fn height(&self) -> u16 {
769        let mut height = 0;
770        height += 2 * !self.is_empty() as u16;
771        height += self.padding.top + self.padding.bottom;
772        height += (!self.title.is_empty() as u16).saturating_sub(!self.is_empty() as u16);
773
774        height
775    }
776
777    pub fn width(&self) -> u16 {
778        let mut width = 0;
779        width += 2 * !self.is_empty() as u16;
780        width += self.padding.left + self.padding.right;
781
782        width
783    }
784
785    pub fn left(&self) -> u16 {
786        let mut width = 0;
787        width += !self.is_empty() as u16;
788        width += self.padding.left;
789
790        width
791    }
792
793    pub fn top(&self) -> u16 {
794        let mut height = 0;
795        height += !self.is_empty() as u16;
796        height += self.padding.top;
797        height += (!self.title.is_empty() as u16).saturating_sub(!self.is_empty() as u16);
798
799        height
800    }
801}
802
803// how to determine how many rows to allocate?
804#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
805#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
806pub struct TerminalLayoutSettings {
807    /// Percentage of total rows to occupy.
808    #[partial(alias = "p")]
809    pub percentage: Percentage,
810    pub min: u16,
811    pub max: u16, // 0 for terminal height cap
812}
813
814impl Default for TerminalLayoutSettings {
815    fn default() -> Self {
816        Self {
817            percentage: Percentage::new(50),
818            min: 10,
819            max: 120,
820        }
821    }
822}
823
824#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
825#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
826pub struct PreviewSetting {
827    #[serde(flatten)]
828    #[partial(recurse)]
829    pub layout: PreviewLayout,
830    #[partial(recurse)]
831    pub border: Option<BorderSetting>,
832    #[serde(default, alias = "cmd", alias = "x")]
833    pub command: String,
834}
835
836#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
837#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
838pub struct PreviewLayout {
839    pub side: Side,
840    /// Percentage of total rows/columns to occupy.
841    #[serde(alias = "p")]
842    // we need serde here since its specified inside the value but i don't think there's another case for it.
843    pub percentage: Percentage,
844    pub min: i16,
845    pub max: i16,
846}
847
848impl Default for PreviewLayout {
849    fn default() -> Self {
850        Self {
851            side: Side::Right,
852            percentage: Percentage::new(60),
853            min: 30,
854            max: 120,
855        }
856    }
857}
858
859use crate::utils::serde::bounded_usize;
860#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
861#[serde(default, deny_unknown_fields)]
862#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
863pub struct ColumnsConfig {
864    /// The strategy of how columns are parsed from input lines
865    #[partial(alias = "s")]
866    pub split: Split,
867    /// Column names
868    #[partial(alias = "n")]
869    pub names: Vec<ColumnSetting>,
870    /// Maximum number of columns to autogenerate when names is unspecified. Maximum of 16, minimum of 1.
871    #[serde(deserialize_with = "bounded_usize::<_, 1, {crate::MAX_SPLITS}>")]
872    #[partial(alias = "mc")]
873    max_columns: usize,
874}
875
876impl ColumnsConfig {
877    pub fn max_cols(&self) -> usize {
878        self.max_columns.min(MAX_SPLITS).max(1)
879    }
880}
881
882impl Default for ColumnsConfig {
883    fn default() -> Self {
884        Self {
885            split: Default::default(),
886            names: Default::default(),
887            max_columns: 6,
888        }
889    }
890}
891
892// ----------- Nucleo config helper
893#[derive(Debug, Clone, PartialEq)]
894pub struct NucleoMatcherConfig(pub nucleo::Config);
895
896impl Default for NucleoMatcherConfig {
897    fn default() -> Self {
898        Self(nucleo::Config::DEFAULT)
899    }
900}
901
902#[derive(Debug, Clone, Serialize, Deserialize)]
903#[serde(default)]
904#[derive(Default)]
905struct MatcherConfigHelper {
906    pub normalize: Option<bool>,
907    pub ignore_case: Option<bool>,
908    pub prefer_prefix: Option<bool>,
909}
910
911impl serde::Serialize for NucleoMatcherConfig {
912    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
913    where
914        S: serde::Serializer,
915    {
916        let helper = MatcherConfigHelper {
917            normalize: Some(self.0.normalize),
918            ignore_case: Some(self.0.ignore_case),
919            prefer_prefix: Some(self.0.prefer_prefix),
920        };
921        helper.serialize(serializer)
922    }
923}
924
925impl<'de> Deserialize<'de> for NucleoMatcherConfig {
926    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
927    where
928        D: serde::Deserializer<'de>,
929    {
930        let helper = MatcherConfigHelper::deserialize(deserializer)?;
931        let mut config = nucleo::Config::DEFAULT;
932
933        if let Some(norm) = helper.normalize {
934            config.normalize = norm;
935        }
936        if let Some(ic) = helper.ignore_case {
937            config.ignore_case = ic;
938        }
939        if let Some(pp) = helper.prefer_prefix {
940            config.prefer_prefix = pp;
941        }
942
943        Ok(NucleoMatcherConfig(config))
944    }
945}