intent_engine/cli_handlers/
utils.rs

1//! Utility functions for CLI handlers
2//!
3//! Helper functions for reading stdin, formatting status badges, and printing task contexts.
4
5use crate::error::Result;
6use crate::tasks::TaskContext;
7use std::io::{self, Read};
8
9/// Read from stdin with proper encoding handling (especially for Windows PowerShell)
10pub 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        // First try UTF-8
19        if let Ok(s) = String::from_utf8(buffer.clone()) {
20            return Ok(s.trim().to_string());
21        }
22
23        // Fall back to GBK decoding (common in Chinese Windows PowerShell)
24        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            // If GBK also fails, return the UTF-8 lossy version
32            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
47/// Get a status badge icon for task status
48pub fn get_status_badge(status: &str) -> &'static str {
49    match status {
50        "done" => "✓",
51        "doing" => "→",
52        "todo" => "○",
53        _ => "?",
54    }
55}
56
57/// Print task context in a human-friendly tree format
58pub fn print_task_context(ctx: &TaskContext) -> Result<()> {
59    // Print task header
60    let badge = get_status_badge(&ctx.task.status);
61    println!("\n{} Task #{}: {}", badge, ctx.task.id, ctx.task.name);
62    println!("Status: {}", ctx.task.status);
63
64    if let Some(spec) = &ctx.task.spec {
65        println!("\nSpec:");
66        for line in spec.lines() {
67            println!("  {}", line);
68        }
69    }
70
71    // Print parent chain
72    if !ctx.ancestors.is_empty() {
73        println!("\nParent Chain:");
74        for (i, ancestor) in ctx.ancestors.iter().enumerate() {
75            let indent = "  ".repeat(i + 1);
76            let ancestor_badge = get_status_badge(&ancestor.status);
77            println!(
78                "{}└─ {} #{}: {}",
79                indent, ancestor_badge, ancestor.id, ancestor.name
80            );
81        }
82    }
83
84    // Print children
85    if !ctx.children.is_empty() {
86        println!("\nChildren:");
87        for child in &ctx.children {
88            let child_badge = get_status_badge(&child.status);
89            println!("  {} #{}: {}", child_badge, child.id, child.name);
90        }
91    }
92
93    // Print siblings
94    if !ctx.siblings.is_empty() {
95        println!("\nSiblings:");
96        for sibling in &ctx.siblings {
97            let sibling_badge = get_status_badge(&sibling.status);
98            println!("  {} #{}: {}", sibling_badge, sibling.id, sibling.name);
99        }
100    }
101
102    // Print dependencies (blocking tasks)
103    if !ctx.dependencies.blocking_tasks.is_empty() {
104        println!("\nDepends on:");
105        for dep in &ctx.dependencies.blocking_tasks {
106            let dep_badge = get_status_badge(&dep.status);
107            println!("  {} #{}: {}", dep_badge, dep.id, dep.name);
108        }
109    }
110
111    // Print dependents (blocked by tasks)
112    if !ctx.dependencies.blocked_by_tasks.is_empty() {
113        println!("\nBlocks:");
114        for dep in &ctx.dependencies.blocked_by_tasks {
115            let dep_badge = get_status_badge(&dep.status);
116            println!("  {} #{}: {}", dep_badge, dep.id, dep.name);
117        }
118    }
119
120    println!();
121    Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::db::models::{Task, TaskContext, TaskDependencies};
128
129    // Helper function to create a test task with minimal boilerplate
130    fn create_test_task(id: i64, name: &str, status: &str, parent_id: Option<i64>) -> Task {
131        Task {
132            id,
133            name: name.to_string(),
134            status: status.to_string(),
135            spec: None,
136            parent_id,
137            priority: Some(5),
138            complexity: None,
139            first_todo_at: None,
140            first_doing_at: None,
141            first_done_at: None,
142            active_form: None,
143        }
144    }
145
146    #[test]
147    fn test_get_status_badge_done() {
148        assert_eq!(get_status_badge("done"), "✓");
149    }
150
151    #[test]
152    fn test_get_status_badge_doing() {
153        assert_eq!(get_status_badge("doing"), "→");
154    }
155
156    #[test]
157    fn test_get_status_badge_todo() {
158        assert_eq!(get_status_badge("todo"), "○");
159    }
160
161    #[test]
162    fn test_get_status_badge_unknown() {
163        assert_eq!(get_status_badge("unknown"), "?");
164        assert_eq!(get_status_badge(""), "?");
165        assert_eq!(get_status_badge("invalid"), "?");
166    }
167
168    #[test]
169    fn test_print_task_context_basic() {
170        let task = create_test_task(1, "Test Task", "todo", None);
171
172        let ctx = TaskContext {
173            task,
174            ancestors: vec![],
175            children: vec![],
176            siblings: vec![],
177            dependencies: TaskDependencies {
178                blocking_tasks: vec![],
179                blocked_by_tasks: vec![],
180            },
181        };
182
183        // Should not panic and should execute all branches
184        let result = print_task_context(&ctx);
185        assert!(result.is_ok());
186    }
187
188    #[test]
189    fn test_print_task_context_with_spec() {
190        let mut task = create_test_task(2, "Task with Spec", "doing", None);
191        task.spec = Some("This is a\nmulti-line\nspecification".to_string());
192
193        let ctx = TaskContext {
194            task,
195            ancestors: vec![],
196            children: vec![],
197            siblings: vec![],
198            dependencies: TaskDependencies {
199                blocking_tasks: vec![],
200                blocked_by_tasks: vec![],
201            },
202        };
203
204        let result = print_task_context(&ctx);
205        assert!(result.is_ok());
206    }
207
208    #[test]
209    fn test_print_task_context_with_children() {
210        let task = create_test_task(3, "Parent Task", "doing", None);
211        let child1 = create_test_task(4, "Child Task 1", "todo", Some(3));
212        let child2 = create_test_task(5, "Child Task 2", "done", Some(3));
213
214        let ctx = TaskContext {
215            task,
216            ancestors: vec![],
217            children: vec![child1, child2],
218            siblings: vec![],
219            dependencies: TaskDependencies {
220                blocking_tasks: vec![],
221                blocked_by_tasks: vec![],
222            },
223        };
224
225        let result = print_task_context(&ctx);
226        assert!(result.is_ok());
227    }
228
229    #[test]
230    fn test_print_task_context_with_ancestors() {
231        let task = create_test_task(6, "Nested Task", "doing", Some(7));
232        let parent = create_test_task(7, "Parent Task", "doing", None);
233
234        let ctx = TaskContext {
235            task,
236            ancestors: vec![parent],
237            children: vec![],
238            siblings: vec![],
239            dependencies: TaskDependencies {
240                blocking_tasks: vec![],
241                blocked_by_tasks: vec![],
242            },
243        };
244
245        let result = print_task_context(&ctx);
246        assert!(result.is_ok());
247    }
248
249    #[test]
250    fn test_print_task_context_with_dependencies() {
251        let task = create_test_task(8, "Task with Dependencies", "todo", None);
252        let blocker = create_test_task(9, "Blocking Task", "doing", None);
253        let blocked = create_test_task(10, "Blocked Task", "todo", None);
254
255        let ctx = TaskContext {
256            task,
257            ancestors: vec![],
258            children: vec![],
259            siblings: vec![],
260            dependencies: TaskDependencies {
261                blocking_tasks: vec![blocker],
262                blocked_by_tasks: vec![blocked],
263            },
264        };
265
266        let result = print_task_context(&ctx);
267        assert!(result.is_ok());
268    }
269
270    #[test]
271    fn test_print_task_context_with_siblings() {
272        let task = create_test_task(11, "Task with Siblings", "doing", Some(12));
273        let sibling = create_test_task(13, "Sibling Task", "todo", Some(12));
274
275        let ctx = TaskContext {
276            task,
277            ancestors: vec![],
278            children: vec![],
279            siblings: vec![sibling],
280            dependencies: TaskDependencies {
281                blocking_tasks: vec![],
282                blocked_by_tasks: vec![],
283            },
284        };
285
286        let result = print_task_context(&ctx);
287        assert!(result.is_ok());
288    }
289}