d05bbd007b
Extract shared computeTableLayout() into table_helpers.go — takes column definitions with short/full headers, min/max widths, and a flex column that absorbs surplus space. All tabs now use it: - Alerts: CONFIG column is flex, NAME/TYPE/SENT expand with width - Maint: TITLE column is flex, TYPE/MONITORS/STATUS/dates expand - Nodes: NAME column is flex, REGION/LAST SEEN/VERSION expand - Users: PUBLIC KEY column is flex, USERNAME expands - Sites: uses same colDef type (keeps special dual-flex for NAME+HISTORY) Headers auto-switch short/full based on available width across all tabs.
151 lines
3.0 KiB
Go
151 lines
3.0 KiB
Go
package tui
|
|
|
|
import (
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/charmbracelet/lipgloss/table"
|
|
)
|
|
|
|
var (
|
|
tableHeaderStyle lipgloss.Style
|
|
tableCellStyle lipgloss.Style
|
|
tableSelectedStyle lipgloss.Style
|
|
tableBorderStyle lipgloss.Style
|
|
tableZebraStyle lipgloss.Style
|
|
)
|
|
|
|
type StyleOverride func(row, col int) *lipgloss.Style
|
|
|
|
type colDef struct {
|
|
short string
|
|
full string
|
|
minWidth int
|
|
maxWidth int
|
|
flex bool
|
|
}
|
|
|
|
func (m Model) computeTableLayout(cols []colDef, maxContentWidth int) ([]string, []int) {
|
|
numCols := len(cols)
|
|
borderOverhead := 2 + (numCols - 1)
|
|
usable := m.termWidth - chromePadH - 2 - borderOverhead
|
|
if usable < 40 {
|
|
usable = 40
|
|
}
|
|
|
|
fixedMin := 0
|
|
flexIdx := -1
|
|
for i, c := range cols {
|
|
if c.flex {
|
|
flexIdx = i
|
|
continue
|
|
}
|
|
fixedMin += c.minWidth
|
|
}
|
|
|
|
flexW := usable - fixedMin
|
|
if maxContentWidth > 0 && flexW > maxContentWidth {
|
|
flexW = maxContentWidth
|
|
}
|
|
if flexW < 8 {
|
|
flexW = 8
|
|
}
|
|
|
|
surplus := usable - fixedMin - flexW
|
|
if surplus < 0 {
|
|
surplus = 0
|
|
}
|
|
|
|
headers := make([]string, numCols)
|
|
widths := make([]int, numCols)
|
|
for i, c := range cols {
|
|
if c.flex {
|
|
headers[i] = c.full
|
|
widths[i] = flexW
|
|
continue
|
|
}
|
|
|
|
w := c.minWidth
|
|
expand := c.maxWidth - c.minWidth
|
|
if surplus >= expand {
|
|
w = c.maxWidth
|
|
surplus -= expand
|
|
} else if surplus > 0 {
|
|
w += surplus
|
|
surplus = 0
|
|
}
|
|
|
|
if w >= len(c.full)+2 {
|
|
headers[i] = c.full
|
|
} else {
|
|
headers[i] = c.short
|
|
}
|
|
widths[i] = w
|
|
}
|
|
|
|
if surplus > 0 && flexIdx >= 0 {
|
|
widths[flexIdx] += surplus
|
|
}
|
|
|
|
return headers, widths
|
|
}
|
|
|
|
func (m Model) renderTable(headers []string, items int, buildRows func(start, end int) [][]string, colWidths []int, styleOverride StyleOverride) string {
|
|
if items == 0 {
|
|
return ""
|
|
}
|
|
|
|
end := m.tableOffset + m.maxTableRows
|
|
if end > items {
|
|
end = items
|
|
}
|
|
|
|
selectedVisual := m.cursor - m.tableOffset
|
|
rows := buildRows(m.tableOffset, end)
|
|
|
|
tableWidth := m.termWidth - chromePadH - 2
|
|
if tableWidth < 40 {
|
|
tableWidth = 40
|
|
}
|
|
|
|
t := table.New().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderStyle(tableBorderStyle).
|
|
Width(tableWidth).
|
|
Headers(headers...).
|
|
Rows(rows...).
|
|
StyleFunc(func(row, col int) lipgloss.Style {
|
|
if row == table.HeaderRow {
|
|
h := tableHeaderStyle
|
|
if col < len(colWidths) && colWidths[col] > 0 {
|
|
h = h.Width(colWidths[col]).MaxWidth(colWidths[col])
|
|
}
|
|
return h
|
|
}
|
|
isSelected := row == selectedVisual
|
|
if styleOverride != nil {
|
|
if s := styleOverride(row, col); s != nil {
|
|
style := *s
|
|
if isSelected {
|
|
style = tableSelectedStyle.Foreground(s.GetForeground())
|
|
}
|
|
if col < len(colWidths) && colWidths[col] > 0 {
|
|
style = style.Width(colWidths[col]).MaxWidth(colWidths[col])
|
|
}
|
|
return style
|
|
}
|
|
}
|
|
base := tableCellStyle
|
|
if row%2 == 1 {
|
|
base = tableZebraStyle
|
|
}
|
|
if isSelected {
|
|
base = tableSelectedStyle
|
|
}
|
|
if col < len(colWidths) && colWidths[col] > 0 {
|
|
base = base.Width(colWidths[col]).MaxWidth(colWidths[col])
|
|
}
|
|
return base
|
|
})
|
|
|
|
return "\n" + t.Render()
|
|
}
|