feat(tui): stable detail panel with scroll, pinned footer, and 60/40 split #165
+19
-5
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) titledPanelH(title, content string, width, height, scrollOffset int, focused bool) string {
|
func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, focused bool) string {
|
||||||
if height <= 0 {
|
if height <= 0 {
|
||||||
return m.titledPanel(title, content, width, focused)
|
return m.titledPanel(title, content, width, focused)
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,13 @@ func (m Model) titledPanelH(title, content string, width, height, scrollOffset i
|
|||||||
inner := contentStyle.Render(content)
|
inner := contentStyle.Render(content)
|
||||||
contentLines := strings.Split(inner, "\n")
|
contentLines := strings.Split(inner, "\n")
|
||||||
|
|
||||||
bodyH := height - 2
|
var footerLines []string
|
||||||
|
if footer != "" {
|
||||||
|
footerRendered := contentStyle.Render(footer)
|
||||||
|
footerLines = strings.Split(footerRendered, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyH := height - 2 - len(footerLines)
|
||||||
if bodyH < 1 {
|
if bodyH < 1 {
|
||||||
bodyH = 1
|
bodyH = 1
|
||||||
}
|
}
|
||||||
@@ -58,13 +64,21 @@ func (m Model) titledPanelH(title, content string, width, height, scrollOffset i
|
|||||||
}
|
}
|
||||||
visible := contentLines[scrollOffset:end]
|
visible := contentLines[scrollOffset:end]
|
||||||
|
|
||||||
|
borderLine := func(line string) string {
|
||||||
|
return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + bc.Render("│")
|
||||||
|
}
|
||||||
|
emptyLine := borderLine(strings.Repeat(" ", innerW))
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
lines = append(lines, top)
|
lines = append(lines, top)
|
||||||
for _, line := range visible {
|
for _, line := range visible {
|
||||||
lines = append(lines, bc.Render("│")+line+strings.Repeat(" ", max(0, innerW-lipgloss.Width(line)))+bc.Render("│"))
|
lines = append(lines, borderLine(line))
|
||||||
}
|
}
|
||||||
for len(lines) < height-1 {
|
for len(lines) < height-1-len(footerLines) {
|
||||||
lines = append(lines, bc.Render("│")+strings.Repeat(" ", innerW)+bc.Render("│"))
|
lines = append(lines, emptyLine)
|
||||||
|
}
|
||||||
|
for _, line := range footerLines {
|
||||||
|
lines = append(lines, borderLine(line))
|
||||||
}
|
}
|
||||||
lines = append(lines, bottom)
|
lines = append(lines, bottom)
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
}
|
}
|
||||||
monHeight := lipgloss.Height(monPanel)
|
monHeight := lipgloss.Height(monPanel)
|
||||||
detail := m.viewDetailInline(detailW-2, monHeight)
|
detail := m.viewDetailInline(detailW-2, monHeight)
|
||||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail))
|
footer := m.detailFooter(detailW - 2)
|
||||||
|
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail))
|
||||||
topParts = append(topParts, detailPanel)
|
topParts = append(topParts, detailPanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,8 +143,6 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid
|
|||||||
b.WriteString(" " + label.Render("No state changes") + "\n")
|
b.WriteString(" " + label.Render("No state changes") + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString("\n " + m.detailKeys() + "\n")
|
|
||||||
|
|
||||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +214,33 @@ func (m Model) detailKeys() string {
|
|||||||
return m.st.subtleStyle.Render("[e] Edit [h] History [s] SLA [Esc] Back")
|
return m.st.subtleStyle.Render("[e] Edit [h] History [s] SLA [Esc] Back")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) detailFooter(width int) string {
|
||||||
|
label := m.st.subtleStyle
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
switch m.detailMode {
|
||||||
|
case detailSLA:
|
||||||
|
var keys []string
|
||||||
|
for i, p := range slaPeriods {
|
||||||
|
k := fmt.Sprintf("[%s] %s", p.key, p.label)
|
||||||
|
if i == m.slaPeriodIdx {
|
||||||
|
keys = append(keys, m.st.titleStyle.Render(k))
|
||||||
|
} else {
|
||||||
|
keys = append(keys, label.Render(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines = append(lines, " "+strings.Join(keys, " "))
|
||||||
|
lines = append(lines, " "+label.Render("[Esc] Back"))
|
||||||
|
case detailHistory:
|
||||||
|
lines = append(lines, " "+label.Render("[Esc] Back"))
|
||||||
|
default:
|
||||||
|
lines = append(lines, " "+m.detailKeys())
|
||||||
|
}
|
||||||
|
|
||||||
|
content := strings.Join(lines, "\n")
|
||||||
|
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(content)
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) fmtStatusWord(status string) string {
|
func (m Model) fmtStatusWord(status string) string {
|
||||||
switch status {
|
switch status {
|
||||||
case "DOWN", "SSL EXP":
|
case "DOWN", "SSL EXP":
|
||||||
@@ -292,20 +317,6 @@ func (m Model) viewSLASidebar(width, _ int) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
var keys []string
|
|
||||||
for i, p := range slaPeriods {
|
|
||||||
k := fmt.Sprintf("[%s] %s", p.key, p.label)
|
|
||||||
if i == m.slaPeriodIdx {
|
|
||||||
keys = append(keys, m.st.titleStyle.Render(k))
|
|
||||||
} else {
|
|
||||||
keys = append(keys, label.Render(k))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(" " + strings.Join(keys, " ") + "\n")
|
|
||||||
b.WriteString(" " + label.Render("[Esc] Back") + "\n")
|
|
||||||
|
|
||||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +379,6 @@ func (m Model) viewHistorySidebar(width, _ int) string {
|
|||||||
statParts = append(statParts, "avg "+fmtDuration(avg))
|
statParts = append(statParts, "avg "+fmtDuration(avg))
|
||||||
}
|
}
|
||||||
b.WriteString(" " + label.Render(strings.Join(statParts, " │ ")) + "\n")
|
b.WriteString(" " + label.Render(strings.Join(statParts, " │ ")) + "\n")
|
||||||
b.WriteString(" " + label.Render("[Esc] Back") + "\n")
|
|
||||||
|
|
||||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user