show.rs

  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}