Skip to main content

kaish_kernel/
help.rs

1//! Help system for kaish.
2//!
3//! Provides topic-based help content embedded at compile time,
4//! plus dynamic tool help from the tool registry.
5
6use crate::tools::ToolSchema;
7
8/// Help topics available in kaish.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum HelpTopic {
11    /// Overview of kaish with topic list.
12    Overview,
13    /// Syntax reference: variables, quoting, pipes, control flow.
14    Syntax,
15    /// List of all available builtins.
16    Builtins,
17    /// Virtual filesystem mounts and paths.
18    Vfs,
19    /// Scatter/gather parallel processing.
20    Scatter,
21    /// Ignore file configuration.
22    Ignore,
23    /// Output size limit configuration.
24    OutputLimit,
25    /// Known limitations.
26    Limits,
27    /// Help for a specific tool.
28    Tool(String),
29}
30
31impl HelpTopic {
32    /// Parse a topic string into a HelpTopic.
33    ///
34    /// Returns Overview for empty/None, specific topics for known names,
35    /// or Tool(name) for anything else (assumes it's a tool name).
36    pub fn parse_topic(s: &str) -> Self {
37        match s.to_lowercase().as_str() {
38            "" | "overview" | "help" => Self::Overview,
39            "syntax" | "language" | "lang" => Self::Syntax,
40            "builtins" | "tools" | "commands" => Self::Builtins,
41            "vfs" | "filesystem" | "fs" | "paths" => Self::Vfs,
42            "scatter" | "gather" | "parallel" | "散" | "集" => Self::Scatter,
43            "ignore" | "gitignore" | "kaish-ignore" => Self::Ignore,
44            "output-limit" | "spill" | "truncate" | "kaish-output-limit" => Self::OutputLimit,
45            "limits" | "limitations" | "missing" => Self::Limits,
46            other => Self::Tool(other.to_string()),
47        }
48    }
49
50    /// Get a short description of this topic.
51    pub fn description(&self) -> &'static str {
52        match self {
53            Self::Overview => "What kaish is, list of topics",
54            Self::Syntax => "Variables, quoting, pipes, control flow",
55            Self::Builtins => "List of available builtins",
56            Self::Vfs => "Virtual filesystem mounts and paths",
57            Self::Scatter => "Parallel processing (散/集)",
58            Self::Ignore => "Ignore file configuration",
59            Self::OutputLimit => "Output size limit configuration",
60            Self::Limits => "Known limitations",
61            Self::Tool(_) => "Help for a specific tool",
62        }
63    }
64}
65
66// Embed markdown files at compile time from the crate-local docs/help/ directory.
67// The repo-root docs/help symlinks here so paths work both locally and in published crates.
68const OVERVIEW: &str = include_str!("../docs/help/overview.md");
69const SYNTAX: &str = include_str!("../docs/help/syntax.md");
70const VFS: &str = include_str!("../docs/help/vfs.md");
71const SCATTER: &str = include_str!("../docs/help/scatter.md");
72const IGNORE: &str = include_str!("../docs/help/ignore.md");
73const OUTPUT_LIMIT: &str = include_str!("../docs/help/output-limit.md");
74const LIMITS: &str = include_str!("../docs/help/limits.md");
75
76/// Get help content for a topic.
77///
78/// For static topics, returns embedded markdown.
79/// For `Builtins`, generates a tool list from the provided schemas.
80/// For `Tool(name)`, looks up the tool in the schemas.
81pub fn get_help(topic: &HelpTopic, tool_schemas: &[ToolSchema]) -> String {
82    match topic {
83        HelpTopic::Overview => OVERVIEW.to_string(),
84        HelpTopic::Syntax => SYNTAX.to_string(),
85        HelpTopic::Builtins => format_tool_list(tool_schemas),
86        HelpTopic::Vfs => VFS.to_string(),
87        HelpTopic::Scatter => SCATTER.to_string(),
88        HelpTopic::Ignore => IGNORE.to_string(),
89        HelpTopic::OutputLimit => OUTPUT_LIMIT.to_string(),
90        HelpTopic::Limits => LIMITS.to_string(),
91        HelpTopic::Tool(name) => format_tool_help(name, tool_schemas),
92    }
93}
94
95/// Format help for a single tool.
96fn format_tool_help(name: &str, schemas: &[ToolSchema]) -> String {
97    match schemas.iter().find(|s| s.name == name) {
98        Some(schema) => {
99            let mut output = String::new();
100
101            output.push_str(&format!("{} — {}\n\n", schema.name, schema.description));
102
103            if schema.params.is_empty() {
104                output.push_str("No parameters.\n");
105            } else {
106                output.push_str("Parameters:\n");
107                for param in &schema.params {
108                    let req = if param.required { " (required)" } else { "" };
109                    output.push_str(&format!(
110                        "  {} : {}{}\n    {}\n",
111                        param.name, param.param_type, req, param.description
112                    ));
113                }
114            }
115
116            if !schema.examples.is_empty() {
117                output.push_str("\nExamples:\n");
118                for example in &schema.examples {
119                    output.push_str(&format!("  # {}\n", example.description));
120                    output.push_str(&format!("  {}\n\n", example.code));
121                }
122            }
123
124            output
125        }
126        None => format!(
127            "Unknown topic or tool: {}\n\nUse 'help' to see available topics, or 'help builtins' for tool list.",
128            name
129        ),
130    }
131}
132
133/// Format a list of all tools grouped by category.
134fn format_tool_list(schemas: &[ToolSchema]) -> String {
135    let mut output = String::new();
136
137    output.push_str("# Available Builtins\n\n");
138
139    // Find max name length for alignment
140    let max_len = schemas.iter().map(|s| s.name.len()).max().unwrap_or(0);
141
142    // Group tools by category based on name.
143    // Keep in sync with actual builtins in tools/builtin/.
144    let mut text_tools = Vec::new();
145    let mut file_tools = Vec::new();
146    let mut system_tools = Vec::new();
147    let mut json_tools = Vec::new();
148    let mut parallel_tools = Vec::new();
149    let mut process_tools = Vec::new();
150    let mut introspection_tools = Vec::new();
151    let mut other_tools = Vec::new();
152
153    for schema in schemas {
154        let entry = (schema.name.as_str(), schema.description.as_str());
155        match schema.name.as_str() {
156            "grep" | "sed" | "awk" | "cut" | "tr" | "sort" | "uniq" | "wc" | "head" | "tail"
157            | "split" | "diff" => text_tools.push(entry),
158            "cat" | "ls" | "tree" | "cd" | "pwd" | "mkdir" | "rm" | "cp" | "mv" | "touch"
159            | "ln" | "readlink" | "write" | "glob" | "find" | "stat" | "dirname" | "basename"
160            | "realpath" | "mktemp" | "patch" => file_tools.push(entry),
161            "alias" | "unalias" | "echo" | "printf" | "read" | "sleep" | "date" | "env"
162            | "export" | "set" | "unset" | "true" | "false" | "test" | "[" | "assert" | "seq"
163            | "tee" | "hostname" | "uname" | "which" => system_tools.push(entry),
164            "jq" => json_tools.push(entry),
165            "scatter" | "gather" => parallel_tools.push(entry),
166            "exec" | "spawn" | "jobs" | "wait" | "ps" | "git" | "bg" | "fg" | "kill" => process_tools.push(entry),
167            "help" | "kaish-validate" | "kaish-vars" | "kaish-mounts" | "kaish-tools" | "tokens"
168            | "kaish-ast" | "kaish-clear" | "kaish-status" | "kaish-trash" | "kaish-version"
169            | "kaish-ignore" | "kaish-output-limit" => {
170                introspection_tools.push(entry)
171            }
172            _ => other_tools.push(entry),
173        }
174    }
175
176    let format_group = |name: &str, tools: &[(&str, &str)], max: usize| -> String {
177        if tools.is_empty() {
178            return String::new();
179        }
180        let mut s = format!("## {}\n\n", name);
181        for (tool_name, desc) in tools {
182            s.push_str(&format!("  {:width$}  {}\n", tool_name, desc, width = max));
183        }
184        s.push('\n');
185        s
186    };
187
188    output.push_str(&format_group("Text Processing", &text_tools, max_len));
189    output.push_str(&format_group("Files & Directories", &file_tools, max_len));
190    output.push_str(&format_group("JSON", &json_tools, max_len));
191    output.push_str(&format_group("Processes & Jobs", &process_tools, max_len));
192    output.push_str(&format_group("Parallel (散/集)", &parallel_tools, max_len));
193    output.push_str(&format_group("Shell & System", &system_tools, max_len));
194    output.push_str(&format_group("Introspection", &introspection_tools, max_len));
195    output.push_str(&format_group("Other", &other_tools, max_len));
196
197    output.push_str("---\n");
198    output.push_str("Use 'help <tool>' for detailed help on a specific tool.\n");
199    output.push_str("Use 'help syntax' for language syntax reference.\n");
200
201    output
202}
203
204/// List available help topics (for autocomplete, etc.).
205pub fn list_topics() -> Vec<(&'static str, &'static str)> {
206    vec![
207        ("overview", "What kaish is, list of topics"),
208        ("syntax", "Variables, quoting, pipes, control flow"),
209        ("builtins", "List of available builtins"),
210        ("vfs", "Virtual filesystem mounts and paths"),
211        ("scatter", "Parallel processing (散/集)"),
212        ("ignore", "Ignore file configuration"),
213        ("output-limit", "Output size limit configuration"),
214        ("limits", "Known limitations"),
215    ]
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_topic_parsing() {
224        assert_eq!(HelpTopic::parse_topic(""), HelpTopic::Overview);
225        assert_eq!(HelpTopic::parse_topic("overview"), HelpTopic::Overview);
226        assert_eq!(HelpTopic::parse_topic("syntax"), HelpTopic::Syntax);
227        assert_eq!(HelpTopic::parse_topic("SYNTAX"), HelpTopic::Syntax);
228        assert_eq!(HelpTopic::parse_topic("builtins"), HelpTopic::Builtins);
229        assert_eq!(HelpTopic::parse_topic("vfs"), HelpTopic::Vfs);
230        assert_eq!(HelpTopic::parse_topic("scatter"), HelpTopic::Scatter);
231        assert_eq!(HelpTopic::parse_topic("集"), HelpTopic::Scatter);
232        assert_eq!(HelpTopic::parse_topic("output-limit"), HelpTopic::OutputLimit);
233        assert_eq!(HelpTopic::parse_topic("spill"), HelpTopic::OutputLimit);
234        assert_eq!(HelpTopic::parse_topic("kaish-output-limit"), HelpTopic::OutputLimit);
235        assert_eq!(HelpTopic::parse_topic("limits"), HelpTopic::Limits);
236        assert_eq!(
237            HelpTopic::parse_topic("grep"),
238            HelpTopic::Tool("grep".to_string())
239        );
240    }
241
242    #[test]
243    fn test_static_content_embedded() {
244        // Verify the markdown files are embedded
245        assert!(OVERVIEW.contains("kaish"));
246        assert!(SYNTAX.contains("Variables"));
247        assert!(VFS.contains("Mount Points"));
248        assert!(SCATTER.contains("scatter"));
249        assert!(IGNORE.contains("kaish-ignore"));
250        assert!(OUTPUT_LIMIT.contains("kaish-output-limit"));
251        assert!(LIMITS.contains("Limitations"));
252    }
253
254    #[test]
255    fn test_get_help_overview() {
256        let content = get_help(&HelpTopic::Overview, &[]);
257        assert!(content.contains("kaish"));
258        assert!(content.contains("help syntax"));
259    }
260
261    #[test]
262    fn test_get_help_unknown_tool() {
263        let content = get_help(&HelpTopic::Tool("nonexistent".to_string()), &[]);
264        assert!(content.contains("Unknown topic or tool"));
265    }
266}