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