Skip to main content

xbp_cli/cli/
help_render.rs

1//! Styled help rendering for Clap-generated usage text.
2
3use clap::CommandFactory;
4use colored::Colorize;
5
6/// Shared help layout for subcommands (no duplicate name/version header).
7pub const XBP_HELP_TEMPLATE: &str = "\
8{about-with-newline}\
9Usage: {usage}\n\n\
10{all-args}\
11{after-help}";
12
13/// Root help layout; the renderer adds a branded banner above Clap output.
14pub const XBP_ROOT_HELP_TEMPLATE: &str = "\
15{about-with-newline}\
16Usage: {usage}\n\n\
17{all-args}\
18{after-help}";
19
20pub const XBP_ROOT_AFTER_HELP: &str = "\
21Quick start:
22  xbp diag
23  xbp services
24  xbp workers list
25  xbp workers logs -f
26  xbp api health
27
28Discover:
29  xbp <command> -h          Command help and examples
30  xbp <command> <sub> -h    Subcommand help
31  xbp --commands            Full alphabetical command tree
32  xbp install               Browse installable targets";
33
34pub const CONFIG_AFTER_HELP: &str = "\
35Examples:
36  xbp config
37  xbp config --project
38  xbp config cloudflare
39  xbp config cloudflare status
40  xbp config openrouter set-key
41  xbp config linear select-initiative";
42
43pub const VERSION_AFTER_HELP: &str = "\
44Examples:
45  xbp version
46  xbp version patch
47  xbp version 1.2.3
48  xbp version release
49  xbp version workspace check
50  xbp version workspace sync --version 3.16.5 --write";
51
52pub const DIAG_AFTER_HELP: &str = "\
53Examples:
54  xbp diag
55  xbp diag --nginx
56  xbp diag --ports 80,443
57  xbp diag --codetime --cursor";
58
59pub const NGINX_AFTER_HELP: &str = "\
60Examples:
61  xbp nginx list
62  xbp nginx enable api.example.com
63  xbp nginx disable api.example.com
64  xbp nginx upstream list";
65
66pub const COMMIT_AFTER_HELP: &str = "\
67Examples:
68  xbp commit
69  xbp commit --dry-run
70  xbp commit --push
71  xbp commit --scope cli";
72
73pub const LOGS_AFTER_HELP: &str = "\
74Examples:
75  xbp logs
76  xbp logs my-project
77  xbp logs --ssh-host bastion.example.com";
78
79pub const PUBLISH_AFTER_HELP: &str = "\
80Examples:
81  xbp publish --dry-run
82  xbp publish --target npm
83  xbp publish --service web
84  xbp publish --service api --include-prereqs
85  xbp publish --allow-dirty";
86
87pub const DOMAINS_AFTER_HELP: &str = "\
88Examples:
89  xbp domains list
90  xbp domains check --domain example.com
91  xbp domains search --query myapp --extension com";
92
93pub const LOGIN_AFTER_HELP: &str = "\
94Examples:
95  xbp login
96  xbp login status
97  xbp login logout
98  xbp whoami";
99
100pub const SSH_AFTER_HELP: &str = "\
101Examples:
102  xbp ssh
103  xbp ssh --host bastion.example.com
104  xbp ssh --host 10.0.0.5 --command \"uptime\"";
105
106pub const WORKTREE_WATCH_AFTER_HELP: &str = "\
107Examples:
108  xbp worktree-watch start
109  xbp worktree-watch start --detach
110  xbp worktree-watch start --parent C:\\Users\\floris\\Documents\\GitHub --detach
111  xbp worktree-watch stop
112  xbp worktree-watch sync
113  xbp worktree-watch sync --dry-run
114  xbp worktree-watch status
115  xbp worktree-watch status --stats
116  xbp worktree-watch status --stats --repo-activity
117  xbp worktree-watch status --stats --json
118  xbp worktree-watch status --records --record-limit 100
119  xbp worktree-watch status --stats --stats-gap-minutes 30
120
121Local stats are written under ~/.xbp/mutations/<owner>/<repo>/<branch>/stats.json
122and repo rollups to ~/.xbp/mutations/<owner>/<repo>/repo-activity.json.";
123
124pub const WORKTREE_WATCH_STATUS_AFTER_HELP: &str = "\
125Examples:
126  xbp worktree-watch status
127  xbp worktree-watch status --stats
128  xbp worktree-watch status --stats --repo-activity
129  xbp worktree-watch status --stats --json
130  xbp worktree-watch status --records --record-limit 100
131  xbp worktree-watch status --stats --stats-gap-minutes 30
132  xbp worktree-watch status --parent C:\\Users\\floris\\Documents\\GitHub --stats
133
134--stats recomputes activity from local JSONL spools and writes stats.json.
135--repo-activity rolls up every spooled branch into repo-activity.json.";
136
137#[cfg(feature = "openapi-gen")]
138pub const GENERATE_AFTER_HELP: &str = "\
139Examples:
140  xbp generate config
141  xbp generate config --force
142  xbp generate openapi --all
143  xbp generate openapi --service api --format both
144  xbp generate openapi --all --check
145  xbp generate systemd";
146
147#[cfg(not(feature = "openapi-gen"))]
148pub const GENERATE_AFTER_HELP: &str = "\
149Examples:
150  xbp generate config
151  xbp generate config --force
152  xbp generate systemd";
153
154pub const DONE_AFTER_HELP: &str = "\
155Examples:
156  xbp done
157  xbp done --since \"7 days ago\"
158  xbp done --output report.md";
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum HelpScope {
162    Root,
163    Subcommand,
164    Catalog,
165    Auto,
166}
167
168pub fn emit_styled_help(raw: &str, scope: HelpScope) {
169    crate::cli::ui::configure_color_output();
170    let resolved_scope = match scope {
171        HelpScope::Auto => detect_help_scope(raw),
172        other => other,
173    };
174
175    let mut skip_about_line = None::<String>;
176    let mut skip_until_usage = resolved_scope == HelpScope::Catalog;
177    if resolved_scope == HelpScope::Root {
178        print_root_banner(raw);
179    } else if resolved_scope != HelpScope::Catalog {
180        if let Some((_, tagline)) = parse_subcommand_heading(raw) {
181            skip_about_line = Some(tagline);
182            print_subcommand_banner(raw);
183        }
184    }
185
186    let lines: Vec<&str> = raw.lines().collect();
187    let mut index = 0usize;
188    while index < lines.len() {
189        let line = lines[index];
190        let trimmed = line.trim();
191        if skip_until_usage {
192            if trimmed.starts_with("Usage:") {
193                skip_until_usage = false;
194            } else {
195                index += 1;
196                continue;
197            }
198        }
199        if let Some(about) = skip_about_line.as_deref() {
200            if trimmed == about {
201                skip_about_line = None;
202                index += 1;
203                continue;
204            }
205        }
206
207        if is_command_name_line(line)
208            && index + 1 < lines.len()
209            && is_indented_help_text(lines[index + 1], 4)
210        {
211            println!("{}", style_command_pair(line, lines[index + 1]));
212            index += 2;
213            continue;
214        }
215
216        if is_option_flags_line(line)
217            && index + 1 < lines.len()
218            && is_indented_help_text(lines[index + 1], 4)
219        {
220            println!("{}", style_option_pair(line, lines[index + 1]));
221            index += 2;
222            continue;
223        }
224
225        if is_option_entry(line)
226            && index + 1 < lines.len()
227            && is_indented_help_text(lines[index + 1], 8)
228        {
229            println!("{}", style_option_pair(line, lines[index + 1]));
230            index += 2;
231            continue;
232        }
233
234        println!("{}", style_help_line(line));
235        index += 1;
236    }
237    println!();
238}
239
240/// Print the complete Clap command tree with styled formatting.
241///
242/// Includes nested subcommands and their flags (for example
243/// `worktree-watch status --stats`), not only the root command list.
244pub fn print_command_catalog() {
245    crate::cli::ui::configure_color_output();
246    println!();
247    println!("{}", "xbp commands".bright_magenta().bold());
248    println!(
249        "{}",
250        "Complete command reference (nested flags included)".bright_black()
251    );
252    crate::cli::ui::divider(44);
253
254    let cmd = crate::cli::commands::Cli::command();
255    print_catalog_command_tree(&cmd, &[], true);
256    println!();
257}
258
259fn print_catalog_command_tree(cmd: &clap::Command, path: &[&str], is_root: bool) {
260    if !is_root {
261        let command_path = format!("xbp {}", path.join(" "));
262        println!();
263        println!("{}", command_path.bright_cyan().bold());
264        if let Some(about) = cmd.get_about().or_else(|| cmd.get_long_about()) {
265            println!("  {}", about.to_string().bright_black());
266        }
267
268        let mut args: Vec<_> = cmd
269            .get_arguments()
270            .filter(|arg| {
271                !arg.is_hide_set()
272                    && !arg.is_global_set()
273                    && arg.get_id() != "help"
274                    && arg.get_id() != "version"
275                    && arg.get_id() != "debug"
276            })
277            .collect();
278        args.sort_by_key(|arg| arg.get_id().to_string());
279
280        if !args.is_empty() {
281            println!(
282                "  {} {}",
283                "▸".bright_blue().bold(),
284                "Options:".bright_blue().bold()
285            );
286            for arg in args {
287                println!("    {}", style_catalog_arg(arg));
288            }
289        }
290
291        if let Some(after) = cmd.get_after_help().or_else(|| cmd.get_after_long_help()) {
292            let after = after.to_string();
293            let mut in_examples = false;
294            for line in after.lines() {
295                let trimmed = line.trim();
296                if trimmed.is_empty() {
297                    continue;
298                }
299                if trimmed == "Examples:" || trimmed.starts_with("Examples:") {
300                    in_examples = true;
301                    println!(
302                        "  {} {}",
303                        "◇".bright_green().bold(),
304                        "Examples:".bright_green().bold()
305                    );
306                    continue;
307                }
308                if in_examples && (trimmed.starts_with("xbp ") || trimmed.starts_with("xbp.exe ")) {
309                    println!("    {}", highlight_inline_flags(trimmed).dimmed());
310                    continue;
311                }
312                // Keep short notes under examples, stop at unrelated sections.
313                if in_examples && !trimmed.starts_with('-') && !trimmed.starts_with("Local ") {
314                    in_examples = false;
315                }
316                if in_examples {
317                    println!("    {}", trimmed.bright_black());
318                }
319            }
320        }
321    }
322
323    let mut subs: Vec<_> = cmd
324        .get_subcommands()
325        .filter(|sub| !sub.is_hide_set() && sub.get_name() != "help")
326        .collect();
327    subs.sort_by_key(|sub| sub.get_name().to_string());
328
329    for sub in subs {
330        let mut next = path.to_vec();
331        next.push(sub.get_name());
332        print_catalog_command_tree(sub, &next, false);
333    }
334}
335
336fn style_catalog_arg(arg: &clap::Arg) -> String {
337    let mut flags = Vec::new();
338    if let Some(short) = arg.get_short() {
339        flags.push(format!("-{short}"));
340    }
341    if let Some(long) = arg.get_long() {
342        flags.push(format!("--{long}"));
343    }
344    if flags.is_empty() {
345        if let Some(name) = arg.get_value_names().and_then(|names| names.first()) {
346            flags.push(format!("<{name}>"));
347        } else {
348            flags.push(format!("<{}>", arg.get_id()));
349        }
350    } else if arg.get_action().takes_values() {
351        let value_name = arg
352            .get_value_names()
353            .and_then(|names| names.first())
354            .map(|name| name.to_string())
355            .unwrap_or_else(|| arg.get_id().to_string().to_ascii_uppercase());
356        if let Some(last) = flags.last_mut() {
357            *last = format!("{last} <{value_name}>");
358        }
359    }
360
361    let flags_text = flags.join(", ").bright_yellow().to_string();
362    let help = arg
363        .get_help()
364        .or_else(|| arg.get_long_help())
365        .map(|help| help.to_string())
366        .unwrap_or_default();
367    let default = arg
368        .get_default_values()
369        .first()
370        .and_then(|value| value.to_str())
371        .map(|value| format!(" [default: {value}]"))
372        .unwrap_or_default();
373
374    if help.is_empty() {
375        format!("{flags_text}{}", default.bright_black())
376    } else {
377        format!(
378            "{flags_text:<36} {}{}",
379            help.bright_black(),
380            default.bright_black()
381        )
382    }
383}
384
385pub fn emit_version_line(version: &str) {
386    crate::cli::ui::configure_color_output();
387    println!(
388        "{} {}",
389        "xbp".bright_magenta().bold(),
390        version.bright_white().bold()
391    );
392}
393
394pub fn is_root_help_text(raw: &str) -> bool {
395    raw.lines().any(|line| {
396        let trimmed = line.trim();
397        trimmed == "Usage: xbp [OPTIONS] [COMMAND]"
398            || trimmed == "Usage: xbp.exe [OPTIONS] [COMMAND]"
399            || trimmed.starts_with("Usage: xbp [OPTIONS] [COMMAND]")
400            || trimmed.starts_with("Usage: xbp.exe [OPTIONS] [COMMAND]")
401    })
402}
403
404fn detect_help_scope(raw: &str) -> HelpScope {
405    if is_root_help_text(raw) {
406        return HelpScope::Root;
407    }
408    let first_line = raw.lines().next().unwrap_or_default().trim();
409    if first_line.starts_with("xbp ") && first_line.chars().any(|ch| ch.is_ascii_digit()) {
410        return HelpScope::Root;
411    }
412    HelpScope::Subcommand
413}
414
415fn print_root_banner(raw: &str) {
416    let version = parse_version_line(raw).unwrap_or(env!("CARGO_PKG_VERSION"));
417    println!();
418    println!(
419        "{}  {}",
420        "XBP".bright_magenta().bold(),
421        format!("v{version}").bright_white()
422    );
423    println!("{}", "Deploy · operate · debug · ship".bright_black());
424    crate::cli::ui::divider(44);
425}
426
427fn print_subcommand_banner(raw: &str) {
428    let Some((command_path, tagline)) = parse_subcommand_heading(raw) else {
429        return;
430    };
431    println!();
432    println!("{}", command_path.bright_magenta().bold());
433    if !tagline.is_empty() {
434        println!("{}", tagline.bright_black());
435    }
436    crate::cli::ui::divider(command_path.len().max(28));
437}
438
439fn parse_version_line(raw: &str) -> Option<&str> {
440    let first = raw.lines().next()?.trim();
441    let rest = first.strip_prefix("xbp")?.trim();
442    if rest.is_empty() {
443        None
444    } else {
445        Some(rest)
446    }
447}
448
449fn parse_subcommand_heading(raw: &str) -> Option<(String, String)> {
450    let about = raw
451        .lines()
452        .map(str::trim)
453        .find(|line| !line.is_empty() && !line.starts_with("Usage:"))?;
454
455    let usage = raw
456        .lines()
457        .map(str::trim)
458        .find(|line| line.starts_with("Usage:"))?;
459    let command_path = extract_command_path_from_usage(usage)?;
460
461    Some((command_path, about.to_string()))
462}
463
464fn extract_command_path_from_usage(usage: &str) -> Option<String> {
465    let rest = usage.split_once(':')?.1.trim();
466    let path = rest.split('[').next()?.trim();
467    let normalized = path.replace(".exe", "");
468    if normalized.is_empty() {
469        None
470    } else {
471        Some(normalized)
472    }
473}
474
475fn style_help_line(line: &str) -> String {
476    let trimmed = line.trim_start();
477    if trimmed.is_empty() {
478        return String::new();
479    }
480
481    if matches!(
482        trimmed,
483        "Commands:" | "Options:" | "Arguments:" | "Subcommands:"
484    ) {
485        return format!(
486            "\n{} {}",
487            "▸".bright_blue().bold(),
488            trimmed.bright_blue().bold()
489        );
490    }
491
492    if trimmed.starts_with("Usage:") {
493        return style_usage_line(line);
494    }
495
496    if trimmed == "Discover:" {
497        return format!(
498            "\n{} {}",
499            "◇".bright_green().bold(),
500            trimmed.bright_green().bold()
501        );
502    }
503
504    if is_example_section_header(trimmed) {
505        return format!(
506            "\n{} {}",
507            "◇".bright_green().bold(),
508            trimmed.bright_green().bold()
509        );
510    }
511
512    if is_note_section_header(trimmed) {
513        return format!(
514            "\n{} {}",
515            "◇".bright_yellow().bold(),
516            trimmed.bright_yellow().bold()
517        );
518    }
519
520    if is_command_entry(line) {
521        return style_command_entry(line);
522    }
523
524    if is_option_flags_line(line) {
525        return format!("  {}", line.trim().bright_yellow());
526    }
527
528    if is_option_entry(line) {
529        return style_option_entry(line);
530    }
531
532    if is_command_name_line(line) {
533        return format!("  {}", line.trim().bright_cyan().bold());
534    }
535
536    if is_indented_help_text(line, 4) || is_indented_help_text(line, 8) {
537        return format!("      {}", trimmed.bright_black());
538    }
539
540    if is_example_command_line(trimmed) {
541        return format!("  {}", highlight_inline_flags(trimmed).dimmed());
542    }
543
544    if trimmed.starts_with("Run `") || trimmed.starts_with("Use `") || trimmed.starts_with("Pass `")
545    {
546        return trimmed.bright_black().to_string();
547    }
548
549    line.to_string()
550}
551
552fn style_usage_line(line: &str) -> String {
553    let (prefix, rest) = line.split_once(':').unwrap_or((line, ""));
554    format!(
555        "{} {}",
556        format!("{prefix}:").bright_cyan().bold(),
557        highlight_inline_flags(rest.trim()).bright_white()
558    )
559}
560
561fn is_example_section_header(line: &str) -> bool {
562    matches!(
563        line,
564        "Examples:"
565            | "Quick start:"
566            | "Discover:"
567            | "Available targets:"
568            | "List installable targets:"
569    ) || line.starts_with("Quick start")
570        || line.starts_with("List installable")
571}
572
573fn is_note_section_header(line: &str) -> bool {
574    line == "Notes:"
575        || line.starts_with("Tip:")
576        || line.starts_with("More info:")
577        || line.starts_with("Hint:")
578}
579
580fn is_command_entry(line: &str) -> bool {
581    if !line.starts_with("  ") || line.starts_with("    ") {
582        return false;
583    }
584    let trimmed = line.trim_start();
585    let Some((name, _)) = trimmed.split_once("  ") else {
586        return false;
587    };
588    !name.starts_with('-') && !name.contains('<') && name.len() <= 24
589}
590
591fn style_command_entry(line: &str) -> String {
592    let trimmed = line.trim_start();
593    let mut parts = trimmed.splitn(2, "  ");
594    let name = parts.next().unwrap_or_default();
595    let description = parts.next().unwrap_or_default().trim();
596    let alias = extract_alias_suffix(description);
597    let base_description = description
598        .split_once('[')
599        .map(|(left, _)| left.trim())
600        .unwrap_or(description);
601
602    let name = name.bright_cyan().bold();
603    if alias.is_empty() {
604        format!("  {name:<22} {}", base_description.bright_black())
605    } else {
606        format!(
607            "  {name:<22} {} {}",
608            base_description.bright_black(),
609            alias.bright_black()
610        )
611    }
612}
613
614fn extract_alias_suffix(description: &str) -> String {
615    let Some(start) = description.find('[') else {
616        return String::new();
617    };
618    description[start..].to_string()
619}
620
621fn is_option_flags_line(line: &str) -> bool {
622    let trimmed = line.trim_start();
623    line.starts_with("  ")
624        && !line.starts_with("    ")
625        && (trimmed.starts_with("--") || trimmed.starts_with("-"))
626}
627
628fn is_option_entry(line: &str) -> bool {
629    let trimmed = line.trim_start();
630    line.starts_with("      ")
631        && (trimmed.starts_with("--") || trimmed.starts_with("-"))
632        && !trimmed.starts_with("Usage:")
633}
634
635fn is_command_name_line(line: &str) -> bool {
636    if !line.starts_with("  ") || line.starts_with("    ") {
637        return false;
638    }
639    let trimmed = line.trim();
640    !trimmed.is_empty()
641        && !trimmed.ends_with(':')
642        && !trimmed.starts_with('-')
643        && !trimmed.contains(' ')
644        && trimmed.len() <= 24
645}
646
647fn is_indented_help_text(line: &str, min_spaces: usize) -> bool {
648    let leading = line.chars().take_while(|ch| *ch == ' ').count();
649    leading >= min_spaces && !line.trim_start().starts_with('-') && !line.trim().is_empty()
650}
651
652fn style_command_pair(name_line: &str, description_line: &str) -> String {
653    let name = name_line.trim().bright_cyan().bold();
654    let description = description_line.trim();
655    let alias = extract_alias_suffix(description);
656    let base_description = description
657        .split_once('[')
658        .map(|(left, _)| left.trim())
659        .unwrap_or(description);
660
661    if alias.is_empty() {
662        format!("  {name:<22} {}", base_description.bright_black())
663    } else {
664        format!(
665            "  {name:<22} {} {}",
666            base_description.bright_black(),
667            alias.bright_black()
668        )
669    }
670}
671
672fn style_option_pair(flags_line: &str, description_line: &str) -> String {
673    let flags = flags_line.trim();
674    let styled_flags = flags
675        .split(", ")
676        .map(|flag| flag.bright_yellow().to_string())
677        .collect::<Vec<_>>()
678        .join(", ");
679    format!(
680        "      {styled_flags:<28} {}",
681        description_line.trim().bright_black()
682    )
683}
684
685fn style_option_entry(line: &str) -> String {
686    let trimmed = line.trim_start();
687    let mut parts = trimmed.splitn(2, "  ");
688    let flags = parts.next().unwrap_or_default();
689    let description = parts.next().unwrap_or_default().trim();
690
691    let styled_flags = flags
692        .split(", ")
693        .map(|flag| flag.bright_yellow().to_string())
694        .collect::<Vec<_>>()
695        .join(", ");
696
697    if description.is_empty() {
698        format!("      {styled_flags}")
699    } else {
700        format!("      {styled_flags:<28} {}", description.bright_black())
701    }
702}
703
704fn is_example_command_line(line: &str) -> bool {
705    line.starts_with("xbp ") || line.starts_with("cargo ") || line.starts_with("git ")
706}
707
708fn highlight_inline_flags(text: &str) -> String {
709    let mut output = String::new();
710    let mut current = String::new();
711    let mut chars = text.chars().peekable();
712
713    while let Some(ch) = chars.next() {
714        if ch == '-' && matches!(chars.peek(), Some('-' | 'f' | 'h' | 'l' | 'p' | 'v' | 'n')) {
715            if !current.is_empty() {
716                output.push_str(&current);
717                current.clear();
718            }
719            let mut flag = String::from('-');
720            if chars.peek() == Some(&'-') {
721                flag.push(chars.next().expect("dash"));
722            }
723            while let Some(&next) = chars.peek() {
724                if next.is_ascii_alphanumeric() || next == '-' {
725                    flag.push(chars.next().expect("flag char"));
726                } else {
727                    break;
728                }
729            }
730            output.push_str(&flag.bright_yellow().to_string());
731            continue;
732        }
733
734        if ch == '<' {
735            if !current.is_empty() {
736                output.push_str(&current);
737                current.clear();
738            }
739            let mut placeholder = String::from('<');
740            for next in chars.by_ref() {
741                placeholder.push(next);
742                if next == '>' {
743                    break;
744                }
745            }
746            output.push_str(&placeholder.bright_green().to_string());
747            continue;
748        }
749
750        current.push(ch);
751    }
752
753    if !current.is_empty() {
754        output.push_str(&current);
755    }
756    output
757}
758
759#[cfg(test)]
760mod tests {
761    use super::*;
762
763    #[test]
764    fn detects_root_help_scope() {
765        let raw = "xbp 10.30.3\n\nAbout\nUsage: xbp [OPTIONS] [COMMAND]";
766        assert_eq!(detect_help_scope(raw), HelpScope::Root);
767    }
768
769    #[test]
770    fn workers_help_is_not_root() {
771        let raw = "Manage workers\nUsage: xbp.exe workers [OPTIONS] <COMMAND>";
772        assert!(!is_root_help_text(raw));
773    }
774
775    #[test]
776    fn styles_usage_line_with_flags() {
777        let styled = style_help_line("Usage: xbp workers logs [OPTIONS]");
778        assert!(styled.contains("Usage:"));
779        assert!(styled.contains("workers"));
780    }
781
782    #[test]
783    fn styles_command_entry_line() {
784        let styled = style_help_line("  list      List workers [aliases: ls]");
785        assert!(styled.contains("list"));
786    }
787
788    #[test]
789    fn catalog_scope_skips_duplicate_about() {
790        let raw = "Deploy services\nUsage: xbp [OPTIONS] [COMMAND]\n\nCommands:\n  diag\n      Run diagnostics";
791        emit_styled_help(raw, HelpScope::Catalog);
792    }
793}