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            owner: "human".to_string(),
144        }
145    }
146
147    #[test]
148    fn test_get_status_badge_done() {
149        assert_eq!(get_status_badge("done"), "✓");
150    }
151
152    #[test]
153    fn test_get_status_badge_doing() {
154        assert_eq!(get_status_badge("doing"), "→");
155    }
156
157    #[test]
158    fn test_get_status_badge_todo() {
159        assert_eq!(get_status_badge("todo"), "○");
160    }
161
162    #[test]
163    fn test_get_status_badge_unknown() {
164        assert_eq!(get_status_badge("unknown"), "?");
165        assert_eq!(get_status_badge(""), "?");
166        assert_eq!(get_status_badge("invalid"), "?");
167    }
168
169    #[test]
170    fn test_print_task_context_basic() {
171        let task = create_test_task(1, "Test Task", "todo", None);
172
173        let ctx = TaskContext {
174            task,
175            ancestors: vec![],
176            children: vec![],
177            siblings: vec![],
178            dependencies: TaskDependencies {
179                blocking_tasks: vec![],
180                blocked_by_tasks: vec![],
181            },
182        };
183
184        // Should not panic and should execute all branches
185        let result = print_task_context(&ctx);
186        assert!(result.is_ok());
187    }
188
189    #[test]
190    fn test_print_task_context_with_spec() {
191        let mut task = create_test_task(2, "Task with Spec", "doing", None);
192        task.spec = Some("This is a\nmulti-line\nspecification".to_string());
193
194        let ctx = TaskContext {
195            task,
196            ancestors: vec![],
197            children: vec![],
198            siblings: vec![],
199            dependencies: TaskDependencies {
200                blocking_tasks: vec![],
201                blocked_by_tasks: vec![],
202            },
203        };
204
205        let result = print_task_context(&ctx);
206        assert!(result.is_ok());
207    }
208
209    #[test]
210    fn test_print_task_context_with_children() {
211        let task = create_test_task(3, "Parent Task", "doing", None);
212        let child1 = create_test_task(4, "Child Task 1", "todo", Some(3));
213        let child2 = create_test_task(5, "Child Task 2", "done", Some(3));
214
215        let ctx = TaskContext {
216            task,
217            ancestors: vec![],
218            children: vec![child1, child2],
219            siblings: vec![],
220            dependencies: TaskDependencies {
221                blocking_tasks: vec![],
222                blocked_by_tasks: vec![],
223            },
224        };
225
226        let result = print_task_context(&ctx);
227        assert!(result.is_ok());
228    }
229
230    #[test]
231    fn test_print_task_context_with_ancestors() {
232        let task = create_test_task(6, "Nested Task", "doing", Some(7));
233        let parent = create_test_task(7, "Parent Task", "doing", None);
234
235        let ctx = TaskContext {
236            task,
237            ancestors: vec![parent],
238            children: vec![],
239            siblings: vec![],
240            dependencies: TaskDependencies {
241                blocking_tasks: vec![],
242                blocked_by_tasks: vec![],
243            },
244        };
245
246        let result = print_task_context(&ctx);
247        assert!(result.is_ok());
248    }
249
250    #[test]
251    fn test_print_task_context_with_dependencies() {
252        let task = create_test_task(8, "Task with Dependencies", "todo", None);
253        let blocker = create_test_task(9, "Blocking Task", "doing", None);
254        let blocked = create_test_task(10, "Blocked Task", "todo", None);
255
256        let ctx = TaskContext {
257            task,
258            ancestors: vec![],
259            children: vec![],
260            siblings: vec![],
261            dependencies: TaskDependencies {
262                blocking_tasks: vec![blocker],
263                blocked_by_tasks: vec![blocked],
264            },
265        };
266
267        let result = print_task_context(&ctx);
268        assert!(result.is_ok());
269    }
270
271    #[test]
272    fn test_print_task_context_with_siblings() {
273        let task = create_test_task(11, "Task with Siblings", "doing", Some(12));
274        let sibling = create_test_task(13, "Sibling Task", "todo", Some(12));
275
276        let ctx = TaskContext {
277            task,
278            ancestors: vec![],
279            children: vec![],
280            siblings: vec![sibling],
281            dependencies: TaskDependencies {
282                blocking_tasks: vec![],
283                blocked_by_tasks: vec![],
284            },
285        };
286
287        let result = print_task_context(&ctx);
288        assert!(result.is_ok());
289    }
290}