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");
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
66pub 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
83fn 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
121fn format_tool_list(schemas: &[ToolSchema]) -> String {
123 let mut output = String::new();
124
125 output.push_str("# Available Builtins\n\n");
126
127 let max_len = schemas.iter().map(|s| s.name.len()).max().unwrap_or(0);
129
130 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 (散/集)", ¶llel_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
190pub 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 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}