package tui import "github.com/lixenwraith/terminal" // Align specifies text alignment within a column type Align uint8 const ( AlignLeft Align = iota AlignRight AlignCenter ) // TableOpts configures table rendering type TableOpts struct { ColWidths []int // Fixed widths per column, 0 = auto ColAligns []Align // Alignment per column, default AlignLeft HeaderStyle Style RowStyle Style AltRowStyle Style // Alternating row style, zero = same as RowStyle ColSeparator rune // Between columns, 0 = space RowSeparator LineType // Between rows, LineNone = no separator } // DefaultTableOpts returns sensible defaults func DefaultTableOpts() TableOpts { return TableOpts{ HeaderStyle: Style{Attr: terminal.AttrBold}, ColSeparator: ' ', RowSeparator: LineNone, } } // CalculateColumnWidths computes optimal column widths for given data // Returns widths that fit within availableW, respecting fixed widths in opts func CalculateColumnWidths(availableW int, headers []string, rows [][]string, opts TableOpts) []int { if len(headers) == 0 { return nil } cols := len(headers) widths := make([]int, cols) separatorW := 1 // space between columns // Start with header widths for i, h := range headers { widths[i] = RuneLen(h) } // Expand to fit data for _, row := range rows { for i := 0; i < cols && i < len(row); i++ { w := RuneLen(row[i]) if w > widths[i] { widths[i] = w } } } // Apply fixed widths from opts for i := 0; i < cols && i < len(opts.ColWidths); i++ { if opts.ColWidths[i] > 0 { widths[i] = opts.ColWidths[i] } } // Calculate total and scale if needed total := 0 for _, w := range widths { total += w } total += (cols - 1) * separatorW if total > availableW && availableW > cols { // Proportionally shrink contentW := availableW - (cols-1)*separatorW scale := float64(contentW) / float64(total-(cols-1)*separatorW) for i := range widths { widths[i] = int(float64(widths[i]) * scale) if widths[i] < 1 { widths[i] = 1 } } } return widths } // Table renders a table with headers and rows func (r Region) Table(headers []string, rows [][]string, opts TableOpts) { if r.H < 1 || r.W < 1 || len(headers) == 0 { return } widths := CalculateColumnWidths(r.W, headers, rows, opts) sep := opts.ColSeparator if sep == 0 { sep = ' ' } y := 0 // Header row if y < r.H { r.renderTableRow(y, headers, widths, opts.ColAligns, sep, opts.HeaderStyle) y++ } // Header separator if opts.RowSeparator != LineNone && y < r.H { r.HLine(y, opts.RowSeparator, opts.HeaderStyle.Fg) y++ } // Data rows for rowIdx, row := range rows { if y >= r.H { break } style := opts.RowStyle if !opts.AltRowStyle.IsZero() && rowIdx%2 == 1 { style = opts.AltRowStyle } r.renderTableRow(y, row, widths, opts.ColAligns, sep, style) y++ } } // renderTableRow renders a single table row func (r Region) renderTableRow(y int, cells []string, widths []int, aligns []Align, sep rune, style Style) { x := 0 for i, w := range widths { if x >= r.W { break } text := "" if i < len(cells) { text = cells[i] } align := AlignLeft if i < len(aligns) { align = aligns[i] } // Truncate if needed if RuneLen(text) > w { text = Truncate(text, w) } // Render with alignment textLen := RuneLen(text) var startX int switch align { case AlignRight: startX = x + w - textLen case AlignCenter: startX = x + (w-textLen)/2 default: startX = x } for j, ch := range text { if startX+j < r.W { r.Cell(startX+j, y, ch, style.Fg, style.Bg, style.Attr) } } x += w // Column separator if i < len(widths)-1 && x < r.W { r.Cell(x, y, sep, style.Fg, style.Bg, terminal.AttrDim) x++ } } }