scriba 0.5.0

CLI text rendering, prompts, and terminal output utilities
Documentation
use serde_json::Value;
use tabled::{builder::Builder, settings::Style};

use crate::{error::Result, Format, TableLayout};

use super::Table;

pub fn render_table(format: Format, table: &Table) -> Result<String> {
    match format {
        Format::Plain | Format::Text => render_text_table(table),
        Format::Markdown => render_markdown_table(table),
        Format::Json => render_json_table(table),
        Format::Jsonl => render_jsonl_table(table),
    }
}

pub fn render_table_value(format: Format, table: &Table) -> Result<Value> {
    match format {
        Format::Json => Ok(table_to_json_value(table)),
        _ => Ok(Value::String(render_table(format, table)?)),
    }
}

pub fn render_text_table(table: &Table) -> Result<String> {
    match table.layout {
        TableLayout::Full => render_text_table_full(table),
        TableLayout::Compact => render_text_table_compact(table),
        TableLayout::Stacked => render_text_table_stacked(table),
    }
}

pub fn render_text_table_full(table: &Table) -> Result<String> {
    let table = table.materialized();

    let mut builder = Builder::default();
    builder.push_record(table.headers.iter().map(|s| s.as_str()));

    for row in &table.rows {
        builder.push_record(row.iter().map(|s| s.as_str()));
    }

    let mut built = builder.build();
    built.with(Style::rounded());

    Ok(built.to_string())
}

pub fn render_text_table_compact(table: &Table) -> Result<String> {
    let table = table.materialized();

    if table.rows.is_empty() {
        // Just headers
        return Ok(table.headers.join("  "));
    }

    let mut lines = vec![table.headers.join("  ")];

    for row in &table.rows {
        lines.push(row.join("  "));
    }

    Ok(lines.join("\n"))
}

pub fn render_text_table_stacked(table: &Table) -> Result<String> {
    let table = table.materialized();

    if table.rows.is_empty() {
        return Ok(String::new());
    }

    let mut lines = Vec::new();

    for (row_idx, row) in table.rows.iter().enumerate() {
        if row_idx > 0 {
            lines.push("---".to_string());
        }

        for (col_idx, cell) in row.iter().enumerate() {
            if col_idx < table.headers.len() {
                lines.push(format!("{}: {}", table.headers[col_idx], cell));
            }
        }
    }

    Ok(lines.join("\n"))
}

pub fn render_markdown_table(table: &Table) -> Result<String> {
    let table = table.materialized();

    let header = format!("| {} |", table.headers.join(" | "));
    let separator = format!(
        "|{}|",
        table
            .headers
            .iter()
            .map(|_| " --- ")
            .collect::<Vec<_>>()
            .join("|")
    );

    let body = table
        .rows
        .iter()
        .map(|row| format!("| {} |", row.join(" | ")))
        .collect::<Vec<_>>()
        .join("\n");

    if body.is_empty() {
        Ok(format!("{header}\n{separator}"))
    } else {
        Ok(format!("{header}\n{separator}\n{body}"))
    }
}

pub fn render_json_table(table: &Table) -> Result<String> {
    Ok(serde_json::to_string_pretty(&table_to_json_value(table))?)
}

pub fn render_jsonl_table(table: &Table) -> Result<String> {
    let table = table.materialized();

    let lines = rows_to_records(&table)
        .into_iter()
        .map(|row| serde_json::to_string(&row))
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(lines.join("\n"))
}

pub fn table_to_json_value(table: &Table) -> Value {
    serde_json::to_value(table.materialized()).unwrap_or(Value::Null)
}

fn rows_to_records(table: &Table) -> Vec<serde_json::Map<String, Value>> {
    let table = table.materialized();

    table
        .rows
        .iter()
        .map(|row| {
            table
                .headers
                .iter()
                .zip(row.iter())
                .map(|(k, v)| (k.clone(), Value::String(v.clone())))
                .collect::<serde_json::Map<String, Value>>()
        })
        .collect()
}