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