feat(tui): persist bottom panel + scrollbar improvements #173
@@ -50,6 +50,21 @@ func writeCmd(op string, fn func() error) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) saveBottomPanelPref() tea.Cmd {
|
||||
v := "logs"
|
||||
switch m.bottomPanel {
|
||||
case bottomNone:
|
||||
v = "none"
|
||||
case bottomMaint:
|
||||
v = "maint"
|
||||
}
|
||||
st := m.store
|
||||
ctx := m.ctx
|
||||
return writeCmd("Save bottom panel preference", func() error {
|
||||
return st.SetPreference(ctx, "bottom_panel", v)
|
||||
})
|
||||
}
|
||||
|
||||
func sortSitesForDisplay(allSites []models.Site, collapsed map[int]bool, sortCol int, sortAsc bool) []models.Site {
|
||||
var groups, ungrouped []models.Site
|
||||
children := make(map[int][]models.Site)
|
||||
|
||||
+55
-9
@@ -6,7 +6,13 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, focused bool) string {
|
||||
type scrollbar struct {
|
||||
pos int
|
||||
total int
|
||||
visible int
|
||||
}
|
||||
|
||||
func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, sb scrollbar, focused bool) string {
|
||||
if height <= 0 {
|
||||
return m.titledPanel(title, content, width, focused)
|
||||
}
|
||||
@@ -64,21 +70,61 @@ func (m Model) titledPanelH(title, content, footer string, width, height, scroll
|
||||
}
|
||||
visible := contentLines[scrollOffset:end]
|
||||
|
||||
borderLine := func(line string) string {
|
||||
return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + bc.Render("│")
|
||||
if sb.total == 0 && len(contentLines) > bodyH {
|
||||
sb = scrollbar{pos: scrollOffset, total: len(contentLines), visible: bodyH}
|
||||
}
|
||||
sbVisible := sb.visible
|
||||
if sbVisible <= 0 {
|
||||
sbVisible = bodyH
|
||||
}
|
||||
showScrollbar := sb.total > 0 && sb.total > sbVisible
|
||||
var thumbStart, thumbEnd int
|
||||
if showScrollbar {
|
||||
thumbSize := bodyH * sbVisible / sb.total
|
||||
if thumbSize < 1 {
|
||||
thumbSize = 1
|
||||
}
|
||||
scrollRange := sb.total - sbVisible
|
||||
if scrollRange < 1 {
|
||||
scrollRange = 1
|
||||
}
|
||||
trackSpace := bodyH - thumbSize
|
||||
thumbStart = sb.pos * trackSpace / scrollRange
|
||||
if thumbStart < 0 {
|
||||
thumbStart = 0
|
||||
}
|
||||
thumbEnd = thumbStart + thumbSize
|
||||
if thumbEnd > bodyH {
|
||||
thumbEnd = bodyH
|
||||
}
|
||||
}
|
||||
|
||||
scrollTrack := lipgloss.NewStyle().Foreground(m.theme.Border).Render("░")
|
||||
scrollThumb := lipgloss.NewStyle().Foreground(m.theme.Accent).Render("█")
|
||||
|
||||
borderLine := func(line string, idx int) string {
|
||||
rightBorder := bc.Render("│")
|
||||
if showScrollbar && idx >= thumbStart && idx < thumbEnd {
|
||||
rightBorder = scrollThumb
|
||||
} else if showScrollbar {
|
||||
rightBorder = scrollTrack
|
||||
}
|
||||
return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + rightBorder
|
||||
}
|
||||
emptyLine := func(idx int) string {
|
||||
return borderLine(strings.Repeat(" ", innerW), idx)
|
||||
}
|
||||
emptyLine := borderLine(strings.Repeat(" ", innerW))
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, top)
|
||||
for _, line := range visible {
|
||||
lines = append(lines, borderLine(line))
|
||||
for i, line := range visible {
|
||||
lines = append(lines, borderLine(line, i))
|
||||
}
|
||||
for len(lines) < height-1-len(footerLines) {
|
||||
lines = append(lines, emptyLine)
|
||||
for i := len(visible); len(lines) < height-1-len(footerLines); i++ {
|
||||
lines = append(lines, emptyLine(i))
|
||||
}
|
||||
for _, line := range footerLines {
|
||||
lines = append(lines, borderLine(line))
|
||||
lines = append(lines, borderLine(line, -1))
|
||||
}
|
||||
lines = append(lines, bottom)
|
||||
|
||||
|
||||
@@ -72,6 +72,20 @@ func (m Model) viewLogsStrip(width, maxLines int) string {
|
||||
return style.Render(strings.Join(visible, "\n"))
|
||||
}
|
||||
|
||||
func (m Model) filteredLogCount() int {
|
||||
count := 0
|
||||
for _, entry := range m.engine.GetLogs() {
|
||||
if strings.TrimSpace(entry.Message) == "" {
|
||||
continue
|
||||
}
|
||||
if m.logFilterImportant && !isImportantLog(classifyLog(entry.Message)) {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (m *Model) scrollLogs(delta int) {
|
||||
logs := m.engine.GetLogs()
|
||||
total := 0
|
||||
|
||||
+11
-1
@@ -243,6 +243,16 @@ func InitialModel(ctx context.Context, isAdmin bool, s store.Store, eng *monitor
|
||||
|
||||
detailPref, _ := s.GetPreference(ctx, "detail_open")
|
||||
|
||||
bp := bottomLogs
|
||||
if bpPref, _ := s.GetPreference(ctx, "bottom_panel"); bpPref != "" {
|
||||
switch bpPref {
|
||||
case "none":
|
||||
bp = bottomNone
|
||||
case "maint":
|
||||
bp = bottomMaint
|
||||
}
|
||||
}
|
||||
|
||||
return Model{
|
||||
ctx: ctx,
|
||||
state: stateDashboard,
|
||||
@@ -257,7 +267,7 @@ func InitialModel(ctx context.Context, isAdmin bool, s store.Store, eng *monitor
|
||||
theme: theme,
|
||||
themeIndex: themeIdx,
|
||||
st: newStyles(theme),
|
||||
bottomPanel: bottomLogs,
|
||||
bottomPanel: bp,
|
||||
detailOpen: detailPref == "true",
|
||||
demoMode: os.Getenv("UPTOP_DEMO") == "1",
|
||||
version: version,
|
||||
|
||||
@@ -662,6 +662,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.focusedPanel = panelMaint
|
||||
}
|
||||
m.recalcLayout()
|
||||
return m, m.saveBottomPanelPref()
|
||||
case "l":
|
||||
if m.bottomPanel == bottomLogs {
|
||||
m.bottomPanel = bottomNone
|
||||
@@ -671,6 +672,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.focusedPanel = panelLogs
|
||||
}
|
||||
m.recalcLayout()
|
||||
return m, m.saveBottomPanelPref()
|
||||
case "up", "k":
|
||||
if m.focusedPanel == panelDetail && m.detailOpen {
|
||||
m.detailScrollOffset--
|
||||
|
||||
@@ -163,7 +163,7 @@ func (m Model) viewMonitorsLayout() string {
|
||||
|
||||
monTargetH := m.maxTableRows + 5
|
||||
monitors := m.viewSitesTab()
|
||||
monPanel := m.zones.Mark("panel-monitors", m.titledPanelH("Monitors", monitors, "", monW, monTargetH, 0, m.focusedPanel == panelMonitors))
|
||||
monPanel := m.zones.Mark("panel-monitors", m.titledPanelH("Monitors", monitors, "", monW, monTargetH, 0, scrollbar{pos: m.tableOffset, total: len(m.sites), visible: m.maxTableRows}, m.focusedPanel == panelMonitors))
|
||||
|
||||
var topParts []string
|
||||
topParts = append(topParts, monPanel)
|
||||
@@ -181,7 +181,7 @@ func (m Model) viewMonitorsLayout() string {
|
||||
monHeight := lipgloss.Height(monPanel)
|
||||
detail := m.viewDetailInline(detailW-2, monHeight)
|
||||
footer := m.detailFooter(detailW - 2)
|
||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail))
|
||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, scrollbar{}, m.focusedPanel == panelDetail))
|
||||
topParts = append(topParts, detailPanel)
|
||||
}
|
||||
|
||||
@@ -189,12 +189,16 @@ func (m Model) viewMonitorsLayout() string {
|
||||
|
||||
switch m.bottomPanel {
|
||||
case bottomLogs:
|
||||
logContent := m.viewLogsStrip(availW-2, logsStripHeight-2)
|
||||
logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", logContent, availW, m.focusedPanel == panelLogs))
|
||||
maxLines := logsStripHeight - 2
|
||||
logContent := m.viewLogsStrip(availW-2, maxLines)
|
||||
totalLogs := m.filteredLogCount()
|
||||
logPanel := m.zones.Mark("panel-logs", m.titledPanelH("Logs", logContent, "", availW, logsStripHeight, 0, scrollbar{pos: m.logScrollOffset, total: totalLogs, visible: maxLines}, m.focusedPanel == panelLogs))
|
||||
return top + "\n" + logPanel
|
||||
case bottomMaint:
|
||||
maintContent := m.viewMaintStrip(availW-2, logsStripHeight-2)
|
||||
maintPanel := m.zones.Mark("panel-maint", m.titledPanel("Maint", maintContent, availW, m.focusedPanel == panelMaint))
|
||||
maxLines := logsStripHeight - 2
|
||||
maintContent := m.viewMaintStrip(availW-2, maxLines)
|
||||
totalMaint := len(m.activeMaintWindows())
|
||||
maintPanel := m.zones.Mark("panel-maint", m.titledPanelH("Maint", maintContent, "", availW, logsStripHeight, 0, scrollbar{pos: 0, total: totalMaint, visible: maxLines}, m.focusedPanel == panelMaint))
|
||||
return top + "\n" + maintPanel
|
||||
}
|
||||
return top
|
||||
|
||||
@@ -75,7 +75,7 @@ func (m Model) viewDetailFullscreen() string {
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().Padding(1, 2).Render(
|
||||
m.titledPanelH(title, content, footer, availW, panelH, m.detailScrollOffset, true))
|
||||
m.titledPanelH(title, content, footer, availW, panelH, m.detailScrollOffset, scrollbar{}, true))
|
||||
}
|
||||
|
||||
func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {
|
||||
|
||||
Reference in New Issue
Block a user