use std::fmt::Write as _;
use crate::api::{Board, Sprint};
use crate::render::Context;
use crate::render::style::Palette;
use crate::render::table::{Column, render, tally};
#[must_use]
pub fn boards(boards: &[Board], ctx: &Context) -> String {
let columns = [
Column::whole("ID", 8, Palette::key()),
Column::new("NAME", 36, anstyle::Style::new()),
Column::whole("COLUMNS", 8, anstyle::Style::new()),
Column::new("ESTIMATE", 14, Palette::label()),
];
let rows: Vec<Vec<String>> = boards
.iter()
.map(|board| {
vec![
board.id.clone(),
board.name.clone(),
board.columns.len().to_string(),
board.estimate_by.as_deref().unwrap_or("-").to_owned(),
]
})
.collect();
let mut out = render(&columns, &rows, ctx);
out.push_str(&tally(boards.len(), Some(boards.len() as u64), None, ctx));
out
}
#[must_use]
pub fn board(board: &Board, ctx: &Context) -> String {
let mut out = String::with_capacity(240);
let paint = ctx.painter();
let label = |text: &str| paint.paint(text, Palette::label());
let _ = writeln!(
out,
"{} {}",
paint.paint(&board.id, Palette::key()),
board.name
);
let _ = writeln!(
out,
"{} {} {} {}",
label("estimate:"),
board.estimate_by.as_deref().unwrap_or("-"),
label("owner:"),
board.owner.as_deref().unwrap_or("-"),
);
let _ = writeln!(
out,
"{} {}",
label("columns:"),
if board.columns.is_empty() {
"-".to_owned()
} else {
board.columns.join(" → ")
}
);
out
}
fn state(status: Option<&str>, ctx: &Context) -> String {
let word = status.unwrap_or("-");
if !ctx.is_human() {
return word.to_owned();
}
let (glyph, style) = match status {
Some("in_progress") => ("\u{25cf}", Palette::ok()),
Some("draft" | "planned") => ("\u{25d0}", Palette::warn()),
_ => ("\u{25cb}", Palette::label()),
};
format!("{} {word}", ctx.painter().paint(glyph, style))
}
#[must_use]
pub fn sprint(sprint: &Sprint, counts: Option<(u64, u64)>, today: &str, ctx: &Context) -> String {
let mut out = String::with_capacity(240);
let paint = ctx.painter();
let label = |text: &str| paint.paint(text, Palette::label());
let _ = writeln!(
out,
"{} {}",
paint.paint(&sprint.id, Palette::key()),
sprint.name
);
let _ = writeln!(
out,
"{} {} {} {}",
label("status:"),
state(sprint.status.as_deref(), ctx),
label("board:"),
sprint.board.as_deref().unwrap_or("-"),
);
let start = sprint.start.as_deref().unwrap_or("-");
let end = sprint.end.as_deref().unwrap_or("-");
let _ = write!(out, "{} {start} \u{2192} {end}", label("dates:"));
if let Some((elapsed, length)) = days(sprint.start.as_deref(), sprint.end.as_deref(), today) {
let _ = write!(
out,
" {} days",
crate::render::bar::ratio(elapsed, length, ctx)
);
}
let _ = writeln!(out);
if let Some((resolved, total)) = counts {
let _ = writeln!(
out,
"{} {} resolved",
label("issues:"),
crate::render::bar::ratio(resolved, total, ctx)
);
}
out
}
fn days(start: Option<&str>, end: Option<&str>, today: &str) -> Option<(u64, u64)> {
let start: jiff::civil::Date = start?.parse().ok()?;
let end: jiff::civil::Date = end?.parse().ok()?;
let today: jiff::civil::Date = today.parse().ok()?;
let length = (end - start).get_days().checked_add(1)?;
let elapsed = (today - start).get_days().checked_add(1)?;
u64::try_from(length)
.ok()
.map(|length| (u64::try_from(elapsed).unwrap_or(0), length))
}
#[must_use]
pub fn all_sprints(sprints: &[Sprint], ctx: &Context) -> String {
let columns = [
Column::whole("ID", 8, Palette::key()),
Column::new("NAME", 26, anstyle::Style::new()),
Column::new("BOARD", 20, Palette::label()),
Column::new("STATUS", 12, anstyle::Style::new()),
Column::whole("START", 12, anstyle::Style::new()),
Column::whole("END", 12, anstyle::Style::new()),
];
let rows: Vec<Vec<String>> = sprints
.iter()
.map(|sprint| {
vec![
sprint.id.clone(),
sprint.name.clone(),
sprint.board.as_deref().unwrap_or("-").to_owned(),
sprint.status.as_deref().unwrap_or("-").to_owned(),
sprint.start.as_deref().unwrap_or("-").to_owned(),
sprint.end.as_deref().unwrap_or("-").to_owned(),
]
})
.collect();
let mut out = render(&columns, &rows, ctx);
out.push_str(&tally(sprints.len(), Some(sprints.len() as u64), None, ctx));
out
}
#[must_use]
pub fn sprints(board: &str, sprints: &[Sprint], ctx: &Context) -> String {
let columns = [
Column::whole("ID", 8, Palette::key()),
Column::new("NAME", 30, anstyle::Style::new()),
Column::new("STATUS", 14, anstyle::Style::new()),
Column::whole("START", 12, anstyle::Style::new()),
Column::whole("END", 12, anstyle::Style::new()),
];
let rows: Vec<Vec<String>> = sprints
.iter()
.map(|sprint| {
vec![
sprint.id.clone(),
sprint.name.clone(),
sprint.status.as_deref().unwrap_or("-").to_owned(),
sprint.start.as_deref().unwrap_or("-").to_owned(),
sprint.end.as_deref().unwrap_or("-").to_owned(),
]
})
.collect();
let mut out = render(&columns, &rows, ctx);
let paint = ctx.painter();
let _ = writeln!(
out,
"{}",
paint.paint(
&format!(
"shown {} of {} for board {board}",
sprints.len(),
sprints.len()
),
Palette::label()
)
);
out
}