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 flat alphabetical list of all available tools.
134///
135/// Schemas arrive sorted from the registry; only registered tools appear,
136/// so feature-gated or unloaded builtins are omitted naturally.
137fn format_tool_list(schemas: &[ToolSchema]) -> String {
138    let mut output = String::from("# Available Builtins\n\n");
139
140    let max_len = schemas.iter().map(|s| s.name.len()).max().unwrap_or(0);
141
142    for schema in schemas {
143        output.push_str(&format!(
144            "  {:width$}  {}\n",
145            schema.name,
146            schema.description,
147            width = max_len
148        ));
149    }
150
151    output.push_str("\n---\n");
152    output.push_str("Use 'help <tool>' for detailed help on a specific tool.\n");
153    output.push_str("Use 'help syntax' for language syntax reference.\n");
154
155    output
156}
157
158/// List available help topics (for autocomplete, etc.).
159pub fn list_topics() -> Vec<(&'static str, &'static str)> {
160    vec![
161        ("overview", "What kaish is, list of topics"),
162        ("syntax", "Variables, quoting, pipes, control flow"),
163        ("builtins", "List of available builtins"),
164        ("vfs", "Virtual filesystem mounts and paths"),
165        ("scatter", "Parallel processing (散/集)"),
166        ("ignore", "Ignore file configuration"),
167        ("output-limit", "Output size limit configuration"),
168        ("limits", "Known limitations"),
169    ]
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_topic_parsing() {
178        assert_eq!(HelpTopic::parse_topic(""), HelpTopic::Overview);
179        assert_eq!(HelpTopic::parse_topic("overview"), HelpTopic::Overview);
180        assert_eq!(HelpTopic::parse_topic("syntax"), HelpTopic::Syntax);
181        assert_eq!(HelpTopic::parse_topic("SYNTAX"), HelpTopic::Syntax);
182        assert_eq!(HelpTopic::parse_topic("builtins"), HelpTopic::Builtins);
183        assert_eq!(HelpTopic::parse_topic("vfs"), HelpTopic::Vfs);
184        assert_eq!(HelpTopic::parse_topic("scatter"), HelpTopic::Scatter);
185        assert_eq!(HelpTopic::parse_topic("集"), HelpTopic::Scatter);
186        assert_eq!(HelpTopic::parse_topic("output-limit"), HelpTopic::OutputLimit);
187        assert_eq!(HelpTopic::parse_topic("spill"), HelpTopic::OutputLimit);
188        assert_eq!(HelpTopic::parse_topic("kaish-output-limit"), HelpTopic::OutputLimit);
189        assert_eq!(HelpTopic::parse_topic("limits"), HelpTopic::Limits);
190        assert_eq!(
191            HelpTopic::parse_topic("grep"),
192            HelpTopic::Tool("grep".to_string())
193        );
194    }
195
196    #[test]
197    fn test_static_content_embedded() {
198        // Verify the markdown files are embedded
199        assert!(OVERVIEW.contains("kaish"));
200        assert!(SYNTAX.contains("Variables"));
201        assert!(VFS.contains("Mount Points"));
202        assert!(SCATTER.contains("scatter"));
203        assert!(IGNORE.contains("kaish-ignore"));
204        assert!(OUTPUT_LIMIT.contains("kaish-output-limit"));
205        assert!(LIMITS.contains("Limitations"));
206    }
207
208    #[test]
209    fn test_get_help_overview() {
210        let content = get_help(&HelpTopic::Overview, &[]);
211        assert!(content.contains("kaish"));
212        assert!(content.contains("help syntax"));
213    }
214
215    #[test]
216    fn test_get_help_unknown_tool() {
217        let content = get_help(&HelpTopic::Tool("nonexistent".to_string()), &[]);
218        assert!(content.contains("Unknown topic or tool"));
219    }
220}