///|
pub struct IterGrid {
records : IterRecords
config : SpannedConfig
dimension : IterGridDimension
}
///|
pub fn IterGrid::new(
records : IterRecords,
config : SpannedConfig,
dimension : IterGridDimension,
) -> IterGrid {
{ records, config, dimension, }
}
///|
fn render_indent(size : Int, fill : Char, color : ANSIBuf?) -> String {
if size <= 0 {
return ""
}
render_colored_text(String::make(size, fill), color)
}
///|
fn render_cell(
line : CellLine,
width : Int,
padding_left : Int,
padding_right : Int,
align : HAlign,
records : IterRecords,
config : SpannedConfig,
source : (Int, Int),
) -> String {
let fills = config.padding_fill_at(source.0, source.1)
let colors = config.get_padding_color(source.0, source.1)
let content = match line {
Content(content) => content
TopPadding => return render_indent(width, fills.top, colors.top)
BottomPadding => return render_indent(width, fills.bottom, colors.bottom)
}
let format = config.text_format_at(source.0, source.1)
let content = if format.formatting.trim_horizontal {
trim_ansi_whitespace(content)
} else {
content
}
let available = width - padding_left - padding_right
let line_width = string_display_width(content)
let cell_width = if format.formatting.align_lines {
line_width
} else {
processed_lines(
records.get_cell(source.0, source.1),
config.width_mode_at(source.0, source.1),
config.height_mode_at(source.0, source.1),
).fold(init=0, (width, line) => {
let line = if format.formatting.trim_horizontal {
line.trim().to_owned()
} else {
line
}
Int::max(width, string_display_width(line))
})
}
let gap = Int::max(0, available - cell_width)
let (left_fill, right_fill) = match align {
Left => (0, gap)
Right => (gap, 0)
Center => (gap / 2, gap - gap / 2)
}
render_indent(padding_left, fills.left, colors.left) +
render_justification(format, left_fill) +
render_colored_text(content, format.color) +
render_justification(format, right_fill) +
render_justification(format, Int::max(0, cell_width - line_width)) +
render_indent(padding_right, fills.right, colors.right)
}
///|
fn render_colored_text(text : String, color : ANSIBuf?) -> String {
match color {
Some(color) => color.prefix + text + color.suffix
None => text
}
}
///|
fn render_justification(format : TextFormat, count : Int) -> String {
if count <= 0 {
return ""
}
render_colored_text(
format.justification.to_string().repeat(count),
format.justification_color,
)
}
///|
priv enum CellLine {
Content(String)
TopPadding
BottomPadding
}
///|
fn CellLine::has_text(self : CellLine) -> Bool {
match self {
Content(text) => text != ""
_ => false
}
}
///|
fn display_lines(
records : IterRecords,
config : SpannedConfig,
source : (Int, Int),
) -> Array[String] {
let lines = processed_lines(
records.get_cell(source.0, source.1),
config.width_mode_at(source.0, source.1),
config.height_mode_at(source.0, source.1),
)
if !config.get_formatting(source.0, source.1).trim_vertical {
return lines
}
let mut start = 0
let mut end_ = lines.length()
while start < end_ && lines[start].trim().is_empty() {
start += 1
}
while end_ > start && lines[end_ - 1].trim().is_empty() {
end_ -= 1
}
lines[start:end_].to_owned()
}
///|
fn span_width(
widths : Array[Int],
borders : Borders,
start : Int,
span : Int,
) -> Int {
let end_ = if start + span < widths.length() {
start + span
} else {
widths.length()
}
let mut total = 0
for col = start; col < end_; col = col + 1 {
total = total + widths[col]
}
if borders.vertical is Some(_) && end_ - start > 1 {
total = total + (end_ - start - 1)
}
total
}
///|
fn blank_values(count_cols : Int, kind : CellLine) -> Array[CellLine] {
let values = []
for _ in 0.. Int {
let mut total = 0
for index = start; index < end_; index = index + 1 {
total = total + values[index]
}
total
}
///|
fn boundary_has_line(
config : SpannedConfig,
boundary_row : Int,
count_rows : Int,
) -> Bool {
if boundary_row <= 0 || boundary_row >= count_rows {
return false
}
let standard = match config.horizontal_lines() {
All => true
HeaderOnly => boundary_row == 1
None => false
}
standard || config.custom_horizontal_line(boundary_row) is Some(_)
}
///|
fn count_range_boundaries(
config : SpannedConfig,
count_rows : Int,
start : Int,
end_ : Int,
) -> Int {
let mut total = 0
for boundary_row = start + 1
boundary_row < end_
boundary_row = boundary_row + 1 {
if boundary_has_line(config, boundary_row, count_rows) {
total += 1
}
}
total
}
///|
fn inc_range(values : Array[Int], size : Int, start : Int, end_ : Int) -> Unit {
let span = end_ - start
if span <= 0 || size <= 0 {
return
}
let one = size / span
let rest = size - span * one
for index = start; index < end_; index = index + 1 {
if index == start {
values[index] = values[index] + one + rest
} else {
values[index] = values[index] + one
}
}
}
///|
fn row_span_anchor(config : SpannedConfig, row : Int, col : Int) -> (Int, Int)? {
let mut index = config.row_spans.length() - 1
while index >= 0 {
let item = config.row_spans[index]
let colspan = config.col_span_at(item.0, item.1)
if row > item.0 &&
row < item.0 + item.2 &&
col >= item.1 &&
col < item.1 + colspan {
return Some((item.0, item.1))
}
index = index - 1
}
None
}
///|
fn row_span_crosses_boundary(
config : SpannedConfig,
boundary_row : Int,
col : Int,
) -> Bool {
for index in 0.. item.0 &&
boundary_row < item.0 + item.2 &&
col >= item.1 &&
col < item.1 + colspan {
return true
}
}
false
}
///|
fn row_span_boundary_anchor(
config : SpannedConfig,
boundary_row : Int,
col : Int,
) -> (Int, Int)? {
match row_span_anchor(config, boundary_row, col) {
Some(anchor) =>
if boundary_row > anchor.0 &&
boundary_row < anchor.0 + config.row_span_at(anchor.0, anchor.1) {
Some(anchor)
} else {
None
}
None => None
}
}
///|
fn display_anchor_for(
config : SpannedConfig,
row : Int,
col : Int,
) -> (Int, Int)? {
match row_span_anchor(config, row, col) {
Some(anchor) => if anchor.1 == col { Some(anchor) } else { None }
None =>
if config.is_cell_visible(row, col) {
Some((row, col))
} else {
None
}
}
}
///|
fn span_source_for_anchor(
config : SpannedConfig,
anchor_row : Int,
anchor_col : Int,
) -> (Int, Int) {
config.span_source_at(anchor_row, anchor_col)
}
///|
fn row_metrics(
records : IterRecords,
count_cols : Int,
config : SpannedConfig,
) -> Array[Int] {
let count_rows = records.count_rows()
let content_heights = Array::make(count_rows, 0)
let spans : Array[(Int, Int, Int)] = []
for row in 0.. 1 {
spans.push((row, rowspan, height))
} else {
content_heights[row] = Int::max(content_heights[row], height)
}
}
}
for entry in spans {
let (row, span, needed) = entry
let end_ = Int::min(count_rows, row + span)
let current = sum_range(content_heights, row, end_) +
count_range_boundaries(config, count_rows, row, end_)
if needed > current {
inc_range(content_heights, needed - current, row, end_)
}
}
content_heights
}
///|
fn cell_content_for_row(
records : IterRecords,
config : SpannedConfig,
row_heights : Array[Int],
row : Int,
col : Int,
line_index : Int,
) -> CellLine {
let anchor = match display_anchor_for(config, row, col) {
Some(anchor) => anchor
None => return BottomPadding
}
let source = span_source_for_anchor(config, anchor.0, anchor.1)
if anchor.0 == row && config.row_span_at(anchor.0, anchor.1) <= 1 {
let lines = display_lines(records, config, source)
let (_, _, top, _) = config.padding_at(source.0, source.1)
let cell_height = Int::min(lines.length(), row_heights[row])
let missing = Int::max(0, row_heights[row] - top - cell_height)
let valign = config.valign_at(source.0, source.1)
let top_fill = top +
(match valign {
Top => 0
Bottom => missing
Center => missing / 2
})
if line_index < top_fill {
return TopPadding
}
if line_index >= top_fill + cell_height {
return BottomPadding
}
return Content(lines[line_index - top_fill])
}
let rowspan = config.row_span_at(anchor.0, anchor.1)
let end_ = if anchor.0 + rowspan < row_heights.length() {
anchor.0 + rowspan
} else {
row_heights.length()
}
let total_height = sum_range(row_heights, anchor.0, end_) +
count_range_boundaries(config, row_heights.length(), anchor.0, end_)
let lines = display_lines(records, config, source)
let (_, _, top, _) = config.padding_at(source.0, source.1)
let cell_height = Int::min(lines.length(), total_height)
let missing = Int::max(0, total_height - top - cell_height)
let valign = config.valign_at(source.0, source.1)
let top_fill = top +
(match valign {
Top => 0
Bottom => missing
Center => missing / 2
})
let mut offset = 0
for index = anchor.0; index < row; index = index + 1 {
offset = offset + row_heights[index]
if boundary_has_line(config, index + 1, row_heights.length()) {
offset += 1
}
}
let absolute = offset + line_index
if absolute < top_fill {
TopPadding
} else if absolute >= top_fill + cell_height {
BottomPadding
} else {
Content(lines[absolute - top_fill])
}
}
///|
fn cell_content_for_boundary(
records : IterRecords,
config : SpannedConfig,
row_heights : Array[Int],
boundary_row : Int,
col : Int,
) -> CellLine {
let anchor = match row_span_boundary_anchor(config, boundary_row, col) {
Some(anchor) => anchor
None => return BottomPadding
}
let source = span_source_for_anchor(config, anchor.0, anchor.1)
let rowspan = config.row_span_at(anchor.0, anchor.1)
let end_ = if anchor.0 + rowspan < row_heights.length() {
anchor.0 + rowspan
} else {
row_heights.length()
}
let total_height = sum_range(row_heights, anchor.0, end_) +
count_range_boundaries(config, row_heights.length(), anchor.0, end_)
let lines = display_lines(records, config, source)
let (_, _, top, _) = config.padding_at(source.0, source.1)
let cell_height = Int::min(lines.length(), total_height)
let missing = Int::max(0, total_height - top - cell_height)
let valign = config.valign_at(source.0, source.1)
let top_fill = top +
(match valign {
Top => 0
Bottom => missing
Center => missing / 2
})
let mut absolute = 0
for row = anchor.0; row < boundary_row; row = row + 1 {
absolute = absolute + row_heights[row]
if row + 1 < boundary_row &&
boundary_has_line(config, row + 1, row_heights.length()) {
absolute += 1
}
}
if absolute < top_fill {
TopPadding
} else if absolute >= top_fill + cell_height {
BottomPadding
} else {
Content(lines[absolute - top_fill])
}
}
///|
fn render_split_line(
records : IterRecords,
borders : Borders,
widths : Array[Int],
boundary_row : Int,
count_cols : Int,
config : SpannedConfig,
row_heights : Array[Int],
) -> String {
let mut has_uncovered = false
let mut has_content = false
for col in 0.. ch.to_string()
None =>
match first_char([borders.left_intersection, borders.left]) {
Some(value) => value.to_string()
None => ""
}
}
let mut col = 0
while col < count_cols {
let covered = row_span_crosses_boundary(config, boundary_row, col)
let fill = match row_span_boundary_anchor(config, boundary_row, col) {
Some(anchor) =>
if anchor.1 == col {
let source = span_source_for_anchor(config, anchor.0, anchor.1)
let (padding_left, padding_right, _, _) = config.padding_at(
source.0,
source.1,
)
let align = config.align_at(source.0, source.1)
let colspan = config.col_span_at(anchor.0, anchor.1)
col = col + colspan
render_cell(
cell_content_for_boundary(
records,
config,
row_heights,
boundary_row,
col - colspan,
),
span_width(widths, borders, anchor.1, colspan),
padding_left,
padding_right,
align,
records,
config,
source,
)
} else {
col = col + 1
continue
}
None => {
col = col + 1
if covered {
" ".repeat(widths[col - 1])
} else {
match borders.horizontal {
Some(value) => value.to_string().repeat(widths[col - 1])
None => " ".repeat(widths[col - 1])
}
}
}
}
out = out + fill
if col < count_cols {
let current_anchor = row_span_boundary_anchor(
config,
boundary_row,
col - 1,
)
let next_anchor = row_span_boundary_anchor(config, boundary_row, col)
let join = match config.intersection_override_at(boundary_row, col) {
Some(ch) =>
if current_anchor == next_anchor && current_anchor is Some(_) {
""
} else {
ch.to_string()
}
None =>
match current_anchor {
Some(_) =>
if current_anchor == next_anchor {
""
} else {
match
first_char([
borders.intersection,
borders.vertical,
borders.horizontal,
]) {
Some(value) => value.to_string()
None => ""
}
}
None =>
match
first_char([
borders.intersection,
borders.vertical,
borders.horizontal,
]) {
Some(value) => value.to_string()
None => ""
}
}
}
out = out + join
}
}
out +
(match config.intersection_override_at(boundary_row, count_cols) {
Some(ch) => ch.to_string()
None =>
match first_char([borders.right_intersection, borders.right]) {
Some(value) => value.to_string()
None => ""
}
})
}
///|
fn render_row_line(
records : IterRecords,
borders : Borders,
widths : Array[Int],
values : Array[CellLine],
row_index : Int,
count_cols : Int,
config : SpannedConfig,
) -> String {
let has_overrides = config.has_cell_borders()
let left_ch = if has_overrides {
match config.vertical_override_at(row_index, 0) {
Some(ch) => Some(ch)
None => borders.left
}
} else {
borders.left
}
let mut out = match left_ch {
Some(value) => value.to_string()
None => ""
}
let mut col = 0
while col < count_cols {
let anchor = match display_anchor_for(config, row_index, col) {
Some(anchor) => anchor
None => {
col = col + 1
continue
}
}
if anchor.1 != col {
col = col + 1
continue
}
let content = if col < values.length() {
values[col]
} else {
BottomPadding
}
let (padding_left, padding_right, _, _) = config.padding_at(
anchor.0,
anchor.1,
)
let align = config.align_at(anchor.0, anchor.1)
let colspan = config.col_span_at(anchor.0, anchor.1)
out = out +
render_cell(
content,
span_width(widths, borders, anchor.1, colspan),
padding_left,
padding_right,
align,
records,
config,
span_source_for_anchor(config, anchor.0, anchor.1),
)
col = col + colspan
if col < count_cols {
let sep_ch = if has_overrides {
match config.vertical_override_at(row_index, col) {
Some(ch) => Some(ch)
None => borders.vertical
}
} else {
borders.vertical
}
out = out +
(match sep_ch {
Some(value) => value.to_string()
None => ""
})
}
}
let right_ch = if has_overrides {
match config.vertical_override_at(row_index, count_cols) {
Some(ch) => Some(ch)
None => borders.right
}
} else {
borders.right
}
out +
(match right_ch {
Some(value) => value.to_string()
None => ""
})
}
///|
fn render_row(
records : IterRecords,
borders : Borders,
widths : Array[Int],
row_index : Int,
count_cols : Int,
config : SpannedConfig,
row_heights : Array[Int],
) -> Array[String] {
let lines = []
for line_index = 0
line_index < row_heights[row_index]
line_index = line_index + 1 {
let values = blank_values(count_cols, BottomPadding)
for col in 0.. String {
let left = match config.intersection_override_at(row_boundary, 0) {
Some(ch) => Some(ch)
None => default_left
}
let mut out = match left {
Some(value) => value.to_string()
None => ""
}
for col in 0.. Some(ch)
None => default_fill
}
let w = widths[col]
out = out +
(match fill {
Some(value) => value.to_string().repeat(w)
None => "".repeat(w)
})
if col + 1 < count_cols {
let ix = match config.intersection_override_at(row_boundary, col + 1) {
Some(ch) => Some(ch)
None => default_intersection
}
out = out +
(match ix {
Some(value) => value.to_string()
None => ""
})
}
}
let right = match config.intersection_override_at(row_boundary, count_cols) {
Some(ch) => Some(ch)
None => default_right
}
out +
(match right {
Some(value) => value.to_string()
None => ""
})
}
///|
pub fn IterGrid::to_string(self : IterGrid) -> String {
let widths = self.dimension.widths()
let borders = self.config.borders()
if widths.is_empty() {
return ""
}
let lines : Array[String] = []
let has_overrides = self.config.has_cell_borders()
let count_cols = self.records.count_cols()
let top_left = match borders.top {
Some(_) => first_char([borders.top_left, borders.left])
None => None
}
let top_intersection = match borders.top {
Some(_) => first_char([borders.top_intersection, borders.top])
None => None
}
let top_right = match borders.top {
Some(_) => first_char([borders.top_right, borders.right])
None => None
}
let top = if has_overrides {
build_line_with_overrides(
self.config,
widths,
top_left,
borders.top,
top_intersection,
top_right,
0,
count_cols,
)
} else {
borders.build_line(
top_left,
borders.top,
top_intersection,
top_right,
widths,
)
}
if top != "" {
lines.push(top)
}
let rows = self.records.rows()
let row_heights = match self.dimension.heights {
Some(heights) => heights
None => row_metrics(self.records, count_cols, self.config)
}
let horizontal_lines = self.config.horizontal_lines()
for row_index in 0.. lines.push(line))
let show_middle = match horizontal_lines {
All => row_index + 1 < rows.length()
HeaderOnly => row_index == 0 && row_index + 1 < rows.length()
None => false
}
let custom_middle = self.config.custom_horizontal_line(row_index + 1)
if show_middle || custom_middle is Some(_) {
let boundary = row_index + 1
let middle = match custom_middle {
Some(line) =>
if self.config.has_row_spans() {
render_split_line(
self.records,
{
..borders,
horizontal: line.main,
left_intersection: first_char([
line.left,
borders.left_intersection,
borders.left,
]),
intersection: first_char([
line.intersection,
borders.intersection,
borders.vertical,
line.main,
]),
right_intersection: first_char([
line.right,
borders.right_intersection,
borders.right,
]),
},
widths,
boundary,
count_cols,
self.config,
row_heights,
)
} else if has_overrides {
build_line_with_overrides(
self.config,
widths,
first_char([line.left, borders.left_intersection, borders.left]),
line.main,
first_char([
line.intersection,
borders.intersection,
borders.vertical,
line.main,
]),
first_char([line.right, borders.right_intersection, borders.right]),
boundary,
count_cols,
)
} else {
borders.build_line(
first_char([line.left, borders.left_intersection, borders.left]),
line.main,
first_char([
line.intersection,
borders.intersection,
borders.vertical,
line.main,
]),
first_char([line.right, borders.right_intersection, borders.right]),
widths,
)
}
None =>
if self.config.has_row_spans() {
render_split_line(
self.records,
borders,
widths,
boundary,
count_cols,
self.config,
row_heights,
)
} else if has_overrides {
build_line_with_overrides(
self.config,
widths,
first_char([borders.left_intersection, borders.left]),
borders.horizontal,
first_char([
borders.intersection,
borders.vertical,
borders.horizontal,
]),
first_char([borders.right_intersection, borders.right]),
boundary,
count_cols,
)
} else {
borders.build_line(
first_char([borders.left_intersection, borders.left]),
borders.horizontal,
first_char([
borders.intersection,
borders.vertical,
borders.horizontal,
]),
first_char([borders.right_intersection, borders.right]),
widths,
)
}
}
if middle != "" {
lines.push(middle)
}
}
}
let bottom_left = match borders.bottom {
Some(_) => first_char([borders.bottom_left, borders.left])
None => None
}
let bottom_intersection = match borders.bottom {
Some(_) => first_char([borders.bottom_intersection, borders.bottom])
None => None
}
let bottom_right = match borders.bottom {
Some(_) => first_char([borders.bottom_right, borders.right])
None => None
}
let bottom = if has_overrides {
build_line_with_overrides(
self.config,
widths,
bottom_left,
borders.bottom,
bottom_intersection,
bottom_right,
rows.length(),
count_cols,
)
} else {
borders.build_line(
bottom_left,
borders.bottom,
bottom_intersection,
bottom_right,
widths,
)
}
if bottom != "" {
lines.push(bottom)
}
// Apply margin if configured
if self.config.has_margin() {
let total_width = widths.iter().take(count_cols).fold(init=0, Int::add) +
self.config.count_vertical(count_cols)
let full_width = total_width +
self.config.margin_left +
self.config.margin_right
let table_height = lines.length()
// Add left/right margins to each line
if self.config.margin_left > 0 || self.config.margin_right > 0 {
for i in 0.. 0 {
let top_lines = margin_horizontal_lines(
self.config.margin_fill_top,
self.config.margin_top,
self.config.margin_offset_top,
self.config.margin_colors.top,
full_width,
)
for i = top_lines.length() - 1; i >= 0; i = i - 1 {
lines.insert(0, top_lines[i])
}
}
// Append bottom margin lines
if self.config.margin_bottom > 0 {
let bottom_lines = margin_horizontal_lines(
self.config.margin_fill_bottom,
self.config.margin_bottom,
self.config.margin_offset_bottom,
self.config.margin_colors.bottom,
full_width,
)
for i in 0.. String {
if size == 0 {
return ""
}
let offset = if offset > height { height } else { offset }
if offset >= 0 {
// Start offset: line < offset → spaces, line >= offset → fill
if line >= offset {
render_indent(size, fill, color)
} else {
" ".repeat(size)
}
} else {
// End offset: from end
let abs_off = -offset
let abs_off = if abs_off > height { height } else { abs_off }
let pos = height - abs_off
if line >= pos {
" ".repeat(size)
} else {
render_indent(size, fill, color)
}
}
}
///|
/// Build horizontal margin lines (top or bottom).
/// `offset` uses Start semantics: cols before offset get spaces, rest get fill.
fn margin_horizontal_lines(
fill : Char,
size : Int,
offset : Int,
color : ANSIBuf?,
width : Int,
) -> Array[String] {
if size == 0 {
return []
}
let start_offset = if offset >= 0 {
if offset > width {
width
} else {
offset
}
} else {
0
}
let end_offset = if offset < 0 {
let abs_off = -offset
if abs_off > width {
width
} else {
abs_off
}
} else {
0
}
let indent_size = width - start_offset - end_offset
let result = []
for _ in 0..