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