1use std::fmt::Write as _;
4
5use crate::api::models::{Attachment, Entity, Page};
6use crate::render::Context;
7use crate::render::style::Palette;
8use crate::render::table::{Column, render, tally};
9
10#[must_use]
16pub fn entities(page: &Page<Entity>, ctx: &Context) -> String {
17 let columns = [
18 Column::whole("SHORT", 8, Palette::key()),
19 Column::new("ID", 26, Palette::label()),
20 Column::new("STATUS", 14, anstyle::Style::new()),
21 Column::new("SUMMARY", 50, anstyle::Style::new()),
22 ];
23 let rows: Vec<Vec<String>> = page
24 .items
25 .iter()
26 .map(|entity| {
27 vec![
28 entity
29 .short_id
30 .map_or_else(|| "-".to_owned(), |id| id.to_string()),
31 entity.id.clone(),
32 entity.status.as_deref().unwrap_or("-").to_owned(),
33 entity.summary.clone(),
34 ]
35 })
36 .collect();
37
38 let mut out = render(&columns, &rows, ctx);
39 out.push_str(&tally(
40 page.items.len(),
41 page.total,
42 page.has_more().then_some(page.page + 1),
43 ctx,
44 ));
45 out
46}
47
48#[must_use]
50pub fn entity(entity: &Entity, ctx: &Context) -> String {
51 let mut out = String::with_capacity(320);
52 let paint = ctx.painter();
53 let label = |text: &str| paint.paint(text, Palette::label());
54
55 let _ = writeln!(
56 out,
57 "{} {}",
58 paint.paint(&entity.id, Palette::key()),
59 entity.summary
60 );
61 let _ = writeln!(
62 out,
63 "{} {} {} {} {} {}",
64 label("short id:"),
65 paint.paint(
66 &entity
67 .short_id
68 .map_or_else(|| "-".to_owned(), |id| id.to_string()),
69 Palette::key()
70 ),
71 label("status:"),
72 entity.status.as_deref().unwrap_or("-"),
73 label("lead:"),
74 entity
75 .lead
76 .as_ref()
77 .and_then(|lead| lead.login.as_deref().or(lead.display.as_deref()))
78 .unwrap_or("-"),
79 );
80 let _ = writeln!(
81 out,
82 "{} {} {} {}",
83 label("start:"),
84 entity.start.as_deref().unwrap_or("-"),
85 label("end:"),
86 entity.end.as_deref().unwrap_or("-"),
87 );
88 if let Some(parent) = entity.parent.as_deref() {
89 let _ = writeln!(out, "{} {}", label("in portfolio:"), parent);
90 }
91
92 if let Some(description) = entity.description.as_deref().filter(|d| !d.is_empty()) {
93 let (body, withheld) = crate::render::untrusted::head(description, ctx.description_lines);
94 crate::render::text::quoted_block(
95 &mut out,
96 &format!("{}/description", entity.id),
97 crate::render::untrusted::Author::Tracker,
98 &body,
99 withheld,
100 ctx,
101 );
102 }
103
104 out
105}
106
107#[must_use]
113pub fn contents(page: &Page<Entity>, ctx: &Context) -> String {
114 let columns = [
115 Column::whole("SHORT", 8, Palette::key()),
116 Column::new("TYPE", 10, Palette::label()),
117 Column::new("ID", 26, Palette::label()),
118 Column::new("STATUS", 14, anstyle::Style::new()),
119 Column::new("SUMMARY", 40, anstyle::Style::new()),
120 ];
121 let rows: Vec<Vec<String>> = page
122 .items
123 .iter()
124 .map(|entity| {
125 vec![
126 entity
127 .short_id
128 .map_or_else(|| "-".to_owned(), |id| id.to_string()),
129 entity.entity_type.as_deref().unwrap_or("-").to_owned(),
130 entity.id.clone(),
131 entity.status.as_deref().unwrap_or("-").to_owned(),
132 entity.summary.clone(),
133 ]
134 })
135 .collect();
136
137 let mut out = render(&columns, &rows, ctx);
138 out.push_str(&tally(
139 page.items.len(),
140 page.total,
141 page.has_more().then_some(page.page + 1),
142 ctx,
143 ));
144 out
145}
146
147#[must_use]
152pub fn attachments(key: &str, attachments: &[Attachment], ctx: &Context) -> String {
153 let columns = [
154 Column::whole("ID", 14, Palette::key()),
155 Column::whole("SIZE", 10, anstyle::Style::new()),
156 Column::new("TYPE", 18, anstyle::Style::new()),
157 Column::whole("NAME", 40, Palette::untrusted()),
158 ];
159 let rows: Vec<Vec<String>> = attachments
160 .iter()
161 .map(|attachment| {
162 vec![
163 attachment.id.clone(),
164 attachment.size.map_or_else(|| "-".to_owned(), human_size),
165 attachment.mimetype.as_deref().unwrap_or("-").to_owned(),
166 attachment.name.clone(),
167 ]
168 })
169 .collect();
170
171 let mut out = render(&columns, &rows, ctx);
172 let paint = ctx.painter();
173 let _ = writeln!(
174 out,
175 "{}",
176 paint.paint(
177 &format!(
178 "shown {} of {} for {key}",
179 attachments.len(),
180 attachments.len()
181 ),
182 Palette::label()
183 )
184 );
185 out
186}
187
188#[must_use]
190pub fn human_size(bytes: u64) -> String {
191 const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
192 #[allow(clippy::cast_precision_loss)]
195 let mut size = bytes as f64;
196 let mut unit = 0;
197 while size >= 1024.0 && unit < UNITS.len() - 1 {
198 size /= 1024.0;
199 unit += 1;
200 }
201 if unit == 0 {
202 format!("{bytes} B")
203 } else {
204 format!("{size:.1} {}", UNITS[unit])
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn sizes_are_readable_without_losing_small_ones() {
214 assert_eq!(human_size(512), "512 B");
215 assert_eq!(human_size(2048), "2.0 KB");
216 assert_eq!(human_size(5 * 1024 * 1024), "5.0 MB");
217 }
218}