Skip to main content

ctl_core/
layout.rs

1//! Terminal width helpers for help tables.
2
3use comfy_table::presets::NOTHING;
4use comfy_table::{ContentArrangement, Table};
5
6const MIN_WIDTH: u16 = 20;
7
8#[must_use]
9/// Detected TTY width, or `COLUMNS` when at least 20.
10pub fn terminal_width() -> Option<u16> {
11    Table::new().width().or_else(|| {
12        std::env::var("COLUMNS")
13            .ok()?
14            .parse::<u16>()
15            .ok()
16            .filter(|width| *width >= MIN_WIDTH)
17    })
18}
19
20/// Cap a table at the terminal width when known.
21pub fn constrain(table: &mut Table) {
22    if let Some(width) = terminal_width() {
23        table.set_width(width);
24    }
25}
26
27#[must_use]
28/// Wrap `value` to the terminal width.
29pub fn wrap(value: &str) -> String {
30    terminal_width().map_or_else(|| value.to_string(), |width| wrap_to(value, width))
31}
32
33fn wrap_to(value: &str, width: u16) -> String {
34    let mut table = Table::new();
35    table
36        .load_style(NOTHING)
37        .set_content_arrangement(ContentArrangement::Dynamic)
38        .set_width(width)
39        .add_row([value]);
40    if let Some(column) = table.column_mut(0) {
41        column.set_padding((0, 0));
42    }
43    table
44        .to_string()
45        .lines()
46        .map(str::trim_end)
47        .collect::<Vec<_>>()
48        .join("\n")
49}
50
51/// Append wrapped `value` indented by `indentation` spaces.
52pub fn push_indented(output: &mut String, value: &str, indentation: u16) {
53    let prefix = " ".repeat(usize::from(indentation));
54    let wrapped = terminal_width().map_or_else(
55        || value.to_string(),
56        |width| wrap_to(value, width.saturating_sub(indentation)),
57    );
58    for line in wrapped.lines() {
59        output.push_str(&prefix);
60        output.push_str(line);
61        output.push('\n');
62    }
63}
64
65/// Append wrapped `value` and a trailing newline.
66pub fn push_line(output: &mut String, value: &str) {
67    output.push_str(&wrap(value));
68    if !output.ends_with('\n') {
69        output.push('\n');
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::{push_indented, push_line};
76
77    #[test]
78    fn push_line_terminates() {
79        let mut out = String::new();
80        push_line(&mut out, "hello");
81        assert!(out.ends_with('\n'));
82        assert!(out.contains("hello"));
83    }
84
85    #[test]
86    fn indent_prefixes() {
87        let mut out = String::new();
88        push_indented(&mut out, "x", 2);
89        assert!(out.starts_with("  x"));
90    }
91}