refactor(tui): two-tier responsive table layout (compact/wide at 120 cols)
CI / test (pull_request) Successful in 2m54s
CI / lint (pull_request) Failing after 1m12s
CI / vulncheck (pull_request) Successful in 56s

Replace continuous surplus distribution with two fixed layouts per table.
Breakpoint at 120 columns — matches how btop/k9s do it.

Compact (<120): short headers (LAT, UP%, RT, ST, MON, SENT, VER),
  tight fixed widths, no surplus guessing.

Wide (≥120): full headers (LATENCY, UPTIME, RETRIES, STATUS, MONITORS,
  LAST SENT, VERSION), generous widths.

Sites tab keeps content-aware NAME sizing + sparkline flex.
All other tabs (Alerts, Maint, Nodes, Users) use simple fixed tiers.

Removed old computeTableLayout/colDef/tierCol/pickTier — no longer needed.
This commit is contained in:
2026-05-28 15:50:23 -04:00
parent a84f4894f8
commit 5401266e83
6 changed files with 64 additions and 179 deletions
+11 -17
View File
@@ -148,23 +148,17 @@ func (m Model) viewAlertsTab() string {
return "\n No alert channels configured. Press [n] to add one." return "\n No alert channels configured. Press [n] to add one."
} }
maxName := 0 var headers []string
for _, a := range m.alerts { var widths []int
if n := len([]rune(a.Name)); n > maxName { if m.isWide() {
maxName = n headers = []string{"#", "", "NAME", "TYPE", "CONFIG", "LAST SENT"}
} widths = []int{4, 3, 18, 12, 40, 12}
} else {
headers = []string{"#", "", "NAME", "TYPE", "CONFIG", "SENT"}
widths = []int{4, 3, 14, 10, 24, 8}
} }
cols := []colDef{
{"#", "#", 4, 4, false},
{"", "", 3, 3, false},
{"NAME", "NAME", 10, 20, false},
{"TYPE", "TYPE", 10, 12, false},
{"CONFIG", "CONFIG", 15, 40, true},
{"SENT", "LAST SENT", 8, 12, false},
}
headers, widths := m.computeTableLayout(cols, 0)
nameW := widths[2] nameW := widths[2]
cfgW := widths[4]
return m.renderTable( return m.renderTable(
headers, headers,
@@ -179,10 +173,10 @@ func (m Model) viewAlertsTab() string {
fmtAlertHealth(h), fmtAlertHealth(h),
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)), m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
fmtAlertType(a.Type), fmtAlertType(a.Type),
fmtAlertConfig(struct { limitStr(fmtAlertConfig(struct {
Type string Type string
Settings map[string]string Settings map[string]string
}{a.Type, a.Settings}), }{a.Type, a.Settings}), cfgW-2),
fmtAlertLastSent(h), fmtAlertLastSent(h),
}) })
} }
+8 -9
View File
@@ -100,16 +100,15 @@ func (m Model) viewMaintTab() string {
return "\n No maintenance windows or incidents. Press [n] to create one." return "\n No maintenance windows or incidents. Press [n] to create one."
} }
cols := []colDef{ var headers []string
{"#", "#", 4, 4, false}, var widths []int
{"TITLE", "TITLE", 12, 24, false}, if m.isWide() {
{"TYPE", "TYPE", 13, 14, false}, headers = []string{"#", "TITLE", "TYPE", "MONITORS", "STATUS", "STARTED", "ENDS"}
{"MON", "MONITORS", 14, 22, true}, widths = []int{4, 24, 14, 22, 12, 16, 16}
{"ST", "STATUS", 11, 12, false}, } else {
{"START", "STARTED", 14, 16, false}, headers = []string{"#", "TITLE", "TYPE", "MON", "ST", "START", "ENDS"}
{"ENDS", "ENDS", 14, 16, false}, widths = []int{4, 14, 13, 14, 11, 14, 14}
} }
headers, widths := m.computeTableLayout(cols, 0)
titleW := widths[1] titleW := widths[1]
monW := widths[3] monW := widths[3]
timeW := widths[5] timeW := widths[5]
+8 -7
View File
@@ -10,14 +10,15 @@ func (m Model) viewNodesTab() string {
return "\n No probe nodes connected." return "\n No probe nodes connected."
} }
cols := []colDef{ var headers []string
{"NAME", "NAME", 12, 24, true}, var widths []int
{"REGION", "REGION", 8, 14, false}, if m.isWide() {
{"SEEN", "LAST SEEN", 8, 20, false}, headers = []string{"NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"}
{"VER", "VERSION", 8, 12, false}, widths = []int{24, 14, 16, 12, 10}
{"STATUS", "STATUS", 8, 10, false}, } else {
headers = []string{"NAME", "REGION", "SEEN", "VER", "STATUS"}
widths = []int{16, 10, 10, 8, 8}
} }
headers, widths := m.computeTableLayout(cols, 0)
nameW := widths[0] nameW := widths[0]
return m.renderTable( return m.renderTable(
+26 -70
View File
@@ -341,31 +341,29 @@ type tableLayout struct {
} }
func (m Model) computeLayout() tableLayout { func (m Model) computeLayout() tableLayout {
cols := []colDef{ wide := m.isWide()
{"#", "#", 4, 4, false},
{"", "", 0, 0, false}, // NAME (special) var fixed int
{"TYPE", "TYPE", 8, 10, false}, var headers []string
{"STATUS", "STATUS", 8, 10, false}, var widths []int
{"LAT", "LATENCY", 7, 10, false},
{"UP%", "UPTIME", 8, 8, false}, if wide {
{"", "", 0, 0, false}, // HISTORY (special) // # NAME TYPE STATUS LATENCY UPTIME HISTORY SSL RETRIES
{"SSL", "SSL", 5, 5, false}, headers = []string{"#", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRIES"}
{"RT", "RETRIES", 5, 9, false}, widths = []int{4, 0, 10, 10, 10, 8, 0, 7, 9}
fixed = 4 + 10 + 10 + 10 + 8 + 7 + 9
} else {
// # NAME TYPE STATUS LAT UP% HISTORY SSL RT
headers = []string{"#", "NAME", "TYPE", "STATUS", "LAT", "UP%", "HISTORY", "SSL", "RT"}
widths = []int{4, 0, 8, 8, 7, 7, 0, 5, 5}
fixed = 4 + 8 + 8 + 7 + 7 + 5 + 5
} }
numCols := len(cols) numCols := len(headers)
borderOverhead := 2 + (numCols - 1) // left + right border + column separators borderOverhead := 2 + (numCols - 1)
usable := m.termWidth - chromePadH - 2 - borderOverhead avail := m.termWidth - chromePadH - 2 - borderOverhead - fixed
if usable < 80 { if avail < 20 {
usable = 80 avail = 20
}
fixedMin := 0
for i, c := range cols {
if i == 1 || i == 6 {
continue
}
fixedMin += c.minWidth
} }
maxName := 0 maxName := 0
@@ -374,68 +372,26 @@ func (m Model) computeLayout() tableLayout {
maxName = n maxName = n
} }
} }
maxName += 4 // icon + padding + error preview room maxName += 4
avail := usable - fixedMin
nameW := avail / 2 nameW := avail / 2
if nameW > maxName { if nameW > maxName {
nameW = maxName nameW = maxName
} }
sparkW := avail - nameW - 2
if nameW < 13 { if nameW < 13 {
nameW = 13 nameW = 13
} }
if nameW > 40 { if nameW > 40 {
nameW = 40 nameW = 40
} }
sparkW := avail - nameW
if sparkW < 10 { if sparkW < 10 {
sparkW = 10 sparkW = 10
} }
if sparkW > 60 {
sparkW = 60
}
surplus := usable - fixedMin - nameW - sparkW - 2 widths[1] = nameW
if surplus < 0 { widths[6] = sparkW
surplus = 0
}
headers := make([]string, len(cols))
widths := make([]int, len(cols))
for i, c := range cols {
if i == 1 {
headers[i] = "NAME"
widths[i] = nameW
continue
}
if i == 6 {
headers[i] = "HISTORY"
widths[i] = sparkW + 2
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 {
widths[6] += surplus
}
return tableLayout{ return tableLayout{
nameW: nameW, nameW: nameW,
+8 -6
View File
@@ -32,13 +32,15 @@ func (m Model) viewUsersTab() string {
return "\n No users configured. Press [n] to add one." return "\n No users configured. Press [n] to add one."
} }
cols := []colDef{ var headers []string
{"#", "#", 4, 4, false}, var widths []int
{"USER", "USERNAME", 10, 18, false}, if m.isWide() {
{"ROLE", "ROLE", 8, 10, false}, headers = []string{"#", "USERNAME", "ROLE", "PUBLIC KEY"}
{"KEY", "PUBLIC KEY", 20, 60, true}, widths = []int{4, 18, 10, 50}
} else {
headers = []string{"#", "USER", "ROLE", "KEY"}
widths = []int{4, 14, 8, 30}
} }
headers, widths := m.computeTableLayout(cols, 0)
userW := widths[1] userW := widths[1]
return m.renderTable( return m.renderTable(
+3 -70
View File
@@ -15,77 +15,10 @@ var (
type StyleOverride func(row, col int) *lipgloss.Style type StyleOverride func(row, col int) *lipgloss.Style
type colDef struct { const wideBreakpoint = 120
short string
full string
minWidth int
maxWidth int
flex bool
}
func (m Model) computeTableLayout(cols []colDef, maxContentWidth int) ([]string, []int) { func (m Model) isWide() bool {
numCols := len(cols) return m.termWidth >= wideBreakpoint
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 { func (m Model) renderTable(headers []string, items int, buildRows func(start, end int) [][]string, colWidths []int, styleOverride StyleOverride) string {