1use anyhow::{bail, Result};
2use std::path::Path;
3
4use crate::db;
5
6pub fn run(root: &Path, id: &str, json: bool) -> Result<()> {
7 let conn = db::open(root)?;
8
9 let exists: bool = conn.query_row("SELECT COUNT(*) FROM tasks WHERE id = ?1", [id], |r| {
10 r.get::<_, i64>(0).map(|n| n > 0)
11 })?;
12
13 if !exists {
14 bail!("task {id} not found");
15 }
16
17 let detail = db::load_task_detail(&conn, id)?;
18
19 if json {
20 println!("{}", serde_json::to_string(&detail)?);
21 return Ok(());
22 }
23
24 let c = crate::color::stdout_theme();
25 let t = &detail.task;
26
27 // Title as a heading with status tag
28 println!(
29 "{}# {}{} {}[{}]{}",
30 c.bold, t.title, c.reset, c.yellow, t.status, c.reset
31 );
32
33 // Description as body text, only when present
34 if !t.description.is_empty() {
35 println!();
36 println!("{}", t.description);
37 }
38
39 // Metadata line: id 路 type 路 priority 路 effort
40 println!();
41 println!(
42 "{}{}{} 路 {} 路 {}{}{} priority 路 {}{}{} effort",
43 c.bold,
44 t.id,
45 c.reset,
46 t.task_type,
47 c.red,
48 db::priority_label(t.priority),
49 c.reset,
50 c.blue,
51 db::effort_label(t.effort),
52 c.reset,
53 );
54
55 // Labels, only when present
56 if !detail.labels.is_empty() {
57 println!("labels: {}", detail.labels.join(", "));
58 }
59
60 // Blockers, only when present
61 let (open_blockers, closed_blockers) = db::load_blockers_partitioned(&conn, &t.id)?;
62 let total = open_blockers.len() + closed_blockers.len();
63 if total > 0 {
64 let label = if total == 1 { "blocker" } else { "blockers" };
65 let mut ids: Vec<String> = Vec::new();
66 for id in &open_blockers {
67 ids.push(id.clone());
68 }
69 for id in &closed_blockers {
70 ids.push(format!("{id} [closed]"));
71 }
72
73 let value = if open_blockers.is_empty() {
74 format!("[all closed] {}", ids.join(", "))
75 } else {
76 ids.join(", ")
77 };
78
79 println!("{label}: {value}");
80 }
81
82 // Timestamps at the bottom
83 println!("created {} 路 updated {}", t.created, t.updated);
84
85 Ok(())
86}