use std::fmt::Write;
use std::io;
use std::path::Path;
use super::history::{resolve_dead, Dead};
use super::{claim_age, journal, json_line, task_json, Catalog, Entry, Flags, Style};
use crate::civil::iso8601;
use crate::task::Task;
pub(crate) fn dispatch(store: &Path, cat: &Catalog, flags: &Flags, style: &Style, folded: &str, now: i64) -> io::Result<String> {
let id = flags.target.as_deref().expect("parser guarantees show has a target");
if let Some(err) = cat.corruption(id) {
return Err(io::Error::other(format!("tasks/{id}.md: {err}")));
}
match cat.get(id) {
Some(e) => journaled(render_live(cat, e, flags, style, folded, store, now)?, store, id, flags),
None if flags.legacy.is_some() => Err(io::Error::other(format!("no such legacy ball: {id}"))),
None => match resolve_dead(store, id)? {
Some(dead) => journaled(render_dead(&dead, flags, style, folded), store, id, flags),
None => Err(io::Error::other(format!("no such ball: {id}"))),
},
}
}
fn journaled(mut out: String, store: &Path, id: &str, flags: &Flags) -> io::Result<String> {
if flags.json || flags.legacy.is_some() {
return Ok(out);
}
let section = journal::section(store, id)?;
if !section.is_empty() {
if !out.ends_with('\n') {
out.push('\n');
}
out.push('\n');
out.push_str(§ion);
}
Ok(out)
}
fn render_live(cat: &Catalog, e: &Entry, flags: &Flags, style: &Style, folded: &str, store: &Path, now: i64) -> io::Result<String> {
if flags.json {
return Ok(json_line(&task_json(&e.id, &e.task)));
}
let badge = style.badge(cat.status(e));
let mut out = header(&badge, &e.id, &e.task);
field(&mut out, "status", cat.status(e).word());
let claimed = claimed_line(e, flags, store, now)?;
body_block(&mut out, &e.task, &claimed, |out| {
kids(out, cat, &child_ids(cat, &e.id), style);
out.push_str(folded);
});
Ok(out)
}
fn claimed_line(e: &Entry, flags: &Flags, store: &Path, now: i64) -> io::Result<String> {
if flags.legacy.is_some() || e.task.claimant.is_none() {
return Ok(String::new());
}
let mut line = String::new();
if let Some(t) = claim_age::claimed_at(store, &e.id)? {
field(&mut line, "claimed", &format!("{} ({} ago)", iso8601(t), claim_age::humanize(now - t)));
}
Ok(line)
}
fn render_dead(d: &Dead, flags: &Flags, style: &Style, folded: &str) -> String {
if flags.json {
return json_line(&task_json(&d.id, &d.task));
}
let badge = style.retired_badge();
let mut out = header(&badge, &d.id, &d.task);
field(&mut out, "status", "closed");
field(&mut out, "retired", &iso8601(d.retired_at));
body_block(&mut out, &d.task, "", |out| out.push_str(folded));
out
}
fn header(badge: &str, id: &str, task: &Task) -> String {
format!("{badge} {id} {}\n", task.title)
}
fn body_block(out: &mut String, task: &Task, claimed: &str, extra: impl FnOnce(&mut String)) {
field(out, "created", &iso8601(task.created));
field(out, "updated", &iso8601(task.updated));
if let Some(c) = &task.claimant {
field(out, "claimant", c);
out.push_str(claimed);
}
if let Some(p) = task.priority {
field(out, "priority", &p.to_string());
}
if let Some(p) = &task.parent {
field(out, "parent", p);
}
if !task.tags.is_empty() {
field(out, "tags", &task.tags.join(", "));
}
blockers(out, task);
extra(out);
if !task.body.is_empty() {
out.push('\n');
out.push_str(&task.body);
}
}
fn child_ids<'a>(cat: &'a Catalog, id: &str) -> Vec<&'a Entry> {
cat.entries()
.iter()
.filter(|c| c.task.parent.as_deref() == Some(id))
.collect()
}
fn field(out: &mut String, label: &str, value: &str) {
let _ = writeln!(out, " {label:<9}{value}");
}
fn blockers(out: &mut String, task: &Task) {
if task.blockers.is_empty() {
return;
}
out.push_str(" blockers\n");
for b in &task.blockers {
let _ = writeln!(out, " {} (on {})", b.id, b.on.token());
}
}
fn kids(out: &mut String, cat: &Catalog, children: &[&Entry], style: &Style) {
if children.is_empty() {
return;
}
out.push_str(" children\n");
for c in children {
let _ = writeln!(out, " {} {} {}", style.badge(cat.status(c)), c.id, c.task.title);
}
}
#[cfg(test)]
#[path = "show_tests.rs"]
mod tests;