fix(tui): visual polish and layout improvements #18
@@ -132,6 +132,93 @@ func heartbeatSparkline(statuses []bool, width int) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (m Model) groupSparkline(groupID int, width int) string {
|
||||
allSites := m.engine.GetAllSites()
|
||||
var childStatuses [][]bool
|
||||
for _, s := range allSites {
|
||||
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
||||
hist, _ := m.engine.GetHistory(s.ID)
|
||||
if len(hist.Statuses) > 0 {
|
||||
childStatuses = append(childStatuses, hist.Statuses)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(childStatuses) == 0 {
|
||||
return subtleStyle.Render(strings.Repeat("·", width))
|
||||
}
|
||||
|
||||
maxLen := 0
|
||||
for _, s := range childStatuses {
|
||||
if len(s) > maxLen {
|
||||
maxLen = len(s)
|
||||
}
|
||||
}
|
||||
if maxLen > width {
|
||||
maxLen = width
|
||||
}
|
||||
|
||||
aggregated := make([]bool, maxLen)
|
||||
for i := 0; i < maxLen; i++ {
|
||||
allUp := true
|
||||
for _, statuses := range childStatuses {
|
||||
idx := len(statuses) - maxLen + i
|
||||
if idx >= 0 && !statuses[idx] {
|
||||
allUp = false
|
||||
break
|
||||
}
|
||||
}
|
||||
aggregated[i] = allUp
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
if remaining := width - len(aggregated); remaining > 0 {
|
||||
sb.WriteString(subtleStyle.Render(strings.Repeat("·", remaining)))
|
||||
}
|
||||
for _, up := range aggregated {
|
||||
if up {
|
||||
sb.WriteString(specialStyle.Render("●"))
|
||||
} else {
|
||||
sb.WriteString(dangerStyle.Render("●"))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (m Model) groupUptime(groupID int) string {
|
||||
allSites := m.engine.GetAllSites()
|
||||
var allStatuses [][]bool
|
||||
for _, s := range allSites {
|
||||
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
||||
hist, _ := m.engine.GetHistory(s.ID)
|
||||
if len(hist.Statuses) > 0 {
|
||||
allStatuses = append(allStatuses, hist.Statuses)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(allStatuses) == 0 {
|
||||
return subtleStyle.Render("—")
|
||||
}
|
||||
total, up := 0, 0
|
||||
for _, statuses := range allStatuses {
|
||||
for _, s := range statuses {
|
||||
total++
|
||||
if s {
|
||||
up++
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmtUptime(func() []bool {
|
||||
out := make([]bool, total)
|
||||
idx := 0
|
||||
for _, statuses := range allStatuses {
|
||||
copy(out[idx:], statuses)
|
||||
idx += len(statuses)
|
||||
}
|
||||
return out
|
||||
}())
|
||||
}
|
||||
|
||||
func fmtLatency(d time.Duration) string {
|
||||
ms := d.Milliseconds()
|
||||
if ms == 0 {
|
||||
@@ -227,7 +314,7 @@ func fmtStatus(status string, paused bool, inMaint bool) string {
|
||||
func (m Model) dynamicWidths() (nameW, sparkW int) {
|
||||
fixed := 6 + 10 + 10 + 8 + 8 + 7 + 9 // #, TYPE, STATUS, LATENCY, UPTIME, SSL, RETRY
|
||||
overhead := 30 // cell padding + borders
|
||||
avail := m.termWidth - 6 - fixed - overhead
|
||||
avail := m.termWidth - chromePadH - 2 - fixed - overhead
|
||||
if avail < 30 {
|
||||
avail = 30
|
||||
}
|
||||
@@ -285,8 +372,8 @@ func (m Model) viewSitesTab() string {
|
||||
"group",
|
||||
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
||||
subtleStyle.Render("—"),
|
||||
subtleStyle.Render("—"),
|
||||
subtleStyle.Render(strings.Repeat("·", sparkWidth)),
|
||||
m.groupUptime(site.ID),
|
||||
m.groupSparkline(site.ID, sparkWidth),
|
||||
subtleStyle.Render("-"),
|
||||
subtleStyle.Render("—"),
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
||||
selectedVisual := m.cursor - m.tableOffset
|
||||
rows := buildRows(m.tableOffset, end)
|
||||
|
||||
tableWidth := m.termWidth - 6
|
||||
tableWidth := m.termWidth - chromePadH - 2
|
||||
if tableWidth < 40 {
|
||||
tableWidth = 40
|
||||
}
|
||||
@@ -53,16 +53,21 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
||||
if row == table.HeaderRow {
|
||||
return tableHeaderStyle
|
||||
}
|
||||
isSelected := row == selectedVisual
|
||||
if styleOverride != nil {
|
||||
if s := styleOverride(row, col); s != nil {
|
||||
if col < len(colWidths) && colWidths[col] > 0 {
|
||||
return s.Width(colWidths[col])
|
||||
style := *s
|
||||
if isSelected {
|
||||
style = tableSelectedStyle.Foreground(s.GetForeground())
|
||||
}
|
||||
return *s
|
||||
if col < len(colWidths) && colWidths[col] > 0 {
|
||||
style = style.Width(colWidths[col])
|
||||
}
|
||||
return style
|
||||
}
|
||||
}
|
||||
base := tableCellStyle
|
||||
if row == selectedVisual {
|
||||
if isSelected {
|
||||
base = tableSelectedStyle
|
||||
}
|
||||
if col < len(colWidths) && colWidths[col] > 0 {
|
||||
|
||||
+14
-5
@@ -31,6 +31,16 @@ var (
|
||||
|
||||
var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||
|
||||
const (
|
||||
chromePadV = 2 // outer Padding(1,2): 1 top + 1 bottom
|
||||
chromePadH = 4 // outer Padding(1,2): 2 left + 2 right
|
||||
chromeHeader = 1 // tab bar line
|
||||
chromeGaps = 2 // "\n" separators: before content + before footer
|
||||
chromeFooter = 2 // footer: "\n" prefix + text line
|
||||
chromeTable = 3 // renderTable "\n" prefix + top border + header + bottom border (lipgloss collapses two into three rendered lines)
|
||||
chromeBase = chromePadV + chromeHeader + chromeGaps + chromeFooter + chromeTable
|
||||
)
|
||||
|
||||
type sessionState int
|
||||
|
||||
const (
|
||||
@@ -198,17 +208,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.termWidth = msg.Width
|
||||
m.termHeight = msg.Height
|
||||
// Chrome: 1 top pad + 1 tabs + 2 newlines + 3 table borders + 1 table header + 1 footer + 1 bottom pad = 10
|
||||
chrome := 10
|
||||
if m.filterText != "" {
|
||||
chrome := chromeBase
|
||||
if m.filterMode || m.filterText != "" {
|
||||
chrome++
|
||||
}
|
||||
m.maxTableRows = msg.Height - chrome
|
||||
if m.maxTableRows < 1 {
|
||||
m.maxTableRows = 1
|
||||
}
|
||||
m.logViewport.Width = msg.Width - 4
|
||||
m.logViewport.Height = msg.Height - 8
|
||||
m.logViewport.Width = msg.Width - chromePadH
|
||||
m.logViewport.Height = msg.Height - (chromePadV + chromeHeader + chromeGaps + chromeFooter)
|
||||
return m, tea.ClearScreen
|
||||
|
||||
case time.Time:
|
||||
|
||||
Reference in New Issue
Block a user