v0.1.0 initial commit

This commit is contained in:
2026-07-12 18:41:05 -04:00
commit aa22225c61
69 changed files with 10971 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
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++
}
}
}