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    pub sort_threshold: u32,
46    /// The name of the default column
47
48    /// TODO: Enable raw mode where non-matching items are also displayed in a dimmed color.
49    #[partial(alias = "r")]
50    pub raw: bool,
51    /// TODO: Track the current selection when the result list is updated.
52    pub track: bool,
53    /// Reverse the order of the input
54    pub reverse: bool, // TODO: test with sort_threshold
55}
56
57/// Configures how input is fed to to the worker(s).
58///
59#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(default, deny_unknown_fields)]
61#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
62pub struct StartConfig {
63    #[serde(deserialize_with = "escaped_opt_char")]
64    #[partial(alias = "is")]
65    pub input_separator: Option<char>,
66    #[serde(deserialize_with = "escaped_opt_string")]
67    #[partial(alias = "os")]
68    pub output_separator: Option<String>,
69
70    /// Format string to print accepted items as.
71    #[partial(alias = "ot")]
72    #[serde(alias = "output")]
73    pub output_template: Option<String>,
74
75    /// Default command to execute when stdin is not being read.
76    #[partial(alias = "cmd", alias = "x")]
77    pub command: String,
78    /// (cli only) Additional command which can be cycled through using Action::ReloadNext
79    #[partial(alias = "ax")]
80    pub additional_commands: Vec<String>,
81    pub sync: bool,
82
83    /// Whether to parse ansi sequences from input
84    #[partial(alias = "a")]
85    pub ansi: bool,
86    /// Trim the input
87    #[partial(alias = "t")]
88    pub trim: bool,
89}
90
91/// Exit conditions of the render loop.
92#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(default, deny_unknown_fields)]
94#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
95pub struct ExitConfig {
96    /// Exit automatically if there is only one match.
97    pub select_1: bool,
98    /// Allow returning without any items selected.
99    pub allow_empty: bool,
100    /// Abort if no items.
101    pub abort_empty: bool,
102    /// Last processed key is written here.
103    /// Set to an empty path to disable.
104    pub last_key_path: Option<std::path::PathBuf>,
105}
106
107/// The ui config.
108#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
109#[serde(default, deny_unknown_fields)]
110#[partial(recurse, path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
111pub struct RenderConfig {
112    /// The default overlay style
113    pub ui: UiConfig,
114    /// The input bar style
115    #[partial(alias = "i")]
116    pub input: InputConfig,
117    /// The results table style
118    #[partial(alias = "r")]
119    pub results: ResultsConfig,
120
121    /// The results status style
122    pub status: StatusConfig,
123    /// The preview panel style
124    #[partial(alias = "p")]
125    pub preview: PreviewConfig,
126    #[partial(alias = "f")]
127    pub footer: DisplayConfig,
128    #[partial(alias = "h")]
129    pub header: DisplayConfig,
130}
131
132impl RenderConfig {
133    pub fn tick_rate(&self) -> u8 {
134        self.ui.tick_rate
135    }
136}
137
138/// Terminal settings.
139#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141#[serde(default, deny_unknown_fields)]
142pub struct TerminalConfig {
143    pub stream: IoStream, // consumed
144    pub restore_fullscreen: bool,
145    pub redraw_on_resize: bool,
146    // https://docs.rs/crossterm/latest/crossterm/event/struct.PushKeyboardEnhancementFlags.html
147    pub extended_keys: bool,
148    #[serde(with = "serde_duration_ms")]
149    pub sleep_ms: std::time::Duration, // necessary to give ratatui a small delay before resizing after entering and exiting
150    #[serde(flatten)]
151    #[partial(recurse)]
152    pub layout: Option<TerminalLayoutSettings>, // None for fullscreen
153    pub clear_on_exit: bool,
154    // experimental: makes exits cleaner, but success get joined
155    pub move_up_on_exit: bool,
156}
157
158impl Default for TerminalConfig {
159    fn default() -> Self {
160        Self {
161            stream: IoStream::default(),
162            restore_fullscreen: true,
163            redraw_on_resize: bool::default(),
164            sleep_ms: std::time::Duration::default(),
165            layout: Option::default(),
166            extended_keys: true,
167            clear_on_exit: true,
168            move_up_on_exit: false,
169        }
170    }
171}
172
173/// The container ui.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175#[serde(default, deny_unknown_fields)]
176#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
177pub struct UiConfig {
178    #[partial(recurse)]
179    pub border: BorderSetting,
180    pub tick_rate: u8, // separate from render, but best place ig
181}
182
183impl Default for UiConfig {
184    fn default() -> Self {
185        Self {
186            border: Default::default(),
187            tick_rate: 60,
188        }
189    }
190}
191
192/// The query (input) bar ui.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194#[serde(default, deny_unknown_fields)]
195#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
196pub struct InputConfig {
197    #[partial(recurse)]
198    pub border: BorderSetting,
199
200    // text styles
201    #[serde(deserialize_with = "camelcase_normalized")]
202    pub fg: Color,
203    pub bg: Color,
204    // #[serde(deserialize_with = "transform_uppercase")]
205    pub modifier: Modifier,
206
207    #[serde(deserialize_with = "camelcase_normalized")]
208    pub prompt_fg: Color,
209    pub prompt_bg: Color,
210    // #[serde(deserialize_with = "transform_uppercase")]
211    pub prompt_modifier: Modifier,
212
213    /// The prompt prefix.
214    #[serde(deserialize_with = "deserialize_string_or_char_as_double_width")]
215    pub prompt: String,
216    /// Cursor style.
217    pub cursor: CursorSetting,
218
219    /// Initial text in the input bar.
220    #[partial(alias = "i")]
221    pub initial: String,
222
223    /// Maintain padding when moving the cursor in the bar.
224    pub scroll_padding: bool,
225}
226
227impl Default for InputConfig {
228    fn default() -> Self {
229        Self {
230            border: Default::default(),
231            fg: Default::default(),
232            bg: Default::default(),
233            modifier: Default::default(),
234            prompt_fg: Default::default(),
235            prompt_bg: Default::default(),
236            prompt_modifier: Default::default(),
237            prompt: "> ".to_string(),
238            cursor: Default::default(),
239            initial: Default::default(),
240
241            scroll_padding: true,
242        }
243    }
244}
245
246impl InputConfig {
247    pub fn text_style(&self) -> Style {
248        Style::default()
249            .fg(self.fg)
250            .bg(self.bg)
251            .remove_modifier(Modifier::all())
252            .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: 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: Default::default(),
415            inactive_bg: Default::default(),
416
417            inactive_current_fg: Color::DarkGray,
418            inactive_current_modifier: Default::default(),
419            inactive_current_bg: Color::Black,
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: RowConnectionStyle::Capped,
428
429            scroll_wrap: true,
430            scroll_padding: 2,
431            reverse: Default::default(),
432
433            wrap: Default::default(),
434            min_wrap_width: 4,
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: 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: 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: 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: 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, deny_unknown_fields)]
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    #[serde(alias = "scroll")]
577    #[partial(recurse)]
578    #[partial(alias = "i")]
579    pub initial: PreviewInitialSetting,
580    /// Whether to cycle to top after scrolling to the bottom and vice versa.
581    #[partial(alias = "c")]
582    #[serde(alias = "cycle")]
583    pub scroll_wrap: bool,
584    pub wrap: bool,
585    /// Whether to show the preview pane initially.
586    /// Can either be a boolean or a number which the relevant dimension of the available ui area must exceed.
587    pub show: ShowCondition,
588
589    pub reevaluate_show_on_resize: bool,
590}
591
592impl Default for PreviewConfig {
593    fn default() -> Self {
594        PreviewConfig {
595            border: BorderSetting {
596                padding: Padding(ratatui::widgets::Padding::left(2)),
597                ..Default::default()
598            },
599            initial: Default::default(),
600            layout: Default::default(),
601            scroll_wrap: true,
602            wrap: Default::default(),
603            show: Default::default(),
604            reevaluate_show_on_resize: false,
605        }
606    }
607}
608
609/// Determines the initial scroll offset of the preview window.
610#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
612#[serde(default, deny_unknown_fields)]
613pub struct PreviewInitialSetting {
614    /// Extract the initial display index `n` of the preview window from this column.
615    /// `n` lines are skipped after the header lines are consumed.
616    pub index: Option<StringValue>,
617    /// For adjusting the initial scroll index.
618    #[partial(alias = "o")]
619    pub offset: isize,
620    /// How far from the bottom of the preview window the scroll offset should appear.
621    #[partial(alias = "p")]
622    pub percentage: Percentage,
623    /// Keep the top N lines as the fixed header so that they are always visible.
624    #[partial(alias = "h")]
625    pub header_lines: usize,
626}
627
628impl Default for PreviewInitialSetting {
629    fn default() -> Self {
630        Self {
631            index: Default::default(),
632            offset: -1,
633            percentage: Default::default(),
634            header_lines: Default::default(),
635        }
636    }
637}
638
639#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
640#[serde(default, deny_unknown_fields)]
641pub struct PreviewerConfig {
642    pub try_lossy: bool,
643
644    // todo
645    pub cache: u8,
646
647    pub help_colors: HelpColorConfig,
648}
649
650/// Help coloring
651#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
652pub struct HelpColorConfig {
653    #[serde(deserialize_with = "camelcase_normalized")]
654    pub section: Color,
655    #[serde(deserialize_with = "camelcase_normalized")]
656    pub key: Color,
657    #[serde(deserialize_with = "camelcase_normalized")]
658    pub value: Color,
659}
660
661impl Default for HelpColorConfig {
662    fn default() -> Self {
663        Self {
664            section: Color::Blue,
665            key: Color::Green,
666            value: Color::White,
667        }
668    }
669}
670
671// ----------- SETTING TYPES -------------------------
672
673#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
674#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
675#[serde(default, deny_unknown_fields)]
676pub struct BorderSetting {
677    #[serde(deserialize_with = "camelcase_normalized_option")]
678    pub r#type: Option<BorderType>,
679    #[serde(deserialize_with = "camelcase_normalized")]
680    pub color: Color,
681    /// Given as sides joined by `|`. i.e.:
682    /// `sides = "TOP | BOTTOM"``
683    /// `sides = "ALL"`
684    /// 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.
685    ///
686    /// An empty string enforces no sides:
687    /// `sides = ""`
688    // #[serde(deserialize_with = "uppercase_normalized_option")] // need ratatui bitflags to use transparent
689    pub sides: Option<Borders>,
690    /// Supply as either 1, 2, or 4 numbers for:
691    ///
692    /// - Same padding on all sides
693    /// - Vertical and horizontal padding values
694    /// - Top, Right, Bottom, Left padding values
695    ///
696    /// respectively.
697    pub padding: Padding,
698    pub title: String,
699    // #[serde(deserialize_with = "transform_uppercase")]
700    pub title_modifier: Modifier,
701    pub modifier: Modifier,
702    #[serde(deserialize_with = "camelcase_normalized")]
703    pub bg: Color,
704}
705
706impl BorderSetting {
707    pub fn as_block(&self) -> ratatui::widgets::Block<'_> {
708        let mut ret = ratatui::widgets::Block::default()
709            .padding(self.padding.0)
710            .style(Style::default().bg(self.bg).add_modifier(self.modifier));
711
712        if !self.title.is_empty() {
713            let title = Span::styled(
714                &self.title,
715                Style::default().add_modifier(self.title_modifier),
716            );
717
718            ret = ret.title(title)
719        };
720
721        if !self.is_empty() {
722            ret = ret
723                .borders(self.sides())
724                .border_type(self.r#type.unwrap_or_default())
725                .border_style(ratatui::style::Style::default().fg(self.color))
726        }
727
728        ret
729    }
730
731    pub fn sides(&self) -> Borders {
732        if let Some(s) = self.sides {
733            s
734        } else if self.color != Default::default() || self.r#type != Default::default() {
735            Borders::ALL
736        } else {
737            Borders::NONE
738        }
739    }
740
741    pub fn as_static_block(&self) -> ratatui::widgets::Block<'static> {
742        let mut ret = ratatui::widgets::Block::default()
743            .padding(self.padding.0)
744            .style(Style::default().bg(self.bg).add_modifier(self.modifier));
745
746        if !self.title.is_empty() {
747            let title: Span<'static> = Span::styled(
748                self.title.clone(),
749                Style::default().add_modifier(self.title_modifier),
750            );
751
752            ret = ret.title(title)
753        };
754
755        if !self.is_empty() {
756            ret = ret
757                .borders(self.sides())
758                .border_type(self.r#type.unwrap_or_default())
759                .border_style(ratatui::style::Style::default().fg(self.color))
760        }
761
762        ret
763    }
764
765    pub fn is_empty(&self) -> bool {
766        self.sides() == Borders::NONE
767    }
768
769    pub fn height(&self) -> u16 {
770        let mut height = 0;
771        height += 2 * !self.is_empty() as u16;
772        height += self.padding.top + self.padding.bottom;
773        height += (!self.title.is_empty() as u16).saturating_sub(!self.is_empty() as u16);
774
775        height
776    }
777
778    pub fn width(&self) -> u16 {
779        let mut width = 0;
780        width += 2 * !self.is_empty() as u16;
781        width += self.padding.left + self.padding.right;
782
783        width
784    }
785
786    pub fn left(&self) -> u16 {
787        let mut width = 0;
788        width += !self.is_empty() as u16;
789        width += self.padding.left;
790
791        width
792    }
793
794    pub fn top(&self) -> u16 {
795        let mut height = 0;
796        height += !self.is_empty() as u16;
797        height += self.padding.top;
798        height += (!self.title.is_empty() as u16).saturating_sub(!self.is_empty() as u16);
799
800        height
801    }
802}
803
804// how to determine how many rows to allocate?
805#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
806#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
807pub struct TerminalLayoutSettings {
808    /// Percentage of total rows to occupy.
809    #[partial(alias = "p")]
810    pub percentage: Percentage,
811    pub min: u16,
812    pub max: u16, // 0 for terminal height cap
813}
814
815impl Default for TerminalLayoutSettings {
816    fn default() -> Self {
817        Self {
818            percentage: Percentage::new(50),
819            min: 10,
820            max: 120,
821        }
822    }
823}
824
825#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
826#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
827#[serde(default)]
828pub struct PreviewSetting {
829    #[serde(flatten)]
830    #[partial(recurse)]
831    pub layout: PreviewLayout,
832    #[partial(recurse)]
833    pub border: Option<BorderSetting>,
834    #[serde(default, alias = "cmd", alias = "x")]
835    pub command: String,
836}
837
838#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
839#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
840#[serde(default)]
841pub struct PreviewLayout {
842    pub side: Side,
843    /// Percentage of total rows/columns to occupy.
844    #[serde(alias = "p")]
845    // we need serde here since its specified inside the value but i don't think there's another case for it.
846    pub percentage: Percentage,
847    pub min: i16,
848    pub max: i16,
849}
850
851impl Default for PreviewLayout {
852    fn default() -> Self {
853        Self {
854            side: Side::Right,
855            percentage: Percentage::new(60),
856            min: 15,
857            max: 120,
858        }
859    }
860}
861
862use crate::utils::serde::bounded_usize;
863#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
864#[serde(default, deny_unknown_fields)]
865#[partial(path, derive(Debug, Clone, PartialEq, Deserialize, Serialize))]
866pub struct ColumnsConfig {
867    /// The strategy of how columns are parsed from input lines
868    #[partial(alias = "s")]
869    pub split: Split,
870    /// Column names
871    #[partial(alias = "n")]
872    pub names: Vec<ColumnSetting>,
873    /// Maximum number of columns to autogenerate when names is unspecified. Maximum of 16, minimum of 1.
874    #[serde(deserialize_with = "bounded_usize::<_, 1, {crate::MAX_SPLITS}>")]
875    #[partial(alias = "mc")]
876    max: usize,
877    #[partial(alias = "i")]
878    pub default: Option<StringValue>,
879    /// When autogenerating column names, start from 0 instead of 1.
880    pub names_from_zero: bool,
881}
882
883impl ColumnsConfig {
884    pub fn max_cols(&self) -> usize {
885        self.max.min(MAX_SPLITS).max(1)
886    }
887}
888
889impl Default for ColumnsConfig {
890    fn default() -> Self {
891        Self {
892            split: Default::default(),
893            names: Default::default(),
894            max: 6,
895            default: None,
896            names_from_zero: false,
897        }
898    }
899}
900
901// ----------- Nucleo config helper
902#[derive(Debug, Clone, PartialEq)]
903pub struct NucleoMatcherConfig(pub nucleo::Config);
904
905impl Default for NucleoMatcherConfig {
906    fn default() -> Self {
907        Self(nucleo::Config::DEFAULT)
908    }
909}
910
911#[derive(Debug, Clone, Serialize, Deserialize)]
912#[serde(default)]
913#[derive(Default)]
914struct MatcherConfigHelper {
915    pub normalize: Option<bool>,
916    pub ignore_case: Option<bool>,
917    pub prefer_prefix: Option<bool>,
918}
919
920impl serde::Serialize for NucleoMatcherConfig {
921    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
922    where
923        S: serde::Serializer,
924    {
925        let helper = MatcherConfigHelper {
926            normalize: Some(self.0.normalize),
927            ignore_case: Some(self.0.ignore_case),
928            prefer_prefix: Some(self.0.prefer_prefix),
929        };
930        helper.serialize(serializer)
931    }
932}
933
934impl<'de> Deserialize<'de> for NucleoMatcherConfig {
935    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
936    where
937        D: serde::Deserializer<'de>,
938    {
939        let helper = MatcherConfigHelper::deserialize(deserializer)?;
940        let mut config = nucleo::Config::DEFAULT;
941
942        if let Some(norm) = helper.normalize {
943            config.normalize = norm;
944        }
945        if let Some(ic) = helper.ignore_case {
946            config.ignore_case = ic;
947        }
948        if let Some(pp) = helper.prefer_prefix {
949            config.prefer_prefix = pp;
950        }
951
952        Ok(NucleoMatcherConfig(config))
953    }
954}