diff --git a/internal/tui/format.go b/internal/tui/format.go index f72834a..25fa9cb 100644 --- a/internal/tui/format.go +++ b/internal/tui/format.go @@ -2,11 +2,37 @@ package tui import ( "fmt" + "strings" "time" "gitea.lerkolabs.com/lerkolabs/uptop/internal/models" + "github.com/charmbracelet/lipgloss" ) +func (m Model) dividerWidth() int { + w := m.termWidth - chromePadH - 4 + if w < 40 { + w = 40 + } + return w +} + +func (m Model) divider() string { + return " " + subtleStyle.Render(strings.Repeat("─", m.dividerWidth())) +} + +func (m Model) emptyState(message, hint string) string { + content := message + if hint != "" { + content += "\n\n" + subtleStyle.Render(hint) + } + return "\n" + lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(m.theme.Accent). + Padding(1, 3). + Render(content) +} + func limitStr(text string, max int) string { runes := []rune(text) if len(runes) > max { diff --git a/internal/tui/tab_alerts.go b/internal/tui/tab_alerts.go index 7865e40..cd0971a 100644 --- a/internal/tui/tab_alerts.go +++ b/internal/tui/tab_alerts.go @@ -164,7 +164,7 @@ func fmtAlertLastSent(h monitor.AlertHealth) string { func (m Model) viewAlertsTab() string { if len(m.alerts) == 0 { - return "\n No alert channels configured. Press [n] to add one." + return m.emptyState("No alert channels configured.", "[n] Add your first alert") } var headers []string @@ -214,7 +214,8 @@ func (m Model) viewAlertDetailPanel() string { var b strings.Builder - b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n\n") + b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n") + b.WriteString(m.divider() + "\n") row := func(label, value string) { fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value) @@ -240,12 +241,13 @@ func (m Model) viewAlertDetailPanel() string { row("Last Error", dangerStyle.Render(limitStr(h.LastError, 60))) } - b.WriteString("\n" + subtleStyle.Render(" CONFIGURATION") + "\n") + b.WriteString(m.divider() + "\n") + b.WriteString(subtleStyle.Render(" CONFIGURATION") + "\n") for k, v := range a.Settings { row(k, v) } - b.WriteString("\n\n") + b.WriteString(m.divider() + "\n") b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit")) return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) diff --git a/internal/tui/tab_logs.go b/internal/tui/tab_logs.go index 0633001..415e77d 100644 --- a/internal/tui/tab_logs.go +++ b/internal/tui/tab_logs.go @@ -85,7 +85,7 @@ func renderLogLine(line string) string { func (m Model) viewLogsTab() string { content := m.logViewport.View() if strings.TrimSpace(content) == "" || content == "Waiting for logs..." { - return "\n No log entries yet. Logs appear as monitors run checks." + return m.emptyState("No log entries yet.", "Logs appear as monitors run checks") } lines := strings.Split(content, "\n") @@ -112,7 +112,7 @@ func (m Model) viewLogsTab() string { } header := subtleStyle.Render(fmt.Sprintf( - " %d entries [↑/↓] Scroll [PgUp/PgDn] Page [f] Filter: %s", shown, filterLabel)) + " %d entries Filter: %s", shown, filterLabel)) if m.logFilterImportant && shown < total { header += subtleStyle.Render(fmt.Sprintf(" (%d hidden)", total-shown)) diff --git a/internal/tui/tab_maint.go b/internal/tui/tab_maint.go index 1a3a7ca..1923f3a 100644 --- a/internal/tui/tab_maint.go +++ b/internal/tui/tab_maint.go @@ -93,7 +93,7 @@ func (m Model) isMonitorInMaintenance(monitorID int) bool { func (m Model) viewMaintTab() string { if len(m.maintenanceWindows) == 0 { - return "\n No maintenance windows or incidents. Press [n] to create one." + return m.emptyState("No maintenance windows or incidents.", "[n] Create one") } var headers []string diff --git a/internal/tui/tab_nodes.go b/internal/tui/tab_nodes.go index 3ddb79d..14f49f4 100644 --- a/internal/tui/tab_nodes.go +++ b/internal/tui/tab_nodes.go @@ -7,7 +7,7 @@ import ( func (m Model) viewNodesTab() string { if len(m.nodes) == 0 { - return "\n No probe nodes connected." + return m.emptyState("No probe nodes connected.", "") } var headers []string diff --git a/internal/tui/tab_sites.go b/internal/tui/tab_sites.go index 0644ee3..fbb84cf 100644 --- a/internal/tui/tab_sites.go +++ b/internal/tui/tab_sites.go @@ -101,16 +101,7 @@ func (m Model) computeLayout() tableLayout { func (m Model) viewSitesTab() string { if len(m.sites) == 0 { - welcome := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(m.theme.Accent). - Padding(1, 3). - Render( - titleStyle.Render("uptop") + "\n\n" + - "No monitors configured yet.\n\n" + - subtleStyle.Render("[n] Add your first monitor"), - ) - return "\n" + welcome + return m.emptyState(titleStyle.Render("uptop")+"\n\nNo monitors configured yet.", "[n] Add your first monitor") } layout := m.computeLayout() diff --git a/internal/tui/tab_users.go b/internal/tui/tab_users.go index a02badd..b6967d8 100644 --- a/internal/tui/tab_users.go +++ b/internal/tui/tab_users.go @@ -29,7 +29,7 @@ func fmtKey(key string) string { func (m Model) viewUsersTab() string { if len(m.users) == 0 { - return "\n No users configured. Press [n] to add one." + return m.emptyState("No users configured.", "[n] Add a user") } var headers []string diff --git a/internal/tui/view_dashboard.go b/internal/tui/view_dashboard.go index 3507079..07d52ed 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -266,7 +266,7 @@ func (m Model) renderFooter(stats dashboardStats) string { case 1: keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit" case 2: - keys = "[f]Filter [T]Theme [Tab]Switch [q]Quit" + keys = "[↑/↓]Scroll [PgUp/PgDn]Page [f]Filter [T]Theme [Tab]Switch [q]Quit" case 4: keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit" case 5: diff --git a/internal/tui/view_detail.go b/internal/tui/view_detail.go index 20c8542..5431d0c 100644 --- a/internal/tui/view_detail.go +++ b/internal/tui/view_detail.go @@ -30,7 +30,8 @@ func (m Model) viewDetailPanel() string { if breadcrumb == "" { breadcrumb = subtleStyle.Render(" Sites > ") + titleStyle.Render(site.Name) } - b.WriteString(breadcrumb + "\n\n") + b.WriteString(breadcrumb + "\n") + b.WriteString(m.divider() + "\n") row := func(label, value string) { fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value) @@ -200,7 +201,7 @@ func (m Model) viewDetailPanel() string { b.WriteString(" " + subtleStyle.Render("[h] History") + "\n") } - b.WriteString("\n") + b.WriteString(m.divider() + "\n") const sparkWidth = 40 if site.Type == "push" { b.WriteString(" " + heartbeatSparkline(hist.Statuses, sparkWidth)) @@ -242,7 +243,8 @@ func (m Model) viewDetailPanel() string { } } - b.WriteString("\n\n") + b.WriteString("\n") + b.WriteString(m.divider() + "\n") b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [h] History [s] SLA [q] Quit")) return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) diff --git a/internal/tui/view_history.go b/internal/tui/view_history.go index 5dcbbd7..40072eb 100644 --- a/internal/tui/view_history.go +++ b/internal/tui/view_history.go @@ -153,16 +153,13 @@ func (m Model) viewHistoryPanel() string { header += " " + subtleStyle.Render("[q] Back") b.WriteString(header + "\n") - divWidth := m.termWidth - chromePadH - 4 - if divWidth < 40 { - divWidth = 40 - } - b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") + divWidth := m.dividerWidth() + b.WriteString(m.divider() + "\n") sparkline := stateChangeSparkline(m.historyChanges, divWidth) if sparkline != "" { b.WriteString(" " + sparkline + "\n") - b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") + b.WriteString(m.divider() + "\n") } fmt.Fprintf(&b, " %-18s %-17s %-12s %s\n", @@ -177,7 +174,7 @@ func (m Model) viewHistoryPanel() string { b.WriteString(m.historyViewport.View()) } - b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") + b.WriteString("\n" + m.divider() + "\n") stats := computeHistoryStats(m.historyChanges) parts := []string{fmt.Sprintf("%d events", stats.totalEvents)} diff --git a/internal/tui/view_sla.go b/internal/tui/view_sla.go index cd6bac7..980881e 100644 --- a/internal/tui/view_sla.go +++ b/internal/tui/view_sla.go @@ -27,20 +27,14 @@ func (m Model) viewSLAPanel() string { header := " " + titleStyle.Render("SLA REPORT: "+m.slaSiteName) header += " " + subtleStyle.Render("[q] Back") b.WriteString(header + "\n") - - divWidth := m.termWidth - chromePadH - 4 - if divWidth < 40 { - divWidth = 40 - } - b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") + b.WriteString(m.divider() + "\n") period := slaPeriods[m.slaPeriodIdx] b.WriteString(" " + subtleStyle.Render("Period: Last "+period.label) + "\n\n") r := m.slaReport - // Uptime bar - barWidth := divWidth - 30 + barWidth := m.dividerWidth() - 30 if barWidth < 10 { barWidth = 10 } @@ -52,23 +46,23 @@ func (m Model) viewSLAPanel() string { if r.UptimePct < 99.0 { uptimeColor = dangerStyle } - fmt.Fprintf(&b, " %-14s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar) - fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("Downtime"), fmtDuration(r.Downtime)) - fmt.Fprintf(&b, " %-14s %d\n", subtleStyle.Render("Outages"), r.OutageCount) + fmt.Fprintf(&b, " %-16s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar) + fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Downtime"), fmtDuration(r.Downtime)) + fmt.Fprintf(&b, " %-16s %d\n", subtleStyle.Render("Outages"), r.OutageCount) if r.OutageCount > 0 { - fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut)) - fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR)) - fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF)) + fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut)) + fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR)) + fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF)) } - b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") + b.WriteString("\n" + m.divider() + "\n") if len(m.slaDailyBreakdown) > 0 { b.WriteString(m.slaViewport.View()) } - b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") + b.WriteString("\n" + m.divider() + "\n") var keys []string for i, p := range slaPeriods { @@ -80,7 +74,7 @@ func (m Model) viewSLAPanel() string { } } b.WriteString(" " + strings.Join(keys, " ")) - b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll")) + b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back")) return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) } @@ -88,7 +82,7 @@ func (m Model) viewSLAPanel() string { func (m Model) buildSLADailyContent() string { var b strings.Builder - barWidth := m.termWidth - chromePadH - 30 + barWidth := m.dividerWidth() - 30 if barWidth < 10 { barWidth = 10 }