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 &body,
98 withheld,
99 ctx,
100 );
101 }
102
103 out
104}
105
106#[must_use]
112pub fn contents(page: &Page<Entity>, ctx: &Context) -> String {
113 let columns = [
114 Column::whole("SHORT", 8, Palette::key()),
115 Column::new("TYPE", 10, Palette::label()),
116 Column::new("ID", 26, Palette::label()),
117 Column::new("STATUS", 14, anstyle::Style::new()),
118 Column::new("SUMMARY", 40, anstyle::Style::new()),
119 ];
120 let rows: Vec<Vec<String>> = page
121 .items
122 .iter()
123 .map(|entity| {
124 vec![
125 entity
126 .short_id
127 .map_or_else(|| "-".to_owned(), |id| id.to_string()),
128 entity.entity_type.as_deref().unwrap_or("-").to_owned(),
129 entity.id.clone(),
130 entity.status.as_deref().unwrap_or("-").to_owned(),
131 entity.summary.clone(),
132 ]
133 })
134 .collect();
135
136 let mut out = render(&columns, &rows, ctx);
137 out.push_str(&tally(
138 page.items.len(),
139 page.total,
140 page.has_more().then_some(page.page + 1),
141 ctx,
142 ));
143 out
144}
145
146#[must_use]
151pub fn attachments(key: &str, attachments: &[Attachment], ctx: &Context) -> String {
152 let columns = [
153 Column::whole("ID", 14, Palette::key()),
154 Column::whole("SIZE", 10, anstyle::Style::new()),
155 Column::new("TYPE", 18, anstyle::Style::new()),
156 Column::whole("NAME", 40, Palette::untrusted()),
157 ];
158 let rows: Vec<Vec<String>> = attachments
159 .iter()
160 .map(|attachment| {
161 vec![
162 attachment.id.clone(),
163 attachment.size.map_or_else(|| "-".to_owned(), human_size),
164 attachment.mimetype.as_deref().unwrap_or("-").to_owned(),
165 attachment.name.clone(),
166 ]
167 })
168 .collect();
169
170 let mut out = render(&columns, &rows, ctx);
171 let paint = ctx.painter();
172 let _ = writeln!(
173 out,
174 "{}",
175 paint.paint(
176 &format!(
177 "shown {} of {} for {key}",
178 attachments.len(),
179 attachments.len()
180 ),
181 Palette::label()
182 )
183 );
184 out
185}
186
187#[must_use]
189pub fn human_size(bytes: u64) -> String {
190 const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
191 #[allow(clippy::cast_precision_loss)]
194 let mut size = bytes as f64;
195 let mut unit = 0;
196 while size >= 1024.0 && unit < UNITS.len() - 1 {
197 size /= 1024.0;
198 unit += 1;
199 }
200 if unit == 0 {
201 format!("{bytes} B")
202 } else {
203 format!("{size:.1} {}", UNITS[unit])
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn sizes_are_readable_without_losing_small_ones() {
213 assert_eq!(human_size(512), "512 B");
214 assert_eq!(human_size(2048), "2.0 KB");
215 assert_eq!(human_size(5 * 1024 * 1024), "5.0 MB");
216 }
217}