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