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!("Session: {}", item.session_id.as_deref().unwrap_or("none"));
55
56    if !item.blocked_by.is_empty() {
57        println!("Blocked by: {}", item.blocked_by.join(", "));
58    }
59
60    if let Some(ref wt) = item.worktree {
61        let branch = wt.branch.as_deref().unwrap_or("");
62        let path = wt.path.as_deref().unwrap_or("");
63        println!("Worktree: {} {}", branch, path);
64    }
65
66    if let serde_json::Value::Object(ref map) = item.metadata {
67        if !map.is_empty() {
68            println!("Metadata:");
69            for (k, v) in map {
70                println!("  {}: {}", k, v);
71            }
72        }
73    }
74
75    println!("Sources: ({})", item.sources.len());
76    for (i, source) in item.sources.iter().enumerate() {
77        print_source(source, i);
78    }
79}
80
81/// Print a single source entry.
82fn print_source(source: &crate::queue::Source, index: usize) {
83    let location = if let Some(ref path) = source.path {
84        path.clone()
85    } else if source.content.is_some() {
86        "[inline]".to_string()
87    } else {
88        "[empty]".to_string()
89    };
90
91    println!("  [{}] {}: {}", index, source.type_, location);
92
93    if let (Some(ref content), None) = (&source.content, &source.path) {
94        let lines: Vec<&str> = content.lines().collect();
95        let preview: Vec<&str> = lines.iter().take(3).copied().collect();
96        let preview_str = preview.join("\n      ");
97        println!("      {}", preview_str);
98        if lines.len() > 3 {
99            println!("      ...");
100        }
101    }
102}
103
104/// Determine display status (may show "blocked" for pending+blocked items).
105fn resolve_display_status(item: &Item, pending_ids: Option<&HashSet<String>>) -> String {
106    if !item.pending() || !item.blocked() {
107        return item.status.clone();
108    }
109    match pending_ids {
110        None => "blocked".to_string(),
111        Some(ids) => {
112            if item.blocked_by.iter().any(|id| ids.contains(id)) {
113                "blocked".to_string()
114            } else {
115                item.status.clone()
116            }
117        }
118    }
119}