Skip to main content

sift_queue/cli/
formatters.rs

1use crate::queue::Item;
2use std::collections::HashSet;
3
4/// Print a one-line summary for an item (used by `sq list`).
5/// Format: {id}  [{status}]  {title_or_description}  {source_types}  {created_at}
6pub fn print_item_summary(item: &Item, pending_ids: Option<&HashSet<String>>) {
7    let display_status = resolve_display_status(item, pending_ids);
8
9    // Tally source types
10    let mut type_counts: Vec<(String, usize)> = Vec::new();
11    for source in &item.sources {
12        if let Some(entry) = type_counts.iter_mut().find(|(t, _)| *t == source.type_) {
13            entry.1 += 1;
14        } else {
15            type_counts.push((source.type_.clone(), 1));
16        }
17    }
18    let source_types: String = type_counts
19        .iter()
20        .map(|(t, c)| {
21            if *c > 1 {
22                format!("{}:{}", t, c)
23            } else {
24                t.clone()
25            }
26        })
27        .collect::<Vec<_>>()
28        .join(",");
29
30    let label = match (&item.title, &item.description) {
31        (Some(t), _) => t.clone(),
32        (None, Some(d)) => d.clone(),
33        (None, None) => String::new(),
34    };
35
36    println!(
37        "{}  [{}]  {}  {}  {}",
38        item.id, display_status, label, source_types, item.created_at
39    );
40}
41
42/// Print detailed view for an item (used by `sq show`).
43pub fn print_item_detail(item: &Item) {
44    println!("Item: {}", item.id);
45    if let Some(ref title) = item.title {
46        println!("Title: {}", title);
47    }
48    if let Some(ref description) = item.description {
49        println!("Description: {}", description);
50    }
51    println!("Status: {}", item.status);
52    println!("Created: {}", item.created_at);
53    println!("Updated: {}", item.updated_at);
54    println!(
55        "Session: {}",
56        item.session_id.as_deref().unwrap_or("none")
57    );
58
59    if !item.blocked_by.is_empty() {
60        println!("Blocked by: {}", item.blocked_by.join(", "));
61    }
62
63    if let Some(ref wt) = item.worktree {
64        let branch = wt.branch.as_deref().unwrap_or("");
65        let path = wt.path.as_deref().unwrap_or("");
66        println!("Worktree: {} {}", branch, path);
67    }
68
69    if let serde_json::Value::Object(ref map) = item.metadata {
70        if !map.is_empty() {
71            println!("Metadata:");
72            for (k, v) in map {
73                println!("  {}: {}", k, v);
74            }
75        }
76    }
77
78    println!("Sources: ({})", item.sources.len());
79    for (i, source) in item.sources.iter().enumerate() {
80        print_source(source, i);
81    }
82}
83
84/// Print a single source entry.
85fn print_source(source: &crate::queue::Source, index: usize) {
86    let location = if let Some(ref path) = source.path {
87        path.clone()
88    } else if source.content.is_some() {
89        "[inline]".to_string()
90    } else {
91        "[empty]".to_string()
92    };
93
94    println!("  [{}] {}: {}", index, source.type_, location);
95
96    if let (Some(ref content), None) = (&source.content, &source.path) {
97        let lines: Vec<&str> = content.lines().collect();
98        let preview: Vec<&str> = lines.iter().take(3).copied().collect();
99        let preview_str = preview.join("\n      ");
100        println!("      {}", preview_str);
101        if lines.len() > 3 {
102            println!("      ...");
103        }
104    }
105}
106
107/// Determine display status (may show "blocked" for pending+blocked items).
108fn resolve_display_status(item: &Item, pending_ids: Option<&HashSet<String>>) -> String {
109    if !item.pending() || !item.blocked() {
110        return item.status.clone();
111    }
112    match pending_ids {
113        None => "blocked".to_string(),
114        Some(ids) => {
115            if item.blocked_by.iter().any(|id| ids.contains(id)) {
116                "blocked".to_string()
117            } else {
118                item.status.clone()
119            }
120        }
121    }
122}