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