querypie-cli 0.1.0

Query QueryPie databases from the terminal with webview authentication.
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

pub(crate) fn print_table<I>(headers: &[&str], rows: I, truncate: bool)
where
    I: IntoIterator<Item = Vec<String>>,
{
    let rows = rows.into_iter().collect::<Vec<_>>();
    if headers.is_empty() {
        for row in rows {
            println!("{}", row.join("\t"));
        }
        return;
    }

    let mut widths = headers
        .iter()
        .map(|header| display_width(header))
        .collect::<Vec<_>>();
    for row in &rows {
        for (index, cell) in row.iter().enumerate() {
            if let Some(width) = widths.get_mut(index) {
                *width = (*width).max(display_width(cell));
            }
        }
    }
    if truncate {
        fit_widths_to_terminal(&mut widths);
    }

    print_row(
        headers.iter().map(|value| value.to_string()).collect(),
        &widths,
    );
    print_row(
        widths.iter().map(|width| "-".repeat(*width)).collect(),
        &widths,
    );
    for row in rows {
        print_row(row, &widths);
    }
}

fn print_row(row: Vec<String>, widths: &[usize]) {
    let cells = row
        .into_iter()
        .enumerate()
        .map(|(index, value)| {
            let width = widths.get(index).copied().unwrap_or(0);
            pad_to_width(truncate_to_width(&value, width), width)
        })
        .collect::<Vec<_>>();
    println!("{}", cells.join("  "));
}

fn fit_widths_to_terminal(widths: &mut [usize]) {
    let Some(term_width) = terminal_width() else {
        return;
    };
    let separators = widths.len().saturating_sub(1) * 2;
    let min_width = 8;
    while widths.iter().sum::<usize>() + separators > term_width {
        let Some(width) = widths.iter_mut().filter(|width| **width > min_width).max() else {
            break;
        };
        *width -= 1;
    }
}

fn terminal_width() -> Option<usize> {
    terminal_size::terminal_size().map(|(terminal_size::Width(width), _)| width as usize)
}

fn truncate_to_width(value: &str, width: usize) -> String {
    if display_width(value) <= width {
        return value.to_string();
    }
    if width == 0 {
        return String::new();
    }
    if width == 1 {
        return "".to_string();
    }

    let ellipsis = "";
    let target = width.saturating_sub(display_width(ellipsis));
    let mut out = String::new();
    let mut used = 0;
    for ch in value.chars() {
        let ch_width = ch.width().unwrap_or(0);
        if used + ch_width > target {
            break;
        }
        out.push(ch);
        used += ch_width;
    }
    out.push_str(ellipsis);
    out
}

fn pad_to_width(value: String, width: usize) -> String {
    let padding = width.saturating_sub(display_width(&value));
    format!("{value}{}", " ".repeat(padding))
}

fn display_width(value: &str) -> usize {
    UnicodeWidthStr::width(strip_ansi(value).as_str())
}

fn strip_ansi(value: &str) -> String {
    let mut out = String::new();
    let mut chars = value.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '\x1b' && chars.peek() == Some(&'[') {
            let _ = chars.next();
            for ch in chars.by_ref() {
                if ch.is_ascii_alphabetic() {
                    break;
                }
            }
        } else {
            out.push(ch);
        }
    }
    out
}