Skip to main content

sift_queue/cli/commands/
list.rs

1use crate::cli::formatters;
2use crate::cli::help::{HelpDoc, HelpSection};
3use crate::queue::{parse_priority_value, Item, Queue, VALID_DISPLAY_STATUSES};
4use crate::ListArgs;
5use anyhow::Result;
6use clap::builder::{StyledStr, Styles};
7use std::collections::HashSet;
8use std::io::Write;
9use std::path::PathBuf;
10use std::process::{Command, Stdio};
11
12pub fn after_help(styles: &Styles) -> StyledStr {
13    HelpDoc::new()
14        .section(
15            HelpSection::new("Views:")
16                .item(
17                    "sq list --ready",
18                    "Show only actionable work: pending items with no open blockers",
19                )
20                .item(
21                    "sq list",
22                    "Default view: show all non-closed items so blocked dependencies and in_progress work remain visible",
23                )
24                .item("sq list --all", "Include closed items for history"),
25        )
26        .section(
27            HelpSection::new("Filters:")
28                .item(
29                    "--status <STATUS>",
30                    "Restrict to visible states; repeat to include multiple (pending|blocked|in_progress|closed)",
31                )
32                .item(
33                    "--priority <PRIORITY>",
34                    "Repeat to include multiple priorities",
35                )
36                .item(
37                    "--filter <EXPR>",
38                    "Apply a jq select expression after built-in filtering",
39                )
40                .item("--sort <PATH>", "Sort by a jq path expression")
41                .item("--reverse", "Reverse the selected sort order"),
42        )
43        .section(
44            HelpSection::new("Dependencies:")
45                .text("Use --blocked-by <id1,id2> on sq add or sq collect to declare blockers.")
46                .text("Use sq edit <id> --set-blocked-by ... to update blockers later."),
47        )
48        .section(
49            HelpSection::new("Examples:")
50                .item("sq list --ready", "Focus on the next actionable task")
51                .item(
52                    "sq list --priority 0 --priority 1",
53                    "Review the highest-priority work first",
54                )
55                .item(
56                    "sq list --status in_progress --json",
57                    "Inspect active work in machine-readable form",
58                ),
59        )
60        .render(styles)
61}
62
63/// Execute the `sq list` command.
64pub fn execute(args: &ListArgs, queue_path: PathBuf) -> Result<i32> {
65    let queue = Queue::new(queue_path);
66
67    for status in &args.status {
68        if !VALID_DISPLAY_STATUSES.contains(&status.as_str()) {
69            eprintln!(
70                "Error: Invalid status: {}. Valid: {}",
71                status,
72                VALID_DISPLAY_STATUSES.join(", ")
73            );
74            return Ok(1);
75        }
76    }
77
78    let mut items: Vec<Item> = if args.ready {
79        queue.items_with_computed_status(queue.ready())
80    } else if args.all || !args.status.is_empty() {
81        queue.all_with_computed_status()
82    } else {
83        queue
84            .all_with_computed_status()
85            .into_iter()
86            .filter(|item| item.status != "closed")
87            .collect()
88    };
89
90    if !args.status.is_empty() {
91        let requested_statuses: HashSet<&str> =
92            args.status.iter().map(|status| status.as_str()).collect();
93        items.retain(|item| requested_statuses.contains(item.status.as_str()));
94    }
95
96    if !args.priority.is_empty() {
97        let requested_priorities: HashSet<u8> = args
98            .priority
99            .iter()
100            .map(|value| parse_priority_value(value))
101            .collect::<Result<_>>()?;
102
103        items.retain(|item| {
104            item.priority
105                .is_some_and(|priority| requested_priorities.contains(&priority))
106        });
107    }
108
109    // Apply jq filter
110    if let Some(ref filter_expr) = args.filter {
111        let expr = format!("[.[] | {}]", filter_expr);
112        match jq_filter(&items, &expr) {
113            Some(filtered) => items = filtered,
114            None => return Ok(1),
115        }
116    }
117
118    // Apply jq sort
119    if let Some(ref sort_path) = args.sort {
120        let expr = format!("sort_by({} // infinite)", sort_path);
121        match jq_filter(&items, &expr) {
122            Some(sorted) => items = sorted,
123            None => return Ok(1),
124        }
125    } else {
126        items.sort_by_key(|item| {
127            (
128                item.priority.unwrap_or(5),
129                item.created_at.clone(),
130                item.id.clone(),
131            )
132        });
133    }
134
135    // Apply reverse
136    if args.reverse {
137        items.reverse();
138    }
139
140    if args.json {
141        let values: Vec<serde_json::Value> =
142            items.iter().map(|i: &Item| i.to_json_value()).collect();
143        let json = serde_json::to_string_pretty(&values)?;
144        println!("{}", json);
145    } else if items.is_empty() {
146        eprintln!("No items found");
147    } else {
148        for item in &items {
149            formatters::print_item_summary(item);
150        }
151        eprintln!("{} item(s)", items.len());
152    }
153
154    Ok(0)
155}
156
157/// Run a jq expression on items, returning parsed results or None on error.
158fn jq_filter(items: &[Item], expr: &str) -> Option<Vec<Item>> {
159    let json_values: Vec<serde_json::Value> =
160        items.iter().map(|i: &Item| i.to_json_value()).collect();
161    let json = serde_json::to_string(&json_values).ok()?;
162
163    let mut child = Command::new("jq")
164        .arg("-e")
165        .arg(expr)
166        .stdin(Stdio::piped())
167        .stdout(Stdio::piped())
168        .stderr(Stdio::piped())
169        .spawn()
170        .map_err(|e| {
171            eprintln!("Error: Failed to run jq: {}", e);
172        })
173        .ok()?;
174
175    if let Some(ref mut stdin) = child.stdin {
176        stdin.write_all(json.as_bytes()).ok()?;
177    }
178    // Close stdin
179    drop(child.stdin.take());
180
181    let output = child.wait_with_output().ok()?;
182
183    if !output.status.success() {
184        let stderr = String::from_utf8_lossy(&output.stderr);
185        eprintln!("Error: Filter failed: {}", stderr.trim());
186        return None;
187    }
188
189    let parsed: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout).ok()?;
190    Some(
191        parsed
192            .into_iter()
193            .filter_map(|v| serde_json::from_value::<Item>(v).ok())
194            .collect(),
195    )
196}