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.
59const OVERVIEW: &str = include_str!("../docs/help/overview.md");
60const SYNTAX: &str = include_str!("../docs/help/syntax.md");
61const VFS: &str = include_str!("../docs/help/vfs.md");
62const SCATTER: &str = include_str!("../docs/help/scatter.md");
63const LIMITS: &str = include_str!("../docs/help/limits.md");
64
65/// Get help content for a topic.
66///
67/// For static topics, returns embedded markdown.
68/// For `Builtins`, generates a tool list from the provided schemas.
69/// For `Tool(name)`, looks up the tool in the schemas.
70pub fn get_help(topic: &HelpTopic, tool_schemas: &[ToolSchema]) -> String {
71    match topic {
72        HelpTopic::Overview => OVERVIEW.to_string(),
73        HelpTopic::Syntax => SYNTAX.to_string(),
74        HelpTopic::Builtins => format_tool_list(tool_schemas),
75        HelpTopic::Vfs => VFS.to_string(),
76        HelpTopic::Scatter => SCATTER.to_string(),
77        HelpTopic::Limits => LIMITS.to_string(),
78        HelpTopic::Tool(name) => format_tool_help(name, tool_schemas),
79    }
80}
81
82/// Format help for a single tool.
83fn format_tool_help(name: &str, schemas: &[ToolSchema]) -> String {
84    match schemas.iter().find(|s| s.name == name) {
85        Some(schema) => {
86            let mut output = String::new();
87
88            output.push_str(&format!("{} — {}\n\n", schema.name, schema.description));
89
90            if schema.params.is_empty() {
91                output.push_str("No parameters.\n");
92            } else {
93                output.push_str("Parameters:\n");
94                for param in &schema.params {
95                    let req = if param.required { " (required)" } else { "" };
96                    output.push_str(&format!(
97                        "  {} : {}{}\n    {}\n",
98                        param.name, param.param_type, req, param.description
99                    ));
100                }
101            }
102
103            if !schema.examples.is_empty() {
104                output.push_str("\nExamples:\n");
105                for example in &schema.examples {
106                    output.push_str(&format!("  # {}\n", example.description));
107                    output.push_str(&format!("  {}\n\n", example.code));
108                }
109            }
110
111            output
112        }
113        None => format!(
114            "Unknown topic or tool: {}\n\nUse 'help' to see available topics, or 'help builtins' for tool list.",
115            name
116        ),
117    }
118}
119
120/// Format a list of all tools grouped by category.
121fn format_tool_list(schemas: &[ToolSchema]) -> String {
122    let mut output = String::new();
123
124    output.push_str("# Available Builtins\n\n");
125
126    // Find max name length for alignment
127    let max_len = schemas.iter().map(|s| s.name.len()).max().unwrap_or(0);
128
129    // Group tools by rough category based on name patterns
130    let mut text_tools = Vec::new();
131    let mut file_tools = Vec::new();
132    let mut system_tools = Vec::new();
133    let mut json_tools = Vec::new();
134    let mut other_tools = Vec::new();
135
136    for schema in schemas {
137        let entry = (schema.name.as_str(), schema.description.as_str());
138        match schema.name.as_str() {
139            "grep" | "sed" | "awk" | "cut" | "tr" | "sort" | "uniq" | "wc" | "head" | "tail"
140            | "split" => text_tools.push(entry),
141            "cat" | "ls" | "cd" | "pwd" | "mkdir" | "rm" | "cp" | "mv" | "touch" | "chmod"
142            | "ln" | "readlink" | "write" | "glob" | "find" | "stat" | "dirname" | "basename"
143            | "realpath" => file_tools.push(entry),
144            "echo" | "printf" | "read" | "sleep" | "date" | "env" | "export" | "set" | "unset"
145            | "source" | "exit" | "return" | "break" | "continue" | "true" | "false" | "test"
146            | "help" | "jobs" | "wait" | "kill" | "ps" | "whoami" | "hostname" | "which"
147            | "type" | "hash" | "xargs" | "tee" | "seq" | "validate" => system_tools.push(entry),
148            "jq" => json_tools.push(entry),
149            _ => other_tools.push(entry),
150        }
151    }
152
153    let format_group = |name: &str, tools: &[(&str, &str)], max: usize| -> String {
154        if tools.is_empty() {
155            return String::new();
156        }
157        let mut s = format!("## {}\n\n", name);
158        for (tool_name, desc) in tools {
159            s.push_str(&format!("  {:width$}  {}\n", tool_name, desc, width = max));
160        }
161        s.push('\n');
162        s
163    };
164
165    output.push_str(&format_group("Text Processing", &text_tools, max_len));
166    output.push_str(&format_group("Files & I/O", &file_tools, max_len));
167    output.push_str(&format_group("JSON", &json_tools, max_len));
168    output.push_str(&format_group("System & Shell", &system_tools, max_len));
169    output.push_str(&format_group("Other", &other_tools, max_len));
170
171    output.push_str("---\n");
172    output.push_str("Use 'help <tool>' for detailed help on a specific tool.\n");
173    output.push_str("Use 'help syntax' for language syntax reference.\n");
174
175    output
176}
177
178/// List available help topics (for autocomplete, etc.).
179pub fn list_topics() -> Vec<(&'static str, &'static str)> {
180    vec![
181        ("overview", "What kaish is, list of topics"),
182        ("syntax", "Variables, quoting, pipes, control flow"),
183        ("builtins", "List of available builtins"),
184        ("vfs", "Virtual filesystem mounts and paths"),
185        ("scatter", "Parallel processing (散/集)"),
186        ("limits", "Known limitations"),
187    ]
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_topic_parsing() {
196        assert_eq!(HelpTopic::parse_topic(""), HelpTopic::Overview);
197        assert_eq!(HelpTopic::parse_topic("overview"), HelpTopic::Overview);
198        assert_eq!(HelpTopic::parse_topic("syntax"), HelpTopic::Syntax);
199        assert_eq!(HelpTopic::parse_topic("SYNTAX"), HelpTopic::Syntax);
200        assert_eq!(HelpTopic::parse_topic("builtins"), HelpTopic::Builtins);
201        assert_eq!(HelpTopic::parse_topic("vfs"), HelpTopic::Vfs);
202        assert_eq!(HelpTopic::parse_topic("scatter"), HelpTopic::Scatter);
203        assert_eq!(HelpTopic::parse_topic("集"), HelpTopic::Scatter);
204        assert_eq!(HelpTopic::parse_topic("limits"), HelpTopic::Limits);
205        assert_eq!(
206            HelpTopic::parse_topic("grep"),
207            HelpTopic::Tool("grep".to_string())
208        );
209    }
210
211    #[test]
212    fn test_static_content_embedded() {
213        // Verify the markdown files are embedded
214        assert!(OVERVIEW.contains("kaish"));
215        assert!(SYNTAX.contains("Variables"));
216        assert!(VFS.contains("Mount Modes"));
217        assert!(SCATTER.contains("scatter"));
218        assert!(LIMITS.contains("Limitations"));
219    }
220
221    #[test]
222    fn test_get_help_overview() {
223        let content = get_help(&HelpTopic::Overview, &[]);
224        assert!(content.contains("kaish"));
225        assert!(content.contains("help syntax"));
226    }
227
228    #[test]
229    fn test_get_help_unknown_tool() {
230        let content = get_help(&HelpTopic::Tool("nonexistent".to_string()), &[]);
231        assert!(content.contains("Unknown topic or tool"));
232    }
233}