use crate::ListTarget;
use crate::OutputFormat;
use crate::config::Config;
use crate::diagnostic::Diagnostic;
use crate::load::load_project;
use crate::model::WorkItemStatus;
use crate::parse::load_guards_with_warnings;
use crate::theme::{SemanticColor, status_semantic};
use crate::ui::stdout_supports_color;
use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets::UTF8_FULL};
use serde::Serialize;
fn use_colors() -> bool {
stdout_supports_color()
}
fn cell(text: &str) -> Cell {
Cell::new(text)
}
fn id_cell(text: &str) -> Cell {
if use_colors() {
Cell::new(text)
.fg(SemanticColor::Info.to_comfy())
.add_attribute(Attribute::Bold)
} else {
Cell::new(text)
}
}
fn status_cell(status: &str) -> Cell {
if use_colors() {
Cell::new(status).fg(status_semantic(status).to_comfy())
} else {
Cell::new(status)
}
}
fn header_cell(text: &str) -> Cell {
if use_colors() {
Cell::new(text).add_attribute(Attribute::Bold)
} else {
Cell::new(text)
}
}
fn truncate_chars(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let truncated: String = s.chars().take(max).collect();
format!("{truncated}…")
}
}
pub fn list(
config: &Config,
target: ListTarget,
filter: Option<&str>,
limit: Option<usize>,
output: OutputFormat,
) -> anyhow::Result<Vec<Diagnostic>> {
if target == ListTarget::Guard {
let result = load_guards_with_warnings(config).map_err(anyhow::Error::from)?;
list_guards(&result.items, filter, limit, output);
return Ok(result.warnings);
}
let index = match load_project(config) {
Ok(idx) => idx,
Err(diags) => return Ok(diags),
};
match target {
ListTarget::Rfc => list_rfcs(&index, filter, limit, output),
ListTarget::Clause => list_clauses(&index, filter, limit, output),
ListTarget::Adr => list_adrs(&index, filter, limit, output),
ListTarget::Work => list_work_items(&index, filter, limit, output),
ListTarget::Guard => unreachable!("handled above"),
}
Ok(vec![])
}
fn output_list<T: Serialize>(
items: &[T],
headers: &[&str],
format: OutputFormat,
to_row: impl Fn(&T) -> Vec<String>,
) {
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(items).unwrap_or_else(|_| "[]".to_string())
);
}
OutputFormat::Plain => {
for item in items {
let row = to_row(item);
println!("{}", row.join("\t"));
}
}
OutputFormat::Table => {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(headers.iter().map(|h| header_cell(h)).collect::<Vec<_>>());
for item in items {
let row = to_row(item);
table.add_row(
row.iter()
.enumerate()
.map(|(i, v)| {
if i == 0 {
id_cell(v)
} else if headers
.get(i)
.is_some_and(|h| *h == "Status" || *h == "Phase")
{
status_cell(v)
} else {
cell(v)
}
})
.collect::<Vec<_>>(),
);
}
println!("{table}");
}
}
}
#[derive(Serialize)]
struct RfcSummary {
id: String,
version: String,
status: String,
phase: String,
title: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
amended: bool,
}
fn list_rfcs(
index: &crate::model::ProjectIndex,
filter: Option<&str>,
limit: Option<usize>,
output: OutputFormat,
) {
let mut rfcs: Vec<_> = index.rfcs.iter().collect();
if let Some(f) = filter {
rfcs.retain(|r| {
r.rfc.status.as_ref() == f || r.rfc.phase.as_ref() == f || r.rfc.rfc_id.contains(f)
});
}
rfcs.sort_by(|a, b| a.rfc.rfc_id.cmp(&b.rfc.rfc_id));
if let Some(n) = limit {
rfcs.truncate(n);
}
let summaries: Vec<RfcSummary> = rfcs
.iter()
.map(|rfc| {
let amended = crate::signature::is_rfc_amended(rfc);
RfcSummary {
id: if amended {
format!("{}*", rfc.rfc.rfc_id)
} else {
rfc.rfc.rfc_id.clone()
},
version: rfc.rfc.version.clone(),
status: rfc.rfc.status.as_ref().to_string(),
phase: rfc.rfc.phase.as_ref().to_string(),
title: rfc.rfc.title.clone(),
amended,
}
})
.collect();
output_list(
&summaries,
&["RFC", "Version", "Status", "Phase", "Title"],
output,
|s| {
vec![
s.id.clone(),
s.version.clone(),
s.status.clone(),
s.phase.clone(),
s.title.clone(),
]
},
);
}
#[derive(Serialize)]
struct ClauseSummary {
id: String,
rfc_id: String,
kind: String,
status: String,
title: String,
}
fn list_clauses(
index: &crate::model::ProjectIndex,
filter: Option<&str>,
limit: Option<usize>,
output: OutputFormat,
) {
let mut clauses: Vec<_> = index
.iter_clauses()
.map(|(rfc, clause)| (rfc.rfc.rfc_id.clone(), clause))
.collect();
if let Some(f) = filter {
clauses.retain(|(rfc_id, c)| {
rfc_id == f || c.spec.clause_id.contains(f) || c.spec.status.as_ref() == f
});
}
clauses.sort_by(|a, b| {
a.0.cmp(&b.0)
.then_with(|| a.1.spec.clause_id.cmp(&b.1.spec.clause_id))
});
if let Some(n) = limit {
clauses.truncate(n);
}
let summaries: Vec<ClauseSummary> = clauses
.iter()
.map(|(rfc_id, clause)| ClauseSummary {
id: clause.spec.clause_id.clone(),
rfc_id: rfc_id.clone(),
kind: clause.spec.kind.as_ref().to_string(),
status: clause.spec.status.as_ref().to_string(),
title: clause.spec.title.clone(),
})
.collect();
output_list(
&summaries,
&["Clause", "RFC", "Kind", "Status", "Title"],
output,
|s| {
vec![
s.id.clone(),
s.rfc_id.clone(),
s.kind.clone(),
s.status.clone(),
s.title.clone(),
]
},
);
}
#[derive(Serialize)]
struct AdrSummary {
id: String,
status: String,
date: String,
title: String,
}
fn list_adrs(
index: &crate::model::ProjectIndex,
filter: Option<&str>,
limit: Option<usize>,
output: OutputFormat,
) {
let mut adrs: Vec<_> = index.adrs.iter().collect();
if let Some(f) = filter {
adrs.retain(|a| a.meta().status.as_ref() == f || a.meta().id.contains(f));
}
adrs.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));
if let Some(n) = limit {
adrs.truncate(n);
}
let summaries: Vec<AdrSummary> = adrs
.iter()
.map(|adr| AdrSummary {
id: adr.meta().id.clone(),
status: adr.meta().status.as_ref().to_string(),
date: adr.meta().date.clone(),
title: adr.meta().title.clone(),
})
.collect();
output_list(
&summaries,
&["ADR", "Status", "Date", "Title"],
output,
|s| {
vec![
s.id.clone(),
s.status.clone(),
s.date.clone(),
s.title.clone(),
]
},
);
}
#[derive(Serialize)]
struct WorkItemSummary {
id: String,
status: String,
title: String,
}
#[derive(Serialize)]
struct GuardSummary {
id: String,
title: String,
command: String,
}
fn list_guards(
guards: &[crate::model::GuardEntry],
filter: Option<&str>,
limit: Option<usize>,
output: OutputFormat,
) {
let mut items: Vec<_> = guards.iter().collect();
if let Some(f) = filter {
items.retain(|g| g.meta().id.contains(f) || g.meta().title.contains(f));
}
items.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));
if let Some(n) = limit {
items.truncate(n);
}
let summaries: Vec<GuardSummary> = items
.iter()
.map(|g| GuardSummary {
id: g.meta().id.clone(),
title: g.meta().title.clone(),
command: g.spec.check.command.clone(),
})
.collect();
output_list(&summaries, &["Guard", "Title", "Command"], output, |s| {
let cmd_display = truncate_chars(&s.command, 50);
vec![s.id.clone(), s.title.clone(), cmd_display]
});
}
fn list_work_items(
index: &crate::model::ProjectIndex,
filter: Option<&str>,
limit: Option<usize>,
output: OutputFormat,
) {
let mut items: Vec<_> = index.work_items.iter().collect();
if let Some(f) = filter {
match f {
"all" => {}
"pending" => {
items.retain(|i| {
i.meta().status == WorkItemStatus::Queue
|| i.meta().status == WorkItemStatus::Active
});
}
"queue" => items.retain(|i| i.meta().status == WorkItemStatus::Queue),
"active" => items.retain(|i| i.meta().status == WorkItemStatus::Active),
"done" => items.retain(|i| i.meta().status == WorkItemStatus::Done),
"cancelled" => items.retain(|i| i.meta().status == WorkItemStatus::Cancelled),
other => {
items.retain(|i| i.meta().status.as_ref() == other || i.meta().id.contains(other));
}
}
}
items.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));
if let Some(n) = limit {
items.truncate(n);
}
let summaries: Vec<WorkItemSummary> = items
.iter()
.map(|item| WorkItemSummary {
id: item.meta().id.clone(),
status: item.meta().status.as_ref().to_string(),
title: item.meta().title.clone(),
})
.collect();
output_list(&summaries, &["ID", "Status", "Title"], output, |s| {
vec![s.id.clone(), s.status.clone(), s.title.clone()]
});
}