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 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
63pub 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 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 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 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
157fn 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 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}