ctl-core 0.0.2

Shared clap chassis for the *ctl CLIs
Documentation
//! Terminal width helpers for help tables.

use comfy_table::presets::NOTHING;
use comfy_table::{ContentArrangement, Table};

const MIN_WIDTH: u16 = 20;

#[must_use]
/// Detected TTY width, or `COLUMNS` when at least 20.
pub fn terminal_width() -> Option<u16> {
    Table::new().width().or_else(|| {
        std::env::var("COLUMNS")
            .ok()?
            .parse::<u16>()
            .ok()
            .filter(|width| *width >= MIN_WIDTH)
    })
}

/// Cap a table at the terminal width when known.
pub fn constrain(table: &mut Table) {
    if let Some(width) = terminal_width() {
        table.set_width(width);
    }
}

#[must_use]
/// Wrap `value` to the terminal width.
pub fn wrap(value: &str) -> String {
    terminal_width().map_or_else(|| value.to_string(), |width| wrap_to(value, width))
}

fn wrap_to(value: &str, width: u16) -> String {
    let mut table = Table::new();
    table
        .load_style(NOTHING)
        .set_content_arrangement(ContentArrangement::Dynamic)
        .set_width(width)
        .add_row([value]);
    if let Some(column) = table.column_mut(0) {
        column.set_padding((0, 0));
    }
    table
        .to_string()
        .lines()
        .map(str::trim_end)
        .collect::<Vec<_>>()
        .join("\n")
}

/// Append wrapped `value` indented by `indentation` spaces.
pub fn push_indented(output: &mut String, value: &str, indentation: u16) {
    let prefix = " ".repeat(usize::from(indentation));
    let wrapped = terminal_width().map_or_else(
        || value.to_string(),
        |width| wrap_to(value, width.saturating_sub(indentation)),
    );
    for line in wrapped.lines() {
        output.push_str(&prefix);
        output.push_str(line);
        output.push('\n');
    }
}

/// Append wrapped `value` and a trailing newline.
pub fn push_line(output: &mut String, value: &str) {
    output.push_str(&wrap(value));
    if !output.ends_with('\n') {
        output.push('\n');
    }
}

#[cfg(test)]
mod tests {
    use super::{push_indented, push_line};

    #[test]
    fn push_line_terminates() {
        let mut out = String::new();
        push_line(&mut out, "hello");
        assert!(out.ends_with('\n'));
        assert!(out.contains("hello"));
    }

    #[test]
    fn indent_prefixes() {
        let mut out = String::new();
        push_indented(&mut out, "x", 2);
        assert!(out.starts_with("  x"));
    }
}