fix(tui): visual polish and layout improvements #18
@@ -132,6 +132,93 @@ func heartbeatSparkline(statuses []bool, width int) string {
|
|||||||
return sb.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 {
|
func fmtLatency(d time.Duration) string {
|
||||||
ms := d.Milliseconds()
|
ms := d.Milliseconds()
|
||||||
if ms == 0 {
|
if ms == 0 {
|
||||||
@@ -227,7 +314,7 @@ func fmtStatus(status string, paused bool, inMaint bool) string {
|
|||||||
func (m Model) dynamicWidths() (nameW, sparkW int) {
|
func (m Model) dynamicWidths() (nameW, sparkW int) {
|
||||||
fixed := 6 + 10 + 10 + 8 + 8 + 7 + 9 // #, TYPE, STATUS, LATENCY, UPTIME, SSL, RETRY
|
fixed := 6 + 10 + 10 + 8 + 8 + 7 + 9 // #, TYPE, STATUS, LATENCY, UPTIME, SSL, RETRY
|
||||||
overhead := 30 // cell padding + borders
|
overhead := 30 // cell padding + borders
|
||||||
avail := m.termWidth - 6 - fixed - overhead
|
avail := m.termWidth - chromePadH - 2 - fixed - overhead
|
||||||
if avail < 30 {
|
if avail < 30 {
|
||||||
avail = 30
|
avail = 30
|
||||||
}
|
}
|
||||||
@@ -285,8 +372,8 @@ func (m Model) viewSitesTab() string {
|
|||||||
"group",
|
"group",
|
||||||
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
||||||
subtleStyle.Render("—"),
|
subtleStyle.Render("—"),
|
||||||
subtleStyle.Render("—"),
|
m.groupUptime(site.ID),
|
||||||
subtleStyle.Render(strings.Repeat("·", sparkWidth)),
|
m.groupSparkline(site.ID, sparkWidth),
|
||||||
subtleStyle.Render("-"),
|
subtleStyle.Render("-"),
|
||||||
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
|
selectedVisual := m.cursor - m.tableOffset
|
||||||
rows := buildRows(m.tableOffset, end)
|
rows := buildRows(m.tableOffset, end)
|
||||||
|
|
||||||
tableWidth := m.termWidth - 6
|
tableWidth := m.termWidth - chromePadH - 2
|
||||||
if tableWidth < 40 {
|
if tableWidth < 40 {
|
||||||
tableWidth = 40
|
tableWidth = 40
|
||||||
}
|
}
|
||||||
@@ -53,16 +53,21 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
if row == table.HeaderRow {
|
if row == table.HeaderRow {
|
||||||
return tableHeaderStyle
|
return tableHeaderStyle
|
||||||
}
|
}
|
||||||
|
isSelected := row == selectedVisual
|
||||||
if styleOverride != nil {
|
if styleOverride != nil {
|
||||||
if s := styleOverride(row, col); s != nil {
|
if s := styleOverride(row, col); s != nil {
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
style := *s
|
||||||
return s.Width(colWidths[col])
|
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
|
base := tableCellStyle
|
||||||
if row == selectedVisual {
|
if isSelected {
|
||||||
base = tableSelectedStyle
|
base = tableSelectedStyle
|
||||||
}
|
}
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
|
|||||||
+14
-5
@@ -31,6 +31,16 @@ var (
|
|||||||
|
|
||||||
var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
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
|
type sessionState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -198,17 +208,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.termWidth = msg.Width
|
m.termWidth = msg.Width
|
||||||
m.termHeight = msg.Height
|
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 := chromeBase
|
||||||
chrome := 10
|
if m.filterMode || m.filterText != "" {
|
||||||
if m.filterText != "" {
|
|
||||||
chrome++
|
chrome++
|
||||||
}
|
}
|
||||||
m.maxTableRows = msg.Height - chrome
|
m.maxTableRows = msg.Height - chrome
|
||||||
if m.maxTableRows < 1 {
|
if m.maxTableRows < 1 {
|
||||||
m.maxTableRows = 1
|
m.maxTableRows = 1
|
||||||
}
|
}
|
||||||
m.logViewport.Width = msg.Width - 4
|
m.logViewport.Width = msg.Width - chromePadH
|
||||||
m.logViewport.Height = msg.Height - 8
|
m.logViewport.Height = msg.Height - (chromePadV + chromeHeader + chromeGaps + chromeFooter)
|
||||||
return m, tea.ClearScreen
|
return m, tea.ClearScreen
|
||||||
|
|
||||||
case time.Time:
|
case time.Time:
|
||||||
|
|||||||
Reference in New Issue
Block a user