1use comfy_table::presets::UTF8_FULL_CONDENSED;
4use comfy_table::{Cell, ContentArrangement, Table};
5
6use crate::style::{HEADING, OPTION, styled};
7
8#[must_use]
10pub fn kv(rows: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>) -> String {
11 let cells = rows
12 .into_iter()
13 .map(|(token, value)| vec![token.as_ref().to_owned(), value.as_ref().to_owned()]);
14 render(None, cells, true)
15}
16
17#[must_use]
19pub fn grid(headers: &[&str], rows: impl IntoIterator<Item = Vec<String>>) -> String {
20 render(Some(headers), rows, true)
21}
22
23fn render(
24 headers: Option<&[&str]>,
25 rows: impl IntoIterator<Item = Vec<String>>,
26 token_first: bool,
27) -> String {
28 let mut table = Table::new();
29 table
30 .load_style(UTF8_FULL_CONDENSED)
31 .set_content_arrangement(ContentArrangement::Dynamic);
32 if let Some(headers) = headers {
33 table.set_header(
34 headers
35 .iter()
36 .map(|header| Cell::new(styled(HEADING, header))),
37 );
38 }
39 for row in rows {
40 let cells = row.into_iter().enumerate().map(|(index, cell)| {
41 if token_first && index == 0 {
42 Cell::new(styled(OPTION, &cell))
43 } else {
44 Cell::new(cell)
45 }
46 });
47 table.add_row(cells);
48 }
49 format!("{table}")
50}
51
52#[cfg(test)]
53mod tests {
54 use super::{grid, kv};
55 use crate::style::OPTION;
56
57 #[test]
58 fn kv_is_a_table_not_spaces() {
59 let out = kv([("crate", "demo@0.0.1"), ("package", "@org/pkg@0.0.1")]);
60 assert!(out.contains("crate"), "{out}");
61 assert!(out.contains("demo@0.0.1"), "{out}");
62 assert!(out.contains("package"), "{out}");
63 assert!(out.contains('│') || out.contains('|'), "{out}");
64 assert!(out.contains(&OPTION.render().to_string()), "{out}");
65 }
66
67 #[test]
68 fn grid_has_headers() {
69 let out = grid(
70 &["id", "runner"],
71 [vec!["linux-x64".into(), "ubuntu-latest".into()]],
72 );
73 assert!(out.contains("linux-x64"), "{out}");
74 assert!(out.contains("ubuntu-latest"), "{out}");
75 assert!(out.contains("id"), "{out}");
76 }
77}