From 351848d4076cd0cb7560c1b2760438ce89378380 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Wed, 1 Jul 2026 21:04:39 -0400 Subject: [PATCH] feat(tui): add scrollbar gutter to titledPanelH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opt-in scrollbar track on the right border edge when totalItems > bodyH. Thin track (│) with muted thumb (┃) showing viewport position. Monitors panel passes len(sites) to enable it. Detail and fullscreen panels pass 0 to opt out. Any panel can opt in via the totalItems param. --- internal/tui/panel.go | 45 ++++++++++++++++++++++++------ internal/tui/view_dashboard.go | 4 +-- internal/tui/view_detail_inline.go | 2 +- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/internal/tui/panel.go b/internal/tui/panel.go index 4d90525..d02480e 100644 --- a/internal/tui/panel.go +++ b/internal/tui/panel.go @@ -6,7 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" ) -func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, focused bool) string { +func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset, totalItems int, focused bool) string { if height <= 0 { return m.titledPanel(title, content, width, focused) } @@ -64,21 +64,48 @@ 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("│") + showScrollbar := totalItems > 0 && totalItems > bodyH + var thumbStart, thumbEnd int + if showScrollbar { + thumbSize := bodyH * bodyH / totalItems + if thumbSize < 1 { + thumbSize = 1 + } + scrollRange := totalItems - bodyH + if scrollRange < 1 { + scrollRange = 1 + } + trackSpace := bodyH - thumbSize + thumbStart = scrollOffset * trackSpace / scrollRange + thumbEnd = thumbStart + thumbSize + } + + scrollTrack := bc.Render("│") + scrollThumb := lipgloss.NewStyle().Foreground(m.theme.Muted).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) diff --git a/internal/tui/view_dashboard.go b/internal/tui/view_dashboard.go index f653ec0..195536f 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -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, len(m.sites), 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, 0, m.focusedPanel == panelDetail)) topParts = append(topParts, detailPanel) } diff --git a/internal/tui/view_detail_inline.go b/internal/tui/view_detail_inline.go index 8ee610d..58e53f4 100644 --- a/internal/tui/view_detail_inline.go +++ b/internal/tui/view_detail_inline.go @@ -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, 0, true)) } func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {