Current section
Files
Jump to
Current section
Files
src/trellis.gleam
import gleam/int
import gleam/list
import gleam/string
const vertical = "│"
const horizontal = "─"
const top_left = "┌"
const top_right = "┐"
const middle_left = "┤"
const middle_right = "├"
const middle_bottom = "┴"
const middle_top = "┬"
const bottom_left = "└"
const bottom_right = "┘"
const cross = "┼"
/// A type representing a formatted table with headers and rows of data
///
/// ## Example
///
/// ```gleam
/// let table =
/// table([user1, user2, user3])
/// |> with("Name", Left, {
/// use user <- param
/// user.name
/// })
/// |> with("Age", Right, {
/// use user <- param
/// user.age |> int.to_string
/// })
/// ```
pub type Table(value) {
Table(columns: List(Column(value)), rows: List(value))
}
/// A type representing a column in the table with header, alignment, and value getter
pub type Column(value) {
Column(header: String, align: Align, getter: fn(value) -> String)
}
/// Alignment options for column content
pub type Align {
Left
Right
Center
}
/// Creates a new table with the given rows of data
pub fn table(data rows: List(value)) -> Table(value) {
Table(columns: [], rows: rows)
}
/// Helper function to wrap a function for use in column definitions
///
/// This is a utility function that can make column definitions more readable
/// when used with partial application
pub fn param(f: fn(a) -> b) -> fn(a) -> b {
f
}
/// Adds a new column to the table definition
pub fn with(
builder: Table(value),
header header: String,
align align: Align,
getter getter: fn(value) -> String,
) -> Table(value) {
let Table(columns: columns, rows: rows) = builder
Table(columns: [Column(header, align, getter), ..columns], rows: rows)
}
/// Calculates the required width for each column based on content
///
/// Takes into account both header width and the maximum width of values
/// in each column to determine the appropriate column widths
fn calculate_widths(headers: List(String), values: List(String)) -> List(Int) {
let lengths = list.sized_chunk(values, list.length(headers))
list.map2(headers, list.transpose(lengths), fn(header, col_values) {
let max_value_length =
list.fold(col_values, 0, fn(acc, val) { int.max(acc, string.length(val)) })
int.max(string.length(header), max_value_length)
})
}
/// Converts the table to a formatted string representation
///
/// Creates a string with Unicode box-drawing characters, properly aligned content,
/// and separators between header and rows
pub fn to_string(table: Table(value)) -> String {
let Table(columns: columns, rows: rows) = table
let columns = list.reverse(columns)
let headers = list.map(columns, fn(col) { col.header })
let aligns = list.map(columns, fn(col) { col.align })
let values =
list.flat_map(rows, fn(row) {
list.map(columns, fn(col) { col.getter(row) })
})
let widths = calculate_widths(headers, values)
let separator_top = make_separator(widths, True, True)
let header_row =
make_row(
list.map(list.range(1, list.length(columns)), fn(_) { Center }),
widths,
headers,
)
let separator_header = make_separator(widths, True, False)
let body_rows =
list.map(rows, fn(row) {
let row_values = list.map(columns, fn(col) { col.getter(row) })
make_row(aligns, widths, row_values)
})
let separator_bottom = make_separator(widths, False, False)
[separator_top, header_row, separator_header]
|> list.append(body_rows)
|> list.append([separator_bottom])
|> string.join("\n")
}
/// Creates a horizontal separator line for the table
fn make_separator(columns: List(Int), is_header: Bool, is_top: Bool) -> String {
let start = case is_top, is_header {
True, _ -> top_left
False, True -> middle_right
False, False -> bottom_left
}
let end = case is_top, is_header {
True, _ -> top_right
False, True -> middle_left
False, False -> bottom_right
}
let middle = case is_top, is_header {
True, _ -> middle_top
False, True -> cross
False, False -> middle_bottom
}
let line =
list.fold(columns, start, fn(acc, col) {
acc <> string.repeat(horizontal, col + 2) <> middle
})
string.slice(line, 0, string.length(line) - 1) <> end
}
/// Creates a row in the table with properly aligned content
fn make_row(
aligns: List(Align),
widths: List(Int),
values: List(String),
) -> String {
let row =
{
use #(align, width), value <- list.map2(
{
use align, width <- list.map2(aligns, widths)
#(align, width)
},
values,
)
let padded = case align {
Left -> pad_right(value, width)
Right -> pad_left(value, width)
Center -> pad_center(value, width)
}
" " <> padded <> " "
}
|> string.join(vertical)
vertical <> row <> vertical
}
/// Left-pads a string with spaces to reach the specified width
fn pad_left(text: String, width: Int) -> String {
let padding = width - string.length(text)
string.repeat(" ", padding) <> text
}
/// Right-pads a string with spaces to reach the specified width
fn pad_right(text: String, width: Int) -> String {
let padding = width - string.length(text)
text <> string.repeat(" ", padding)
}
/// Centers a string by padding both sides with spaces to reach the specified width
fn pad_center(text: String, width: Int) -> String {
let padding = width - string.length(text)
let left = padding / 2
let right = padding - left
string.repeat(" ", left) <> text <> string.repeat(" ", right)
}