use std::fmt::Write;
use std::io;
use std::path::Path;
use serde_json::Value;
use super::history::Dead;
use super::{claim_age, filter, json_line, scope, target, task_json, Catalog, Entry, Flags, Style};
use crate::layout::Xdg;
use crate::task::Task;
pub(crate) struct Ctx<'a> {
pub store: &'a Path,
pub now: i64,
pub invocation: &'a Path,
pub xdg: &'a Xdg,
}
enum Row<'a> {
Live(&'a Entry),
Dead(&'a Dead),
}
impl Row<'_> {
fn id(&self) -> &str {
match self {
Row::Live(e) => &e.id,
Row::Dead(d) => &d.id,
}
}
fn task(&self) -> &Task {
match self {
Row::Live(e) => &e.task,
Row::Dead(d) => &d.task,
}
}
}
pub(crate) fn render_list(cat: &Catalog, dead: &[Dead], flags: &Flags, style: &Style, ctx: &Ctx) -> io::Result<String> {
let mut rows: Vec<Row> = Vec::new();
if flags.reach.live() {
rows.extend(
cat.entries()
.iter()
.filter(|e| flags.status.is_none_or(|want| cat.status(e) == want))
.filter(|e| filter::matches(&e.task, e.task.updated, flags))
.map(Row::Live),
);
}
if flags.reach.dead() {
rows.extend(
dead.iter().filter(|d| filter::matches(&d.task, d.retired_at, flags)).map(Row::Dead),
);
}
let needs_roots = rows.iter().any(|r| r.task().root_commit.is_some());
let this_roots = scope::checkout_roots(ctx.invocation, needs_roots);
if !flags.everywhere {
rows.retain(|r| crate::change::admits(r.task().root_commit.as_deref(), &this_roots));
}
rows.sort_by(|a, b| order_key(a).cmp(&order_key(b)));
render(cat, &rows, flags, style, ctx, &this_roots)
}
fn order_key<'a>(r: &'a Row) -> (bool, i64, i64, &'a str) {
let t = r.task();
(t.priority.is_none(), t.priority.unwrap_or(0), t.created, r.id())
}
fn render(cat: &Catalog, rows: &[Row], flags: &Flags, style: &Style, ctx: &Ctx, this_roots: &[String]) -> io::Result<String> {
if flags.json {
let arr: Vec<Value> = rows.iter().map(|r| task_json(r.id(), r.task())).collect();
return Ok(json_line(&Value::Array(arr)));
}
let has_foreign = flags.everywhere
&& rows.iter().any(|r| scope::is_foreign(r.task().root_commit.as_deref(), this_roots));
let labels = has_foreign.then(|| scope::enrolled_labels(ctx.xdg));
let mut out = String::new();
for r in rows {
let age = age_hint(r, flags, ctx.store, ctx.now)?;
let label = scope::row_label(labels.as_ref(), r.task().root_commit.as_deref(), this_roots);
let into = target::row_marker(cat, r.id(), r.task());
out.push_str(&line(&badge(cat, r, style), r.id(), r.task(), &age, &label, &into));
}
Ok(out)
}
fn age_hint(r: &Row, flags: &Flags, store: &Path, now: i64) -> io::Result<String> {
let Row::Live(e) = r else { return Ok(String::new()) };
if flags.legacy.is_some() || e.task.claimant.is_none() {
return Ok(String::new());
}
let hint = claim_age::claimed_at(store, &e.id)?
.map_or(String::new(), |t| format!(" ({})", claim_age::humanize(now - t)));
Ok(hint)
}
fn badge(cat: &Catalog, r: &Row, style: &Style) -> String {
match r {
Row::Live(e) => style.badge(cat.status(e)),
Row::Dead(_) => style.retired_badge(),
}
}
fn line(badge: &str, id: &str, task: &Task, age: &str, label: &str, into: &str) -> String {
let mut row = format!("{badge} {id} {}", task.title);
if let Some(p) = task.priority {
let _ = write!(row, " p{p}");
}
if let Some(c) = &task.claimant {
let _ = write!(row, " @{c}{age}");
}
row.push_str(into);
row.push_str(label);
row.push('\n');
row
}
impl Catalog {
pub fn entries(&self) -> &[Entry] {
&self.entries
}
}
#[cfg(test)]
#[path = "list_tests.rs"]
mod tests;