sift_queue/cli/commands/
list.rs1use 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 one visible state (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
63pub fn execute(args: &ListArgs, queue_path: PathBuf) -> Result<i32> {
65 let queue = Queue::new(queue_path);
66
67 if let Some(status) = args.status.as_deref() {
68 if !VALID_DISPLAY_STATUSES.contains(&status) {
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_some() {
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 let Some(status) = args.status.as_deref() {
91 items.retain(|item| item.status == status);
92 }
93
94 if !args.priority.is_empty() {
95 let requested_priorities: HashSet<u8> = args
96 .priority
97 .iter()
98 .map(|value| parse_priority_value(value))
99 .collect::<Result<_>>()?;
100
101 items.retain(|item| {
102 item.priority
103 .is_some_and(|priority| requested_priorities.contains(&priority))
104 });
105 }
106
107 if let Some(ref filter_expr) = args.filter {
109 let expr = format!("[.[] | {}]", filter_expr);
110 match jq_filter(&items, &expr) {
111 Some(filtered) => items = filtered,
112 None => return Ok(1),
113 }
114 }
115
116 if let Some(ref sort_path) = args.sort {
118 let expr = format!("sort_by({} // infinite)", sort_path);
119 match jq_filter(&items, &expr) {
120 Some(sorted) => items = sorted,
121 None => return Ok(1),
122 }
123 } else {
124 items.sort_by_key(|item| {
125 (
126 item.priority.unwrap_or(5),
127 item.created_at.clone(),
128 item.id.clone(),
129 )
130 });
131 }
132
133 if args.reverse {
135 items.reverse();
136 }
137
138 if args.json {
139 let values: Vec<serde_json::Value> =
140 items.iter().map(|i: &Item| i.to_json_value()).collect();
141 let json = serde_json::to_string_pretty(&values)?;
142 println!("{}", json);
143 } else if items.is_empty() {
144 eprintln!("No items found");
145 } else {
146 for item in &items {
147 formatters::print_item_summary(item);
148 }
149 eprintln!("{} item(s)", items.len());
150 }
151
152 Ok(0)
153}
154
155fn jq_filter(items: &[Item], expr: &str) -> Option<Vec<Item>> {
157 let json_values: Vec<serde_json::Value> =
158 items.iter().map(|i: &Item| i.to_json_value()).collect();
159 let json = serde_json::to_string(&json_values).ok()?;
160
161 let mut child = Command::new("jq")
162 .arg("-e")
163 .arg(expr)
164 .stdin(Stdio::piped())
165 .stdout(Stdio::piped())
166 .stderr(Stdio::piped())
167 .spawn()
168 .map_err(|e| {
169 eprintln!("Error: Failed to run jq: {}", e);
170 })
171 .ok()?;
172
173 if let Some(ref mut stdin) = child.stdin {
174 stdin.write_all(json.as_bytes()).ok()?;
175 }
176 drop(child.stdin.take());
178
179 let output = child.wait_with_output().ok()?;
180
181 if !output.status.success() {
182 let stderr = String::from_utf8_lossy(&output.stderr);
183 eprintln!("Error: Filter failed: {}", stderr.trim());
184 return None;
185 }
186
187 let parsed: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout).ok()?;
188 Some(
189 parsed
190 .into_iter()
191 .filter_map(|v| serde_json::from_value::<Item>(v).ok())
192 .collect(),
193 )
194}