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