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