mi6_cli/
help.rs

1use clap::CommandFactory;
2use std::io::{IsTerminal, Write};
3
4/// Get ANSI escape codes, respecting TTY and NO_COLOR.
5fn ansi_codes() -> (&'static str, &'static str, &'static str, &'static str) {
6    // Disable colors if not a TTY or NO_COLOR is set
7    if !std::io::stdout().is_terminal() || std::env::var_os("NO_COLOR").is_some() {
8        return ("", "", "", "");
9    }
10    ("\x1b[0m", "\x1b[1m", "\x1b[32m", "\x1b[37m")
11}
12
13/// Capitalize the first letter of a string.
14fn capitalize(s: &str) -> String {
15    let mut chars = s.chars();
16    match chars.next() {
17        None => String::new(),
18        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
19    }
20}
21
22/// Print styled help for a command path (e.g., `["ingest"]` or `["ingest", "transcript"]`).
23pub fn print_command_help(path: &[&str]) -> bool {
24    let app = crate::Cli::command();
25
26    // Navigate to the command at the given path
27    let mut current_cmd = &app;
28    for name in path {
29        let Some(cmd) = current_cmd
30            .get_subcommands()
31            .find(|c| c.get_name() == *name)
32        else {
33            return false;
34        };
35        current_cmd = cmd;
36    }
37
38    let mut stdout = std::io::stdout();
39    let (reset, bold, green, white) = ansi_codes();
40
41    // Build the full command string (e.g., "mi6 ingest transcript")
42    let full_cmd = format!("mi6 {}", path.join(" "));
43
44    // Usage line
45    let _ = writeln!(
46        stdout,
47        "{bold}{green}usage:{reset} {bold}{white}{full_cmd} [options]{reset}"
48    );
49    let _ = writeln!(stdout);
50
51    // Description
52    if let Some(about) = current_cmd.get_about() {
53        let _ = writeln!(stdout, "{}", about);
54        let _ = writeln!(stdout);
55    }
56
57    // Check if this command has subcommands
58    let subcommands: Vec<_> = current_cmd
59        .get_subcommands()
60        .filter(|c| !c.is_hide_set())
61        .collect();
62    let has_subcommands = !subcommands.is_empty();
63
64    if has_subcommands {
65        // Use the last path component for the header (e.g., "Ingest Commands")
66        let cmd_name = capitalize(path.last().unwrap_or(&""));
67        let _ = writeln!(stdout, "{bold}{green}{cmd_name} Commands{reset}");
68        for sub in &subcommands {
69            let name = sub.get_name();
70            let desc = sub.get_about().map(|s| s.to_string()).unwrap_or_default();
71            let _ = writeln!(stdout, "    {bold}{white}{name:<20}{reset} {desc}");
72        }
73        let _ = writeln!(stdout);
74    }
75
76    // Arguments (positional)
77    let positionals: Vec<_> = current_cmd.get_positionals().collect();
78    let has_positionals = !positionals.is_empty();
79    if has_positionals {
80        let _ = writeln!(stdout, "{bold}{green}Arguments{reset}");
81        for arg in positionals {
82            let name = arg.get_id().as_str();
83            let help = arg.get_help().map(|s| s.to_string()).unwrap_or_default();
84            let _ = writeln!(stdout, "    {bold}{white}<{name}>{reset}  {help}");
85        }
86    }
87
88    // Options
89    let options: Vec<_> = current_cmd
90        .get_opts()
91        .filter(|a| !a.is_hide_set() && !a.is_positional())
92        .collect();
93    let has_options = !options.is_empty();
94
95    // Add separator after Arguments if more content follows
96    if has_positionals && (has_options || has_subcommands) {
97        let _ = writeln!(stdout);
98    }
99
100    if has_options {
101        let _ = writeln!(stdout, "{bold}{green}Options{reset}");
102        for opt in options {
103            let short = opt
104                .get_short()
105                .map(|c| format!("-{c}, "))
106                .unwrap_or_default();
107            let long = opt
108                .get_long()
109                .map_or_else(|| opt.get_id().to_string(), |l| format!("--{l}"));
110            let help = opt.get_help().map(|s| s.to_string()).unwrap_or_default();
111            let _ = writeln!(stdout, "    {bold}{white}{short}{long}{reset}  {help}");
112        }
113        // Only add blank line if footer follows
114        if has_subcommands {
115            let _ = writeln!(stdout);
116        }
117    }
118
119    // Footer for commands with subcommands
120    if has_subcommands {
121        let _ = writeln!(
122            stdout,
123            "See arguments for each command with {bold}{white}{full_cmd} <command> -h{reset}"
124        );
125    }
126
127    true
128}
129
130/// Print styled help for a subcommand (single level).
131pub fn print_subcommand_help(subcommand: &str) -> bool {
132    print_command_help(&[subcommand])
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136pub enum CommandCategory {
137    Inputs,
138    Outputs,
139    Meta,
140}
141
142impl CommandCategory {
143    pub fn name(&self) -> &str {
144        match self {
145            CommandCategory::Inputs => "Input Commands",
146            CommandCategory::Outputs => "Output Commands",
147            CommandCategory::Meta => "Meta Commands",
148        }
149    }
150}
151
152/// Map command names to their categories.
153///
154/// NOTE: When adding a new command to the CLI, update this function to assign
155/// it to the appropriate category. Unknown commands default to Meta.
156fn get_command_category(name: &str) -> CommandCategory {
157    match name {
158        // Inputs - commands that ingest data
159        "ingest" => CommandCategory::Inputs,
160
161        // Outputs - commands that display/output data
162        "session" | "tui" | "watch" => CommandCategory::Outputs,
163
164        // Meta - commands for setup/management
165        "enable" | "disable" | "status" => CommandCategory::Meta,
166
167        // Default fallback for unknown commands
168        _ => CommandCategory::Meta,
169    }
170}
171
172/// Print custom help matching jtool's style.
173pub fn print_help() {
174    let mut stdout = std::io::stdout();
175    let (reset, bold, green, white) = ansi_codes();
176
177    let _ = writeln!(
178        stdout,
179        "{bold}{green}usage:{reset} {bold}{white}mi6 <command> [<args>]{reset}"
180    );
181    let _ = writeln!(stdout);
182    let _ = writeln!(
183        stdout,
184        "Tool for monitoring and managing agentic coding sessions"
185    );
186    let _ = writeln!(stdout);
187
188    // Dynamically get all commands from the Commands enum using clap
189    let app = crate::Cli::command();
190    let mut commands_by_category: std::collections::HashMap<
191        CommandCategory,
192        Vec<(String, String)>,
193    > = std::collections::HashMap::new();
194
195    for subcommand in app.get_subcommands() {
196        // Skip hidden commands
197        if subcommand.is_hide_set() {
198            continue;
199        }
200
201        // Skip the help command (clap adds it automatically)
202        let name = subcommand.get_name();
203        if name == "help" {
204            continue;
205        }
206
207        let name = name.to_string();
208        let description = subcommand
209            .get_about()
210            .map(|s| s.to_string())
211            .unwrap_or_default();
212        let category = get_command_category(&name);
213
214        commands_by_category
215            .entry(category)
216            .or_default()
217            .push((name, description));
218    }
219
220    // Display commands grouped by category in a specific order
221    let categories = [
222        CommandCategory::Inputs,
223        CommandCategory::Outputs,
224        CommandCategory::Meta,
225    ];
226
227    let mut first = true;
228    for category in &categories {
229        if let Some(mut commands) = commands_by_category.remove(category) {
230            // Sort commands alphabetically within each category
231            commands.sort_by(|a, b| a.0.cmp(&b.0));
232
233            // Print blank line between categories (not before first)
234            if !first {
235                let _ = writeln!(stdout);
236            }
237            first = false;
238
239            let _ = writeln!(stdout, "{bold}{green}{}{reset}", category.name());
240
241            for (name, description) in commands {
242                let _ = writeln!(stdout, "    {bold}{white}{name:<20}{reset} {description}");
243            }
244        }
245    }
246
247    let _ = writeln!(stdout);
248    let _ = writeln!(
249        stdout,
250        "See arguments for each command with {bold}{white}mi6 <command> -h{reset}"
251    );
252}