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::color::ColorMode;
7use crate::style::{HEADING, OPTION, styled};
8
9/// Two-column token / value table. Tokens are styled unless `color` is never.
10#[must_use]
11pub fn kv(
12    color: ColorMode,
13    rows: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
14) -> String {
15    let cells = rows
16        .into_iter()
17        .map(|(token, value)| vec![token.as_ref().to_owned(), value.as_ref().to_owned()]);
18    render(color, None, cells, true)
19}
20
21/// Headered grid. Styled unless `color` is never.
22#[must_use]
23pub fn grid(
24    color: ColorMode,
25    headers: &[&str],
26    rows: impl IntoIterator<Item = Vec<String>>,
27) -> String {
28    render(color, Some(headers), rows, true)
29}
30
31fn paint(color: ColorMode, style: anstyle::Style, value: &str) -> String {
32    if color == ColorMode::Never {
33        value.to_owned()
34    } else {
35        styled(style, value)
36    }
37}
38
39fn render(
40    color: ColorMode,
41    headers: Option<&[&str]>,
42    rows: impl IntoIterator<Item = Vec<String>>,
43    token_first: bool,
44) -> String {
45    let mut table = Table::new();
46    table
47        .load_style(UTF8_FULL_CONDENSED)
48        .set_content_arrangement(ContentArrangement::Dynamic);
49    if let Some(headers) = headers {
50        table.set_header(
51            headers
52                .iter()
53                .map(|header| Cell::new(paint(color, HEADING, header))),
54        );
55    }
56    for row in rows {
57        let cells = row.into_iter().enumerate().map(|(index, cell)| {
58            if token_first && index == 0 {
59                Cell::new(paint(color, OPTION, &cell))
60            } else {
61                Cell::new(cell)
62            }
63        });
64        table.add_row(cells);
65    }
66    format!("{table}")
67}
68
69#[cfg(test)]
70mod tests {
71    use super::{grid, kv};
72    use crate::color::ColorMode;
73    use crate::style::OPTION;
74
75    #[test]
76    fn kv_is_a_table_not_spaces() {
77        let out = kv(
78            ColorMode::Never,
79            [("crate", "demo@0.0.1"), ("package", "@org/pkg@0.0.1")],
80        );
81        assert!(out.contains("crate"), "{out}");
82        assert!(out.contains("demo@0.0.1"), "{out}");
83        assert!(out.contains("package"), "{out}");
84        assert!(out.contains('│') || out.contains('|'), "{out}");
85        assert!(!out.contains(&OPTION.render().to_string()), "{out}");
86    }
87
88    #[test]
89    fn never_has_no_ansi() {
90        let out = kv(ColorMode::Never, [("release", "v0.0.1")]);
91        assert!(!out.contains('\u{1b}'), "{out:?}");
92        assert!(out.contains("release"), "{out}");
93    }
94
95    #[test]
96    fn grid_has_headers() {
97        let out = grid(
98            ColorMode::Never,
99            &["id", "runner"],
100            [vec!["linux-x64".into(), "ubuntu-latest".into()]],
101        );
102        assert!(out.contains("linux-x64"), "{out}");
103        assert!(out.contains("ubuntu-latest"), "{out}");
104        assert!(out.contains("id"), "{out}");
105    }
106}