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