1use std::fmt::Write as _;
4
5use crate::api::{Board, Sprint};
6use crate::render::Context;
7use crate::render::style::Palette;
8use crate::render::table::{Column, render, tally};
9
10#[must_use]
15pub fn boards(boards: &[Board], ctx: &Context) -> String {
16 let columns = [
17 Column::whole("ID", 8, Palette::key()),
18 Column::new("NAME", 36, anstyle::Style::new()),
19 Column::whole("COLUMNS", 8, anstyle::Style::new()),
20 Column::new("ESTIMATE", 14, Palette::label()),
21 ];
22 let rows: Vec<Vec<String>> = boards
23 .iter()
24 .map(|board| {
25 vec![
26 board.id.clone(),
27 board.name.clone(),
28 board.columns.len().to_string(),
29 board.estimate_by.as_deref().unwrap_or("-").to_owned(),
30 ]
31 })
32 .collect();
33
34 let mut out = render(&columns, &rows, ctx);
35 out.push_str(&tally(boards.len(), Some(boards.len() as u64), None, ctx));
36 out
37}
38
39#[must_use]
41pub fn board(board: &Board, ctx: &Context) -> String {
42 let mut out = String::with_capacity(240);
43 let paint = ctx.painter();
44 let label = |text: &str| paint.paint(text, Palette::label());
45
46 let _ = writeln!(
47 out,
48 "{} {}",
49 paint.paint(&board.id, Palette::key()),
50 board.name
51 );
52 let _ = writeln!(
53 out,
54 "{} {} {} {}",
55 label("estimate:"),
56 board.estimate_by.as_deref().unwrap_or("-"),
57 label("owner:"),
58 board.owner.as_deref().unwrap_or("-"),
59 );
60 let _ = writeln!(
61 out,
62 "{} {}",
63 label("columns:"),
64 if board.columns.is_empty() {
65 "-".to_owned()
66 } else {
67 board.columns.join(" → ")
68 }
69 );
70
71 out
72}
73
74fn state(status: Option<&str>, ctx: &Context) -> String {
80 let word = status.unwrap_or("-");
81 if !ctx.is_human() {
82 return word.to_owned();
83 }
84
85 let (glyph, style) = match status {
86 Some("in_progress") => ("\u{25cf}", Palette::ok()),
87 Some("draft" | "planned") => ("\u{25d0}", Palette::warn()),
88 _ => ("\u{25cb}", Palette::label()),
89 };
90 format!("{} {word}", ctx.painter().paint(glyph, style))
91}
92
93#[must_use]
103pub fn sprint(sprint: &Sprint, counts: Option<(u64, u64)>, today: &str, ctx: &Context) -> String {
104 let mut out = String::with_capacity(240);
105 let paint = ctx.painter();
106 let label = |text: &str| paint.paint(text, Palette::label());
107
108 let _ = writeln!(
109 out,
110 "{} {}",
111 paint.paint(&sprint.id, Palette::key()),
112 sprint.name
113 );
114 let _ = writeln!(
115 out,
116 "{} {} {} {}",
117 label("status:"),
118 state(sprint.status.as_deref(), ctx),
119 label("board:"),
120 sprint.board.as_deref().unwrap_or("-"),
121 );
122
123 let start = sprint.start.as_deref().unwrap_or("-");
124 let end = sprint.end.as_deref().unwrap_or("-");
125 let _ = write!(out, "{} {start} \u{2192} {end}", label("dates:"));
126 if let Some((elapsed, length)) = days(sprint.start.as_deref(), sprint.end.as_deref(), today) {
127 let _ = write!(
128 out,
129 " {} days",
130 crate::render::bar::ratio(elapsed, length, ctx)
131 );
132 }
133 let _ = writeln!(out);
134
135 if let Some((resolved, total)) = counts {
136 let _ = writeln!(
137 out,
138 "{} {} resolved",
139 label("issues:"),
140 crate::render::bar::ratio(resolved, total, ctx)
141 );
142 }
143
144 out
145}
146
147fn days(start: Option<&str>, end: Option<&str>, today: &str) -> Option<(u64, u64)> {
154 let start: jiff::civil::Date = start?.parse().ok()?;
155 let end: jiff::civil::Date = end?.parse().ok()?;
156 let today: jiff::civil::Date = today.parse().ok()?;
157
158 let length = (end - start).get_days().checked_add(1)?;
159 let elapsed = (today - start).get_days().checked_add(1)?;
160 u64::try_from(length)
161 .ok()
162 .map(|length| (u64::try_from(elapsed).unwrap_or(0), length))
163}
164
165#[must_use]
171pub fn all_sprints(sprints: &[Sprint], ctx: &Context) -> String {
172 let columns = [
173 Column::whole("ID", 8, Palette::key()),
174 Column::new("NAME", 26, anstyle::Style::new()),
175 Column::new("BOARD", 20, Palette::label()),
176 Column::new("STATUS", 12, anstyle::Style::new()),
177 Column::whole("START", 12, anstyle::Style::new()),
178 Column::whole("END", 12, anstyle::Style::new()),
179 ];
180 let rows: Vec<Vec<String>> = sprints
181 .iter()
182 .map(|sprint| {
183 vec![
184 sprint.id.clone(),
185 sprint.name.clone(),
186 sprint.board.as_deref().unwrap_or("-").to_owned(),
187 sprint.status.as_deref().unwrap_or("-").to_owned(),
188 sprint.start.as_deref().unwrap_or("-").to_owned(),
189 sprint.end.as_deref().unwrap_or("-").to_owned(),
190 ]
191 })
192 .collect();
193
194 let mut out = render(&columns, &rows, ctx);
195 out.push_str(&tally(sprints.len(), Some(sprints.len() as u64), None, ctx));
196 out
197}
198
199#[must_use]
201pub fn sprints(board: &str, sprints: &[Sprint], ctx: &Context) -> String {
202 let columns = [
203 Column::whole("ID", 8, Palette::key()),
204 Column::new("NAME", 30, anstyle::Style::new()),
205 Column::new("STATUS", 14, anstyle::Style::new()),
206 Column::whole("START", 12, anstyle::Style::new()),
207 Column::whole("END", 12, anstyle::Style::new()),
208 ];
209 let rows: Vec<Vec<String>> = sprints
210 .iter()
211 .map(|sprint| {
212 vec![
213 sprint.id.clone(),
214 sprint.name.clone(),
215 sprint.status.as_deref().unwrap_or("-").to_owned(),
216 sprint.start.as_deref().unwrap_or("-").to_owned(),
217 sprint.end.as_deref().unwrap_or("-").to_owned(),
218 ]
219 })
220 .collect();
221
222 let mut out = render(&columns, &rows, ctx);
223 let paint = ctx.painter();
224 let _ = writeln!(
225 out,
226 "{}",
227 paint.paint(
228 &format!(
229 "shown {} of {} for board {board}",
230 sprints.len(),
231 sprints.len()
232 ),
233 Palette::label()
234 )
235 );
236 out
237}