1use crate::tools::ToolSchema;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum HelpTopic {
11 Overview,
13 Syntax,
15 Builtins,
17 Vfs,
19 Scatter,
21 Limits,
23 Tool(String),
25}
26
27impl HelpTopic {
28 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 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
58const 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
65pub 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
82fn 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
120fn format_tool_list(schemas: &[ToolSchema]) -> String {
122 let mut output = String::new();
123
124 output.push_str("# Available Builtins\n\n");
125
126 let max_len = schemas.iter().map(|s| s.name.len()).max().unwrap_or(0);
128
129 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
178pub 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 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}