1use crate::db::models::{EventsSummary, Task, TaskContext};
6use crate::error::{IntentError, Result};
7use std::io::{self, Read};
8
9pub fn read_stdin() -> Result<String> {
11 #[cfg(windows)]
12 {
13 use encoding_rs::GBK;
14
15 let mut buffer = Vec::new();
16 io::stdin().read_to_end(&mut buffer)?;
17
18 if let Ok(s) = String::from_utf8(buffer.clone()) {
20 return Ok(s.trim().to_string());
21 }
22
23 let (decoded, _, had_errors) = GBK.decode(&buffer);
25 if !had_errors {
26 tracing::debug!(
27 "Successfully decoded stdin from GBK encoding (Chinese Windows detected)"
28 );
29 Ok(decoded.trim().to_string())
30 } else {
31 tracing::warn!(
33 "Failed to decode stdin from both UTF-8 and GBK, using lossy UTF-8 conversion"
34 );
35 Ok(String::from_utf8_lossy(&buffer).trim().to_string())
36 }
37 }
38
39 #[cfg(not(windows))]
40 {
41 let mut buffer = String::new();
42 io::stdin().read_to_string(&mut buffer)?;
43 Ok(buffer.trim().to_string())
44 }
45}
46
47pub fn get_status_badge(status: &str) -> &'static str {
49 match status {
50 "done" => "✓",
51 "doing" => "→",
52 "todo" => "○",
53 _ => "?",
54 }
55}
56
57pub fn status_icon(status: &str) -> &'static str {
59 match status {
60 "todo" => "○",
61 "doing" => "●",
62 "done" => "✓",
63 _ => "?",
64 }
65}
66
67pub fn print_task_tree(tasks: &[crate::db::models::Task]) {
69 use std::collections::HashMap;
70
71 let mut children_map: HashMap<Option<i64>, Vec<&crate::db::models::Task>> = HashMap::new();
73 for task in tasks {
74 children_map.entry(task.parent_id).or_default().push(task);
75 }
76
77 fn print_subtree(
78 children_map: &HashMap<Option<i64>, Vec<&crate::db::models::Task>>,
79 parent_id: Option<i64>,
80 indent: &str,
81 ) {
82 if let Some(children) = children_map.get(&parent_id) {
83 for (i, task) in children.iter().enumerate() {
84 let is_last_child = i == children.len() - 1;
85 let connector = if indent.is_empty() {
86 ""
87 } else if is_last_child {
88 "└─ "
89 } else {
90 "├─ "
91 };
92 let icon = status_icon(&task.status);
93 let priority_info = task
94 .priority
95 .map(|p| format!(" [P{}]", p))
96 .unwrap_or_default();
97
98 println!(
99 " {}{}{} #{} {}{}",
100 indent, connector, icon, task.id, task.name, priority_info
101 );
102
103 let new_indent = if indent.is_empty() {
104 "".to_string()
105 } else if is_last_child {
106 format!("{} ", indent)
107 } else {
108 format!("{}│ ", indent)
109 };
110 print_subtree(children_map, Some(task.id), &new_indent);
111 }
112 }
113 }
114
115 let task_ids: std::collections::HashSet<i64> = tasks.iter().map(|t| t.id).collect();
117 let roots: Vec<&crate::db::models::Task> = tasks
118 .iter()
119 .filter(|t| t.parent_id.is_none() || !task_ids.contains(&t.parent_id.unwrap_or(-1)))
120 .collect();
121
122 for task in &roots {
123 let icon = status_icon(&task.status);
124 let priority_info = task
125 .priority
126 .map(|p| format!(" [P{}]", p))
127 .unwrap_or_default();
128 println!(" {} #{} {}{}", icon, task.id, task.name, priority_info);
129 print_subtree(&children_map, Some(task.id), " ");
130 }
131}
132
133pub fn print_task_summary(task: &Task) {
135 let icon = status_icon(&task.status);
136 println!(" {} #{} {}", icon, task.id, task.name);
137 println!(" Status: {}", task.status);
138 if let Some(pid) = task.parent_id {
139 println!(" Parent: #{}", pid);
140 }
141 if let Some(p) = task.priority {
142 println!(" Priority: {}", p);
143 }
144 if let Some(spec) = &task.spec {
145 if !spec.is_empty() {
146 println!(" Spec: {}", spec);
147 }
148 }
149 println!(" Owner: {}", task.owner);
150 if let Some(af) = &task.active_form {
151 println!(" Active form: {}", af);
152 }
153 if let Some(meta) = &task.metadata {
154 println!(" Metadata: {}", meta);
155 }
156}
157
158pub fn print_task_context(ctx: &TaskContext) {
160 let icon = status_icon(&ctx.task.status);
161 println!("\n{} Task #{}: {}", icon, ctx.task.id, ctx.task.name);
162 println!("Status: {}", ctx.task.status);
163
164 if let Some(spec) = &ctx.task.spec {
165 println!("\nSpec:");
166 for line in spec.lines() {
167 println!(" {}", line);
168 }
169 }
170
171 if !ctx.ancestors.is_empty() {
173 println!("\nParent Chain:");
174 for (i, ancestor) in ctx.ancestors.iter().enumerate() {
175 let indent = " ".repeat(i + 1);
176 println!(
177 "{}└─ {} #{}: {}",
178 indent,
179 status_icon(&ancestor.status),
180 ancestor.id,
181 ancestor.name
182 );
183 }
184 }
185
186 if !ctx.children.is_empty() {
188 println!("\nChildren:");
189 for child in &ctx.children {
190 println!(
191 " {} #{}: {}",
192 status_icon(&child.status),
193 child.id,
194 child.name
195 );
196 }
197 }
198
199 if !ctx.siblings.is_empty() {
201 println!("\nSiblings:");
202 for sibling in &ctx.siblings {
203 println!(
204 " {} #{}: {}",
205 status_icon(&sibling.status),
206 sibling.id,
207 sibling.name
208 );
209 }
210 }
211
212 if !ctx.dependencies.blocking_tasks.is_empty() {
214 println!("\nDepends on:");
215 for dep in &ctx.dependencies.blocking_tasks {
216 println!(" {} #{}: {}", status_icon(&dep.status), dep.id, dep.name);
217 }
218 }
219
220 if !ctx.dependencies.blocked_by_tasks.is_empty() {
222 println!("\nBlocks:");
223 for dep in &ctx.dependencies.blocked_by_tasks {
224 println!(" {} #{}: {}", status_icon(&dep.status), dep.id, dep.name);
225 }
226 }
227
228 println!();
229}
230
231pub fn print_events_summary(summary: &EventsSummary) {
233 println!("Events ({}):", summary.total_count);
234 for event in summary.recent_events.iter().take(10) {
235 println!(
236 " [{}] {} — {}",
237 event.log_type,
238 event.timestamp.format("%Y-%m-%d %H:%M:%S"),
239 event.discussion_data
240 );
241 }
242}
243
244pub fn parse_task_id_query(query: &str) -> Option<i64> {
247 let query = query.trim();
248 if !query.starts_with('#') || query.len() < 2 {
249 return None;
250 }
251 query[1..].parse::<i64>().ok()
252}
253
254pub fn parse_status_keywords(query: &str) -> Option<Vec<String>> {
257 let query_lower = query.to_lowercase();
258 let words: Vec<&str> = query_lower.split_whitespace().collect();
259
260 if words.is_empty() {
261 return None;
262 }
263
264 let valid_statuses = ["todo", "doing", "done"];
265 let mut statuses: Vec<String> = Vec::new();
266
267 for word in words {
268 if valid_statuses.contains(&word) {
269 if !statuses.iter().any(|s| s == word) {
270 statuses.push(word.to_string());
271 }
272 } else {
273 return None;
274 }
275 }
276
277 Some(statuses)
278}
279
280pub fn parse_metadata(pairs: &[String]) -> Result<serde_json::Value> {
283 let mut map = serde_json::Map::new();
284 for pair in pairs {
285 if let Some(eq_pos) = pair.find('=') {
286 let key = pair[..eq_pos].trim().to_string();
287 let value = pair[eq_pos + 1..].trim().to_string();
288 if key.is_empty() {
289 return Err(IntentError::InvalidInput(format!(
290 "Invalid metadata: empty key in '{}'",
291 pair
292 )));
293 }
294 if value.is_empty() {
295 map.insert(key, serde_json::Value::Null);
297 } else {
298 map.insert(key, serde_json::Value::String(value));
299 }
300 } else {
301 return Err(IntentError::InvalidInput(format!(
302 "Invalid metadata format: '{}'. Expected 'key=value'",
303 pair
304 )));
305 }
306 }
307 Ok(serde_json::Value::Object(map))
308}
309
310pub fn merge_metadata(existing: Option<&str>, new_meta: &serde_json::Value) -> Option<String> {
313 let mut base: serde_json::Map<String, serde_json::Value> = existing
314 .and_then(|s| serde_json::from_str(s).ok())
315 .unwrap_or_default();
316
317 if let serde_json::Value::Object(new_map) = new_meta {
318 for (key, value) in new_map {
319 if value.is_null() {
320 base.remove(key);
321 } else {
322 base.insert(key.clone(), value.clone());
323 }
324 }
325 }
326
327 if base.is_empty() {
328 None
329 } else {
330 Some(serde_json::to_string(&base).unwrap_or_default())
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::db::models::{Task, TaskContext, TaskDependencies};
338
339 fn create_test_task(id: i64, name: &str, status: &str, parent_id: Option<i64>) -> Task {
341 Task {
342 id,
343 name: name.to_string(),
344 status: status.to_string(),
345 spec: None,
346 parent_id,
347 priority: Some(5),
348 complexity: None,
349 first_todo_at: None,
350 first_doing_at: None,
351 first_done_at: None,
352 active_form: None,
353 owner: "human".to_string(),
354 metadata: None,
355 }
356 }
357
358 #[test]
359 fn test_get_status_badge_done() {
360 assert_eq!(get_status_badge("done"), "✓");
361 }
362
363 #[test]
364 fn test_get_status_badge_doing() {
365 assert_eq!(get_status_badge("doing"), "→");
366 }
367
368 #[test]
369 fn test_get_status_badge_todo() {
370 assert_eq!(get_status_badge("todo"), "○");
371 }
372
373 #[test]
374 fn test_get_status_badge_unknown() {
375 assert_eq!(get_status_badge("unknown"), "?");
376 assert_eq!(get_status_badge(""), "?");
377 assert_eq!(get_status_badge("invalid"), "?");
378 }
379
380 #[test]
381 fn test_status_icon() {
382 assert_eq!(status_icon("todo"), "○");
383 assert_eq!(status_icon("doing"), "●");
384 assert_eq!(status_icon("done"), "✓");
385 assert_eq!(status_icon("unknown"), "?");
386 }
387
388 #[test]
389 fn test_print_task_context_basic() {
390 let task = create_test_task(1, "Test Task", "todo", None);
391
392 let ctx = TaskContext {
393 task,
394 ancestors: vec![],
395 children: vec![],
396 siblings: vec![],
397 dependencies: TaskDependencies {
398 blocking_tasks: vec![],
399 blocked_by_tasks: vec![],
400 },
401 };
402
403 print_task_context(&ctx); }
406
407 #[test]
408 fn test_print_task_context_with_spec() {
409 let mut task = create_test_task(2, "Task with Spec", "doing", None);
410 task.spec = Some("This is a\nmulti-line\nspecification".to_string());
411
412 let ctx = TaskContext {
413 task,
414 ancestors: vec![],
415 children: vec![],
416 siblings: vec![],
417 dependencies: TaskDependencies {
418 blocking_tasks: vec![],
419 blocked_by_tasks: vec![],
420 },
421 };
422
423 print_task_context(&ctx); }
425
426 #[test]
427 fn test_print_task_context_with_children() {
428 let task = create_test_task(3, "Parent Task", "doing", None);
429 let child1 = create_test_task(4, "Child Task 1", "todo", Some(3));
430 let child2 = create_test_task(5, "Child Task 2", "done", Some(3));
431
432 let ctx = TaskContext {
433 task,
434 ancestors: vec![],
435 children: vec![child1, child2],
436 siblings: vec![],
437 dependencies: TaskDependencies {
438 blocking_tasks: vec![],
439 blocked_by_tasks: vec![],
440 },
441 };
442
443 print_task_context(&ctx); }
445
446 #[test]
447 fn test_print_task_context_with_ancestors() {
448 let task = create_test_task(6, "Nested Task", "doing", Some(7));
449 let parent = create_test_task(7, "Parent Task", "doing", None);
450
451 let ctx = TaskContext {
452 task,
453 ancestors: vec![parent],
454 children: vec![],
455 siblings: vec![],
456 dependencies: TaskDependencies {
457 blocking_tasks: vec![],
458 blocked_by_tasks: vec![],
459 },
460 };
461
462 print_task_context(&ctx); }
464
465 #[test]
466 fn test_print_task_context_with_dependencies() {
467 let task = create_test_task(8, "Task with Dependencies", "todo", None);
468 let blocker = create_test_task(9, "Blocking Task", "doing", None);
469 let blocked = create_test_task(10, "Blocked Task", "todo", None);
470
471 let ctx = TaskContext {
472 task,
473 ancestors: vec![],
474 children: vec![],
475 siblings: vec![],
476 dependencies: TaskDependencies {
477 blocking_tasks: vec![blocker],
478 blocked_by_tasks: vec![blocked],
479 },
480 };
481
482 print_task_context(&ctx); }
484
485 #[test]
486 fn test_print_task_context_with_siblings() {
487 let task = create_test_task(11, "Task with Siblings", "doing", Some(12));
488 let sibling = create_test_task(13, "Sibling Task", "todo", Some(12));
489
490 let ctx = TaskContext {
491 task,
492 ancestors: vec![],
493 children: vec![],
494 siblings: vec![sibling],
495 dependencies: TaskDependencies {
496 blocking_tasks: vec![],
497 blocked_by_tasks: vec![],
498 },
499 };
500
501 print_task_context(&ctx); }
503
504 #[test]
509 fn test_parse_task_id_query_valid() {
510 assert_eq!(parse_task_id_query("#1"), Some(1));
511 assert_eq!(parse_task_id_query("#123"), Some(123));
512 assert_eq!(parse_task_id_query("#999999"), Some(999999));
513 }
514
515 #[test]
516 fn test_parse_task_id_query_with_whitespace() {
517 assert_eq!(parse_task_id_query(" #1 "), Some(1));
518 assert_eq!(parse_task_id_query("\t#42\n"), Some(42));
519 }
520
521 #[test]
522 fn test_parse_task_id_query_invalid() {
523 assert_eq!(parse_task_id_query("123"), None);
524 assert_eq!(parse_task_id_query("task"), None);
525 assert_eq!(parse_task_id_query("#"), None);
526 assert_eq!(parse_task_id_query("#abc"), None);
527 assert_eq!(parse_task_id_query("#1a"), None);
528 assert_eq!(parse_task_id_query("#a1"), None);
529 assert_eq!(parse_task_id_query("#123 task"), None);
530 assert_eq!(parse_task_id_query("task #123"), None);
531 assert_eq!(parse_task_id_query("#-1"), Some(-1));
532 assert_eq!(parse_task_id_query(""), None);
533 }
534
535 #[test]
540 fn test_parse_status_keywords_valid() {
541 assert_eq!(
542 parse_status_keywords("todo"),
543 Some(vec!["todo".to_string()])
544 );
545 assert_eq!(
546 parse_status_keywords("doing"),
547 Some(vec!["doing".to_string()])
548 );
549 assert_eq!(
550 parse_status_keywords("done"),
551 Some(vec!["done".to_string()])
552 );
553 }
554
555 #[test]
556 fn test_parse_status_keywords_multiple() {
557 let result = parse_status_keywords("todo doing");
558 assert!(result.is_some());
559 let statuses = result.unwrap();
560 assert!(statuses.contains(&"todo".to_string()));
561 assert!(statuses.contains(&"doing".to_string()));
562 }
563
564 #[test]
565 fn test_parse_status_keywords_case_insensitive() {
566 assert_eq!(
567 parse_status_keywords("TODO"),
568 Some(vec!["todo".to_string()])
569 );
570 assert_eq!(
571 parse_status_keywords("DoInG"),
572 Some(vec!["doing".to_string()])
573 );
574 }
575
576 #[test]
577 fn test_parse_status_keywords_invalid() {
578 assert_eq!(parse_status_keywords("todo task"), None);
579 assert_eq!(parse_status_keywords("search term"), None);
580 assert_eq!(parse_status_keywords(""), None);
581 assert_eq!(parse_status_keywords(" "), None);
582 }
583
584 #[test]
585 fn test_parse_status_keywords_dedup() {
586 let result = parse_status_keywords("todo todo todo");
587 assert!(result.is_some());
588 let statuses = result.unwrap();
589 assert_eq!(statuses.len(), 1);
590 assert_eq!(statuses[0], "todo");
591 }
592}