Skip to main content

ctl_core/
table.rs

1//! Compact pretty tables for command output.
2
3use comfy_table::presets::UTF8_FULL_CONDENSED;
4use comfy_table::{Cell, ContentArrangement, Table};
5
6use crate::style::{HEADING, OPTION, styled};
7
8/// Two-column token / value table. The token is styled.
9#[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/// Headered grid. Headers are styled; the first column is a token.
18#[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}