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    let priority = item
37        .priority
38        .map(|value| format!("  [priority:{}]", value))
39        .unwrap_or_default();
40
41    println!(
42        "{}  [{}]{}  {}  {}  {}",
43        item.id, display_status, priority, label, source_types, item.created_at
44    );
45}
46
47/// Print detailed view for an item (used by `sq show`).
48pub fn print_item_detail(item: &Item) {
49    println!("Item: {}", item.id);
50    if let Some(ref title) = item.title {
51        println!("Title: {}", title);
52    }
53    if let Some(ref description) = item.description {
54        println!("Description: {}", description);
55    }
56    println!("Status: {}", item.status);
57    if let Some(priority) = item.priority {
58        println!("Priority: {}", priority);
59    }
60    println!("Created: {}", item.created_at);
61    println!("Updated: {}", item.updated_at);
62
63    if !item.blocked_by.is_empty() {
64        println!("Blocked by: {}", item.blocked_by.join(", "));
65    }
66
67    if let serde_json::Value::Object(ref map) = item.metadata {
68        if !map.is_empty() {
69            println!("Metadata:");
70            for (k, v) in map {
71                println!("  {}: {}", k, v);
72            }
73        }
74    }
75
76    println!("Sources: ({})", item.sources.len());
77    for (i, source) in item.sources.iter().enumerate() {
78        print_source(source, i);
79    }
80}
81
82/// Print a single source entry.
83fn print_source(source: &crate::queue::Source, index: usize) {
84    let location = if let Some(ref path) = source.path {
85        path.clone()
86    } else if source.content.is_some() {
87        "[inline]".to_string()
88    } else {
89        "[empty]".to_string()
90    };
91
92    println!("  [{}] {}: {}", index, source.type_, location);
93
94    if let (Some(ref content), None) = (&source.content, &source.path) {
95        let lines: Vec<&str> = content.lines().collect();
96        let preview: Vec<&str> = lines.iter().take(3).copied().collect();
97        let preview_str = preview.join("\n      ");
98        println!("      {}", preview_str);
99        if lines.len() > 3 {
100            println!("      ...");
101        }
102    }
103}
104
105/// Determine display status (may show "blocked" for pending+blocked items).
106fn resolve_display_status(item: &Item, pending_ids: Option<&HashSet<String>>) -> String {
107    if !item.pending() || !item.blocked() {
108        return item.status.clone();
109    }
110    match pending_ids {
111        None => "blocked".to_string(),
112        Some(ids) => {
113            if item.blocked_by.iter().any(|id| ids.contains(id)) {
114                "blocked".to_string()
115            } else {
116                item.status.clone()
117            }
118        }
119    }
120}