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