claude_code_statusline_core/
parser.rs1use crate::error::CoreError;
18use crate::types::claude::ClaudeInput;
19use crate::types::context::Context;
20use std::collections::HashMap;
21
22pub fn parse_claude_input(json_str: &str) -> Result<ClaudeInput, CoreError> {
46 Ok(serde_json::from_str(json_str)?)
47}
48
49pub fn parse_format(
83 format: &str,
84 _context: &Context,
85 module_outputs: &HashMap<String, String>,
86) -> String {
87 let bytes = format.as_bytes();
91 let mut i = 0;
92 let mut out = String::with_capacity(format.len());
93 while i < bytes.len() {
94 if bytes[i] == b'$' {
95 let start = i;
96 let j = i + 1;
97 let mut k = j;
99 if k < bytes.len() {
100 let c = bytes[k] as char;
101 if c.is_ascii_alphabetic() || c == '_' {
102 k += 1;
103 while k < bytes.len() {
105 let c2 = bytes[k] as char;
106 if c2.is_ascii_alphanumeric() || c2 == '_' {
107 k += 1;
108 } else {
109 break;
110 }
111 }
112 let name = &format[j..k];
113 if let Some(val) = module_outputs.get(name) {
115 out.push_str(val);
116 }
117 i = k;
118 continue;
119 }
120 }
121 out.push('$');
123 i = start + 1;
124 } else {
125 out.push(bytes[i] as char);
126 i += 1;
127 }
128 }
129 out.trim_end().to_string()
134}
135
136pub fn extract_modules_from_format(format: &str) -> Vec<String> {
158 use std::collections::HashSet;
161 let bytes = format.as_bytes();
162 let mut i = 0;
163 let mut out: Vec<String> = Vec::new();
164 let mut seen: HashSet<String> = HashSet::new();
165 while i < bytes.len() {
166 if bytes[i] == b'$' {
167 let j = i + 1;
168 let mut k = j;
169 if k < bytes.len() {
170 let c = bytes[k] as char;
171 if c.is_ascii_alphabetic() || c == '_' {
172 k += 1;
173 while k < bytes.len() {
174 let c2 = bytes[k] as char;
175 if c2.is_ascii_alphanumeric() || c2 == '_' {
176 k += 1;
177 } else {
178 break;
179 }
180 }
181 let name = &format[j..k];
182 if seen.insert(name.to_string()) {
183 out.push(name.to_string());
184 }
185 i = k;
186 continue;
187 }
188 }
189 }
190 i += 1;
191 }
192 out
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::config::Config;
199 use crate::types::claude::{ModelInfo, WorkspaceInfo};
200
201 #[test]
202 fn test_extract_modules_from_format() {
203 let format = "$directory $claude_model $character";
204 let modules = extract_modules_from_format(format);
205 assert_eq!(modules, vec!["directory", "claude_model", "character"]);
206 }
207
208 #[test]
209 fn test_extract_modules_from_format_with_extra_text() {
210 let format = "prefix $directory middle $claude_model suffix";
211 let modules = extract_modules_from_format(format);
212 assert_eq!(modules, vec!["directory", "claude_model"]);
213 }
214
215 #[test]
216 fn test_extract_modules_from_format_empty() {
217 let format = "no variables here";
218 let modules = extract_modules_from_format(format);
219 assert_eq!(modules, Vec::<String>::new());
220 }
221
222 #[test]
223 fn test_parse_format() {
224 let input = ClaudeInput {
225 hook_event_name: Some("Status".to_string()),
226 session_id: "test-123".to_string(),
227 transcript_path: Some("/test/transcript.json".to_string()),
228 cwd: "/test/dir".to_string(),
229 model: ModelInfo {
230 id: "claude-opus".to_string(),
231 display_name: "Opus".to_string(),
232 },
233 workspace: Some(WorkspaceInfo {
234 current_dir: "/test/dir".to_string(),
235 project_dir: Some("/test/project".to_string()),
236 }),
237 version: Some("1.0.0".to_string()),
238 output_style: None,
239 };
240
241 let config = Config::default();
242 let context = Context::new(input, config);
243
244 let mut module_outputs = HashMap::new();
245 module_outputs.insert("directory".to_string(), "~/project".to_string());
246 module_outputs.insert("claude_model".to_string(), "Opus".to_string());
247
248 let format = "$directory $claude_model $character";
249 let result = parse_format(format, &context, &module_outputs);
250
251 assert_eq!(result, "~/project Opus");
253 }
254
255 #[test]
256 fn test_parse_format_with_text() {
257 let input = ClaudeInput {
258 hook_event_name: Some("Status".to_string()),
259 session_id: "test-123".to_string(),
260 transcript_path: Some("/test/transcript.json".to_string()),
261 cwd: "/test/dir".to_string(),
262 model: ModelInfo {
263 id: "claude-opus".to_string(),
264 display_name: "Opus".to_string(),
265 },
266 workspace: None,
267 version: Some("1.0.0".to_string()),
268 output_style: None,
269 };
270
271 let config = Config::default();
272 let context = Context::new(input, config);
273
274 let mut module_outputs = HashMap::new();
275 module_outputs.insert("directory".to_string(), "~/project".to_string());
276 module_outputs.insert("character".to_string(), ">".to_string());
277
278 let format = "prefix $directory middle $character suffix";
279 let result = parse_format(format, &context, &module_outputs);
280
281 assert_eq!(result, "prefix ~/project middle > suffix");
282 }
283
284 #[test]
285 fn test_parse_format_edge_cases() {
286 let input = ClaudeInput {
287 hook_event_name: Some("Status".to_string()),
288 session_id: "test-123".to_string(),
289 transcript_path: Some("/test/transcript.json".to_string()),
290 cwd: "/test/dir".to_string(),
291 model: ModelInfo {
292 id: "claude-opus".to_string(),
293 display_name: "Opus".to_string(),
294 },
295 workspace: None,
296 version: Some("1.0.0".to_string()),
297 output_style: None,
298 };
299
300 let config = Config::default();
301 let context = Context::new(input, config);
302
303 let mut module_outputs = HashMap::new();
305 module_outputs.insert("dir".to_string(), "short".to_string());
306 module_outputs.insert("directory".to_string(), "long".to_string());
307
308 let format = "$directory $dir";
310 let result = parse_format(format, &context, &module_outputs);
311 assert_eq!(result, "long short");
312
313 let format = "prefix$directory suffix";
315 let result = parse_format(format, &context, &module_outputs);
316 assert_eq!(result, "prefixlong suffix");
317 }
318
319 #[test]
320 fn test_parse_valid_claude_input() {
321 let json_str = r#"{
322 "hook_event_name": "Status",
323 "session_id": "test-session-123",
324 "transcript_path": "/path/to/transcript.json",
325 "cwd": "/test/directory",
326 "model": {
327 "id": "claude-opus-4-1",
328 "display_name": "Opus"
329 },
330 "workspace": {
331 "current_dir": "/test/directory",
332 "project_dir": "/test/project"
333 },
334 "version": "1.0.0",
335 "output_style": {
336 "name": "default"
337 }
338 }"#;
339
340 let result = parse_claude_input(json_str);
341 assert!(result.is_ok());
342
343 let input = result.unwrap();
344 assert_eq!(input.session_id, "test-session-123");
345 assert_eq!(input.model.display_name, "Opus");
346 assert_eq!(input.cwd, "/test/directory");
347 }
348
349 #[test]
350 fn test_parse_invalid_json() {
351 let invalid_json = "not a valid json";
352 let result = parse_claude_input(invalid_json);
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_parse_missing_required_field() {
358 let json_str = r#"{
360 "hook_event_name": "Status",
361 "session_id": "test-session-123",
362 "transcript_path": "/path/to/transcript.json",
363 "cwd": "/test/directory",
364 "workspace": {
365 "current_dir": "/test/directory",
366 "project_dir": "/test/project"
367 },
368 "version": "1.0.0",
369 "output_style": {
370 "name": "default"
371 }
372 }"#;
373
374 let result = parse_claude_input(json_str);
375 assert!(result.is_err());
376 }
377}