Skip to main content

matchmaker/
config.rs

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