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();
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
179pub 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 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}