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 rough category based on name patterns
131    let mut text_tools = Vec::new();
132    let mut file_tools = Vec::new();
133    let mut system_tools = Vec::new();
134    let mut json_tools = Vec::new();
135    let mut other_tools = Vec::new();
136
137    for schema in schemas {
138        let entry = (schema.name.as_str(), schema.description.as_str());
139        match schema.name.as_str() {
140            "grep" | "sed" | "awk" | "cut" | "tr" | "sort" | "uniq" | "wc" | "head" | "tail"
141            | "split" => text_tools.push(entry),
142            "cat" | "ls" | "cd" | "pwd" | "mkdir" | "rm" | "cp" | "mv" | "touch" | "chmod"
143            | "ln" | "readlink" | "write" | "glob" | "find" | "stat" | "dirname" | "basename"
144            | "realpath" => file_tools.push(entry),
145            "echo" | "printf" | "read" | "sleep" | "date" | "env" | "export" | "set" | "unset"
146            | "source" | "exit" | "return" | "break" | "continue" | "true" | "false" | "test"
147            | "help" | "jobs" | "wait" | "kill" | "ps" | "whoami" | "hostname" | "which"
148            | "type" | "hash" | "xargs" | "tee" | "seq" | "validate" => system_tools.push(entry),
149            "jq" => json_tools.push(entry),
150            _ => other_tools.push(entry),
151        }
152    }
153
154    let format_group = |name: &str, tools: &[(&str, &str)], max: usize| -> String {
155        if tools.is_empty() {
156            return String::new();
157        }
158        let mut s = format!("## {}\n\n", name);
159        for (tool_name, desc) in tools {
160            s.push_str(&format!("  {:width$}  {}\n", tool_name, desc, width = max));
161        }
162        s.push('\n');
163        s
164    };
165
166    output.push_str(&format_group("Text Processing", &text_tools, max_len));
167    output.push_str(&format_group("Files & I/O", &file_tools, max_len));
168    output.push_str(&format_group("JSON", &json_tools, max_len));
169    output.push_str(&format_group("System & Shell", &system_tools, max_len));
170    output.push_str(&format_group("Other", &other_tools, max_len));
171
172    output.push_str("---\n");
173    output.push_str("Use 'help <tool>' for detailed help on a specific tool.\n");
174    output.push_str("Use 'help syntax' for language syntax reference.\n");
175
176    output
177}
178
179/// List available help topics (for autocomplete, etc.).
180pub fn list_topics() -> Vec<(&'static str, &'static str)> {
181    vec![
182        ("overview", "What kaish is, list of topics"),
183        ("syntax", "Variables, quoting, pipes, control flow"),
184        ("builtins", "List of available builtins"),
185        ("vfs", "Virtual filesystem mounts and paths"),
186        ("scatter", "Parallel processing (散/集)"),
187        ("limits", "Known limitations"),
188    ]
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_topic_parsing() {
197        assert_eq!(HelpTopic::parse_topic(""), HelpTopic::Overview);
198        assert_eq!(HelpTopic::parse_topic("overview"), HelpTopic::Overview);
199        assert_eq!(HelpTopic::parse_topic("syntax"), HelpTopic::Syntax);
200        assert_eq!(HelpTopic::parse_topic("SYNTAX"), HelpTopic::Syntax);
201        assert_eq!(HelpTopic::parse_topic("builtins"), HelpTopic::Builtins);
202        assert_eq!(HelpTopic::parse_topic("vfs"), HelpTopic::Vfs);
203        assert_eq!(HelpTopic::parse_topic("scatter"), HelpTopic::Scatter);
204        assert_eq!(HelpTopic::parse_topic("集"), HelpTopic::Scatter);
205        assert_eq!(HelpTopic::parse_topic("limits"), HelpTopic::Limits);
206        assert_eq!(
207            HelpTopic::parse_topic("grep"),
208            HelpTopic::Tool("grep".to_string())
209        );
210    }
211
212    #[test]
213    fn test_static_content_embedded() {
214        // Verify the markdown files are embedded
215        assert!(OVERVIEW.contains("kaish"));
216        assert!(SYNTAX.contains("Variables"));
217        assert!(VFS.contains("Mount Modes"));
218        assert!(SCATTER.contains("scatter"));
219        assert!(LIMITS.contains("Limitations"));
220    }
221
222    #[test]
223    fn test_get_help_overview() {
224        let content = get_help(&HelpTopic::Overview, &[]);
225        assert!(content.contains("kaish"));
226        assert!(content.contains("help syntax"));
227    }
228
229    #[test]
230    fn test_get_help_unknown_tool() {
231        let content = get_help(&HelpTopic::Tool("nonexistent".to_string()), &[]);
232        assert!(content.contains("Unknown topic or tool"));
233    }
234}