Skip to main content

hexz_cli/ui/
help.rs

1use clap::Command;
2
3const BOLD: &str = "\x1b[1m";
4const RESET: &str = "\x1b[0m";
5const YELLOW: &str = "\x1b[33m";
6const GREEN: &str = "\x1b[32m";
7const CYAN: &str = "\x1b[36m";
8
9/// Custom help output printer for the Hexz CLI.
10#[derive(Debug)]
11pub struct Printer {
12    cmd: Command,
13}
14
15impl Printer {
16    /// Create a new `Printer` wrapping the given clap `Command`.
17    pub const fn new(cmd: Command) -> Self {
18        Self { cmd }
19    }
20
21    /// Prints the top-level help menu (categories and list of commands)
22    pub fn print_help(&mut self) {
23        let bin_name = self.cmd.get_bin_name().unwrap_or("hexz").to_string();
24
25        println!(
26            "{BOLD}Usage:{RESET} {GREEN}{bin_name}{RESET} {CYAN}[OPTIONS]{RESET} {YELLOW}COMMAND{RESET}"
27        );
28        println!();
29        if let Some(about) = self.cmd.get_about() {
30            println!("{about}");
31        }
32        println!();
33
34        let mut create_cmds = Vec::new();
35        let mut inspect_cmds = Vec::new();
36        let mut network_cmds = Vec::new();
37        let mut infra_cmds = Vec::new();
38
39        let subcommands: Vec<Command> = self.cmd.get_subcommands().cloned().collect();
40
41        for sub in subcommands {
42            let name = sub.get_name().to_string();
43            if name == "help" {
44                continue;
45            }
46
47            let about = sub.get_about().map(ToString::to_string).unwrap_or_default();
48            let item = (name.clone(), about);
49
50            match name.as_str() {
51                "pack" | "extract" | "init" | "checkout" | "commit" | "status" => create_cmds.push(item),
52                "inspect" | "show" | "diff" | "log" | "ls" | "predict" | "convert" => inspect_cmds.push(item),
53                "mount" | "unmount" | "shell" | "serve" | "remote" | "push" | "pull" => network_cmds.push(item),
54                "keygen" | "sign" | "verify" | "doctor" => infra_cmds.push(item),
55                _ => {}
56            }
57        }
58
59        self.print_section("core archive & workspace workflows", create_cmds);
60        self.print_section("data inspection & conversion", inspect_cmds);
61        self.print_section("networking & cloud collaboration", network_cmds);
62        self.print_section("security & system health", infra_cmds);
63
64        println!("{BOLD}Options:{RESET}");
65        println!("  {GREEN}{:<15}{RESET} Print help", "-h, --help");
66        println!("  {GREEN}{:<15}{RESET} Print version", "-V, --version");
67        println!();
68        println!(
69            "Run '{BOLD}{YELLOW}{bin_name} COMMAND --help{RESET}' for more information on a command."
70        );
71    }
72
73    #[allow(clippy::unused_self)]
74    fn print_section(&self, header: &str, cmds: Vec<(String, String)>) {
75        if cmds.is_empty() {
76            return;
77        }
78
79        println!("{BOLD}{YELLOW}{header}:{RESET}");
80
81        for (name, about) in cmds {
82            println!("  {GREEN}{name:<12}{RESET} {about}");
83        }
84        println!();
85    }
86
87    /// Prints detailed help for a specific subcommand
88    pub fn print_subcommand_help(&mut self, sub_name: &str) {
89        let Some(sub) = self.cmd.find_subcommand(sub_name) else {
90            return;
91        };
92
93        let bin_name = self.cmd.get_bin_name().unwrap_or("hexz");
94
95        // 1. Usage
96        println!(
97            "{BOLD}Usage:{RESET} {GREEN} {bin_name} {sub_name} {RESET} {CYAN}[OPTIONS] [ARGS]{RESET}"
98        );
99        println!();
100
101        // 2. Detailed Description (long_about)
102        if let Some(about) = sub.get_long_about().or_else(|| sub.get_about()) {
103            println!("{about}");
104        }
105        println!();
106
107        // Collect all arguments
108        // Partition into Positionals (Arguments) and Options (Flags)
109        // Robust check: Positionals are arguments that have NO short flag AND NO long flag.
110        let (mut positionals, mut flags): (Vec<_>, Vec<_>) = sub
111            .get_arguments()
112            .filter(|a| a.get_id() != "help" && a.get_id() != "version")
113            .partition(|a| a.get_short().is_none() && a.get_long().is_none());
114
115        // Sort positionals by index (so SOURCE comes before OUTPUT)
116        // If index is missing, we push it to the end.
117        positionals.sort_by_key(|a| a.get_index().unwrap_or(usize::MAX));
118
119        // Sort flags alphabetically
120        flags.sort_by(|a, b| a.get_id().cmp(b.get_id()));
121
122        // 3. Arguments Section (Positional)
123        if !positionals.is_empty() {
124            println!("{BOLD}Arguments:{RESET}");
125            for arg in positionals {
126                let name = arg.get_id().as_str().to_uppercase();
127                let help = arg.get_help().map(ToString::to_string).unwrap_or_default();
128
129                // Check if required
130                let required_note = if arg.is_required_set() {
131                    format!("{YELLOW} (required){RESET}")
132                } else {
133                    String::new()
134                };
135
136                println!("  {GREEN}{name:<28}{RESET} {help}{required_note}");
137            }
138            println!();
139        }
140
141        // 4. Options Section (Flags)
142        println!("{BOLD}Options:{RESET}");
143
144        for arg in flags {
145            let short = arg
146                .get_short()
147                .map(|s| format!("-{s},"))
148                .unwrap_or_default();
149            let long = arg
150                .get_long()
151                .map(|l| format!("--{l}"))
152                .unwrap_or_default();
153
154            // Handle values like <OUTPUT>
155            let value = if arg.get_action().takes_values() {
156                let val_name = arg
157                    .get_value_names()
158                    .and_then(|names| names.first())
159                    .map_or_else(|| "VAL".to_string(), ToString::to_string);
160                format!(" <{}>", val_name.to_uppercase())
161            } else {
162                String::new()
163            };
164
165            let flag_str = format!("{short} {long}{value}");
166            let help_text = arg.get_help().map(ToString::to_string).unwrap_or_default();
167
168            let required_note = if arg.is_required_set() {
169                format!("{YELLOW} (required){RESET}")
170            } else {
171                String::new()
172            };
173
174            let trimmed = flag_str.trim();
175            println!(
176                "  {GREEN}{trimmed:<28}{RESET} {help_text}{required_note}"
177            );
178        }
179
180        // Always show help flag
181        println!("  {GREEN}{:<28}{RESET} Print help", "-h, --help");
182        println!();
183
184        // 5. Example Usage
185        println!("{BOLD}Example:{RESET}");
186        if let Some(example) = sub.get_after_help() {
187            println!("  {example}");
188        } else {
189            println!("  {bin_name} {sub_name} [flags] [args]");
190        }
191        println!();
192    }
193}