Skip to main content

rich_click_rs/
config.rs

1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use rich_rs::r#box::{
5    ASCII, ASCII2, ASCII_DOUBLE_HEAD, DOUBLE, DOUBLE_EDGE, HEAVY, HEAVY_EDGE, HEAVY_HEAD,
6    HORIZONTALS, MARKDOWN, MINIMAL, MINIMAL_DOUBLE_HEAD, MINIMAL_HEAVY_HEAD, ROUNDED, SIMPLE,
7    SIMPLE_HEAD, SIMPLE_HEAVY, SQUARE, SQUARE_DOUBLE_HEAD,
8};
9use rich_rs::{AlignMethod, ColorSystem, PaddingDimensions, Style};
10
11use crate::theme::{apply_theme, ThemeError};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ColorSystemMode {
15    Auto,
16    Standard,
17    EightBit,
18    TrueColor,
19    Windows,
20    None,
21}
22
23impl Default for ColorSystemMode {
24    fn default() -> Self {
25        Self::Auto
26    }
27}
28
29impl ColorSystemMode {
30    pub fn to_color_system(self, current: Option<ColorSystem>) -> Option<ColorSystem> {
31        match self {
32            ColorSystemMode::Auto => current,
33            ColorSystemMode::Standard => Some(ColorSystem::Standard),
34            ColorSystemMode::EightBit => Some(ColorSystem::EightBit),
35            ColorSystemMode::TrueColor => Some(ColorSystem::TrueColor),
36            ColorSystemMode::Windows => Some(ColorSystem::Windows),
37            ColorSystemMode::None => None,
38        }
39    }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum TextMarkup {
44    Ansi,
45    Rich,
46    Markdown,
47    None,
48}
49
50impl Default for TextMarkup {
51    fn default() -> Self {
52        TextMarkup::Ansi
53    }
54}
55
56/// Panel configuration for help sections.
57#[derive(Debug, Clone)]
58pub struct PanelConfig {
59    pub box_type: rich_rs::r#box::Box,
60    pub border_style: Style,
61    pub title_style: Style,
62    pub panel_style: Style,
63    pub padding: PaddingDimensions,
64    pub align: AlignMethod,
65    pub expand: bool,
66}
67
68impl Default for PanelConfig {
69    fn default() -> Self {
70        Self {
71            box_type: ROUNDED,
72            border_style: parse_style("dim"),
73            title_style: Style::default(),
74            panel_style: Style::default(),
75            padding: PaddingDimensions::from((0, 1)),
76            align: AlignMethod::Left,
77            expand: true,
78        }
79    }
80}
81
82/// Table configuration for option/command tables.
83#[derive(Debug, Clone)]
84pub struct TableConfig {
85    pub show_lines: bool,
86    pub leading: usize,
87    pub pad_edge: bool,
88    pub padding: (usize, usize),
89    pub collapse_padding: bool,
90    pub expand: bool,
91    pub box_type: Option<rich_rs::r#box::Box>,
92    pub row_styles: Vec<Style>,
93    pub border_style: Style,
94}
95
96impl Default for TableConfig {
97    fn default() -> Self {
98        Self {
99            show_lines: false,
100            leading: 0,
101            pad_edge: false,
102            padding: (0, 1),
103            collapse_padding: false,
104            expand: false,
105            box_type: None,
106            row_styles: Vec::new(),
107            border_style: parse_style("dim"),
108        }
109    }
110}
111
112/// Rich help configuration (subset of rich-click options).
113#[derive(Debug, Clone)]
114pub struct RichHelpConfig {
115    pub theme: Option<String>,
116    pub enable_theme_env_var: bool,
117
118    pub width: Option<usize>,
119    pub max_width: Option<usize>,
120    pub color_system: ColorSystemMode,
121    pub force_terminal: Option<bool>,
122
123    pub style_option: Style,
124    pub style_option_negative: Option<Style>,
125    pub style_argument: Style,
126    pub style_command: Style,
127    pub style_command_aliases: Style,
128    pub style_switch: Style,
129    pub style_switch_negative: Option<Style>,
130    pub style_metavar: Style,
131    pub style_metavar_append: Style,
132    pub style_metavar_separator: Style,
133    pub style_range_append: Option<Style>,
134    pub style_header_text: Style,
135    pub style_epilog_text: Style,
136    pub style_footer_text: Style,
137    pub style_usage: Style,
138    pub style_usage_command: Style,
139    pub style_usage_separator: Style,
140    pub style_deprecated: Style,
141    pub style_helptext_first_line: Style,
142    pub style_helptext: Style,
143    pub style_helptext_aliases: Option<Style>,
144    pub style_option_help: Style,
145    pub style_command_help: Style,
146    pub style_option_default: Style,
147    pub style_option_envvar: Style,
148    pub style_required_short: Style,
149    pub style_required_long: Style,
150    pub style_options_panel_border: Style,
151    pub style_options_panel_box: Option<rich_rs::r#box::Box>,
152    pub style_options_panel_help_style: Style,
153    pub style_options_panel_title_style: Style,
154    pub style_options_panel_padding: PaddingDimensions,
155    pub style_options_panel_style: Style,
156    pub align_options_panel: AlignMethod,
157    pub style_options_table_show_lines: bool,
158    pub style_options_table_leading: usize,
159    pub style_options_table_pad_edge: bool,
160    pub style_options_table_padding: PaddingDimensions,
161    pub style_options_table_collapse_padding: bool,
162    pub style_options_table_expand: bool,
163    pub style_options_table_box: Option<rich_rs::r#box::Box>,
164    pub style_options_table_row_styles: Vec<Style>,
165    pub style_options_table_border_style: Style,
166    pub style_commands_panel_border: Style,
167    pub panel_inline_help_in_title: bool,
168    pub panel_inline_help_delimiter: String,
169    pub style_commands_panel_box: Option<rich_rs::r#box::Box>,
170    pub style_commands_panel_help_style: Style,
171    pub style_commands_panel_title_style: Style,
172    pub style_commands_panel_padding: PaddingDimensions,
173    pub style_commands_panel_style: Style,
174    pub align_commands_panel: AlignMethod,
175    pub style_commands_table_show_lines: bool,
176    pub style_commands_table_leading: usize,
177    pub style_commands_table_pad_edge: bool,
178    pub style_commands_table_padding: PaddingDimensions,
179    pub style_commands_table_collapse_padding: bool,
180    pub style_commands_table_expand: bool,
181    pub style_commands_table_box: Option<rich_rs::r#box::Box>,
182    pub style_commands_table_row_styles: Vec<Style>,
183    pub style_commands_table_border_style: Style,
184    pub style_commands_table_column_width_ratio: Option<(Option<usize>, Option<usize>)>,
185    pub style_errors_panel_border: Style,
186    pub style_errors_panel_box: Option<rich_rs::r#box::Box>,
187    pub align_errors_panel: AlignMethod,
188    pub style_errors_suggestion: Option<Style>,
189    pub style_errors_suggestion_command: Option<Style>,
190    pub style_padding_errors: Style,
191    pub style_aborted: Style,
192    pub style_padding_usage: Style,
193    pub style_padding_helptext: Style,
194    pub style_padding_epilog: Style,
195
196    pub panel_title_padding: usize,
197
198    pub options_table_column_types: Vec<String>,
199    pub commands_table_column_types: Vec<String>,
200    pub options_table_help_sections: Vec<String>,
201    pub commands_table_help_sections: Vec<String>,
202
203    pub header_text: Option<String>,
204    pub footer_text: Option<String>,
205    pub panel_title_string: String,
206    pub deprecated_string: String,
207    pub deprecated_with_reason_string: String,
208    pub default_string: String,
209    pub envvar_string: String,
210    pub required_short_string: String,
211    pub required_long_string: String,
212    pub range_string: String,
213    pub append_metavars_help_string: String,
214    pub append_range_help_string: String,
215    pub helptext_aliases_string: String,
216    pub arguments_panel_title: String,
217    pub options_panel_title: String,
218    pub commands_panel_title: String,
219    pub errors_panel_title: String,
220    pub delimiter_comma: String,
221    pub delimiter_slash: String,
222    pub errors_suggestion: Option<String>,
223    pub errors_epilogue: Option<String>,
224    pub aborted_text: String,
225    pub errors_show_param_source: bool,
226    pub errors_param_source_format: String,
227
228    pub padding_header_text: PaddingDimensions,
229    pub padding_usage: PaddingDimensions,
230    pub padding_helptext: PaddingDimensions,
231    pub padding_helptext_deprecated: PaddingDimensions,
232    pub padding_helptext_first_line: PaddingDimensions,
233    pub padding_epilog: PaddingDimensions,
234    pub padding_footer_text: PaddingDimensions,
235    pub padding_errors_panel: PaddingDimensions,
236    pub padding_errors_suggestion: PaddingDimensions,
237    pub padding_errors_epilogue: PaddingDimensions,
238
239    pub show_arguments: Option<bool>,
240    pub show_commands: Option<bool>,
241    pub show_metavars_column: Option<bool>,
242    pub commands_before_options: bool,
243    pub default_panels_first: bool,
244    pub append_metavars_help: Option<bool>,
245    pub group_arguments_options: bool,
246    pub option_envvar_first: Option<bool>,
247    pub text_markup: TextMarkup,
248    pub text_emojis: Option<bool>,
249    pub text_paragraph_linebreaks: Option<String>,
250    pub use_click_short_help: bool,
251    pub helptext_show_aliases: bool,
252    pub show_prompt: bool,
253    pub prompt_string: String,
254    pub prompt_confirm_string: String,
255    pub prompt_hidden_string: String,
256    pub prompt_confirm_hidden_string: String,
257    pub command_aliases: HashMap<String, Vec<String>>,
258    pub option_groups: Vec<GroupConfig>,
259    pub command_groups: Vec<GroupConfig>,
260
261    pub panel_options: PanelConfig,
262    pub panel_commands: PanelConfig,
263    pub panel_arguments: PanelConfig,
264    pub table_options: TableConfig,
265    pub table_commands: TableConfig,
266    pub table_arguments: TableConfig,
267}
268
269#[derive(Debug, Clone)]
270pub struct GroupConfig {
271    pub name: String,
272    pub items: Vec<String>,
273    pub help: Option<String>,
274    pub inline_help_in_title: Option<bool>,
275    pub title_style: Option<Style>,
276    pub help_style: Option<Style>,
277}
278
279impl Default for RichHelpConfig {
280    fn default() -> Self {
281        let mut cfg = Self {
282            theme: None,
283            enable_theme_env_var: true,
284            width: None,
285            max_width: None,
286            color_system: ColorSystemMode::Auto,
287            force_terminal: None,
288            style_option: Style::default(),
289            style_option_negative: None,
290            style_argument: Style::default(),
291            style_command: Style::default(),
292            style_command_aliases: Style::default(),
293            style_switch: Style::default(),
294            style_switch_negative: None,
295            style_metavar: Style::default(),
296            style_metavar_append: Style::default(),
297            style_metavar_separator: Style::default(),
298            style_range_append: None,
299            style_header_text: Style::default(),
300            style_epilog_text: Style::default(),
301            style_footer_text: Style::default(),
302            style_usage: Style::default(),
303            style_usage_command: Style::default(),
304            style_usage_separator: Style::default(),
305            style_deprecated: Style::default(),
306            style_helptext_first_line: Style::default(),
307            style_helptext: Style::default(),
308            style_helptext_aliases: None,
309            style_option_help: Style::default(),
310            style_command_help: Style::default(),
311            style_option_default: Style::default(),
312            style_option_envvar: Style::default(),
313            style_required_short: Style::default(),
314            style_required_long: Style::default(),
315            style_options_panel_border: Style::default(),
316            style_options_panel_box: None,
317            style_options_panel_help_style: Style::default(),
318            style_options_panel_title_style: Style::default(),
319            style_options_panel_padding: PaddingDimensions::from((0, 1)),
320            style_options_panel_style: Style::default(),
321            align_options_panel: AlignMethod::Left,
322            style_options_table_show_lines: false,
323            style_options_table_leading: 0,
324            style_options_table_pad_edge: false,
325            style_options_table_padding: PaddingDimensions::from((0, 1)),
326            style_options_table_collapse_padding: false,
327            style_options_table_expand: true,
328            style_options_table_box: None,
329            style_options_table_row_styles: Vec::new(),
330            style_options_table_border_style: Style::default(),
331            style_commands_panel_border: Style::default(),
332            panel_inline_help_in_title: false,
333            panel_inline_help_delimiter: " - ".to_string(),
334            style_commands_panel_box: None,
335            style_commands_panel_help_style: Style::default(),
336            style_commands_panel_title_style: Style::default(),
337            style_commands_panel_padding: PaddingDimensions::from((0, 1)),
338            style_commands_panel_style: Style::default(),
339            align_commands_panel: AlignMethod::Left,
340            style_commands_table_show_lines: false,
341            style_commands_table_leading: 0,
342            style_commands_table_pad_edge: false,
343            style_commands_table_padding: PaddingDimensions::from((0, 1)),
344            style_commands_table_collapse_padding: false,
345            style_commands_table_expand: true,
346            style_commands_table_box: None,
347            style_commands_table_row_styles: Vec::new(),
348            style_commands_table_border_style: Style::default(),
349            style_commands_table_column_width_ratio: None,
350            style_errors_panel_border: Style::default(),
351            style_errors_panel_box: None,
352            align_errors_panel: AlignMethod::Left,
353            style_errors_suggestion: None,
354            style_errors_suggestion_command: None,
355            style_padding_errors: Style::default(),
356            style_aborted: parse_style("red"),
357            style_padding_usage: Style::default(),
358            style_padding_helptext: Style::default(),
359            style_padding_epilog: Style::default(),
360            panel_title_padding: 1,
361            options_table_column_types: vec![
362                "required".to_string(),
363                "opt_long".to_string(),
364                "opt_short".to_string(),
365                "metavar".to_string(),
366                "help".to_string(),
367            ],
368            commands_table_column_types: vec![
369                "name".to_string(),
370                "aliases".to_string(),
371                "help".to_string(),
372            ],
373            options_table_help_sections: vec![
374                "help".to_string(),
375                "deprecated".to_string(),
376                "envvar".to_string(),
377                "default".to_string(),
378                "required".to_string(),
379            ],
380            commands_table_help_sections: vec!["help".to_string(), "deprecated".to_string()],
381            header_text: None,
382            footer_text: None,
383            panel_title_string: "{}".to_string(),
384            deprecated_string: "[deprecated]".to_string(),
385            deprecated_with_reason_string: "[deprecated: {}]".to_string(),
386            default_string: "[default: {}]".to_string(),
387            envvar_string: "[env var: {}]".to_string(),
388            required_short_string: "*".to_string(),
389            required_long_string: "[required]".to_string(),
390            range_string: "[{}]".to_string(),
391            append_metavars_help_string: "({})".to_string(),
392            append_range_help_string: "[range: {}]".to_string(),
393            helptext_aliases_string: "Aliases: {}".to_string(),
394            arguments_panel_title: "Arguments".to_string(),
395            options_panel_title: "Options".to_string(),
396            commands_panel_title: "Commands".to_string(),
397            errors_panel_title: "Error".to_string(),
398            delimiter_comma: ",".to_string(),
399            delimiter_slash: "/".to_string(),
400            errors_suggestion: None,
401            errors_epilogue: None,
402            aborted_text: "Aborted.".to_string(),
403            errors_show_param_source: false,
404            errors_param_source_format: " (from {})".to_string(),
405            padding_header_text: PaddingDimensions::from((1, 1, 0, 1)),
406            padding_usage: PaddingDimensions::from(1),
407            padding_helptext: PaddingDimensions::from((0, 1, 1, 1)),
408            padding_helptext_deprecated: PaddingDimensions::from(0),
409            padding_helptext_first_line: PaddingDimensions::from(0),
410            padding_epilog: PaddingDimensions::from(1),
411            padding_footer_text: PaddingDimensions::from(1),
412            padding_errors_panel: PaddingDimensions::from((0, 0, 1, 0)),
413            padding_errors_suggestion: PaddingDimensions::from((0, 1, 0, 1)),
414            padding_errors_epilogue: PaddingDimensions::from((0, 1, 1, 1)),
415            show_arguments: None,
416            show_commands: None,
417            show_metavars_column: None,
418            commands_before_options: false,
419            default_panels_first: false,
420            append_metavars_help: None,
421            group_arguments_options: false,
422            option_envvar_first: None,
423            text_markup: TextMarkup::Ansi,
424            text_emojis: None,
425            text_paragraph_linebreaks: None,
426            use_click_short_help: false,
427            helptext_show_aliases: false,
428            show_prompt: false,
429            prompt_string: "[prompt]".to_string(),
430            prompt_confirm_string: "[prompt (confirm)]".to_string(),
431            prompt_hidden_string: "[prompt (hidden)]".to_string(),
432            prompt_confirm_hidden_string: "[prompt (confirm, hidden)]".to_string(),
433            command_aliases: HashMap::new(),
434            option_groups: Vec::new(),
435            command_groups: Vec::new(),
436            panel_options: PanelConfig::default(),
437            panel_commands: PanelConfig::default(),
438            panel_arguments: PanelConfig::default(),
439            table_options: TableConfig::default(),
440            table_commands: TableConfig::default(),
441            table_arguments: TableConfig::default(),
442        };
443
444        let _ = cfg.apply_theme_name("default-box");
445        cfg
446    }
447}
448
449impl RichHelpConfig {
450    pub fn global() -> &'static Self {
451        static GLOBAL: OnceLock<RichHelpConfig> = OnceLock::new();
452        GLOBAL.get_or_init(|| RichHelpConfig::load_from_env())
453    }
454
455    pub fn builder() -> RichHelpConfigBuilder {
456        RichHelpConfigBuilder {
457            config: RichHelpConfig::global().clone(),
458        }
459    }
460
461    pub fn load_from_env() -> Self {
462        let mut cfg = RichHelpConfig::default();
463        if !cfg.enable_theme_env_var {
464            return cfg;
465        }
466        apply_terminal_width_env(&mut cfg);
467        apply_force_terminal_env(&mut cfg);
468        let env = std::env::var("RICH_CLICK_THEME").ok();
469        if let Some(value) = env {
470            if value.trim_start().starts_with('{') {
471                if let Ok(json) = serde_json::from_str::<serde_json::Value>(&value) {
472                    cfg.apply_json_overrides(&json);
473                }
474            } else {
475                cfg.theme = Some(value.trim().to_string());
476                let _ = cfg.apply_theme_name(value.trim());
477            }
478        }
479        cfg.sync_render_config();
480        cfg
481    }
482
483    pub fn apply_theme_name(&mut self, theme: &str) -> Result<(), ThemeError> {
484        let result = apply_theme(self, theme);
485        if result.is_ok() {
486            self.sync_render_config();
487        }
488        result
489    }
490
491    fn apply_json_overrides(&mut self, value: &serde_json::Value) {
492        if let Some(theme) = value.get("theme").and_then(|v| v.as_str()) {
493            self.theme = Some(theme.to_string());
494            let _ = self.apply_theme_name(theme);
495        }
496        if let Some(obj) = value.as_object() {
497            for (key, v) in obj {
498                if key == "theme" {
499                    continue;
500                }
501                self.apply_override(key, v);
502            }
503        }
504    }
505
506    fn apply_override(&mut self, key: &str, value: &serde_json::Value) {
507        match key {
508            "width" => self.width = value.as_u64().map(|v| v as usize),
509            "max_width" => self.max_width = value.as_u64().map(|v| v as usize),
510            "color_system" => {
511                if let Some(mode) = parse_color_system_mode(value) {
512                    self.color_system = mode;
513                }
514            }
515            "force_terminal" => self.force_terminal = value.as_bool(),
516            "panel_title_string" => {
517                if let Some(v) = value.as_str() {
518                    self.panel_title_string = v.to_string();
519                }
520            }
521            "deprecated_string" => {
522                if let Some(v) = value.as_str() {
523                    self.deprecated_string = v.to_string();
524                }
525            }
526            "deprecated_with_reason_string" => {
527                if let Some(v) = value.as_str() {
528                    self.deprecated_with_reason_string = v.to_string();
529                }
530            }
531            "default_string" => {
532                if let Some(v) = value.as_str() {
533                    self.default_string = v.to_string();
534                }
535            }
536            "envvar_string" => {
537                if let Some(v) = value.as_str() {
538                    self.envvar_string = v.to_string();
539                }
540            }
541            "required_short_string" => {
542                if let Some(v) = value.as_str() {
543                    self.required_short_string = v.to_string();
544                }
545            }
546            "required_long_string" => {
547                if let Some(v) = value.as_str() {
548                    self.required_long_string = v.to_string();
549                }
550            }
551            "range_string" => {
552                if let Some(v) = value.as_str() {
553                    self.range_string = v.to_string();
554                }
555            }
556            "append_metavars_help_string" => {
557                if let Some(v) = value.as_str() {
558                    self.append_metavars_help_string = v.to_string();
559                }
560            }
561            "append_range_help_string" => {
562                if let Some(v) = value.as_str() {
563                    self.append_range_help_string = v.to_string();
564                }
565            }
566            "delimiter_comma" => {
567                if let Some(v) = value.as_str() {
568                    self.delimiter_comma = v.to_string();
569                }
570            }
571            "delimiter_slash" => {
572                if let Some(v) = value.as_str() {
573                    self.delimiter_slash = v.to_string();
574                }
575            }
576            "options_panel_title" => {
577                if let Some(v) = value.as_str() {
578                    self.options_panel_title = v.to_string();
579                }
580            }
581            "commands_panel_title" => {
582                if let Some(v) = value.as_str() {
583                    self.commands_panel_title = v.to_string();
584                }
585            }
586            "arguments_panel_title" => {
587                if let Some(v) = value.as_str() {
588                    self.arguments_panel_title = v.to_string();
589                }
590            }
591            "errors_panel_title" => {
592                if let Some(v) = value.as_str() {
593                    self.errors_panel_title = v.to_string();
594                }
595            }
596            "aborted_text" => {
597                if let Some(v) = value.as_str() {
598                    self.aborted_text = v.to_string();
599                }
600            }
601            "errors_show_param_source" => {
602                if let Some(v) = value.as_bool() {
603                    self.errors_show_param_source = v;
604                }
605            }
606            "errors_param_source_format" => {
607                if let Some(v) = value.as_str() {
608                    self.errors_param_source_format = v.to_string();
609                }
610            }
611            "show_prompt" => {
612                if let Some(v) = value.as_bool() {
613                    self.show_prompt = v;
614                }
615            }
616            "prompt_string" => {
617                if let Some(v) = value.as_str() {
618                    self.prompt_string = v.to_string();
619                }
620            }
621            "prompt_confirm_string" => {
622                if let Some(v) = value.as_str() {
623                    self.prompt_confirm_string = v.to_string();
624                }
625            }
626            "prompt_hidden_string" => {
627                if let Some(v) = value.as_str() {
628                    self.prompt_hidden_string = v.to_string();
629                }
630            }
631            "prompt_confirm_hidden_string" => {
632                if let Some(v) = value.as_str() {
633                    self.prompt_confirm_hidden_string = v.to_string();
634                }
635            }
636            "style_option" => {
637                if let Some(v) = value.as_str() {
638                    self.style_option = parse_style(v);
639                }
640            }
641            "style_option_negative" => self.style_option_negative = parse_optional_style(value),
642            "style_argument" => {
643                if let Some(v) = value.as_str() {
644                    self.style_argument = parse_style(v);
645                }
646            }
647            "style_command" => {
648                if let Some(v) = value.as_str() {
649                    self.style_command = parse_style(v);
650                }
651            }
652            "style_command_aliases" => {
653                if let Some(v) = value.as_str() {
654                    self.style_command_aliases = parse_style(v);
655                }
656            }
657            "style_switch" => {
658                if let Some(v) = value.as_str() {
659                    self.style_switch = parse_style(v);
660                }
661            }
662            "style_switch_negative" => self.style_switch_negative = parse_optional_style(value),
663            "style_metavar" => {
664                if let Some(v) = value.as_str() {
665                    self.style_metavar = parse_style(v);
666                }
667            }
668            "style_metavar_append" => {
669                if let Some(v) = value.as_str() {
670                    self.style_metavar_append = parse_style(v);
671                }
672            }
673            "style_metavar_separator" => {
674                if let Some(v) = value.as_str() {
675                    self.style_metavar_separator = parse_style(v);
676                }
677            }
678            "style_range_append" => self.style_range_append = parse_optional_style(value),
679            "style_header_text" => {
680                if let Some(v) = value.as_str() {
681                    self.style_header_text = parse_style(v);
682                }
683            }
684            "style_epilog_text" => {
685                if let Some(v) = value.as_str() {
686                    self.style_epilog_text = parse_style(v);
687                }
688            }
689            "style_footer_text" => {
690                if let Some(v) = value.as_str() {
691                    self.style_footer_text = parse_style(v);
692                }
693            }
694            "style_usage" => {
695                if let Some(v) = value.as_str() {
696                    self.style_usage = parse_style(v);
697                }
698            }
699            "style_usage_command" => {
700                if let Some(v) = value.as_str() {
701                    self.style_usage_command = parse_style(v);
702                }
703            }
704            "style_usage_separator" => {
705                if let Some(v) = value.as_str() {
706                    self.style_usage_separator = parse_style(v);
707                }
708            }
709            "style_deprecated" => {
710                if let Some(v) = value.as_str() {
711                    self.style_deprecated = parse_style(v);
712                }
713            }
714            "style_helptext_first_line" => {
715                if let Some(v) = value.as_str() {
716                    self.style_helptext_first_line = parse_style(v);
717                }
718            }
719            "style_helptext" => {
720                if let Some(v) = value.as_str() {
721                    self.style_helptext = parse_style(v);
722                }
723            }
724            "style_helptext_aliases" => self.style_helptext_aliases = parse_optional_style(value),
725            "style_option_help" => {
726                if let Some(v) = value.as_str() {
727                    self.style_option_help = parse_style(v);
728                }
729            }
730            "style_command_help" => {
731                if let Some(v) = value.as_str() {
732                    self.style_command_help = parse_style(v);
733                }
734            }
735            "style_option_default" => {
736                if let Some(v) = value.as_str() {
737                    self.style_option_default = parse_style(v);
738                }
739            }
740            "style_option_envvar" => {
741                if let Some(v) = value.as_str() {
742                    self.style_option_envvar = parse_style(v);
743                }
744            }
745            "style_required_short" => {
746                if let Some(v) = value.as_str() {
747                    self.style_required_short = parse_style(v);
748                }
749            }
750            "style_required_long" => {
751                if let Some(v) = value.as_str() {
752                    self.style_required_long = parse_style(v);
753                }
754            }
755            "style_options_panel_border" => {
756                if let Some(v) = value.as_str() {
757                    self.style_options_panel_border = parse_style(v);
758                }
759            }
760            "style_commands_panel_border" => {
761                if let Some(v) = value.as_str() {
762                    self.style_commands_panel_border = parse_style(v);
763                }
764            }
765            "style_errors_panel_border" => {
766                if let Some(v) = value.as_str() {
767                    self.style_errors_panel_border = parse_style(v);
768                }
769            }
770            "style_options_panel_box" => self.style_options_panel_box = parse_box_value(value),
771            "style_commands_panel_box" => self.style_commands_panel_box = parse_box_value(value),
772            "style_errors_panel_box" => self.style_errors_panel_box = parse_box_value(value),
773            "style_options_panel_help_style" => {
774                if let Some(v) = value.as_str() {
775                    self.style_options_panel_help_style = parse_style(v);
776                }
777            }
778            "style_commands_panel_help_style" => {
779                if let Some(v) = value.as_str() {
780                    self.style_commands_panel_help_style = parse_style(v);
781                }
782            }
783            "style_options_panel_title_style" => {
784                if let Some(v) = value.as_str() {
785                    self.style_options_panel_title_style = parse_style(v);
786                }
787            }
788            "style_commands_panel_title_style" => {
789                if let Some(v) = value.as_str() {
790                    self.style_commands_panel_title_style = parse_style(v);
791                }
792            }
793            "style_options_panel_style" => {
794                if let Some(v) = value.as_str() {
795                    self.style_options_panel_style = parse_style(v);
796                }
797            }
798            "style_commands_panel_style" => {
799                if let Some(v) = value.as_str() {
800                    self.style_commands_panel_style = parse_style(v);
801                }
802            }
803            "style_padding_usage" => {
804                if let Some(v) = value.as_str() {
805                    self.style_padding_usage = parse_style(v);
806                }
807            }
808            "style_padding_helptext" => {
809                if let Some(v) = value.as_str() {
810                    self.style_padding_helptext = parse_style(v);
811                }
812            }
813            "style_padding_epilog" => {
814                if let Some(v) = value.as_str() {
815                    self.style_padding_epilog = parse_style(v);
816                }
817            }
818            "style_padding_errors" => {
819                if let Some(v) = value.as_str() {
820                    self.style_padding_errors = parse_style(v);
821                }
822            }
823            "style_aborted" => {
824                if let Some(v) = value.as_str() {
825                    self.style_aborted = parse_style(v);
826                }
827            }
828            "style_options_table_border_style" => {
829                if let Some(v) = value.as_str() {
830                    self.style_options_table_border_style = parse_style(v);
831                }
832            }
833            "style_commands_table_border_style" => {
834                if let Some(v) = value.as_str() {
835                    self.style_commands_table_border_style = parse_style(v);
836                }
837            }
838            "style_commands_table_column_width_ratio" => {
839                if let Some(v) = parse_ratio_pair(value) {
840                    self.style_commands_table_column_width_ratio = Some(v);
841                }
842            }
843            "style_options_table_show_lines" => {
844                if let Some(v) = value.as_bool() {
845                    self.style_options_table_show_lines = v;
846                }
847            }
848            "style_commands_table_show_lines" => {
849                if let Some(v) = value.as_bool() {
850                    self.style_commands_table_show_lines = v;
851                }
852            }
853            "style_options_table_leading" => {
854                if let Some(v) = value.as_u64() {
855                    self.style_options_table_leading = v as usize;
856                }
857            }
858            "style_commands_table_leading" => {
859                if let Some(v) = value.as_u64() {
860                    self.style_commands_table_leading = v as usize;
861                }
862            }
863            "style_options_table_pad_edge" => {
864                if let Some(v) = value.as_bool() {
865                    self.style_options_table_pad_edge = v;
866                }
867            }
868            "style_commands_table_pad_edge" => {
869                if let Some(v) = value.as_bool() {
870                    self.style_commands_table_pad_edge = v;
871                }
872            }
873            "style_options_table_collapse_padding" => {
874                if let Some(v) = value.as_bool() {
875                    self.style_options_table_collapse_padding = v;
876                }
877            }
878            "style_commands_table_collapse_padding" => {
879                if let Some(v) = value.as_bool() {
880                    self.style_commands_table_collapse_padding = v;
881                }
882            }
883            "style_options_table_expand" => {
884                if let Some(v) = value.as_bool() {
885                    self.style_options_table_expand = v;
886                }
887            }
888            "style_commands_table_expand" => {
889                if let Some(v) = value.as_bool() {
890                    self.style_commands_table_expand = v;
891                }
892            }
893            "style_options_table_box" => self.style_options_table_box = parse_box_value(value),
894            "style_commands_table_box" => self.style_commands_table_box = parse_box_value(value),
895            "style_options_table_row_styles" => {
896                if let Some(v) = parse_style_list(value) {
897                    self.style_options_table_row_styles = v;
898                }
899            }
900            "style_commands_table_row_styles" => {
901                if let Some(v) = parse_style_list(value) {
902                    self.style_commands_table_row_styles = v;
903                }
904            }
905            "style_options_panel_padding" => {
906                if let Some(v) = parse_padding_value(value) {
907                    self.style_options_panel_padding = v;
908                }
909            }
910            "style_commands_panel_padding" => {
911                if let Some(v) = parse_padding_value(value) {
912                    self.style_commands_panel_padding = v;
913                }
914            }
915            "style_options_table_padding" => {
916                if let Some(v) = parse_padding_value(value) {
917                    self.style_options_table_padding = v;
918                }
919            }
920            "style_commands_table_padding" => {
921                if let Some(v) = parse_padding_value(value) {
922                    self.style_commands_table_padding = v;
923                }
924            }
925            "panel_title_padding" => {
926                if let Some(v) = value.as_u64() {
927                    self.panel_title_padding = v as usize;
928                }
929            }
930            "align_options_panel" => {
931                if let Some(v) = parse_align(value) {
932                    self.align_options_panel = v;
933                }
934            }
935            "align_commands_panel" => {
936                if let Some(v) = parse_align(value) {
937                    self.align_commands_panel = v;
938                }
939            }
940            "align_errors_panel" => {
941                if let Some(v) = parse_align(value) {
942                    self.align_errors_panel = v;
943                }
944            }
945            "panel_inline_help_in_title" => {
946                if let Some(v) = value.as_bool() {
947                    self.panel_inline_help_in_title = v;
948                }
949            }
950            "panel_inline_help_delimiter" => {
951                if let Some(v) = value.as_str() {
952                    self.panel_inline_help_delimiter = v.to_string();
953                }
954            }
955            "options_table_column_types" => {
956                if let Some(v) = parse_string_list(value) {
957                    self.options_table_column_types = v;
958                }
959            }
960            "commands_table_column_types" => {
961                if let Some(v) = parse_string_list(value) {
962                    self.commands_table_column_types = v;
963                }
964            }
965            "options_table_help_sections" => {
966                if let Some(v) = parse_string_list(value) {
967                    self.options_table_help_sections = v;
968                }
969            }
970            "commands_table_help_sections" => {
971                if let Some(v) = parse_string_list(value) {
972                    self.commands_table_help_sections = v;
973                }
974            }
975            "show_arguments" => self.show_arguments = value.as_bool(),
976            "show_commands" => self.show_commands = value.as_bool(),
977            "show_metavars_column" => self.show_metavars_column = value.as_bool(),
978            "commands_before_options" => {
979                if let Some(v) = value.as_bool() {
980                    self.commands_before_options = v;
981                }
982            }
983            "default_panels_first" => {
984                if let Some(v) = value.as_bool() {
985                    self.default_panels_first = v;
986                }
987            }
988            "append_metavars_help" => self.append_metavars_help = value.as_bool(),
989            "group_arguments_options" => {
990                if let Some(v) = value.as_bool() {
991                    self.group_arguments_options = v;
992                }
993            }
994            "option_envvar_first" => self.option_envvar_first = value.as_bool(),
995            "text_markup" => {
996                if let Some(v) = parse_text_markup(value) {
997                    self.text_markup = v;
998                }
999            }
1000            "text_emojis" => self.text_emojis = value.as_bool(),
1001            "text_paragraph_linebreaks" => {
1002                if let Some(v) = value.as_str() {
1003                    self.text_paragraph_linebreaks = Some(v.to_string());
1004                }
1005            }
1006            "use_click_short_help" => {
1007                if let Some(v) = value.as_bool() {
1008                    self.use_click_short_help = v;
1009                }
1010            }
1011            "helptext_show_aliases" => {
1012                if let Some(v) = value.as_bool() {
1013                    self.helptext_show_aliases = v;
1014                }
1015            }
1016            "command_aliases" => {
1017                if let Some(v) = parse_alias_map(value) {
1018                    self.command_aliases = v;
1019                }
1020            }
1021            "option_groups" => {
1022                if let Some(v) = parse_group_list(value, "options") {
1023                    self.option_groups = v;
1024                }
1025            }
1026            "command_groups" => {
1027                if let Some(v) = parse_group_list(value, "commands") {
1028                    self.command_groups = v;
1029                }
1030            }
1031            "padding_header_text" => {
1032                if let Some(v) = parse_padding_value(value) {
1033                    self.padding_header_text = v;
1034                }
1035            }
1036            "padding_usage" => {
1037                if let Some(v) = parse_padding_value(value) {
1038                    self.padding_usage = v;
1039                }
1040            }
1041            "padding_helptext" => {
1042                if let Some(v) = parse_padding_value(value) {
1043                    self.padding_helptext = v;
1044                }
1045            }
1046            "padding_helptext_deprecated" => {
1047                if let Some(v) = parse_padding_value(value) {
1048                    self.padding_helptext_deprecated = v;
1049                }
1050            }
1051            "padding_helptext_first_line" => {
1052                if let Some(v) = parse_padding_value(value) {
1053                    self.padding_helptext_first_line = v;
1054                }
1055            }
1056            "padding_epilog" => {
1057                if let Some(v) = parse_padding_value(value) {
1058                    self.padding_epilog = v;
1059                }
1060            }
1061            "padding_footer_text" => {
1062                if let Some(v) = parse_padding_value(value) {
1063                    self.padding_footer_text = v;
1064                }
1065            }
1066            "padding_errors_panel" => {
1067                if let Some(v) = parse_padding_value(value) {
1068                    self.padding_errors_panel = v;
1069                }
1070            }
1071            "padding_errors_suggestion" => {
1072                if let Some(v) = parse_padding_value(value) {
1073                    self.padding_errors_suggestion = v;
1074                }
1075            }
1076            "padding_errors_epilogue" => {
1077                if let Some(v) = parse_padding_value(value) {
1078                    self.padding_errors_epilogue = v;
1079                }
1080            }
1081            _ => {}
1082        }
1083    }
1084
1085    fn sync_render_config(&mut self) {
1086        if self.text_emojis.is_none() {
1087            self.text_emojis = Some(matches!(
1088                self.text_markup,
1089                TextMarkup::Markdown | TextMarkup::Rich
1090            ));
1091        }
1092
1093        self.panel_options.box_type = self.style_options_panel_box.unwrap_or(ROUNDED);
1094        self.panel_options.border_style = self.style_options_panel_border;
1095        self.panel_options.title_style = self.style_options_panel_title_style;
1096        self.panel_options.panel_style = self.style_options_panel_style;
1097        self.panel_options.padding = self.style_options_panel_padding;
1098        self.panel_options.align = self.align_options_panel;
1099        self.panel_options.expand = true;
1100
1101        self.panel_commands.box_type = self.style_commands_panel_box.unwrap_or(ROUNDED);
1102        self.panel_commands.border_style = self.style_commands_panel_border;
1103        self.panel_commands.title_style = self.style_commands_panel_title_style;
1104        self.panel_commands.panel_style = self.style_commands_panel_style;
1105        self.panel_commands.padding = self.style_commands_panel_padding;
1106        self.panel_commands.align = self.align_commands_panel;
1107        self.panel_commands.expand = true;
1108
1109        self.panel_arguments = self.panel_options.clone();
1110
1111        let padding = self.style_options_table_padding.unpack();
1112        self.table_options.show_lines = self.style_options_table_show_lines;
1113        self.table_options.leading = self.style_options_table_leading;
1114        self.table_options.pad_edge = self.style_options_table_pad_edge;
1115        self.table_options.padding = (padding.3, padding.1);
1116        self.table_options.collapse_padding = self.style_options_table_collapse_padding;
1117        self.table_options.expand = self.style_options_table_expand;
1118        self.table_options.box_type = self.style_options_table_box;
1119        self.table_options.border_style = self.style_options_table_border_style;
1120
1121        let padding = self.style_commands_table_padding.unpack();
1122        self.table_commands.show_lines = self.style_commands_table_show_lines;
1123        self.table_commands.leading = self.style_commands_table_leading;
1124        self.table_commands.pad_edge = self.style_commands_table_pad_edge;
1125        self.table_commands.padding = (padding.3, padding.1);
1126        self.table_commands.collapse_padding = self.style_commands_table_collapse_padding;
1127        self.table_commands.expand = self.style_commands_table_expand;
1128        self.table_commands.box_type = self.style_commands_table_box;
1129        self.table_commands.border_style = self.style_commands_table_border_style;
1130
1131        self.table_arguments = self.table_options.clone();
1132    }
1133}
1134
1135fn apply_terminal_width_env(cfg: &mut RichHelpConfig) {
1136    let width = std::env::var("TERMINAL_WIDTH").ok();
1137    if let Some(value) = width {
1138        if let Ok(parsed) = value.trim().parse::<usize>() {
1139            if cfg.width.is_none() {
1140                cfg.width = Some(parsed);
1141            }
1142            if cfg.max_width.is_none() {
1143                cfg.max_width = Some(parsed);
1144            }
1145        }
1146    }
1147}
1148
1149fn apply_force_terminal_env(cfg: &mut RichHelpConfig) {
1150    if cfg.force_terminal.is_some() {
1151        return;
1152    }
1153    for key in ["FORCE_COLOR", "PY_COLORS", "GITHUB_ACTIONS"] {
1154        if let Ok(value) = std::env::var(key) {
1155            if let Some(parsed) = parse_truthy(&value) {
1156                cfg.force_terminal = Some(parsed);
1157                return;
1158            }
1159        }
1160    }
1161}
1162
1163fn parse_truthy(value: &str) -> Option<bool> {
1164    match value.trim().to_ascii_lowercase().as_str() {
1165        "1" | "true" | "yes" | "on" => Some(true),
1166        "0" | "false" | "no" | "off" => Some(false),
1167        _ => None,
1168    }
1169}
1170
1171pub struct RichHelpConfigBuilder {
1172    config: RichHelpConfig,
1173}
1174
1175impl RichHelpConfigBuilder {
1176    pub fn build(self) -> RichHelpConfig {
1177        let mut cfg = self.config;
1178        cfg.sync_render_config();
1179        cfg
1180    }
1181
1182    pub fn theme(mut self, theme: &str) -> Self {
1183        self.config.theme = Some(theme.to_string());
1184        let _ = self.config.apply_theme_name(theme);
1185        self
1186    }
1187
1188    pub fn width(mut self, width: usize) -> Self {
1189        self.config.width = Some(width);
1190        self
1191    }
1192
1193    pub fn max_width(mut self, width: usize) -> Self {
1194        self.config.max_width = Some(width);
1195        self
1196    }
1197
1198    pub fn color_system(mut self, mode: ColorSystemMode) -> Self {
1199        self.config.color_system = mode;
1200        self
1201    }
1202
1203    pub fn force_terminal(mut self, force: bool) -> Self {
1204        self.config.force_terminal = Some(force);
1205        self
1206    }
1207
1208    pub fn show_arguments(mut self, show: bool) -> Self {
1209        self.config.show_arguments = Some(show);
1210        self
1211    }
1212
1213    pub fn text_markup(mut self, markup: TextMarkup) -> Self {
1214        self.config.text_markup = markup;
1215        self
1216    }
1217
1218    pub fn text_emojis(mut self, enabled: bool) -> Self {
1219        self.config.text_emojis = Some(enabled);
1220        self
1221    }
1222
1223    pub fn text_paragraph_linebreaks(mut self, value: &str) -> Self {
1224        self.config.text_paragraph_linebreaks = Some(value.to_string());
1225        self
1226    }
1227
1228    pub fn commands_before_options(mut self, value: bool) -> Self {
1229        self.config.commands_before_options = value;
1230        self
1231    }
1232}
1233
1234fn parse_style(input: &str) -> Style {
1235    Style::parse(input).unwrap_or_default()
1236}
1237
1238fn parse_optional_style(value: &serde_json::Value) -> Option<Style> {
1239    if value.is_null() {
1240        return None;
1241    }
1242    value.as_str().map(parse_style)
1243}
1244
1245fn parse_style_list(value: &serde_json::Value) -> Option<Vec<Style>> {
1246    let list = value.as_array()?;
1247    let mut styles = Vec::with_capacity(list.len());
1248    for item in list {
1249        if let Some(s) = item.as_str() {
1250            styles.push(parse_style(s));
1251        }
1252    }
1253    Some(styles)
1254}
1255
1256fn parse_string_list(value: &serde_json::Value) -> Option<Vec<String>> {
1257    let list = value.as_array()?;
1258    let mut out = Vec::with_capacity(list.len());
1259    for item in list {
1260        if let Some(s) = item.as_str() {
1261            out.push(s.to_string());
1262        }
1263    }
1264    Some(out)
1265}
1266
1267fn parse_alias_map(value: &serde_json::Value) -> Option<HashMap<String, Vec<String>>> {
1268    let obj = value.as_object()?;
1269    let mut map = HashMap::new();
1270    for (key, val) in obj {
1271        if let Some(list) = parse_string_list(val) {
1272            map.insert(key.to_string(), list);
1273        }
1274    }
1275    Some(map)
1276}
1277
1278fn parse_group_list(value: &serde_json::Value, key: &str) -> Option<Vec<GroupConfig>> {
1279    let list = value.as_array()?;
1280    let mut groups = Vec::new();
1281    for item in list {
1282        let obj = item.as_object()?;
1283        let name = obj
1284            .get("name")
1285            .and_then(|v| v.as_str())
1286            .unwrap_or("Group")
1287            .to_string();
1288        let items = obj
1289            .get(key)
1290            .and_then(parse_string_list)
1291            .or_else(|| obj.get("items").and_then(parse_string_list))
1292            .unwrap_or_default();
1293        let help = obj
1294            .get("help")
1295            .and_then(|v| v.as_str())
1296            .map(|v| v.to_string());
1297        let inline_help_in_title = obj.get("inline_help_in_title").and_then(|v| v.as_bool());
1298        let title_style = obj
1299            .get("title_style")
1300            .and_then(|v| v.as_str())
1301            .map(parse_style);
1302        let help_style = obj
1303            .get("help_style")
1304            .and_then(|v| v.as_str())
1305            .map(parse_style);
1306        groups.push(GroupConfig {
1307            name,
1308            items,
1309            help,
1310            inline_help_in_title,
1311            title_style,
1312            help_style,
1313        });
1314    }
1315    Some(groups)
1316}
1317
1318fn parse_padding_value(value: &serde_json::Value) -> Option<PaddingDimensions> {
1319    if let Some(v) = value.as_u64() {
1320        return Some(PaddingDimensions::from(v as usize));
1321    }
1322    let list = value.as_array()?;
1323    match list.len() {
1324        1 => list[0]
1325            .as_u64()
1326            .map(|v| PaddingDimensions::from(v as usize)),
1327        2 => {
1328            let v0 = list[0].as_u64()?;
1329            let v1 = list[1].as_u64()?;
1330            Some(PaddingDimensions::from((v0 as usize, v1 as usize)))
1331        }
1332        4 => {
1333            let v0 = list[0].as_u64()?;
1334            let v1 = list[1].as_u64()?;
1335            let v2 = list[2].as_u64()?;
1336            let v3 = list[3].as_u64()?;
1337            Some(PaddingDimensions::from((
1338                v0 as usize,
1339                v1 as usize,
1340                v2 as usize,
1341                v3 as usize,
1342            )))
1343        }
1344        _ => None,
1345    }
1346}
1347
1348fn parse_ratio_pair(value: &serde_json::Value) -> Option<(Option<usize>, Option<usize>)> {
1349    let list = value.as_array()?;
1350    if list.len() != 2 {
1351        return None;
1352    }
1353    let left = list[0].as_u64().map(|v| v as usize);
1354    let right = list[1].as_u64().map(|v| v as usize);
1355    Some((left, right))
1356}
1357
1358fn parse_box_value(value: &serde_json::Value) -> Option<rich_rs::r#box::Box> {
1359    if value.is_null() {
1360        return None;
1361    }
1362    let name = value.as_str()?.to_ascii_uppercase();
1363    if name == "BLANK" || name == "NONE" {
1364        return None;
1365    }
1366    Some(match name.as_str() {
1367        "ASCII" => ASCII,
1368        "ASCII2" => ASCII2,
1369        "ASCII_DOUBLE_HEAD" => ASCII_DOUBLE_HEAD,
1370        "SQUARE" => SQUARE,
1371        "SQUARE_DOUBLE_HEAD" => SQUARE_DOUBLE_HEAD,
1372        "MINIMAL" => MINIMAL,
1373        "MINIMAL_HEAVY_HEAD" => MINIMAL_HEAVY_HEAD,
1374        "MINIMAL_DOUBLE_HEAD" => MINIMAL_DOUBLE_HEAD,
1375        "SIMPLE" => SIMPLE,
1376        "SIMPLE_HEAD" => SIMPLE_HEAD,
1377        "SIMPLE_HEAVY" => SIMPLE_HEAVY,
1378        "HORIZONTALS" => HORIZONTALS,
1379        "HORIZONTALS_DOUBLE_TOP" => HORIZONTALS,
1380        "ROUNDED" => ROUNDED,
1381        "HEAVY" => HEAVY,
1382        "HEAVY_EDGE" => HEAVY_EDGE,
1383        "HEAVY_HEAD" => HEAVY_HEAD,
1384        "DOUBLE" => DOUBLE,
1385        "DOUBLE_EDGE" => DOUBLE_EDGE,
1386        "MARKDOWN" => MARKDOWN,
1387        _ => return None,
1388    })
1389}
1390
1391fn parse_align(value: &serde_json::Value) -> Option<AlignMethod> {
1392    match value.as_str()? {
1393        "left" | "Left" | "LEFT" => Some(AlignMethod::Left),
1394        "center" | "Center" | "CENTER" => Some(AlignMethod::Center),
1395        "right" | "Right" | "RIGHT" => Some(AlignMethod::Right),
1396        _ => None,
1397    }
1398}
1399
1400fn parse_color_system_mode(value: &serde_json::Value) -> Option<ColorSystemMode> {
1401    match value.as_str()? {
1402        "auto" => Some(ColorSystemMode::Auto),
1403        "standard" => Some(ColorSystemMode::Standard),
1404        "256" => Some(ColorSystemMode::EightBit),
1405        "truecolor" => Some(ColorSystemMode::TrueColor),
1406        "windows" => Some(ColorSystemMode::Windows),
1407        "none" => Some(ColorSystemMode::None),
1408        _ => None,
1409    }
1410}
1411
1412fn parse_text_markup(value: &serde_json::Value) -> Option<TextMarkup> {
1413    match value.as_str()? {
1414        "ansi" => Some(TextMarkup::Ansi),
1415        "rich" => Some(TextMarkup::Rich),
1416        "markdown" => Some(TextMarkup::Markdown),
1417        "none" => Some(TextMarkup::None),
1418        _ => None,
1419    }
1420}