use crate::cli::formatters;
use crate::cli::help::{HelpDoc, HelpSection};
use crate::queue::{parse_priority_value, Item, Queue, VALID_DISPLAY_STATUSES};
use crate::ListArgs;
use anyhow::Result;
use clap::builder::{StyledStr, Styles};
use std::collections::HashSet;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
pub fn after_help(styles: &Styles) -> StyledStr {
HelpDoc::new()
.section(
HelpSection::new("Views:")
.item(
"sq list --ready",
"Show only actionable work: pending items with no open blockers",
)
.item(
"sq list",
"Default view: show all non-closed items so blocked dependencies and in_progress work remain visible",
)
.item("sq list --all", "Include closed items for history"),
)
.section(
HelpSection::new("Filters:")
.item(
"--status <STATUS>",
"Restrict to visible states; repeat to include multiple (pending|blocked|in_progress|closed)",
)
.item(
"--priority <PRIORITY>",
"Repeat to include multiple priorities",
)
.item(
"--filter <EXPR>",
"Apply a jq select expression after built-in filtering",
)
.item("--sort <PATH>", "Sort by a jq path expression")
.item("--reverse", "Reverse the selected sort order"),
)
.section(
HelpSection::new("Dependencies:")
.text("Use --blocked-by <id1,id2> on sq add or sq collect to declare blockers.")
.text("Use sq edit <id> --set-blocked-by ... to update blockers later."),
)
.section(
HelpSection::new("Examples:")
.item("sq list --ready", "Focus on the next actionable task")
.item(
"sq list --priority 0 --priority 1",
"Review the highest-priority work first",
)
.item(
"sq list --status in_progress --json",
"Inspect active work in machine-readable form",
),
)
.render(styles)
}
pub fn execute(args: &ListArgs, queue_path: PathBuf) -> Result<i32> {
let queue = Queue::new(queue_path);
for status in &args.status {
if !VALID_DISPLAY_STATUSES.contains(&status.as_str()) {
eprintln!(
"Error: Invalid status: {}. Valid: {}",
status,
VALID_DISPLAY_STATUSES.join(", ")
);
return Ok(1);
}
}
let mut items: Vec<Item> = if args.ready {
queue.items_with_computed_status(queue.ready())
} else if args.all || !args.status.is_empty() {
queue.all_with_computed_status()
} else {
queue
.all_with_computed_status()
.into_iter()
.filter(|item| item.status != "closed")
.collect()
};
if !args.status.is_empty() {
let requested_statuses: HashSet<&str> =
args.status.iter().map(|status| status.as_str()).collect();
items.retain(|item| requested_statuses.contains(item.status.as_str()));
}
if !args.priority.is_empty() {
let requested_priorities: HashSet<u8> = args
.priority
.iter()
.map(|value| parse_priority_value(value))
.collect::<Result<_>>()?;
items.retain(|item| {
item.priority
.is_some_and(|priority| requested_priorities.contains(&priority))
});
}
if let Some(ref filter_expr) = args.filter {
let expr = format!("[.[] | {}]", filter_expr);
match jq_filter(&items, &expr) {
Some(filtered) => items = filtered,
None => return Ok(1),
}
}
if let Some(ref sort_path) = args.sort {
let expr = format!("sort_by({} // infinite)", sort_path);
match jq_filter(&items, &expr) {
Some(sorted) => items = sorted,
None => return Ok(1),
}
} else {
items.sort_by_key(|item| {
(
item.priority.unwrap_or(5),
item.created_at.clone(),
item.id.clone(),
)
});
}
if args.reverse {
items.reverse();
}
if args.json {
let values: Vec<serde_json::Value> =
items.iter().map(|i: &Item| i.to_json_value()).collect();
let json = serde_json::to_string_pretty(&values)?;
println!("{}", json);
} else if items.is_empty() {
eprintln!("No items found");
} else {
for item in &items {
formatters::print_item_summary(item);
}
eprintln!("{} item(s)", items.len());
}
Ok(0)
}
fn jq_filter(items: &[Item], expr: &str) -> Option<Vec<Item>> {
let json_values: Vec<serde_json::Value> =
items.iter().map(|i: &Item| i.to_json_value()).collect();
let json = serde_json::to_string(&json_values).ok()?;
let mut child = Command::new("jq")
.arg("-e")
.arg(expr)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
eprintln!("Error: Failed to run jq: {}", e);
})
.ok()?;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(json.as_bytes()).ok()?;
}
drop(child.stdin.take());
let output = child.wait_with_output().ok()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("Error: Filter failed: {}", stderr.trim());
return None;
}
let parsed: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout).ok()?;
Some(
parsed
.into_iter()
.filter_map(|v| serde_json::from_value::<Item>(v).ok())
.collect(),
)
}