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        use std::io::Read;
15
16        let mut buffer = Vec::new();
17        io::stdin().read_to_end(&mut buffer)?;
18
19        // First try UTF-8
20        if let Ok(s) = String::from_utf8(buffer.clone()) {
21            return Ok(s.trim().to_string());
22        }
23
24        // Fall back to GBK decoding (common in Chinese Windows PowerShell)
25        let (decoded, _, had_errors) = GBK.decode(&buffer);
26        if !had_errors {
27            tracing::debug!(
28                "Successfully decoded stdin from GBK encoding (Chinese Windows detected)"
29            );
30            Ok(decoded.trim().to_string())
31        } else {
32            // If GBK also fails, return the UTF-8 lossy version
33            tracing::warn!(
34                "Failed to decode stdin from both UTF-8 and GBK, using lossy UTF-8 conversion"
35            );
36            Ok(String::from_utf8_lossy(&buffer).trim().to_string())
37        }
38    }
39
40    #[cfg(not(windows))]
41    {
42        let mut buffer = String::new();
43        io::stdin().read_to_string(&mut buffer)?;
44        Ok(buffer.trim().to_string())
45    }
46}
47
48/// Get a status badge icon for task status
49pub fn get_status_badge(status: &str) -> &'static str {
50    match status {
51        "done" => "✓",
52        "doing" => "→",
53        "todo" => "○",
54        _ => "?",
55    }
56}
57
58/// Print task context in a human-friendly tree format
59pub fn print_task_context(ctx: &TaskContext) -> Result<()> {
60    // Print task header
61    let badge = get_status_badge(&ctx.task.status);
62    println!("\n{} Task #{}: {}", badge, ctx.task.id, ctx.task.name);
63    println!("Status: {}", ctx.task.status);
64
65    if let Some(spec) = &ctx.task.spec {
66        println!("\nSpec:");
67        for line in spec.lines() {
68            println!("  {}", line);
69        }
70    }
71
72    // Print parent chain
73    if !ctx.ancestors.is_empty() {
74        println!("\nParent Chain:");
75        for (i, ancestor) in ctx.ancestors.iter().enumerate() {
76            let indent = "  ".repeat(i + 1);
77            let ancestor_badge = get_status_badge(&ancestor.status);
78            println!(
79                "{}└─ {} #{}: {}",
80                indent, ancestor_badge, ancestor.id, ancestor.name
81            );
82        }
83    }
84
85    // Print children
86    if !ctx.children.is_empty() {
87        println!("\nChildren:");
88        for child in &ctx.children {
89            let child_badge = get_status_badge(&child.status);
90            println!("  {} #{}: {}", child_badge, child.id, child.name);
91        }
92    }
93
94    // Print siblings
95    if !ctx.siblings.is_empty() {
96        println!("\nSiblings:");
97        for sibling in &ctx.siblings {
98            let sibling_badge = get_status_badge(&sibling.status);
99            println!("  {} #{}: {}", sibling_badge, sibling.id, sibling.name);
100        }
101    }
102
103    // Print dependencies (blocking tasks)
104    if !ctx.dependencies.blocking_tasks.is_empty() {
105        println!("\nDepends on:");
106        for dep in &ctx.dependencies.blocking_tasks {
107            let dep_badge = get_status_badge(&dep.status);
108            println!("  {} #{}: {}", dep_badge, dep.id, dep.name);
109        }
110    }
111
112    // Print dependents (blocked by tasks)
113    if !ctx.dependencies.blocked_by_tasks.is_empty() {
114        println!("\nBlocks:");
115        for dep in &ctx.dependencies.blocked_by_tasks {
116            let dep_badge = get_status_badge(&dep.status);
117            println!("  {} #{}: {}", dep_badge, dep.id, dep.name);
118        }
119    }
120
121    println!();
122    Ok(())
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::db::models::{Task, TaskContext, TaskDependencies};
129
130    // Helper function to create a test task with minimal boilerplate
131    fn create_test_task(id: i64, name: &str, status: &str, parent_id: Option<i64>) -> Task {
132        Task {
133            id,
134            name: name.to_string(),
135            status: status.to_string(),
136            spec: None,
137            parent_id,
138            priority: Some(5),
139            complexity: None,
140            first_todo_at: None,
141            first_doing_at: None,
142            first_done_at: None,
143            active_form: None,
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}