Compare commits

..

1 Commits

Author SHA1 Message Date
lerko 351848d407 feat(tui): add scrollbar gutter to titledPanelH
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.
2026-07-01 21:04:39 -04:00
10 changed files with 65 additions and 361 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# --- Stage 1: Builder --- # --- Stage 1: Builder ---
FROM golang:1.26.5-alpine3.23@sha256:622e56dbc11a8cfe87cafa2331e9a201877271cbff918af53d3be315f3da88cc AS builder FROM golang:1.26.4-alpine3.23@sha256:f23e8b227fb4493eabe03bede4d5a32d04092da71962f1fb79b5f7d1e6c2a17f AS builder
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \ RUN --mount=type=cache,target=/go/pkg/mod \
+1 -1
View File
@@ -1,6 +1,6 @@
module gitea.lerkolabs.com/lerkolabs/uptop module gitea.lerkolabs.com/lerkolabs/uptop
go 1.26.5 go 1.26.4
require ( require (
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
+2 -113
View File
@@ -77,6 +77,8 @@ func sortSitesForDisplay(allSites []models.Site, collapsed map[int]bool, sortCol
ungrouped = append(ungrouped, s) ungrouped = append(ungrouped, s)
} }
} }
sort.Slice(groups, func(i, j int) bool { return groups[i].ID < groups[j].ID })
sortSlice := func(s []models.Site) { sortSlice := func(s []models.Site) {
sort.Slice(s, func(i, j int) bool { return s[i].ID < s[j].ID }) sort.Slice(s, func(i, j int) bool { return s[i].ID < s[j].ID })
sort.SliceStable(s, func(i, j int) bool { sort.SliceStable(s, func(i, j int) bool {
@@ -96,7 +98,6 @@ func sortSitesForDisplay(allSites []models.Site, collapsed map[int]bool, sortCol
}) })
} }
sortSlice(groups)
for pid := range children { for pid := range children {
c := children[pid] c := children[pid]
sortSlice(c) sortSlice(c)
@@ -168,118 +169,6 @@ func (m *Model) clampCursor() {
} }
} }
func (m *Model) detailCmdIfNeeded() tea.Cmd {
if m.focusedPanel == panelMonitors && m.detailOpen && m.cursor < len(m.sites) {
m.detailMode = detailDefault
m.detailScrollOffset = 0
return m.loadDetailCmd(m.sites[m.cursor].ID)
}
return nil
}
func (m *Model) jumpToTop() {
switch m.focusedPanel {
case panelDetail:
m.detailScrollOffset = 0
case panelMaint:
m.maintCursor = 0
case panelLogs:
m.logScrollOffset = 0
default:
m.cursor = 0
m.tableOffset = 0
m.syncSelectedID()
}
}
func (m *Model) jumpToBottom() {
switch m.focusedPanel {
case panelDetail:
m.detailScrollOffset = 9999
case panelMaint:
windows := m.activeMaintWindows()
if len(windows) > 0 {
m.maintCursor = len(windows) - 1
}
case panelLogs:
total := m.filteredLogCount()
if total > 0 {
m.logScrollOffset = total - 1
}
default:
max := m.currentListLen() - 1
if max >= 0 {
m.cursor = max
if m.cursor >= m.tableOffset+m.maxTableRows {
m.tableOffset = m.cursor - m.maxTableRows + 1
}
m.syncSelectedID()
}
}
}
func (m *Model) halfPageUp() {
half := m.maxTableRows / 2
if half < 1 {
half = 1
}
switch m.focusedPanel {
case panelDetail:
m.detailScrollOffset -= half
if m.detailScrollOffset < 0 {
m.detailScrollOffset = 0
}
case panelMaint:
m.maintCursor -= half
if m.maintCursor < 0 {
m.maintCursor = 0
}
case panelLogs:
m.scrollLogs(-half)
default:
m.cursor -= half
if m.cursor < 0 {
m.cursor = 0
}
if m.cursor < m.tableOffset {
m.tableOffset = m.cursor
}
m.syncSelectedID()
}
}
func (m *Model) halfPageDown() {
half := m.maxTableRows / 2
if half < 1 {
half = 1
}
switch m.focusedPanel {
case panelDetail:
m.detailScrollOffset += half
case panelMaint:
windows := m.activeMaintWindows()
m.maintCursor += half
if len(windows) > 0 && m.maintCursor >= len(windows) {
m.maintCursor = len(windows) - 1
}
case panelLogs:
m.scrollLogs(half)
default:
max := m.currentListLen() - 1
m.cursor += half
if m.cursor > max {
m.cursor = max
}
if m.cursor < 0 {
m.cursor = 0
}
if m.cursor >= m.tableOffset+m.maxTableRows {
m.tableOffset = m.cursor - m.maxTableRows + 1
}
m.syncSelectedID()
}
}
// loadTabDataCmd returns a tea.Cmd that loads the DB-backed tab tables off the // loadTabDataCmd returns a tea.Cmd that loads the DB-backed tab tables off the
// UI goroutine. Each call bumps tabSeq and stamps the reply with it, so // UI goroutine. Each call bumps tabSeq and stamps the reply with it, so
// handleTabData can drop out-of-order results from slower earlier loads. The // handleTabData can drop out-of-order results from slower earlier loads. The
+7 -26
View File
@@ -6,13 +6,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
type scrollbar struct { func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset, totalItems int, focused bool) string {
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 { if height <= 0 {
return m.titledPanel(title, content, width, focused) return m.titledPanel(title, content, width, focused)
} }
@@ -70,37 +64,24 @@ func (m Model) titledPanelH(title, content, footer string, width, height, scroll
} }
visible := contentLines[scrollOffset:end] visible := contentLines[scrollOffset:end]
if sb.total == 0 && len(contentLines) > bodyH { showScrollbar := totalItems > 0 && totalItems > 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 var thumbStart, thumbEnd int
if showScrollbar { if showScrollbar {
thumbSize := bodyH * sbVisible / sb.total thumbSize := bodyH * bodyH / totalItems
if thumbSize < 1 { if thumbSize < 1 {
thumbSize = 1 thumbSize = 1
} }
scrollRange := sb.total - sbVisible scrollRange := totalItems - bodyH
if scrollRange < 1 { if scrollRange < 1 {
scrollRange = 1 scrollRange = 1
} }
trackSpace := bodyH - thumbSize trackSpace := bodyH - thumbSize
thumbStart = sb.pos * trackSpace / scrollRange thumbStart = scrollOffset * trackSpace / scrollRange
if thumbStart < 0 {
thumbStart = 0
}
thumbEnd = thumbStart + thumbSize thumbEnd = thumbStart + thumbSize
if thumbEnd > bodyH {
thumbEnd = bodyH
}
} }
scrollTrack := lipgloss.NewStyle().Foreground(m.theme.Border).Render("") scrollTrack := bc.Render("")
scrollThumb := lipgloss.NewStyle().Foreground(m.theme.Accent).Render("") scrollThumb := lipgloss.NewStyle().Foreground(m.theme.Muted).Render("")
borderLine := func(line string, idx int) string { borderLine := func(line string, idx int) string {
rightBorder := bc.Render("│") rightBorder := bc.Render("│")
-14
View File
@@ -72,20 +72,6 @@ func (m Model) viewLogsStrip(width, maxLines int) string {
return style.Render(strings.Join(visible, "\n")) 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) { func (m *Model) scrollLogs(delta int) {
logs := m.engine.GetLogs() logs := m.engine.GetLogs()
total := 0 total := 0
+47 -91
View File
@@ -46,12 +46,10 @@ type Theme struct {
var themes = []Theme{ var themes = []Theme{
themeFlexokiDark, themeFlexokiDark,
themeEverforest,
themeKanagawa,
themeTokyoNight, themeTokyoNight,
themeCatppuccinMocha, themeCatppuccinMocha,
themeRosePine, themeNord,
themeDracula, themeGruvbox,
} }
var themeFlexokiDark = Theme{ var themeFlexokiDark = Theme{
@@ -82,7 +80,7 @@ var themeTokyoNight = Theme{
Panel: cc("#292e42", ""), Panel: cc("#292e42", ""),
Border: cc("#3b4261", "8"), Border: cc("#3b4261", "8"),
Fg: cc("#c0caf5", "15"), Fg: cc("#c0caf5", "15"),
Muted: cc("#7982a9", "7"), Muted: cc("#a9b1d6", "7"),
Subtle: cc("#565f89", "7"), Subtle: cc("#565f89", "7"),
Success: cc("#9ece6a", "10"), Success: cc("#9ece6a", "10"),
Warning: cc("#e0af68", "11"), Warning: cc("#e0af68", "11"),
@@ -93,28 +91,28 @@ var themeTokyoNight = Theme{
Purple: cc("#bb9af7", "13"), Purple: cc("#bb9af7", "13"),
ZebraBg: cc("#1c1d28", ""), ZebraBg: cc("#1c1d28", ""),
SelectedFg: cc("#c0caf5", "15"), SelectedFg: cc("#c0caf5", "15"),
SelectedBg: cc("#363c53", "4"), SelectedBg: cc("#292e42", "4"),
} }
var themeDracula = Theme{ var themeGruvbox = Theme{
Name: "Dracula", Name: "Gruvbox",
Bg: cc("#282a36", ""), Bg: cc("#282828", ""),
Surface: cc("#343746", ""), Surface: cc("#3c3836", ""),
Panel: cc("#44475a", ""), Panel: cc("#504945", ""),
Border: cc("#6272a4", "8"), Border: cc("#665c54", "8"),
Fg: cc("#f8f8f2", "15"), Fg: cc("#ebdbb2", "15"),
Muted: cc("#a9b0cb", "7"), Muted: cc("#bdae93", "7"),
Subtle: cc("#6272a4", "7"), Subtle: cc("#7c6f64", "7"),
Success: cc("#50fa7b", "10"), Success: cc("#b8bb26", "10"),
Warning: cc("#f1fa8c", "11"), Warning: cc("#fabd2f", "11"),
Stale: cc("#ffb86c", "3"), Stale: cc("#fe8019", "3"),
Danger: cc("#ff5555", "9"), Danger: cc("#fb4934", "9"),
Info: cc("#8be9fd", "12"), Info: cc("#83a598", "12"),
Accent: cc("#bd93f9", "14"), Accent: cc("#8ec07c", "14"),
Purple: cc("#ff79c6", "13"), Purple: cc("#d3869b", "13"),
ZebraBg: cc("#2c2e3a", ""), ZebraBg: cc("#2a2a2a", ""),
SelectedFg: cc("#f8f8f2", "15"), SelectedFg: cc("#fbf1c7", "15"),
SelectedBg: cc("#52556b", "4"), SelectedBg: cc("#504945", "4"),
} }
var themeCatppuccinMocha = Theme{ var themeCatppuccinMocha = Theme{
@@ -126,79 +124,37 @@ var themeCatppuccinMocha = Theme{
Fg: cc("#cdd6f4", "15"), Fg: cc("#cdd6f4", "15"),
Muted: cc("#a6adc8", "7"), Muted: cc("#a6adc8", "7"),
Subtle: cc("#6c7086", "7"), Subtle: cc("#6c7086", "7"),
Success: cc("#7dc47a", "10"), Success: cc("#a6e3a1", "10"),
Warning: cc("#f0c644", "11"), Warning: cc("#f9e2af", "11"),
Stale: cc("#fab387", "3"), Stale: cc("#fab387", "3"),
Danger: cc("#e6546e", "9"), Danger: cc("#f38ba8", "9"),
Info: cc("#89b4fa", "12"), Info: cc("#89b4fa", "12"),
Accent: cc("#94e2d5", "14"), Accent: cc("#94e2d5", "14"),
Purple: cc("#cba6f7", "13"), Purple: cc("#cba6f7", "13"),
ZebraBg: cc("#212130", ""), ZebraBg: cc("#232334", ""),
SelectedFg: cc("#cdd6f4", "15"), SelectedFg: cc("#cdd6f4", "15"),
SelectedBg: cc("#585b70", "4"), SelectedBg: cc("#45475a", "4"),
} }
var themeRosePine = Theme{ var themeNord = Theme{
Name: "Rosé Pine", Name: "Nord",
Bg: cc("#191724", ""), Bg: cc("#2e3440", ""),
Surface: cc("#1f1d2e", ""), Surface: cc("#3b4252", ""),
Panel: cc("#26233a", ""), Panel: cc("#434c5e", ""),
Border: cc("#524f67", "8"), Border: cc("#4c566a", "8"),
Fg: cc("#e0def4", "15"), Fg: cc("#d8dee9", "15"),
Muted: cc("#908caa", "7"), Muted: cc("#d8dee9", "7"),
Subtle: cc("#6e6a86", "7"), Subtle: cc("#4c566a", "7"),
Success: cc("#9ccfd8", "10"), Success: cc("#a3be8c", "10"),
Warning: cc("#f6c177", "11"), Warning: cc("#ebcb8b", "11"),
Stale: cc("#ebbcba", "3"), Stale: cc("#d08770", "3"),
Danger: cc("#eb6f92", "9"), Danger: cc("#bf616a", "9"),
Info: cc("#3e8fb0", "12"), Info: cc("#81a1c1", "12"),
Accent: cc("#c4a7e7", "14"), Accent: cc("#88c0d0", "14"),
Purple: cc("#ebbcba", "13"), Purple: cc("#b48ead", "13"),
ZebraBg: cc("#1c1a28", ""), ZebraBg: cc("#323845", ""),
SelectedFg: cc("#e0def4", "15"), SelectedFg: cc("#eceff4", "15"),
SelectedBg: cc("#403d52", "4"), SelectedBg: cc("#434c5e", "4"),
}
var themeKanagawa = Theme{
Name: "Kanagawa",
Bg: cc("#1F1F28", ""),
Surface: cc("#2A2A37", ""),
Panel: cc("#363646", ""),
Border: cc("#54546D", "8"),
Fg: cc("#DCD7BA", "15"),
Muted: cc("#C8C093", "7"),
Subtle: cc("#727169", "7"),
Success: cc("#98BB6C", "10"),
Warning: cc("#E6C384", "11"),
Stale: cc("#FFA066", "3"),
Danger: cc("#E46876", "9"),
Info: cc("#7E9CD8", "12"),
Accent: cc("#7FB4CA", "14"),
Purple: cc("#D27E99", "13"),
ZebraBg: cc("#22222c", ""),
SelectedFg: cc("#DCD7BA", "15"),
SelectedBg: cc("#2D4F67", "4"),
}
var themeEverforest = Theme{
Name: "Everforest",
Bg: cc("#2F383E", ""),
Surface: cc("#343F44", ""),
Panel: cc("#3D484D", ""),
Border: cc("#56635F", "8"),
Fg: cc("#D3C6AA", "15"),
Muted: cc("#9DA9A0", "7"),
Subtle: cc("#7A8478", "7"),
Success: cc("#A7C080", "10"),
Warning: cc("#DBBC7F", "11"),
Stale: cc("#E69875", "3"),
Danger: cc("#E67E80", "9"),
Info: cc("#7FBBB3", "12"),
Accent: cc("#83C092", "14"),
Purple: cc("#D699B6", "13"),
ZebraBg: cc("#30393F", ""),
SelectedFg: cc("#D3C6AA", "15"),
SelectedBg: cc("#475258", "4"),
} }
func (t Theme) HuhTheme() *huh.Theme { func (t Theme) HuhTheme() *huh.Theme {
-2
View File
@@ -218,8 +218,6 @@ type Model struct {
filterMode bool filterMode bool
filterText string filterText string
pendingG bool
// demoMode renders a stable status dot instead of the animated pulse so // demoMode renders a stable status dot instead of the animated pulse so
// screenshots/recordings don't capture the spinner mid-frame. Set via UPTOP_DEMO=1. // screenshots/recordings don't capture the spinner mid-frame. Set via UPTOP_DEMO=1.
demoMode bool demoMode bool
-102
View File
@@ -252,17 +252,6 @@ func (m *Model) testAlertCmd(id int, name string) tea.Cmd {
func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
if msg.String() == "g" {
if m.pendingG {
m.pendingG = false
m.logViewport.GotoTop()
return m, nil
}
m.pendingG = true
return m, nil
}
m.pendingG = false
switch msg.String() { switch msg.String() {
case "esc", "q": case "esc", "q":
m.state = stateDashboard m.state = stateDashboard
@@ -272,12 +261,6 @@ func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
case "f": case "f":
m.logFilterImportant = !m.logFilterImportant m.logFilterImportant = !m.logFilterImportant
m.refreshLogContent() m.refreshLogContent()
case "G":
m.logViewport.GotoBottom()
case "ctrl+u":
m.logViewport.ScrollUp(m.logViewport.Height / 2)
case "ctrl+d":
m.logViewport.ScrollDown(m.logViewport.Height / 2)
case "up", "k": case "up", "k":
m.logViewport.ScrollUp(1) m.logViewport.ScrollUp(1)
case "down", "j": case "down", "j":
@@ -303,17 +286,6 @@ func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *Model) handleDetailFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *Model) handleDetailFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
if msg.String() == "g" {
if m.pendingG {
m.pendingG = false
m.detailScrollOffset = 0
return m, nil
}
m.pendingG = true
return m, nil
}
m.pendingG = false
switch msg.String() { switch msg.String() {
case "esc", "q": case "esc", "q":
m.state = stateDashboard m.state = stateDashboard
@@ -351,15 +323,6 @@ func (m *Model) handleDetailFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.loadSLACmd(m.slaSiteID, idx) return m, m.loadSLACmd(m.slaSiteID, idx)
} }
} }
case "G":
m.detailScrollOffset = 9999
case "ctrl+u":
m.detailScrollOffset -= 5
if m.detailScrollOffset < 0 {
m.detailScrollOffset = 0
}
case "ctrl+d":
m.detailScrollOffset += 5
case "up", "k": case "up", "k":
m.detailScrollOffset-- m.detailScrollOffset--
if m.detailScrollOffset < 0 { if m.detailScrollOffset < 0 {
@@ -581,52 +544,7 @@ func (m *Model) handleAlertDetailKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
func (m *Model) handleSettingsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *Model) handleSettingsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if msg.String() == "g" {
if m.pendingG {
m.pendingG = false
m.settingsCursor = 0
m.settingsOffset = 0
return m, nil
}
m.pendingG = true
return m, nil
}
m.pendingG = false
switch msg.String() { switch msg.String() {
case "G":
max := m.settingsListLen() - 1
if max >= 0 {
m.settingsCursor = max
if m.settingsCursor >= m.settingsOffset+m.maxTableRows {
m.settingsOffset = m.settingsCursor - m.maxTableRows + 1
}
}
case "ctrl+u":
half := m.maxTableRows / 2
if half < 1 {
half = 1
}
m.settingsCursor -= half
if m.settingsCursor < 0 {
m.settingsCursor = 0
}
if m.settingsCursor < m.settingsOffset {
m.settingsOffset = m.settingsCursor
}
case "ctrl+d":
half := m.maxTableRows / 2
if half < 1 {
half = 1
}
max := m.settingsListLen() - 1
m.settingsCursor += half
if m.settingsCursor > max {
m.settingsCursor = max
}
if m.settingsCursor >= m.settingsOffset+m.maxTableRows {
m.settingsOffset = m.settingsCursor - m.maxTableRows + 1
}
case "esc", "S": case "esc", "S":
m.state = stateDashboard m.state = stateDashboard
case "ctrl+c": case "ctrl+c":
@@ -717,27 +635,7 @@ func (m *Model) handleSettingsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if msg.String() == "g" {
if m.pendingG {
m.pendingG = false
m.jumpToTop()
return m, m.detailCmdIfNeeded()
}
m.pendingG = true
return m, nil
}
m.pendingG = false
switch msg.String() { switch msg.String() {
case "G":
m.jumpToBottom()
return m, m.detailCmdIfNeeded()
case "ctrl+u":
m.halfPageUp()
return m, m.detailCmdIfNeeded()
case "ctrl+d":
m.halfPageDown()
return m, m.detailCmdIfNeeded()
case "q": case "q":
return m, tea.Quit return m, tea.Quit
case "/": case "/":
+6 -10
View File
@@ -163,7 +163,7 @@ func (m Model) viewMonitorsLayout() string {
monTargetH := m.maxTableRows + 5 monTargetH := m.maxTableRows + 5
monitors := m.viewSitesTab() monitors := m.viewSitesTab()
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)) monPanel := m.zones.Mark("panel-monitors", m.titledPanelH("Monitors", monitors, "", monW, monTargetH, 0, len(m.sites), m.focusedPanel == panelMonitors))
var topParts []string var topParts []string
topParts = append(topParts, monPanel) topParts = append(topParts, monPanel)
@@ -181,7 +181,7 @@ 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)
footer := m.detailFooter(detailW - 2) footer := m.detailFooter(detailW - 2)
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, scrollbar{}, 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) topParts = append(topParts, detailPanel)
} }
@@ -189,16 +189,12 @@ func (m Model) viewMonitorsLayout() string {
switch m.bottomPanel { switch m.bottomPanel {
case bottomLogs: case bottomLogs:
maxLines := logsStripHeight - 2 logContent := m.viewLogsStrip(availW-2, logsStripHeight-2)
logContent := m.viewLogsStrip(availW-2, maxLines) logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", logContent, availW, m.focusedPanel == panelLogs))
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 return top + "\n" + logPanel
case bottomMaint: case bottomMaint:
maxLines := logsStripHeight - 2 maintContent := m.viewMaintStrip(availW-2, logsStripHeight-2)
maintContent := m.viewMaintStrip(availW-2, maxLines) maintPanel := m.zones.Mark("panel-maint", m.titledPanel("Maint", maintContent, availW, m.focusedPanel == panelMaint))
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 + "\n" + maintPanel
} }
return top return top
+1 -1
View File
@@ -75,7 +75,7 @@ func (m Model) viewDetailFullscreen() string {
} }
return lipgloss.NewStyle().Padding(1, 2).Render( return lipgloss.NewStyle().Padding(1, 2).Render(
m.titledPanelH(title, content, footer, availW, panelH, m.detailScrollOffset, scrollbar{}, 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 { func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {